diff --git a/src/constants.py b/src/constants.py index 3f946ce..0ee2a2c 100644 --- a/src/constants.py +++ b/src/constants.py @@ -12,6 +12,8 @@ class Constants: FEEDBACK_FORWARD_CHAT_ID = -683998033 BOT_ID = 5065052385 + # BOT_ID = 5015215848 # MemRem + class Common: @staticmethod def inactive_user(name: str, language_code: str = "en") -> str: @@ -269,20 +271,25 @@ def already_added(name: str, language_code: str = "en") -> str: @staticmethod def success(name: str, language_code: str = "en", note: str = "") -> str: + words = note.split(" ") + if words[0] == "message_id:": + note_message = "" + else: + note_message = f"*Note*: \n{note}" if language_code == "tr": return ( f"{name}, not kasana eklendi. Merak etme, onu güvende tutacağım {Constants.smile}" - f"\n*Not*: \n{note}" - f"" - f"\n\n Eğer son eklediğin notu silmek istiyorsan, bu komutu kullan */deletelast*" + f"\n{note_message}" + f"\n" + f"\n Eğer son eklediğin notu silmek istiyorsan, bu komutu kullan */deletelastadd*" ) else: return ( - f"{name}, note is added to your memory vault. No worries, I will keep it safe {Constants.smile}" - f"\n*Note*: \n{note}" - f"" - f"\n\nIf you want to delete the last added note, you can use */deletelast*" + f"{name}, the note is added to your memory vault. No worries, I will keep it safe {Constants.smile}" + f"\n{note_message}" + f"\n" + f"\nIf you want to delete the last added note, you can use */deletelastadd*" ) class Delete: @@ -295,18 +302,16 @@ def no_id(name: str, language_code: str = "en") -> str: return f"{name}, need to give me id of the note, ie: *delete 2*, you can get it by using command, *list* or /list" @staticmethod - def success(name: str, language_code: str = "en", note: str = "") -> str: + def success(name: str, language_code: str = "en") -> str: if language_code == "tr": return ( f"{name}, not kasadan silindi. Unutulan hatıraya elveda {Constants.sad}" f"\n*Silinen Not*:" - f"\n{note}" ) else: return ( f"{name}, your note is deleted from your memory vault. Good bye to the forgotten memory {Constants.sad}" f"\n*Deleted Note*:" - f"\n{note}" ) class Schedule: diff --git a/src/events.py b/src/events.py index bcadfa4..7de3d57 100644 --- a/src/events.py +++ b/src/events.py @@ -17,7 +17,7 @@ class Events: TOKEN = os.environ.get("TELEGRAM_TOKEN") TELEGRAM_SEND_MESSAGE_URL = f"https://api.telegram.org/bot{TOKEN}/sendMessage" TELEGRAM_SET_WEBHOOK_URL = f"https://api.telegram.org/bot{TOKEN}/setWebhook" - TELEGRAM_SEND_DOCUMENT_URL = f"https://api.telegram.org/bot{TOKEN}/sendDocument" + TELEGRAM_COPY_MESSAGE_URL = f"https://api.telegram.org/bot{TOKEN}/copyMessage" PORT = 8000 HOST_URL = None @@ -80,69 +80,106 @@ def send_user_hourly_memories( @classmethod async def send_message_list_at_background( - cls, telegram_chat_id: int, message_list: List[str] + cls, + telegram_chat_id: int, + message_list: List[str], + notify: bool = True, ) -> bool: for message in message_list: await Events.send_a_message_to_user( - telegram_id=telegram_chat_id, message=message + chat_id=telegram_chat_id, + message=message, + notify=notify, ) return True @classmethod - async def get_package_message( + async def create_response_message( cls, message: str, - ) -> str: + chat_id: int, + convert: bool, + notify: bool = True, + ) -> (str, ResponseToMessage): """ - Runs the related package if the reminder is a package type. + Creates the response message + + Runs the related package and sends the resulting message if the reminder is a package type. + Sends the photo if the message is photo type. + Sends the document if the message is document type. + Sends the text if the message is text type. Args: message: + chat_id: + convert: Converts the encoded message to related type of message, if True + notify: If false, send the message without notifying. Returns: - converted_message + converted_message: """ - words = message.split(" ") - if words[0] == "package:": - package_id = 0 - try: - package_id = int(words[1]) - except: - return message - return await (Packages.functions[package_id]()) + message_id = None + from_chat_id = None + text = None + print(f"%% {datetime.datetime.now()}: Message is: {message}") + + if convert: + words = message.split(" ") + if len(words) == 2: + if words[0] == "package:": + fn_id = int(words[1]) + text = await (Packages.functions[fn_id]()) + + elif words[0] == "message_id:": + message_id = int(words[1]) + from_chat_id = chat_id + + else: + text = message - return message + else: + text = message + + return cls.TELEGRAM_SEND_MESSAGE_URL, ResponseToMessage( + **{ + "text": text, + "message_id": message_id, + "chat_id": chat_id, + "from_chat_id": from_chat_id, + "disable_notification": notify, + } + ) @classmethod async def send_a_message_to_user( cls, - telegram_id: int, + chat_id: int, message: str, retry_count: int = 3, - sleep_time: float = 0.1, + sleep_time: float = 0.01, + convert: bool = True, + notify: bool = True, ) -> bool: - message = await cls.get_package_message(message) - message = ResponseToMessage( - **{ - "text": message, - "chat_id": telegram_id, - } - ) + url, message = await cls.create_response_message(message, chat_id, convert) + print(f"%% {datetime.datetime.now()}: Message is: {message}") + await asyncio.sleep(sleep_time) for retry in range(retry_count): # Avoid too many requests error from Telegram - response = await cls.request(cls.TELEGRAM_SEND_MESSAGE_URL, message.dict()) + response = await cls.request(url, message.dict()) if response.status_code == 200: - print(f"%% {datetime.datetime.now()}: Sent message {retry}") + # print(f"%% {datetime.datetime.now()}: Sent message ") return True elif response.status_code == 429: retry_after = int(response.json()["parameters"]["retry_after"]) - print(f"%% {datetime.datetime.now()} Retry After: {retry_after}, message: {message}") + print( + f"%% {datetime.datetime.now()} Retry After: {retry_after}, message: {message}" + ) await asyncio.sleep(retry_after) else: print( - f"%% {datetime.datetime.now()} Unhandled response code: {response.status_code}, response: {response.json()}" + f"%% {datetime.datetime.now()} Unhandled response code: {response.status_code}, response: {response.json()}, chat: {chat_id}, message: {message}, url: {url}" ) return False @@ -151,10 +188,7 @@ async def broadcast_message(cls, message: str) -> None: users = db_read_users(limit=100000, only_active_users=False) await asyncio.gather( *( - Events.send_a_message_to_user( - user.telegram_chat_id, - message, - ) + Events.send_a_message_to_user(user.telegram_chat_id, message) for user in users ) ) @@ -179,11 +213,6 @@ async def set_telegram_webhook_url(cls) -> bool: req = await cls.request(cls.TELEGRAM_SET_WEBHOOK_URL, payload) return req.status_code == 200 - @classmethod - def archive_db(cls) -> bool: - command = f'curl -v -F "chat_id={Constants.BROADCAST_CHAT_ID}" -F document=@database.db {cls.TELEGRAM_SEND_DOCUMENT_URL}' - os.system(command) - @classmethod async def get_public_ip(cls): # Reference: https://pytutorial.com/python-get-public-ip diff --git a/src/listener.py b/src/listener.py index 8529202..eaaa945 100644 --- a/src/listener.py +++ b/src/listener.py @@ -1,8 +1,8 @@ import asyncio -import datetime - -from fastapi import FastAPI, Depends, Request -from fastapi.concurrency import run_in_threadpool +import logging +import time +from fastapi import FastAPI, Request +from fastapi.responses import PlainTextResponse from .db import * from .message_validations import MessageBodyModel, ResponseToMessage from .constants import Constants @@ -10,6 +10,7 @@ from .response_logic import ResponseLogic app = FastAPI(openapi_url=None) +logging.basicConfig(filename="exceptions.log", encoding="utf-8", level=logging.ERROR) @app.on_event("startup") @@ -18,6 +19,20 @@ def on_startup(): asyncio.create_task(Events.main_event()) +@app.middleware("http") +async def exception_handler(request: Request, call_next): + start_time = time.time() + try: + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(round(process_time, 4)) + except Exception as ex: + logging.exception(f"$${datetime.datetime.now()}: Exception in Middleware:") + logging.exception(ex) + return PlainTextResponse(str("An Error Occurred"), status_code=200) + return response + + @app.get("/health") async def health(): return {"healthy": True} @@ -27,26 +42,66 @@ async def health(): async def listen_telegram_messages(r: Request, message: MessageBodyModel): print(f"%% {datetime.datetime.now()} Incoming Message: {message.dict()}") print(f"%% {datetime.datetime.now()} Incoming Request: {await r.json()}") + + response_message = None + chat_id = 0 + if message.message: name = message.message.from_field.first_name chat_id = message.message.chat.id - text = message.message.text language_code = message.message.from_field.language_code - if not text: # Edit of message etc. - return - else: + if message.message.photo: + response_message = await ResponseLogic.create_response( + f"add message_id: {message.message.message_id}", + name, + chat_id, + language_code, + ) + elif message.message.document: response_message = await ResponseLogic.create_response( - text, name, chat_id, language_code + f"add message_id: {message.message.message_id}", + name, + chat_id, + language_code, ) - return ResponseToMessage( - **{ - "text": response_message, - "chat_id": chat_id, - } + elif message.message.video: + response_message = await ResponseLogic.create_response( + f"add message_id: {message.message.message_id}", + name, + chat_id, + language_code, + ) + elif message.message.video_note: + response_message = await ResponseLogic.create_response( + f"add message_id: {message.message.message_id}", + name, + chat_id, + language_code, ) + elif message.message.voice: + response_message = await ResponseLogic.create_response( + f"add message_id: {message.message.message_id}", + name, + chat_id, + language_code, + ) + elif message.message.forward_date: + if message.message.text: + response_message = await ResponseLogic.create_response( + f"add message_id: {message.message.message_id}", + name, + chat_id, + language_code, + ) + elif message.message.text: + response_message = await ResponseLogic.create_response( + message.message.text, name, chat_id, language_code + ) + else: + return - if not message.message: # Bot is added to a group + elif not message.message: # Bot is added to a group if not message.my_chat_member: return @@ -60,21 +115,18 @@ async def listen_telegram_messages(r: Request, message: MessageBodyModel): and new_member.user.id == Constants.BOT_ID and new_member.status == "member" ): - start_message = await ResponseLogic.create_response("start", name, chat_id, language_code) - await Events.send_a_message_to_user( - chat_id, start_message + start_message = await ResponseLogic.create_response( + "start", name, chat_id, language_code ) - await Events.send_a_message_to_user( - chat_id, Constants.Start.group_warning(name, language_code) - ) - return - - return - - -@app.post(f"/trigger_archive_db/{Events.TOKEN}") -def trigger_archive_db(): - Events.archive_db() + await Events.send_a_message_to_user(chat_id, start_message) + response_message = Constants.Start.group_warning(name, language_code) + + return ResponseToMessage( + **{ + "text": response_message, + "chat_id": chat_id, + } + ) @app.post(f"/trigger_send_user_hourly_memories/{Events.TOKEN}") diff --git a/src/message_validations.py b/src/message_validations.py index c8be119..f96391c 100644 --- a/src/message_validations.py +++ b/src/message_validations.py @@ -27,7 +27,7 @@ class ReplyMessage(BaseModel): text: Optional[str] -class Photo(BaseModel): +class File(BaseModel): file_id: Optional[str] @@ -36,8 +36,13 @@ class Message(BaseModel): chat: Optional[Chat] message_id: Optional[str] from_field: From = Field(alias="from") + forward_date: Optional[int] text: Optional[str] - photo: Optional[List[Photo]] + photo: Optional[List[File]] + document: Optional[File] + video: Optional[File] + video_note: Optional[File] + voice: Optional[File] class ChatGroup(BaseModel): @@ -74,5 +79,10 @@ class MessageBodyModel(BaseModel): class ResponseToMessage(BaseModel): method: Optional[str] = "sendMessage" chat_id: Optional[int] = 861126057 - text: Optional[str] = "" + from_chat_id: Optional[int] + message_id: Optional[int] + text: Optional[str] + photo: Optional[str] + document: Optional[str] parse_mode: Optional[str] = "Markdown" + disable_notification: Optional[bool] diff --git a/src/packages.py b/src/packages.py index 98ccc89..16ec11a 100644 --- a/src/packages.py +++ b/src/packages.py @@ -25,11 +25,19 @@ async def get_tr_stocks() -> str: GA = round(GA, 2) GA_DEGISIM = float(js["GA"]["degisim"]) GA_DEGISIM = round(GA_DEGISIM, 2) + try: + XU100USD = round(XU100 / USDTRY, 2) + except: + XU100USD = 0 + try: + XU100USD_DEGISIM = round(XU100_DEGISIM / USD_DEGISIM, 2) + except: + XU100USD_DEGISIM = 0 message = ( f"*USD*: {USDTRY} -- % {USD_DEGISIM} \n" f"*XU100*: {XU100} -- % {XU100_DEGISIM} \n" - f"*XU100/USD*: {round(XU100 / USDTRY, 2)} -- % {round(XU100_DEGISIM / USD_DEGISIM, 2)} \n" + f"*XU100/USD*: {XU100USD} -- % {XU100USD_DEGISIM} \n" f"*GA*: {GA} -- % {GA_DEGISIM} \n" ) return message diff --git a/src/response_logic.py b/src/response_logic.py index 39fc7f3..18ab2f7 100644 --- a/src/response_logic.py +++ b/src/response_logic.py @@ -20,7 +20,6 @@ async def create_response( split_text = text.split(" ") first_word = split_text[0] - user = UserCreate( name=name, telegram_chat_id=chat_id, @@ -140,12 +139,14 @@ async def create_response( else: background_message_list = [] for message_id, reminder in enumerate(memories): - background_message_list.append( - f"\n{message_id}: {reminder.reminder}" - ) + background_message_list.append(f"*{message_id}*: ") + background_message_list.append(reminder.reminder) + asyncio.create_task( Events.send_message_list_at_background( - telegram_chat_id=chat_id, message_list=background_message_list + telegram_chat_id=chat_id, + message_list=background_message_list, + notify=False, ) ) @@ -170,8 +171,12 @@ async def create_response( return Constants.Delete.no_id(name, language_code) else: memory = response + await Events.send_a_message_to_user( + chat_id, Constants.Delete.success(name, language_code) + ) + await Events.send_a_message_to_user(chat_id, memory) - return Constants.Delete.success(name, language_code, memory) + return "" except Exception as ex: return Constants.Delete.no_id(name, language_code) @@ -322,14 +327,18 @@ async def create_response( else: return Constants.Feedback.fail(name, language_code) - elif ResponseLogic.check_command_type(first_word, "deletelast"): - response = delete_last_memory(user) - if response is None: + elif ResponseLogic.check_command_type(first_word, "deletelastadd"): + memory = delete_last_memory(user) + if memory is None: return Constants.Common.inactive_user(name, language_code) - elif response is False: + elif memory is False: return Constants.Common.no_memory_found(name, language_code) else: - return Constants.Delete.success(name, language_code, response) + await Events.send_a_message_to_user( + chat_id, Constants.Delete.success(name, language_code) + ) + await Events.send_a_message_to_user(chat_id, memory) + return "" elif ResponseLogic.check_command_type(first_word, "support"): return Constants.Support.support(name, language_code)