From f5441bdde668b77cf7ca620a21699daf1e0ef91b Mon Sep 17 00:00:00 2001 From: Egor Deev Date: Tue, 6 Oct 2026 01:10:52 +0000 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=83=D0=BF=D0=BE=D0=BC=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F,=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D0=B1?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ответ на текст в неправильной раскладке упоминал автора ссылкой tg://user_id?id= — такой нет, упоминание не работало; теперь tg://user?id=; - имена и текст пользователей подставлялись в Markdown как есть: «_» или «*» в имени ломали отправку статистики, ивентов и упоминаний; - каждое голосовое в группе распознавалось через Google прямо в обработчике, и бот на это время не отвечал никому; распознавание, озвучка и запрос совета — в отдельном потоке, у запроса совета есть таймаут; - временные файлы голосовых и озвучки — во временной папке на запрос (раньше общий voice.ogg и файлы со случайным номером, остававшиеся при ошибке); - падения: упоминание двух имён, одно из которых — автор; сообщение из одних знаков препинания; глагол, который pymorphy3 не может поставить в прошедшее время; - script.py берёт соединения с базами из init.py, а не открывает вторые; - токен — из BOT_TOKEN. --- code/base.py | 10 +- code/handlers.py | 238 +++++++++++++++++++++++------------------------ code/init.py | 7 +- code/script.py | 55 +++++++---- 4 files changed, 165 insertions(+), 145 deletions(-) diff --git a/code/base.py b/code/base.py index a173fb0..4a33162 100644 --- a/code/base.py +++ b/code/base.py @@ -1,8 +1,10 @@ -# ДАННЫЕ БОТА -TOKEN = "XXXXXXXXXX" # @chat_abobot -DEBAG = "XXXXXXXXXX" # Технический чат +import os -TEX_GROUP = "XXXXXXXXXX" +# ДАННЫЕ БОТА (из переменных окружения) +TOKEN = os.getenv("BOT_TOKEN", "XXXXXXXXXX") # @chat_abobot +DEBAG = os.getenv("DEBUG_CHAT_ID", "") # Технический чат + +TEX_GROUP = os.getenv("TECH_GROUP_ID", "") # ОБРАЩЕНИЯ К ПОЛЬЗОВАТЕЛЯМ KILL_LIST = ["побить", "отмудохать", "избить", "уебать", "отметелить"] diff --git a/code/handlers.py b/code/handlers.py index 450e06e..df48665 100644 --- a/code/handlers.py +++ b/code/handlers.py @@ -3,17 +3,60 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKe from aiogram.filters import Command, CommandStart from aiogram.enums import ContentType -import logging, random, requests, os, re -import speech_recognition as sr +import asyncio +import logging +import os +import random +import re +import tempfile + +import requests import soundfile as sf +import speech_recognition as sr from gtts import gTTS -from init import * -from script import * import base +from init import bot, db, du, dg, dm, morph, engl_dict +from script import upd_stat, lang_form, notice, checker, translator, revers, md, plain router = Router() + +def recognize_file(oga_path): + """Голосовое (OGG/Opus) → текст через Google Web Speech. Синхронно — вызывается в отдельном потоке""" + data, samplerate = sf.read(oga_path) + wav_path = oga_path[:-4] + ".wav" + sf.write(wav_path, data, samplerate) + + r = sr.Recognizer() + with sr.AudioFile(wav_path) as source: + r.pause_threshold = 100 + audio = r.listen(source) + return r.recognize_google(audio, language='ru-RU') + + +async def recognize_voice(file_id): + """Распознавание без блокировки бота: раньше каждое голосовое в группе распознавалось прямо + в обработчике, и бот на это время не отвечал никому. Файлы — во временной папке на запрос + (раньше ../data/voices/_<случайное число>, при ошибке файлы оставались)""" + with tempfile.TemporaryDirectory() as tmp: + oga = os.path.join(tmp, "voice.oga") + file = await bot.get_file(file_id) + await bot.download_file(file.file_path, oga) + try: + return await asyncio.to_thread(recognize_file, oga) + except sr.UnknownValueError: # речь не разобрана + return None + except Exception: + logging.exception("Ошибка распознавания голосового") + return None + + +def inflect_past(word): + """Глагол в прошедшем времени; если pymorphy3 не справился — как есть""" + form = morph.parse(word.lower())[0].inflect({'past', 'sing', 'indc'}) + return form.word if form else word + # НОВЫЕ УЧАСТНИКИ ГРУППЫ @router.message(F.content_type == ContentType.NEW_CHAT_MEMBERS) async def notification(message: Message): @@ -26,7 +69,7 @@ async def notification(message: Message): await bot.send_message( message.chat.id, - f'*Привет группа {message.chat.title}!*\n\n' + f'*Привет группа {plain(message.chat.title)}!*\n\n' f'Все функции вы можете узнать по команде /help! Для того, чтобы ' f'уведомления по имени и команда /all нормально функционировали, ' f'необходимо чтобы каждый участник группы написал хотя бы одно ' @@ -147,47 +190,20 @@ async def every(message: Message): # КОМАНДА RECOGNIZE @router.message(Command("recognize")) async def recognise(message: Message): - user_id = du.get_user_id(message.from_user.id) + if not (message.reply_to_message and message.reply_to_message.voice): + await message.reply("Ответьте этой командой на голосовое сообщение!") + return - if message.reply_to_message and message.reply_to_message.voice: - num = random.randint(1000, 9999) - audio = f"../data/voices/{user_id}_{num}.oga" - - file_id = message.reply_to_message.voice.file_id - file = await bot.get_file(file_id) - file_path = file.file_path - await bot.download_file(file_path, audio) - - data, samplerate = sf.read(audio) - os.remove(audio) - audio = f"../data/voices/{user_id}_{num}.wav" - sf.write(audio, data, samplerate) - - af = sr.AudioFile(audio) - r = sr.Recognizer() - with af as source: - r.pause_threshold = 100 - source = r.listen(source) - - try: - mes = await bot.send_message( - chat_id=message.chat.id, - text="Распознавание.....", - reply_to_message_id=message.reply_to_message.message_id - ) - try: - query = r.recognize_google(source, language='ru-RU') - os.remove(audio) - await mes.edit_text(f'*{message.reply_to_message.from_user.first_name} сказал(a)* "{query}"') - except Exception: - try: - os.remove(audio) - except Exception: - pass - await mes.edit_text("Распознать сообщение не удалось!") - except Exception as e: - print(repr(e)) - pass + mes = await bot.send_message( + chat_id=message.chat.id, + text="Распознавание.....", + reply_to_message_id=message.reply_to_message.message_id + ) + query = await recognize_voice(message.reply_to_message.voice.file_id) + if query: + await mes.edit_text(f'*{plain(message.reply_to_message.from_user.first_name)} сказал(a)* "{md(query)}"') + else: + await mes.edit_text("Распознать сообщение не удалось!") # СТАТИСТИКА ГРУППЫ @router.message(Command('stat_group')) @@ -231,7 +247,7 @@ async def stat_user(message: Message): name = name.word await message.answer( - text=f'*| СТАТИСТИКА {name.upper()} |*\n\n' + text=f'*| СТАТИСТИКА {plain(name.upper())} |*\n\n' f'*- За всё время* / *За месяц -*\n' f'*>>* Сообщений в базе *[ {group[0]} / {month[0]} ]*\n\n' f'*>* Ответов *- [ {group[1]} / {month[1]} ]*\n' @@ -263,9 +279,9 @@ async def update(message: Message): db.add_edit_user(user_id) dg.update_name(user_id, group_id, name) - await message.reply(f'{name.title()}, ваше имя было успешно изменено)') + await message.reply(f'{md(name.title())}, ваше имя было успешно изменено)') else: - await message.reply(f'{message.from_user.first_name.lower().title()}, такое имя уже присутствует в чате!') + await message.reply(f'{md(message.from_user.first_name.lower().title())}, такое имя уже присутствует в чате!') else: await message.reply('Вы не правильно ввели имя! Имя должно быть ' 'из одного слова и идти сразу после команды!') @@ -282,9 +298,9 @@ async def update_return(message: Message): db.del_edit_user(user_id) dg.update_name(user_id, group_id, name) - await message.reply(f'{name.title()}, вы успешно вернулись к динамическому изменению имени)') + await message.reply(f'{md(name.title())}, вы успешно вернулись к динамическому изменению имени)') else: - await message.reply(f'{name.title()}, вы не устанавливали постоянное имя!') + await message.reply(f'{md(name.title())}, вы не устанавливали постоянное имя!') # ВКЛЮЧЕНИЕ ТЕКСТОВЫХ ИВЕНТОВ @router.message(Command('start_bot')) @@ -361,47 +377,21 @@ async def voice(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 7, message.from_user.first_name, True) - if message.voice and message.chat.id < 0: - if message.voice.duration <= 60: - user_id = du.get_user_id(message.from_user.id) - - num = random.randint(1000, 9999) - audio = f"../data/voices/{user_id}_{num}.oga" + # упоминание по имени, произнесённому в голосовом + if message.voice and message.chat.id < 0 and message.voice.duration <= 60: + query = await recognize_voice(message.voice.file_id) + if not query: + return - file_id = message.voice.file_id - file = await bot.get_file(file_id) - file_path = file.file_path - await bot.download_file(file_path, audio) + group_id = du.get_group_id(message.chat.id) + unsigned = re.sub(r'[^\w\s]', '', query.lower()).split() + first_form = [morph.parse(i)[0].normal_form for i in unsigned] + chat_names = [x[0] for x in dg.all_names(group_id)] + names = [_ for _ in first_form if _ in chat_names] - data, samplerate = sf.read(audio) - os.remove(audio) - audio = f"../data/voices/{user_id}_{num}.wav" - sf.write(audio, data, samplerate) - - af = sr.AudioFile(audio) - r = sr.Recognizer() - with af as source: - r.pause_threshold = 100 - source = r.listen(source) - - try: - query = r.recognize_google(source, language='ru-RU') - os.remove(audio) - - group_id = message.chat.id - unsigned = re.sub(r'[^\w\s]', '', query.lower()).split() - first_form = [morph.parse(i)[0].normal_form for i in unsigned] - names = [_ for _ in first_form if _ in list(map(lambda x: x[0], dg.all_names(du.get_group_id(group_id))))] - - if names: - await message.reply(notice(names, False, du.get_group_id(group_id), message.from_user.id)) - return - - except Exception: - try: - os.remove(audio) - except Exception: - pass + text = notice(names, False, group_id, message.from_user.id) if names else None + if text: + await message.reply(text) # СТИКЕРЫ @router.message(F.content_type == ContentType.STICKER) @@ -431,7 +421,8 @@ async def send_events(message: Message): if message.chat.id < 0: names = [_ for _ in first_form if _ in list(map(lambda x: x[0], dg.all_names(du.get_group_id(group_id))))] - if len(words) >= 2: + # сообщение только из знаков препинания: дальше нужны слова (раньше — IndexError на unsigned[0]) + if len(words) >= 2 and unsigned: # ПОЛЕЗНЫЕ ФУНКЦИИ if len(words) == 5 and "число от" in low_mes: try: @@ -445,29 +436,32 @@ async def send_events(message: Message): if unsigned[0] == 'переведи': if "переведи - " in low_mes: - await message.answer(lang_form([words[_] for _ in range(len(words)) if _ > 1])) - elif f"переведи ({unsigned[1]}) - " in low_mes: + await message.answer(md(lang_form([words[_] for _ in range(len(words)) if _ > 1]))) + elif len(unsigned) > 1 and f"переведи ({unsigned[1]}) - " in low_mes: if len(unsigned[1]) == 1: - await message.answer(lang_form([words[_] for _ in range(len(words)) if _ > 2], unsigned[1])) + await message.answer(md(lang_form([words[_] for _ in range(len(words)) if _ > 2], unsigned[1]))) return if unsigned[0] == 'переверни': if "переверни - " in low_mes: - await message.answer(revers(message.text[12:], True)) + await message.answer(md(revers(message.text[12:], True))) elif "переверни полностью - " in low_mes: - await message.answer(revers(message.text[22:], False)) + await message.answer(md(revers(message.text[22:], False))) return if unsigned[0] == 'озвучь' and "озвучь - " in low_mes: text_to_voice = message.text[9:] - tts = gTTS(text_to_voice, lang='ru') - tts.save('../data/voices/voice.ogg') - await bot.send_voice( - chat_id=group_id, - voice=FSInputFile('../data/voices/voice.ogg'), - caption=f"*{text_to_voice}*" - ) - os.remove('../data/voices/voice.ogg') + # синтез — в отдельном потоке и во временный файл (раньше общий voice.ogg: + # одновременные запросы перезаписывали друг друга) + 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 + await bot.send_voice(chat_id=group_id, voice=FSInputFile(path), caption=f"*{plain(text_to_voice)}*") return # РАНДОМ ИВЕНТЫ @@ -477,14 +471,14 @@ async def send_events(message: Message): if random.randint(0, 1) == 0: await message.answer_photo( photo=FSInputFile(f"../data/fight/({random.randint(1, 8)}).jpg"), - caption=f"{message.from_user.first_name}, ты был унижен {text.title()}" + caption=f"{md(message.from_user.first_name)}, ты был унижен {md(text.title())}" f", с помощью {base.VAR_LOSE[random.randint(0, 3)]}" ) else: await message.answer_photo( photo=FSInputFile(f"../data/fight/({random.randint(1, 8)}).jpg"), - caption=f"{message.from_user.first_name}, ты победил в драке " - f"с {text.title()}, {base.VAR_WIN[random.randint(0, 1)]}" + caption=f"{md(message.from_user.first_name)}, ты победил в драке " + f"с {md(text.title())}, {base.VAR_WIN[random.randint(0, 1)]}" ) # ИВЕНТ ВЗАИМОДЕЙСТВИЯ @@ -493,11 +487,10 @@ async def send_events(message: Message): lst = [words[i] for i in range(len(words)) if i != 0] text = " ".join(lst) - slv = morph.parse(words[0].lower())[0] await message.answer_photo( photo=FSInputFile(f"../data/tmok/({random.randint(1, 4)}).jpg"), - caption=f"{message.from_user.first_name} " - f"{slv.inflect({'past', 'sing', 'indc'}).word} {text}" + caption=f"{md(message.from_user.first_name)} " + f"{inflect_past(words[0])} {md(text)}" ) for word in base.KILL_LIST: @@ -505,36 +498,37 @@ async def send_events(message: Message): lst = [words[i] for i in range(len(words)) if i != 0] text = " ".join(lst) - slv = morph.parse(words[0].lower())[0] await message.answer_photo( photo=FSInputFile(f"../data/kill/({random.randint(1, 6)}).jpg"), - caption=f"{message.from_user.first_name} " - f"{slv.inflect({'past', 'sing', 'indc'}).word} {text}" + caption=f"{md(message.from_user.first_name)} " + f"{inflect_past(words[0])} {md(text)}" ) - if (unsigned[0] in base.QUAT_LIST[0] and + 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: - word = requests.get('http://fucking-great-advice.ru/api/random').json() - await message.reply(word["text"]) - except: - pass + # без таймаута и в обработчике запрос мог надолго заморозить бота + resp = await asyncio.to_thread(requests.get, 'http://fucking-great-advice.ru/api/random', timeout=5) + await message.reply(md(resp.json()["text"])) + except Exception: + logging.warning("Сервис советов недоступен") return if message.chat.id < 0: # УПОМИНАНИЯ ПО ИМЕНАМ if names: - try: - await message.reply(notice(names, False, du.get_group_id(group_id), user_id)) - except Exception: - pass + text = notice(names, False, du.get_group_id(group_id), user_id) + if text: + await message.reply(text) return # ПЕРЕВОДЧИК СЛОВ if (checker([i for i in low_mes], words, group_id, user_id, name.lower()) == len(low_mes) and not any([engl_dict.check(i) for i in unsigned if len(i) > 1])): await message.reply( - f"[{message.from_user.first_name}](tg://user_id?id={user_id}) *>* {translator(words)}" + # раньше tg://user_id?id= — такой ссылки нет, упоминание не работало + f"[{plain(message.from_user.first_name)}](tg://user?id={user_id}) *>* {md(translator(words))}" ) return \ No newline at end of file diff --git a/code/init.py b/code/init.py index 849835e..1d6698f 100644 --- a/code/init.py +++ b/code/init.py @@ -4,7 +4,11 @@ from aiogram.fsm.storage.memory import MemoryStorage from aiogram.client.bot import DefaultBotProperties -import base, enchant, pymorphy3 +import os + +import base +import enchant +import pymorphy3 bot = Bot(token=base.TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.MARKDOWN)) dp = Dispatcher(storage=MemoryStorage()) @@ -15,6 +19,7 @@ engl_dict = enchant.Dict("en_US") import sql +os.makedirs('../db', exist_ok=True) db = sql.Base('../db/base.db') du = sql.User('../db/users.db') dg = sql.Group('../db/groups.db') diff --git a/code/script.py b/code/script.py index 25c25f2..dc3578f 100644 --- a/code/script.py +++ b/code/script.py @@ -1,12 +1,28 @@ -from base import ERR, ERR_, TRU, TRU_, ALB +import re from string import digits, punctuation -import re, sql -# инициализируем соединение с БД -db = sql.Base('../db/base.db') -du = sql.User('../db/users.db') -dg = sql.Group('../db/groups.db') -dm = sql.Month('../db/month.db') +from base import ERR, ERR_, TRU, TRU_, ALB +# одни и те же соединения с базами, что и у обработчиков (раньше здесь открывались вторые) +from init import db, du, dg, dm + +MD_SPECIAL = "_*`[" + + +def md(text): + """Экранирование для Markdown вне разметки: «_» или «*» в имени или тексте иначе ломают отправку""" + return "".join("\\" + ch if ch in MD_SPECIAL else ch for ch in str(text)) + + +def plain(text): + """Текст внутри *жирного* или [ссылки]: экранирование там не работает, спецсимволы убираются""" + return "".join(ch for ch in str(text) if ch not in MD_SPECIAL + "]") + + +def join_names(items): + """«а», «а и б», «а, б и в»""" + if len(items) <= 1: + return "".join(items) + return ", ".join(items[:-1]) + " и " + items[-1] def upd_stat(user_id, group_id, var_id, name, mes=False): @@ -63,23 +79,26 @@ def notice(name, all_users, group_id, author): if all_users: name = dg.user_name(name, group_id) - usr = [f"[{names[i].title()}](tg://user?id={str(ids[i])})" for i in range(len(names)) if names[i] != name.lower()] - usr.append(f'{usr[-2]} и {usr[-1]}') - del usr[-2], usr[-2] - return f'{", ".join(usr)} вас вызывает {name.title()}' + usr = [f"[{plain(names[i].title())}](tg://user?id={str(ids[i])})" for i in range(len(names)) if names[i] != name.lower()] + if len(usr) < 2: + raise ValueError("в группе меньше трёх участников") + return f'{join_names(usr)} вас вызывает {md(name.title())}' else: no_copy = [] for i in name: if i not in no_copy: no_copy.append(i) - if len(no_copy) > 1: # ПЕРЕПИСАТЬ КОД ГАВНА КУСОК - usr = [f"[{_.title()}](tg://user?id={str(ids[names.index(_)])})" for _ in no_copy if int(author) != ids[names.index(_)]] - usr.append(f'{usr[-2]} и {usr[-1]}') - del usr[-2], usr[-2] - return f"{', '.join(usr)} вас упомянули)" + if len(no_copy) > 1: + # раньше при двух именах, одно из которых — автор, падало на usr[-2] + usr = [f"[{plain(_.title())}](tg://user?id={str(ids[names.index(_)])})" for _ in no_copy if int(author) != ids[names.index(_)]] + if len(usr) > 1: + return f"{join_names(usr)} вас упомянули)" + if usr: + return f"{usr[0]}, тебя упомянули)" + return None else: - usr = f"[{no_copy[0].title()}](tg://user?id={str(ids[names.index(no_copy[0])])})" + usr = f"[{plain(no_copy[0].title())}](tg://user?id={str(ids[names.index(no_copy[0])])})" return f"{usr}, тебя упомянули)" @@ -142,7 +161,7 @@ def revers(message, var): pct.append(''.join(pnc)) pnc = [] - wrd, rev, lst, txt = no_pct.split(), [], [], [] + wrd, rev, txt = no_pct.split(), [], [] for i in wrd: word, up, itg = [i[-1 - l].lower() for l in range(len(i))], [], []