1
0
Fork 0
mirror of https://github.com/EDeev/chatping_abobot.git synced 2026-10-07 20:49:45 +03:00
chatping_abobot/code/handlers/events.py
Egor Deev a2a0adcf8e AboBot 4.0: PostgreSQL, обработчики по роутерам, HTML-разметка, новые команды
База — PostgreSQL (asyncpg) вместо четырёх файлов SQLite с таблицей на каждый чат:
chats, users, members, chat_stats и member_stats с полем «период» — месяцы хранятся, а не
обнуляются; счётчики растут одним запросом без блокировки бота. scripts/migrate_sqlite.py
переносит старые базы со сверкой сумм (проверено на копии рабочих баз: 20 694 пользователя,
45 чатов, 3 142 038 сообщений — совпадает).

Учёт бота в чатах: статус и права бота (событие my_chat_member и сверка с Telegram при
запуске и раз в 6 часов), история изменений в bot_status_history; из чатов, где бота
исключили, данные не удаляются — меняется только статус.

Большие чаты (больше 100 участников): /all — только для админов, раз в 5 минут и только
писавшие за 30 дней; упоминание одного человека по имени — не чаще раза в минуту. В любом
чате /all делится на сообщения до 4096 символов (раньше в чате на 14 тысяч падал).

Новое: /settings (упоминания, имена в голосовых, ивенты, удаление служебных сообщений —
меняют админы), /top, /month (итоги месяца по команде). /stop_bot раньше ничего не
выключал — флаг не проверялся; теперь работает. Участник, вышедший из чата, больше не
упоминается, но его статистика сохраняется. Ошибки — в технический чат DEBUG_CHAT_ID.

Код: handlers.py разбит на роутеры, Markdown заменён на HTML (экранирование через html.escape),
тесты на PostgreSQL (18), CI на Python 3.10 и 3.12.
2026-10-06 11:16:00 +00:00

140 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Текстовые сообщения: учёт статистики, ивенты, упоминания по имени, исправление раскладки; медиа и стикеры"""
import asyncio
import logging
import os
import random
import re
import tempfile
import requests
from aiogram import Bot, F, Router
from aiogram.enums import ContentType
from aiogram.types import FSInputFile, Message
import base
import db
from nlp import h, has_url, inflect_past, lang_form, mention, revers, translator, wrong_layout
from .common import count, in_group
from .mentions import mention_in_text
router = Router()
@router.message(F.content_type.in_([
ContentType.LOCATION,
ContentType.CONTACT,
ContentType.VIDEO,
ContentType.PHOTO,
ContentType.AUDIO,
ContentType.DOCUMENT
]))
async def media(message: Message):
await count(message, 5, 1)
@router.message(F.content_type == ContentType.STICKER)
async def stick(message: Message):
await count(message, 6, 1)
async def run_events(message: Message, bot: Bot, low_mes, words, unsigned):
"""Текстовые ивенты; True — если сообщение обработано ивентом"""
if len(words) == 5 and "число от" in low_mes:
try:
num1, num2 = int(words[2]), int(words[4])
await message.answer(f"Число <b>[ {random.randint(min(num1, num2), max(num1, num2))} ]</b>")
except ValueError:
pass
return True
if unsigned[0] == 'переведи':
if "переведи - " in low_mes:
await message.answer(h(lang_form(words[2:])))
elif len(unsigned) > 1 and f"переведи ({unsigned[1]}) - " in low_mes and len(unsigned[1]) == 1:
await message.answer(h(lang_form(words[3:], unsigned[1])))
return True
if unsigned[0] == 'переверни':
if "переверни - " in low_mes:
await message.answer(h(revers(message.text[12:], True)))
elif "переверни полностью - " in low_mes:
await message.answer(h(revers(message.text[22:], False)))
return True
if unsigned[0] == 'озвучь' and "озвучь - " in low_mes:
from gtts import gTTS
text_to_voice = message.text[9:]
# синтез — в отдельном потоке и во временный файл
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "voice.mp3")
try:
await asyncio.to_thread(gTTS(text_to_voice, lang='ru').save, path)
except Exception:
logging.exception("gTTS")
await message.reply("Озвучить не получилось, попробуйте позже")
return True
await message.answer_voice(voice=FSInputFile(path), caption=f"<b>{h(text_to_voice)}</b>")
return True
if words[0].lower() == "подраться" and words[1].lower() == "с":
text = h(" ".join(words[2:]).title())
name = h(message.from_user.first_name)
if random.randint(0, 1) == 0:
caption = f"{name}, ты был унижен {text}, с помощью {random.choice(base.VAR_LOSE)}"
else:
caption = f"{name}, ты победил в драке с {text}, {random.choice(base.VAR_WIN)}"
await message.answer_photo(photo=FSInputFile(f"../data/fight/({random.randint(1, 8)}).jpg"), caption=caption)
for folder, verbs, pictures in (("tmok", base.TMOK_LIST, 4), ("kill", base.KILL_LIST, 6)):
if any(word in low_mes for word in verbs):
await message.answer_photo(
photo=FSInputFile(f"../data/{folder}/({random.randint(1, pictures)}).jpg"),
caption=f"{h(message.from_user.first_name)} {h(inflect_past(words[0]))} {h(' '.join(words[1:]))}")
if (len(unsigned) >= 3 and unsigned[0] in base.QUAT_LIST[0] and unsigned[1] in base.QUAT_LIST[1]
and unsigned[2] in base.QUAT_LIST[2]):
try:
resp = await asyncio.to_thread(requests.get, 'http://fucking-great-advice.ru/api/random', timeout=5)
await message.reply(h(resp.json()["text"]))
except Exception:
logging.warning("Сервис советов недоступен")
return True
return False
@router.message(F.content_type == ContentType.TEXT)
async def text_message(message: Message, bot: Bot):
text = message.text
low_mes = text.lower()
words = text.split()
unsigned = re.sub(r'[^\w\s]', '', low_mes).split()
# статистика: сообщение, команда, ответ, ссылка — одним запросом
if in_group(message):
events = [1]
if len(text) > 1 and text[0] == '/':
events.append(3)
if message.reply_to_message:
events.append(2)
if has_url(text):
events.append(4)
await count(message, *events)
# ивенты (в группах выключаются /stop_bot; раньше флаг нигде не проверялся)
if len(words) >= 2 and unsigned and (not in_group(message) or await db.get_setting(message.chat.id,
"events_enabled")):
if await run_events(message, bot, low_mes, words, unsigned):
return
if not in_group(message):
return
reply = await mention_in_text(bot, message, text)
if reply:
await message.reply(reply)
return
if not has_url(text) and wrong_layout(text):
await message.reply(f"{mention(message.from_user.id, message.from_user.first_name)} <b>></b> "
f"{h(translator(words))}")