Compare commits
2 Commits
d539eab31d
...
eb888c04d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb888c04d6 | ||
|
|
bcd6f3e734 |
10
README.md
10
README.md
@@ -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
167
bot.py
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user