Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit Notification Feature #8

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
MESSAGES_TTL_DAYS = 365
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,6 @@ venv.bak/
# mypy
.mypy_cache/

db/
.idea

db/
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3-alpine
FROM python:3.10-slim-bullseye

WORKDIR /usr/src/app

Expand All @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD [ "python", "./src/monitor.py" ]
CMD [ "python", "./src/monitor.py" ]
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ services:
- ./.env:/usr/src/app/.env:ro
stdin_open: true
tty: true
restart: unless-stopped
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Telethon==1.14.0
python-dotenv==v0.13.0
pylint==2.5.3
Telethon
python-dotenv
pylint
81 changes: 72 additions & 9 deletions src/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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})"
Expand Down Expand Up @@ -85,21 +97,21 @@ 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):
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 = []

for message in messages:
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']:
Expand All @@ -121,6 +133,57 @@ async def on_message_deleted(event: MessageDeleted.Event):
return on_message_deleted


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])
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]

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)

# 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,
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))

Expand Down
9 changes: 7 additions & 2 deletions src/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -32,16 +32,21 @@ 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)

# 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, me.id), 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())