Compare commits

...

2 Commits

Author SHA1 Message Date
sadtweenk
eb888c04d6 feat: react with poop before auto-delete 2026-04-20 16:53:38 +03:00
sadtweenk
bcd6f3e734 fix: improve auto-delete reliability and recent cleanup 2026-04-20 16:44:47 +03:00
2 changed files with 168 additions and 9 deletions

View File

@@ -10,6 +10,8 @@
- Команда разбана: `/РАЗБАН` (тоже можно с `!`). - Команда разбана: `/РАЗБАН` (тоже можно с `!`).
- Обе команды работают только как `reply` на сообщение нужного пользователя. - Обе команды работают только как `reply` на сообщение нужного пользователя.
- Список ID хранится в `data/blocked_users.json` и сохраняется между перезапусками. - Список ID хранится в `data/blocked_users.json` и сохраняется между перезапусками.
- Перед удалением бот всегда пытается поставить реакцию `💩` на сообщение.
- При бане дополнительно чистятся последние сообщения этого пользователя в текущем чате.
## Установка ## Установка
1. Установите Python 3.10+. 1. Установите Python 3.10+.
@@ -35,9 +37,11 @@
## Как пользоваться ## Как пользоваться
1. В группе ответьте на сообщение пользователя командой `/ВБАННАХУЙ!`. 1. В группе ответьте на сообщение пользователя командой `/ВБАННАХУЙ!`.
2. Юзербот покажет короткую анимацию и добавит ID пользователя в автоудаление. 2. Юзербот покажет короткую анимацию и добавит ID пользователя в автоудаление.
3. Новые сообщения этого пользователя в группах будут удаляться только у вас. 3. После бана бот также попробует удалить недавние сообщения этого пользователя в текущем чате.
4. Чтобы отключить автоудаление, ответьте на его сообщение командой `/РАЗБАН`. 4. Новые сообщения этого пользователя в группах будут удаляться только у вас.
5. Чтобы отключить автоудаление, ответьте на его сообщение командой `/РАЗБАН`.
## Ограничения ## Ограничения
- Удаление "только у вас" работает в формате `best effort` и зависит от ограничений Telegram API. - Удаление "только у вас" работает в формате `best effort` и зависит от ограничений Telegram API.
- Если Telegram ограничивает удаление в конкретном типе чата, бот залогирует ошибку и продолжит работу. - В супергруппах Telegram может не дать локально удалять чужие сообщения без админ-прав.
- Если удаление не удалось из-за прав/ограничений, бот отправит диагностическое уведомление в `Избранное`.

167
bot.py
View File

