master
kawaiiDango 2021-10-04 23:09:26 +05:30
commit f93ae32896
5 changed files with 373 additions and 0 deletions

7
.gitignore vendored 100644
View File

@ -0,0 +1,7 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
db/
config.py

11
README.md 100644
View File

@ -0,0 +1,11 @@
Saves deleted and edited messages to another (private) chat.
Saves deleted media by pickling the media metadata to the db.
Rename `config.py.example` to `config.py` and fill in the stuff.
The chat ids there, are bot api style ids.
- Logs a limited amount of messages at a time to reduce chances of getting a FloodWaitError.
You probably don't want to see that spam anyways.
- Telegram doesnt always notify the clients that a message was deleted, so it will miss some
[telethon docs](C:\Users\arn\Dropbox\stuffs\pyweeb\telethon\tg-delete-logger)
- Telegram may not have the media after it was deleted

22
config.py.example 100644
View File

@ -0,0 +1,22 @@
API_ID = 0
API_HASH = '0'
DEBUG_MODE = False
LISTEN_OUTGOING_MESSAGES = True
SAVE_EDITED_MESSAGES = False
DELETE_SENT_GIFS_FROM_SAVED = True
DELETE_SENT_STICKERS_FROM_SAVED = True
PERSIST_TIME_IN_DAYS_BOT = 1
PERSIST_TIME_IN_DAYS_USER = 1
PERSIST_TIME_IN_DAYS_CHANNEl = 1
PERSIST_TIME_IN_DAYS_GROUP = 1
RATE_LIMIT_NUM_MESSAGES = 5
LOG_CHAT_ID = -10000
IGNORED_IDS = {
-10000
}

2
requirements.txt 100644
View File

@ -0,0 +1,2 @@
telethon
cryptg

331
tg_delete_logger.py 100644
View File

