From 8d90cf456375ff404e91a1fa5680fba5c5558277 Mon Sep 17 00:00:00 2001 From: Lennart Buhl Date: Wed, 2 Nov 2022 16:39:51 +0100 Subject: [PATCH 1/2] Edit Notification Feature along with some refactoring and newer versions for docker image and libraries --- .env.example | 10 ++++---- Dockerfile | 4 +-- docker-compose.yml | 1 + requirements.txt | 6 ++--- src/helpers.py | 64 ++++++++++++++++++++++++++++++++++++++++------ src/monitor.py | 6 +++-- 6 files changed, 71 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index d30cdff..6e0bbd1 100644 --- a/.env.example +++ b/.env.example @@ -4,15 +4,15 @@ TELEGRAM_API_ID = 12345 TELEGRAM_API_HASH = 0123456789abcdef0123456789abcdef # Logging level, available values: https://docs.python.org/3/library/logging.html#levels -LOGGING_LEVEL = CRITICAL +LOGGING_LEVEL = WARNING # Correct values: -# '' – You will not be notified about deleted ongoing messages -# '1' – You will be notified about deleted ongoing messages +# '' – You will not be notified about deleted outgoing messages +# '1' – You will be notified about deleted outgoing messages # # Enabled option is useful, when your companion deletes the bunch of his and your messages -NOTIFY_ONGOING_MESSAGES='1' +NOTIFY_OUTGOING_MESSAGES='1' # How many days messages will be stored in the SQLite database # Warning: Database is not constrained by memory it'll occupy, you need to monitor your free disk space manually -MESSAGES_TTL_DAYS = 14 \ No newline at end of file +MESSAGES_TTL_DAYS = 365 diff --git a/Dockerfile b/Dockerfile index cb08dce..1a1ddfe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3-alpine +FROM python:3.10-slim-bullseye WORKDIR /usr/src/app @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD [ "python", "./src/monitor.py" ] \ No newline at end of file +CMD [ "python", "./src/monitor.py" ] diff --git a/docker-compose.yml b/docker-compose.yml index 389b9ff..dc2574b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,3 +8,4 @@ services: - ./.env:/usr/src/app/.env:ro stdin_open: true tty: true + restart: unless-stopped diff --git a/requirements.txt b/requirements.txt index 73cccd5..b5392a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -Telethon==1.14.0 -python-dotenv==v0.13.0 -pylint==2.5.3 \ No newline at end of file +Telethon +python-dotenv +pylint diff --git a/src/helpers.py b/src/helpers.py index 1ba6db7..4f27b25 100644 --- a/src/helpers.py +++ b/src/helpers.py @@ -8,12 +8,12 @@ from typing import List from dotenv import load_dotenv -from telethon.events import NewMessage, MessageDeleted +from telethon.events import NewMessage, MessageDeleted, MessageEdited from telethon import TelegramClient from telethon.hints import Entity from telethon.tl.types import Message -CLEAN_OLD_MESSAGES_EVERY_SECONDS = 60 # 1 minute +CLEAN_OLD_MESSAGES_EVERY_SECONDS = 3600 # 1 hour def load_env(dot_env_folder): @@ -41,21 +41,33 @@ def initialize_messages_db(): async def on_new_message(event: NewMessage.Event): + logging.info(f"Storing message from {event.message.sender_id} with {len(event.message.message)} bytes of text in DB") sqlite_cursor.execute( "INSERT INTO messages (message_id, message_from_id, message, media, created) VALUES (?, ?, ?, ?, ?)", ( event.message.id, - event.message.from_id, + event.message.sender_id, event.message.message, sqlite3.Binary(pickle.dumps(event.message.media)), + # event.message.date would also be an option here 🤔 str(datetime.now()))) sqlite_connection.commit() +async def update_message_content(event: NewMessage.Event): + logging.info(f"Updating message with id {event.message.id} in DB") + sqlite_cursor.execute( + "UPDATE messages SET message=?, media=?, created=? WHERE message_id=?", ( + event.message.message, + sqlite3.Binary(pickle.dumps(event.message.media)), + str(datetime.now()), + event.message.id + )) + sqlite_connection.commit() -def load_messages_from_event(event: MessageDeleted.Event) -> List[Message]: - sql_message_ids = ",".join(str(deleted_id) for deleted_id in event.deleted_ids) +def load_messages_by_message_ids(ids: List[int]) -> List[Message]: + sql_message_ids = ",".join(str(id) for id in ids) db_results = sqlite_cursor.execute( f"SELECT message_id, message_from_id, message, media FROM messages WHERE message_id IN ({sql_message_ids})" @@ -90,7 +102,7 @@ async def get_mention_username(user: Entity): def get_on_message_deleted(client: TelegramClient): async def on_message_deleted(event: MessageDeleted.Event): - messages = load_messages_from_event(event) + messages = load_messages_by_message_ids(event.deleted_ids) log_deleted_usernames = [] @@ -98,8 +110,8 @@ async def on_message_deleted(event: MessageDeleted.Event): user = await client.get_entity(message['message_from_id']) mention_username = await get_mention_username(user) - log_deleted_usernames.append(mention_username + " (" + str(user.id) + ")") - text = "🔥🔥🔥🤫🤐🤭🙊🔥🔥🔥\n**Deleted message from: **[{username}](tg://user?id={id})\n".format( + log_deleted_usernames.append(f"{mention_username} ({str(user.id)})") + text = "🔥🤫🤐🤭🙊🔥 **Deleted message** User: [{username}](tg://user?id={id})\n".format( username=mention_username, id=user.id) if message['message']: @@ -121,6 +133,42 @@ async def on_message_deleted(event: MessageDeleted.Event): return on_message_deleted +def get_on_message_edited(client: TelegramClient): + + async def on_message_edited(event: MessageEdited.Event): + messages = load_messages_by_message_ids([event.message.id]) + if not len(messages): + logging.warning(f"Message id {event.message.id} not found in local SQlite db") + return + + # we only gave one message id, hence we can only get one result + edited_msg = messages[0] + + log_edited_usernames = [] + + user = await client.get_entity(edited_msg['message_from_id']) + mention_username = await get_mention_username(user) + + text = f"🔄🔄🔄👀👀👀 **Edited message** User: [{mention_username}](tg://user?id={user.id})\n" + + if edited_msg['message']: + text += f"**Old message:** {edited_msg['message']}\n" + if event.message.message: + text += f"**New message**: {event.message.message}\n" + if edited_msg['message'] and event.message.message: + await update_message_content(event) + + await client.send_message( + "me", + text, + file=edited_msg['media'] + ) + + logging.info(f"Handling edited message from user: {mention_username} ({str(user.id)})") + + return on_message_edited + + async def cycled_clean_old_messages(): messages_ttl_days = int(os.getenv('MESSAGES_TTL_DAYS', 14)) diff --git a/src/monitor.py b/src/monitor.py index f19652c..92174e0 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -4,7 +4,7 @@ import sys from telethon import TelegramClient, events -from helpers import load_env, on_new_message, get_on_message_deleted, cycled_clean_old_messages +from helpers import load_env, on_new_message, get_on_message_deleted, get_on_message_edited, cycled_clean_old_messages BASE_DIR = (pathlib.Path(__file__).parent / '..').absolute() @@ -32,16 +32,18 @@ async def main(): logging.critical('Please, execute `auth` command before starting the daemon (see `README.md` file)') exit(1) - if bool(os.getenv('NOTIFY_ONGOING_MESSAGES', '1')): + if bool(os.getenv('NOTIFY_OUTGOING_MESSAGES', '1')): new_message_event = events.NewMessage() else: new_message_event = events.NewMessage(incoming=True, outgoing=False) client.add_event_handler(on_new_message, new_message_event) client.add_event_handler(get_on_message_deleted(client), events.MessageDeleted()) + client.add_event_handler(get_on_message_edited(client), events.MessageEdited()) await cycled_clean_old_messages() with TelegramClient('db/user', os.getenv("TELEGRAM_API_ID"), os.getenv("TELEGRAM_API_HASH")) as client: + logging.info('starting up...') client.loop.run_until_complete(main()) From f80fa2a6105137277dbd005ebbbf8df5952973be Mon Sep 17 00:00:00 2001 From: Lennart Buhl Date: Tue, 10 Jan 2023 02:34:27 +0100 Subject: [PATCH 2/2] Don't trigger edit notifications on edits from app user or from reactions --- .gitignore | 4 +++- src/helpers.py | 23 +++++++++++++++++++---- src/monitor.py | 5 ++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index e0407ed..5a86a88 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,6 @@ venv.bak/ # mypy .mypy_cache/ -db/ \ No newline at end of file +.idea + +db/ diff --git a/src/helpers.py b/src/helpers.py index 4f27b25..fac209c 100644 --- a/src/helpers.py +++ b/src/helpers.py @@ -97,7 +97,7 @@ async def get_mention_username(user: Entity): else: mention_username = user.id - return mention_username + return mention_username.strip() def get_on_message_deleted(client: TelegramClient): @@ -133,7 +133,7 @@ async def on_message_deleted(event: MessageDeleted.Event): return on_message_deleted -def get_on_message_edited(client: TelegramClient): +def get_on_message_edited(client: TelegramClient, me_id): async def on_message_edited(event: MessageEdited.Event): messages = load_messages_by_message_ids([event.message.id]) @@ -144,8 +144,6 @@ async def on_message_edited(event: MessageEdited.Event): # we only gave one message id, hence we can only get one result edited_msg = messages[0] - log_edited_usernames = [] - user = await client.get_entity(edited_msg['message_from_id']) mention_username = await get_mention_username(user) @@ -158,6 +156,23 @@ async def on_message_edited(event: MessageEdited.Event): if edited_msg['message'] and event.message.message: await update_message_content(event) + # We put the following two ifs here because we _do_ want the update in the DB to happen + # and only intercept right before send_message is triggered, should either criterion apply + + if me_id == event.message.sender_id: + """ Changes that were done by the user ID running this application do not require a notification. """ + logging.info(f"Skipping edit notification because edit is from application user themselves") + return + + if edited_msg['message'] == event.message.message: + """ Sometimes we get edit events even though the content has not changed at all. + This can happen if a reaction on a message changed. + But since this is not a real edit, we don't want to notify in this case. + """ + logging.info(f"Skipping edit notification from user {mention_username} ({str(user.id)}) because " + f"message text did not change") + return + await client.send_message( "me", text, diff --git a/src/monitor.py b/src/monitor.py index 92174e0..0703775 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -37,9 +37,12 @@ async def main(): else: new_message_event = events.NewMessage(incoming=True, outgoing=False) + # We could do this in the event handlers but calling it often is a waste of resources as this does not change + me = await client.get_me() + client.add_event_handler(on_new_message, new_message_event) client.add_event_handler(get_on_message_deleted(client), events.MessageDeleted()) - client.add_event_handler(get_on_message_edited(client), events.MessageEdited()) + client.add_event_handler(get_on_message_edited(client, me.id), events.MessageEdited()) await cycled_clean_old_messages()