@@ -7,11 +7,15 @@ from typing import Optional
from dotenv import load_dotenv from dotenv import load_dotenv
from telethon import TelegramClient, events from telethon import TelegramClient, events
from telethon.errors import RPCError
from telethon.tl import functions, types
from telethon.tl.custom.message import Message from telethon.tl.custom.message import Message
BAN_COMMAND = "/вбаннахуй" BAN_COMMAND = "/вбаннахуй"
UNBAN_COMMAND = "/разбан" UNBAN_COMMAND = "/разбан"
DATA_PATH = Path("data/blocked_users.json") DATA_PATH = Path("data/blocked_users.json")
RECENT_CLEANUP_LIMIT = 50
POOP_REACTION_EMOJI = "💩"
ANIMATION_FRAMES = [ ANIMATION_FRAMES = [
"Запускаю анти-режим.", "Запускаю анти-режим.",
@@ -127,6 +131,124 @@ async def animate_ban(message: Message, target_user_id: int, is_new: bool) -> No
logging.exception("Не удалось отправить финальный статус анимации.") logging.exception("Не удалось отправить финальный статус анимации.")
async def delete_message_best_effort(
client: TelegramClient, message: Message
) -> tuple[bool, Optional[str]]:
last_error: Optional[str] = None
try:
await message.delete(revoke=False)
return True, None
except Exception as exc:
last_error = f"{type(exc).__name__}: {exc}"
logging.debug(
"Удаление через message.delete(revoke=False) не сработало: %s", exc
)
try:
target = message.input_chat if message.input_chat else message.chat_id
await client.delete_messages(target, [message.id], revoke=False)
return True, None
except Exception as exc:
last_error = f"{type(exc).__name__}: {exc}"
logging.debug(
"Удаление через client.delete_messages(..., revoke=False) не сработало: %s",
exc,
)
try:
await message.delete()
return True, None
except RPCError as exc:
last_error = f"{type(exc).__name__}: {exc}"
logging.warning(
"Не удалось удалить сообщение %s в чате %s: %s",
message.id,
message.chat_id,
exc,
)
except Exception:
last_error = "UnexpectedError"
logging.exception(
"Неожиданная ошибка удаления сообщения %s в чате %s.",
message.id,
message.chat_id,
)
return False, last_error
async def react_with_poop_best_effort(client: TelegramClient, message: Message) -> None:
if message.id is None:
return
try:
peer = message.input_chat if message.input_chat else message.chat_id
await client(
functions.messages.SendReactionRequest(
peer=peer,
msg_id=message.id,
big=False,
add_to_recent=False,
reaction=[types.ReactionEmoji(emoticon=POOP_REACTION_EMOJI)],
)
)
except Exception as exc:
logging.debug(
"Не удалось поставить реакцию %s на сообщение %s: %s",
POOP_REACTION_EMOJI,
message.id,
exc,
)
async def delete_message_with_retries(
client: TelegramClient,
message: Message,
retries: tuple[float, ...] = (0.0, 0.7, 2.0),
) -> tuple[bool, Optional[str]]:
await react_with_poop_best_effort(client, message)
last_error: Optional[str] = None
for delay in retries:
if delay > 0:
await asyncio.sleep(delay)
deleted, error = await delete_message_best_effort(client, message)
if deleted:
return True, None
last_error = error
return False, last_error
async def cleanup_recent_messages_from_user(
client: TelegramClient, chat_id: int, user_id: int, limit: int = RECENT_CLEANUP_LIMIT
) -> int:
deleted_count = 0
async for message in client.iter_messages(chat_id, from_user=user_id, limit=limit):
if message.id is None:
continue
deleted, _ = await delete_message_with_retries(client, message)
if deleted:
deleted_count += 1
return deleted_count
def is_permission_like_delete_error(error_text: Optional[str]) -> bool:
if not error_text:
return False
normalized = error_text.upper()
markers = (
"CHATADMINREQUIRED",
"CHAT_ADMIN_REQUIRED",
"MESSAGEDELETEFORBIDDEN",
"MESSAGE_DELETE_FORBIDDEN",
"RIGHT_FORBIDDEN",
)
return any(marker in normalized for marker in markers)
def load_settings() -> tuple[int, str, str]: def load_settings() -> tuple[int, str, str]:
load_dotenv() load_dotenv()
api_id_raw = os.getenv("API_ID", "").strip() api_id_raw = os.getenv("API_ID", "").strip()
@@ -155,6 +277,7 @@ async def main() -> None:
api_id, api_hash, session_name = load_settings() api_id, api_hash, session_name = load_settings()
blocklist = BlocklistStore(DATA_PATH) blocklist = BlocklistStore(DATA_PATH)
await blocklist.load() await blocklist.load()
notified_delete_failures: set[tuple[int, int]] = set()
client = TelegramClient(session_name, api_id, api_hash) client = TelegramClient(session_name, api_id, api_hash)
@@ -181,6 +304,21 @@ async def main() -> None:
status_message = await event.reply("Подготовка...") status_message = await event.reply("Подготовка...")
added = await blocklist.add(target_user_id) added = await blocklist.add(target_user_id)
await animate_ban(status_message, target_user_id, added) await animate_ban(status_message, target_user_id, added)
chat = await event.get_chat()
if isinstance(chat, types.Channel) and getattr(chat, "megagroup", False):
await event.reply(
"Важно: это супергруппа. В Telegram API локальное удаление чужих "
"сообщений может быть недоступно без прав администратора."
)
cleaned = await cleanup_recent_messages_from_user(
client,
event.chat_id,
target_user_id,
)
if cleaned > 0:
await event.reply(
f"Дополнительно очищено последних сообщений в этом чате: {cleaned}."
)
return return
removed = await blocklist.remove(target_user_id) removed = await blocklist.remove(target_user_id)
@@ -196,7 +334,10 @@ async def main() -> None:
@client.on(events.NewMessage(incoming=True)) @client.on(events.NewMessage(incoming=True))
async def auto_delete_target_messages(event: events.NewMessage.Event) -> None: async def auto_delete_target_messages(event: events.NewMessage.Event) -> None:
if not event.is_group: if event.is_private:
return
if event.is_channel and not event.is_group:
return return
sender_id = event.sender_id sender_id = event.sender_id
@@ -206,15 +347,29 @@ async def main() -> None:
if not await blocklist.contains(int(sender_id)): if not await blocklist.contains(int(sender_id)):
return return
try: deleted, error = await delete_message_with_retries(client, event.message)
await client.delete_messages(event.chat_id, [event.id], revoke=False) if not deleted:
except Exception: logging.warning(
logging.exception( "Пропуск удаления: сообщение %s от пользователя %s в чате %s.",
"Не удалось удалить сообщение %s от пользователя %s в чате %s.",
event.id, event.id,
sender_id, sender_id,
event.chat_id, event.chat_id,
) )
if is_permission_like_delete_error(error):
key = (int(event.chat_id), int(sender_id))
if key not in notified_delete_failures:
notified_delete_failures.add(key)
try:
await client.send_message(
"me",
"Не удалось удалить сообщение по автофильтру.\n"
f"Чат: `{event.chat_id}`\n"
f"Пользователь: `{sender_id}`\n"
f"Причина: `{error}`\n"
"Обычно это ограничение прав/типа чата в Telegram.",
)
except Exception:
logging.exception("Не удалось отправить уведомление в Избранное.")
await client.start() await client.start()
me = await client.get_me() me = await client.get_me()