@ -0,0 +1,331 @@
import logging
import pickle
import sqlite3
import asyncio
from datetime import datetime, timedelta
from typing import List, Union
import os
from asyncio.locks import Event
from telethon import events
import config
from telethon.events import NewMessage, MessageDeleted, MessageEdited
from telethon import TelegramClient
from telethon.hints import Entity
from telethon.tl.functions.messages import SaveGifRequest, SaveRecentStickerRequest
from telethon.tl.types import (
Message,
PeerUser,
PeerChat,
PeerChannel,
DocumentAttributeSticker,
DocumentAttributeVideo,
MessageMediaDice,
MessageMediaWebPage,
MessageMediaGame,
Document,
InputDocument,
DocumentAttributeAnimated
)
TYPE_USER = 1
TYPE_CHANNEL = 2
TYPE_GROUP = 3
TYPE_BOT = 4
TYPE_UNKNOWN = 0
def init_db():
if not os.path.exists('db'):
os.mkdir('db')
connection = sqlite3.connect("db/messages.db")
cursor = connection.cursor()
cursor.execute("""CREATE TABLE IF NOT EXISTS messages
(id INTEGER, from_id INTEGER, chat_id INTEGER,
type INTEGER, msg_text TEXT, media BLOB, created_time TIMESTAMP, edited_time TIMESTAMP,
PRIMARY KEY (chat_id, id, edited_time))""")
cursor.execute(
"CREATE INDEX IF NOT EXISTS messages_created_index ON messages (created_time DESC)")
connection.commit()
return cursor, connection
async def get_chat_type(event: Event) -> int:
chat_type = TYPE_UNKNOWN
if event.is_group: # chats and megagroups
chat_type = TYPE_GROUP
elif event.is_channel: # megagroups and channels
chat_type = TYPE_CHANNEL
elif event.is_private:
if (await event.get_sender()).bot:
chat_type = TYPE_BOT
else:
chat_type = TYPE_USER
return chat_type
async def new_message_handler(event: Union[NewMessage.Event, MessageEdited.Event]):
from_id = 0
chat_id = event.chat_id
if isinstance(event.message.peer_id, PeerUser):
from_id = my_id if event.message.out else event.message.peer_id.user_id
chat_id = event.message.peer_id.user_id
elif isinstance(event.message.peer_id, PeerChannel):
if isinstance(event.message.from_id, PeerUser):
from_id = event.message.from_id.user_id
elif isinstance(event.message.peer_id, PeerChat):
if isinstance(event.message.from_id, PeerUser):
from_id = event.message.from_id.user_id
if from_id in config.IGNORED_IDS or chat_id in config.IGNORED_IDS:
return
edited_time = 0
async with asyncio.Lock():
if isinstance(event, MessageEdited.Event):
edited_time = datetime.now() # event.message.edit_date
await edited_deleted_handler(event)
sqlite_cursor.execute(
"INSERT INTO messages (id, from_id, chat_id, edited_time, type, msg_text, media, created_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(
event.message.id,
from_id,
chat_id,
edited_time,
await get_chat_type(event),
event.message.message,
sqlite3.Binary(pickle.dumps(event.message.media)),
datetime.now()))
sqlite_connection.commit()
def load_messages_from_event(event: Union[MessageDeleted.Event, MessageEdited.Event]) -> List[Message]:
if isinstance(event, MessageDeleted.Event):
ids = event.deleted_ids[:config.RATE_LIMIT_NUM_MESSAGES]
elif isinstance(event, MessageEdited.Event):
ids = [event.message.id]
sql_message_ids = ",".join(str(deleted_id) for deleted_id in ids)
if event.chat_id:
where_clause = f"WHERE chat_id = {event.chat_id} and id IN ({sql_message_ids})"
else:
where_clause = f"WHERE chat_id not like \"-100%\" and id IN ({sql_message_ids})"
query = f"""SELECT * FROM (SELECT id, from_id, chat_id, msg_text, media,
created_time FROM messages {where_clause} ORDER BY edited_time DESC)
GROUP BY chat_id, id ORDER BY created_time ASC"""
db_results = sqlite_cursor.execute(query).fetchall()
messages = []
for db_result in db_results:
messages.append({
"id": db_result[0],
"from_id": db_result[1],
"chat_id": db_result[2],
"msg_text": db_result[3],
"media": pickle.loads(db_result[4])
})
return messages
async def create_mention(user: Entity):
if user.first_name or user.last_name:
mention = \
(user.first_name + " " if user.first_name else "") + \
(user.last_name if user.last_name else "")
elif user.username:
mention = user.username
elif user.phone:
mention = user.phone
else:
mention = user.id
return mention
async def edited_deleted_handler(event: Union[MessageDeleted.Event, MessageEdited.Event]):
if isinstance(event, MessageEdited.Event) and not config.SAVE_EDITED_MESSAGES:
return
messages = load_messages_from_event(event)
log_deleted_usernames = []
for message in messages:
if message['from_id'] in config.IGNORED_IDS or message['chat_id'] in config.IGNORED_IDS:
return
try:
user = await client.get_entity(message['from_id'])
mention_user = await create_mention(user)
except:
user = None
mention_user = "Unknown"
log_deleted_usernames.append(mention_user)
try:
chat = await client.get_entity(message['chat_id'])
try:
mention_chat = chat.title
is_pm = False
except AttributeError:
mention_chat = await create_mention(chat)
is_pm = True
except Exception as e:
mention_chat = "Unknown chat"
chat_id = str(message['chat_id']).replace("-100", "")
if isinstance(event, MessageDeleted.Event):
if user:
text = f"**Deleted message from: **[{mention_user}](tg://user?id={user.id})\n"
else:
text = f"**Deleted message from: **{mention_user}\n"
text += f"in [{mention_chat}](t.me/c/{chat_id}/{message['id']})"
if is_pm:
text += " #pm"
text += '\n'
if message['msg_text']:
text += "**Message:** \n" + message['msg_text']
elif isinstance(event, MessageEdited.Event):
if user:
text = f"**✏Edited message from: **[{mention_user}](tg://user?id={user.id})\n"
else:
text = f"**✏Edited message from: **{mention_user}\n"
text += f"in [{mention_chat}](t.me/c/{chat_id}/{message['id']})\n"
if message['msg_text']:
text += f"**Original message:**\n{message['msg_text']}\n\n"
if event.message.text:
text += f"**Edited message:**\n{event.message.text}"
is_sticker = hasattr(message['media'], "document") and \
message['media'].document.attributes and \
any(isinstance(attr, DocumentAttributeSticker)
for attr in message['media'].document.attributes)
is_gif = hasattr(message['media'], "document") and \
message['media'].document.attributes and \
any(isinstance(attr, DocumentAttributeAnimated)
for attr in message['media'].document.attributes)
is_round_video = hasattr(message['media'], "document") and \
message['media'].document.attributes and \
any((isinstance(attr, DocumentAttributeVideo) and attr.round_message == True)
for attr in message['media'].document.attributes)
is_dice = isinstance(message['media'], MessageMediaDice)
is_instant_view = isinstance(message['media'], MessageMediaWebPage)
is_game = isinstance(message['media'], MessageMediaGame)
if is_sticker or is_round_video or is_dice or is_game:
sent_msg = await client.send_message(config.LOG_CHAT_ID, file=message['media'])
await sent_msg.reply(text)
elif is_instant_view:
await client.send_message(config.LOG_CHAT_ID, text)
else:
await client.send_message(config.LOG_CHAT_ID, text, file=message['media'])
if is_gif and config.DELETE_SENT_GIFS_FROM_SAVED:
await delete_from_saved_gifs(message['media'].document)
if is_sticker and config.DELETE_SENT_STICKERS_FROM_SAVED:
await delete_from_saved_stickers(message['media'].document)
if isinstance(event, MessageDeleted.Event):
if len(event.deleted_ids) > config.RATE_LIMIT_NUM_MESSAGES and len(log_deleted_usernames):
await client.send_message(config.LOG_CHAT_ID, f"{len(event.deleted_ids)} messages deleted. Logged {config.RATE_LIMIT_NUM_MESSAGES}.")
logging.info(
f"Got {len(event.deleted_ids)} deleted messages. DB has {len(messages)}. Users: {', '.join(log_deleted_usernames)}"
)
elif isinstance(event, MessageEdited.Event):
logging.info(
f"Got 1 edited message. DB has {len(messages)}. Users: {', '.join(log_deleted_usernames)}"
)
async def delete_from_saved_gifs(gif: Document):
await client(SaveGifRequest(
id=InputDocument(
id=gif.id,
access_hash=gif.access_hash,
file_reference=gif.file_reference
),
unsave=True
))
async def delete_from_saved_stickers(sticker: Document):
await client(SaveRecentStickerRequest(
id=InputDocument(
id=sticker.id,
access_hash=sticker.access_hash,
file_reference=sticker.file_reference
),
unsave=True
))
async def delete_expired_messages():
while True:
now = datetime.now()
time_user = now - timedelta(days=config.PERSIST_TIME_IN_DAYS_USER)
time_channel = now - \
timedelta(days=config.PERSIST_TIME_IN_DAYS_CHANNEl)
time_group = now - timedelta(days=config.PERSIST_TIME_IN_DAYS_GROUP)
time_bot = now - timedelta(days=config.PERSIST_TIME_IN_DAYS_BOT)
time_unknown = now - timedelta(days=config.PERSIST_TIME_IN_DAYS_GROUP)
sqlite_cursor.execute(
"""DELETE FROM messages WHERE (type = ? and created_time < ?) OR
(type = ? and created_time < ?) OR
(type = ? and created_time < ?) OR
(type = ? and created_time < ?) OR
(type = ? and created_time < ?)""",
(TYPE_USER, time_user,
TYPE_CHANNEL, time_channel,
TYPE_GROUP, time_group,
TYPE_BOT, time_bot,
TYPE_UNKNOWN, time_unknown,
))
logging.info(
f"Deleted {sqlite_cursor.rowcount} expired messages from DB"
)
await asyncio.sleep(300)
async def init(clientp):
global client, my_id
client = clientp
if config.DEBUG_MODE:
logging.basicConfig(level="INFO")
else:
logging.basicConfig(level="WARNING")
config.IGNORED_IDS.add(config.LOG_CHAT_ID)
my_id = (await client.get_me()).id
client.add_event_handler(new_message_handler, events.NewMessage(
incoming=True, outgoing=config.LISTEN_OUTGOING_MESSAGES))
client.add_event_handler(new_message_handler, events.MessageEdited())
client.add_event_handler(edited_deleted_handler, events.MessageDeleted())
await delete_expired_messages()
if __name__ == "__main__":
sqlite_cursor, sqlite_connection = init_db()
with TelegramClient('db/user', config.API_ID, config.API_HASH) as client:
client.loop.run_until_complete(init(client))