From c260529ef24842a40c0032b67b2a51a1d7e60ddf Mon Sep 17 00:00:00 2001 From: khushal Date: Fri, 6 Dec 2024 10:40:36 +0530 Subject: [PATCH] (20241206) Started Telegram integration. --- api/blueprints/chat/__init__.py | 0 api/blueprints/chat/auth.py | 239 ++++++++++++++++++++++++++++++++ api/blueprints/chat/webhook.py | 196 ++++++++++++++++++++++++++ api/blueprints/test/callback.py | 2 +- api/main.py | 4 + models/data/chat/__init__.py | 0 models/data/chat/auth.py | 145 +++++++++++++++++++ 7 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 api/blueprints/chat/__init__.py create mode 100644 api/blueprints/chat/auth.py create mode 100644 api/blueprints/chat/webhook.py create mode 100644 models/data/chat/__init__.py create mode 100644 models/data/chat/auth.py diff --git a/api/blueprints/chat/__init__.py b/api/blueprints/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/chat/auth.py b/api/blueprints/chat/auth.py new file mode 100644 index 0000000..92fea31 --- /dev/null +++ b/api/blueprints/chat/auth.py @@ -0,0 +1,239 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Friday, 6th Dec., 2024 + + OBJECTIVE: + + To register a telegram bot with our system and send a request to Telegram to set it up the webhook. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, request, render_template + +# My utils: +from utils_v2.string import json +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# Data Models: +from models.data.chat.auth import ChatAuthRequestHeaders, ChatAuthRequestData + +# Common: +from shared import constants + +# For asynchronous activities: +import asyncio + +# For random choices: +import random + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +chat_auth_bp = Blueprint("chat_auth_bp", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@chat_auth_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +async def set_telegram_webhook( + bot_token: str, + webhook_url: str +) -> dict: + + """ + To set a webhook to receive messages via a bot. + :param bot_token: The token granted by BotFather when creating the bot. + :param webhook_url: Your endpoint where you will receive messages that people send to your bot. + :return: The JSON response from Telegram. + """ + + # Register the webhook with Telegram: + api_response = await current_app.http_client.post( + url = f"https://api.telegram.org/bot{bot_token}/setWebhook", + data = {"url": webhook_url} + ) + + # Done here: + return api_response.json() + + +# --------------------------------------------------------------------------------------------------------------------- + + +async def get_telegram_bot_info( + bot_token: str, +) -> dict | None: + + """ + To get a bot's basic info. + :param bot_token: The token granted by BotFather when creating the bot. + :return: The JSON response from Telegram. + """ + + # Start by assuming failure: + bot_info = None + + # Register the webhook with Telegram: + api_response = await current_app.http_client.get( + url = f"https://api.telegram.org/bot{bot_token}/getMe", + ) + + # If the call succeeds: + if api_response.status_code in [200]: + if api_response.json()["ok"]: + api_result = api_response.json()["result"] + bot_info = { + "id": api_result["id"], + "displayName": api_result["first_name"], + "username": api_result["username"], + } + + # Done here: + return bot_info + + +# --------------------------------------------------------------------------------------------------------------------- + + +@chat_auth_bp.route("/auth", methods = ["POST"]) +@set_api_version(api_version = "1.0.0") +@read_input(sanitize_headers = False, sanitize_data = False) +@get_session_info(key = "X-Session-Token", session_coro = "get_session") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "chatAuthApi", + log_input = True, + log_output = True, + sensitive_keys = None +) +@log_chain_to_mongo(attr_name = "logs_mongo") +@should_not_be_under_maintenance(attr_name = "is_under_maintenance") +@validate_input( + header_validator = lambda x: ChatAuthRequestHeaders(**x).model_dump(), + data_validator = lambda x: ChatAuthRequestData(**x) +) +@handle_cancelled_request() +async def callback_test( + inbound_headers: dict | ChatAuthRequestHeaders = None, + inbound_data: dict | ChatAuthRequestData = None, + inbound_files: dict = None, + **kwargs +): + + # ┏┓ + # ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏ + # ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛ + # ┛ + + # If the session token is invalid/expired: + if kwargs.get("session_info") is None: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED + ) + + # ┏┓ ┏┳┓ ┓ + # ┣ ┏┓┏┓ ┃ ┏┓┃┏┓┏┓┏┓┏┓┏┳┓ + # ┻ ┗┛┛ ┻ ┗ ┗┗ ┗┫┛ ┗┻┛┗┗ + # ┛ + + if inbound_data.chatClient == "telegram": + + # client_response = await get_telegram_bot_info(bot_token = inbound_data.auth.botToken) + # print("TG BOT:", json.to_string(client_response)) + client_response = await set_telegram_webhook( + bot_token = inbound_data.auth.botToken, + webhook_url = r"https://nexcom.ditscentre.in/test/callback" + ) + + return "ok" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/blueprints/chat/webhook.py b/api/blueprints/chat/webhook.py new file mode 100644 index 0000000..163a352 --- /dev/null +++ b/api/blueprints/chat/webhook.py @@ -0,0 +1,196 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Friday, 6th Dec., 2024 + + OBJECTIVE: + + To register a telegram bot with our system and send a request to Telegram to set it up the webhook. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, request, render_template + +# My utils: +from utils_v2.string import json +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# Data Models: +from models.data.chat.auth import ChatAuthRequestHeaders, ChatAuthRequestData + +# Common: +from shared import constants + +# For asynchronous activities: +import asyncio + +# For random choices: +import random + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +chat_webhook_bp = Blueprint("chat_whook_bp", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@chat_webhook_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@chat_webhook_bp.route("/webhook/", methods = ["POST", "GET"]) +@set_api_version(api_version = "1.0.0") +@read_input(sanitize_headers = False, sanitize_data = False) +# @get_session_info(key = "X-Session-Token", session_coro = "get_session") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "chatWHookApi", + log_input = True, + log_output = True, + sensitive_keys = None +) +@log_chain_to_mongo(attr_name = "logs_mongo") +@should_not_be_under_maintenance(attr_name = "is_under_maintenance") +# @validate_input( +# header_validator = lambda x: ChatAuthRequestHeaders(**x).model_dump(), +# data_validator = lambda x: ChatAuthRequestData(**x) +# ) +@handle_cancelled_request() +async def callback_test( + token_id: str = None, + inbound_headers: dict = None, + inbound_data: dict = None, + inbound_files: dict = None, + **kwargs +): + """ + This URL receives messages from chatbots as webhooks. + :param token_id: The identifier of the bot. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # Construct a message: + message = "🪝 *WEBHOOK/CALLBACK ALERT!* 🪝\n\n" + message += f"Method: *{request.method}*\nLog Id.: `{kwargs.get('log_id')}`\n\n" + message += "*Headers:*\n```json\n" + message += json.to_string({k: v for k, v in request.headers.items()}) + message += "\n```\n" + message += "*Query Args:*\n```json\n" + message += json.to_string(request.args.to_dict()) + message += "\n```\n" + message += "*JSON:*\n```json\n" + message += json.to_string(await request.get_json()) + message += "\n```\n" + message += "*Form-Data:*\n```json\n" + message += json.to_string((await request.form).to_dict()) + message += "\n```\n" + message += "*Form-Files:*\n```json\n" + message += json.to_string(inbound_files, default=str) + message += "\n```\n" + + # Send a message on Telegram: + api_response = await current_app.http_client.post( + url = current_app.script_data["alerts"]["url"], + json = { + "message": message, + "type": "info", + "chatId": "1275560043" # ... KPS + } + ) + + # Return a success response: + return ResponseModel( + status_code = StatusCodes.OK, + data = {"accepted": True} + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/blueprints/test/callback.py b/api/blueprints/test/callback.py index 8779ecc..5aca1f0 100644 --- a/api/blueprints/test/callback.py +++ b/api/blueprints/test/callback.py @@ -77,7 +77,7 @@ import random # Related to Quart: -test_callback_bp = Blueprint("user_cb", __name__) +test_callback_bp = Blueprint("test_cb", __name__) # ***************************************************************************************************************** diff --git a/api/main.py b/api/main.py index 59487be..3d5cd01 100644 --- a/api/main.py +++ b/api/main.py @@ -90,6 +90,8 @@ from api.blueprints.mail.sync import mail_sync_bp from api.blueprints.mail.list import mail_list_bp from api.blueprints.mail.retrieve import mail_retrieve_bp from api.blueprints.sms.auth import sms_auth_bp +from api.blueprints.chat.auth import chat_auth_bp +from api.blueprints.chat.webhook import chat_webhook_bp from api.blueprints.tech.chat_alerts import tech_chat_alert_bp from api.blueprints.test.callback import test_callback_bp from api.blueprints.ai.llm.invoke import llm_invoke_bp @@ -126,6 +128,8 @@ app.register_blueprint(mail_sync_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_list_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(mail_retrieve_bp, url_prefix = f"/{MODULE_BASE}/mail") app.register_blueprint(sms_auth_bp, url_prefix = f"/{MODULE_BASE}/sms") +app.register_blueprint(chat_auth_bp, url_prefix = f"/{MODULE_BASE}/chat") +app.register_blueprint(chat_webhook_bp, url_prefix = f"/{MODULE_BASE}/chat") app.register_blueprint(tech_chat_alert_bp, url_prefix = f"/{MODULE_BASE}/tech/alert") app.register_blueprint(test_callback_bp, url_prefix = f"/{MODULE_BASE}/test") app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai") diff --git a/models/data/chat/__init__.py b/models/data/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/data/chat/auth.py b/models/data/chat/auth.py new file mode 100644 index 0000000..afb0bc8 --- /dev/null +++ b/models/data/chat/auth.py @@ -0,0 +1,145 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Friday, 6th Dec., 2024. + + OBJECTIVE: + + To provide a structure to receive auth details of various chat apps (like Telegram and WhatsApp). + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime +from typing import Optional, Literal, Union + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class TelegramAuth(BaseModel): + + botToken: str = Field( + description = "the token granted by BotFather", + min_length = 1, + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class ChatAuthRequestHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class ChatAuthRequestData(BaseModel): + + chatClient: Literal["telegram", "whatsapp"] + auth: Union[TelegramAuth] + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass