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` на сообщение нужного пользователя.
- Список ID хранится в `data/blocked_users.json` и сохраняется между перезапусками.
- Перед удалением бот всегда пытается поставить реакцию `💩` на сообщение.
- При бане дополнительно чистятся последние сообщения этого пользователя в текущем чате.
## Установка
1. Установите Python 3.10+.
@@ -35,9 +37,11 @@
## Как пользоваться
1. В группе ответьте на сообщение пользователя командой `/ВБАННАХУЙ!`.
2. Юзербот покажет короткую анимацию и добавит ID пользователя в автоудаление.
3. Новые сообщения этого пользователя в группах будут удаляться только у вас.
4. Чтобы отключить автоудаление, ответьте на его сообщение командой `/РАЗБАН`.
3. После бана бот также попробует удалить недавние сообщения этого пользователя в текущем чате.
4. Новые сообщения этого пользователя в группах будут удаляться только у вас.
5. Чтобы отключить автоудаление, ответьте на его сообщение командой `/РАЗБАН`.
## Ограничения
- Удаление "только у вас" работает в формате `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 telethon import TelegramClient, events
from telethon.errors import RPCError
from telethon.tl import functions, types
from telethon.tl.custom.message import Message
BAN_COMMAND = "/вбаннахуй"
UNBAN_COMMAND = "/разбан"
DATA_PATH = Path("data/blocked_users.json")
RECENT_CLEANUP_LIMIT = 50
POOP_REACTION_EMOJI = "💩"
ANIMATION_FRAMES = [
"Запускаю анти-режим.",
@@ -127,6 +131,124 @@ async def animate_ban(message: Message, target_user_id: int, is_new: bool) -> No
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]:
load_dotenv()
api_id_raw = os.getenv("API_ID", "").strip()
@@ -155,6 +277,7 @@ async def main() -> None:
api_id, api_hash, session_name = load_settings()
blocklist = BlocklistStore(DATA_PATH)
await blocklist.load()
notified_delete_failures: set[tuple[int, int]] = set()
client = TelegramClient(session_name, api_id, api_hash)
@@ -181,6 +304,21 @@ async def main() -> None:
status_message = await event.reply("Подготовка...")
added = await blocklist.add(target_user_id)
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
removed = await blocklist.remove(target_user_id)
@@ -196,7 +334,10 @@ async def main() -> None:
@client.on(events.NewMessage(incoming=True))
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
sender_id = event.sender_id
@@ -206,15 +347,29 @@ async def main() -> None:
if not await blocklist.contains(int(sender_id)):
return
try:
await client.delete_messages(event.chat_id, [event.id], revoke=False)
except Exception:
logging.exception(
"Не удалось удалить сообщение %s от пользователя %s в чате %s.",
deleted, error = await delete_message_with_retries(client, event.message)
if not deleted:
logging.warning(
"Пропуск удаления: сообщение %s от пользователя %s в чате %s.",
event.id,
sender_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()
me = await client.get_me()