from aiogram import F, Router from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, FSInputFile from aiogram.filters import Command, CommandStart from aiogram.enums import ContentType 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 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): if not du.group_exists(message.chat.id): du.add_group(message.chat.id) group_id = du.get_group_id(message.chat.id) db.add_group(group_id) dg.created_group(group_id) await bot.send_message( message.chat.id, f'*Привет группа {plain(message.chat.title)}!*\n\n' f'Все функции вы можете узнать по команде /help! Для того, чтобы ' f'уведомления по имени и команда /all нормально функционировали, ' f'необходимо чтобы каждый участник группы написал хотя бы одно ' f'сообщение в чат! Также для того, чтобы системные сообщения удалялись ' f'автоматически, необходимо боту выдать права администратора!' ) else: try: await message.delete() except Exception: return # ОБНОВЛЕНИЕ АЙДИ ГРУППЫ @router.message(F.content_type.in_([ContentType.MIGRATE_TO_CHAT_ID, ContentType.MIGRATE_FROM_CHAT_ID])) async def chat_reload(message: Message): if du.group_exists(message.migrate_from_chat_id): du.update_group_id(du.get_group_id(message.migrate_from_chat_id), message.migrate_to_chat_id) # КОМАНДЫ HELP И START @router.message(CommandStart()) @router.message(Command('help')) async def helps(message: Message): if message.chat.id > 0: user_id = message.from_user.id if not du.user_exists(user_id): du.add_user(user_id) else: group_id, user_id = message.chat.id, message.from_user.id if not du.user_exists(user_id): du.add_user(user_id) if not du.group_exists(group_id): du.add_group(group_id) upd_stat(user_id, group_id, 3, message.from_user.first_name, True) buttons = [ [InlineKeyboardButton(text="КОМАНДЫ", callback_data="com"), InlineKeyboardButton(text="ИВЕНТЫ", callback_data="even")], [InlineKeyboardButton(text="АВТОР", callback_data="auth"), InlineKeyboardButton(text="ФУНКЦИИ", callback_data="fun")] ] keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) try: await bot.send_voice( chat_id=message.chat.id, voice=FSInputFile('../data/tts.ogg'), caption='*-* Этот АБОБОТ поможет вам приятно провести время в чате с различными командами, ' 'ивентами и удобными функциями, которые облегчают использование чата)\n\n' '*-* Также прошу если вам понравился бот, оставить отзыв о его использовании на ' 'команду /report)', reply_markup=keyboard ) except Exception: await bot.send_message( chat_id=message.chat.id, text='*-* Этот АБОБОТ поможет вам приятно провести время в чате с различными командами, ' 'ивентами и удобными функциями, которые облегчают использование чата)\n\n' '*-* Также прошу если вам понравился бот, оставить отзыв о его использовании на ' 'команду /report)', reply_markup=keyboard ) # INLINE КЛАВИАТУРА ОБРАБОТЧИКИ @router.callback_query(F.data == "auth") async def author(call: CallbackQuery): await call.message.answer( text='*| АВТОР |*\n\n*>>* Этот бот, как бы это не печально звучало, но одна из лучших' ' моих работ и если кого-нибудь у меня получится действительно достойный продукт,' ' вы сможете о нём узнать в моём телеграм канале *@programium*' ) @router.callback_query(F.data == "fun") async def function(call: CallbackQuery): await call.message.answer( text='*| ФУНКЦИИ |*\n\n*1.* Возможность перевода случайно написанного текста на ' 'транслите\n*2.* Ведение обширной статистики сообщений\n*3.* Упоминание участника ' 'при написании его имени в чате\n*4.* Автоматическое удаление системных сообщений' ) @router.callback_query(F.data == "com") async def commands(call: CallbackQuery): await call.message.answer( text='*| КОМАНДЫ |*\n\n*/all* - упомянуть всех в чате\n*/help* - полный список функций\n' '*/recognize* - транскрипция отмеченного голосового сообщения в текст\n' '*/stat_group* - полная статистика группы\n*/stat_user* - полная статистика ' 'отправителя\n*/edit и /back_edit* - первая команда даёт возможность ' 'сменить имя для упоминаний на любое слово, а вторая для возврата динамического ' 'имени\n*/start_bot и /stop_bot* - возможность отключения текстовых ивентов' ) @router.callback_query(F.data == "even") async def events(call: CallbackQuery): await call.message.answer( text='*| ИВЕНТЫ |*\n\n*"Чмокнуть"* - сделать кому-нибудь приятно\n*"Отмудохать"* - ' 'выместить злость на кого-нибудь\n*"Число от ... до ..."* - случайное значение из ' 'диапозона\n*"Подраться с ..."* - повод кого-нибудь побить\n*"Переведи (..) - ..."* ' '- перевод слова на керпичный язык\n*"Дай блять совет!"* - даёт рандомный охуенный ' 'совет\n*"Переверни - ..."* - переворачивает слова в предложении\n*"Озвучь - ..."* ' '- озвучивает написанный текст' ) # КОМАНДА ALL @router.message(Command('all')) async def every(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 3, message.from_user.first_name, True) try: await message.reply( notice(du.get_user_id(message.from_user.id), True, du.get_group_id(message.chat.id), message.from_user.id) ) except Exception: await message.reply('В группе состоит менее 3х человек, из-за чего команда не работает!') else: await message.reply('Эта команда предназначена для вызова в чате!') # КОМАНДА RECOGNIZE @router.message(Command("recognize")) async def recognise(message: Message): if not (message.reply_to_message and message.reply_to_message.voice): await message.reply("Ответьте этой командой на голосовое сообщение!") return 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')) async def stat_group(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 3, message.from_user.first_name, True) group_id = du.get_group_id(message.chat.id) group = db.stat_group(group_id) month = db.month_stat_group(group_id) await message.answer( text=f'*| СТАТИСТИКА ГРУППЫ |*\n\n' f'*>>* В целом сообщений *[ {message.message_id} ]*\n\n' f'*- За всё время* / *За месяц -*\n' f'*>>* Сообщений в базе *[ {group[0]} / {month[0]} ]*\n\n' f'*>* Ответов *- [ {group[1]} / {month[1]} ]*\n' f'*>* Команд *- [ {group[2]} / {month[2]} ]*\n' f'*>* Ссылок *- [ {group[3]} / {month[3]} ]*\n' f'*>* Стикеров *- [ {group[5]} / {month[5]} ]*\n' f'*>* Медиа файлов *- [ {group[4]} / {month[4]} ]*\n' f'*>* Голос/Кружочки *- [ {group[6]} / {month[6]} ]*' ) # СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ @router.message(Command('stat_user')) async def stat_user(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 3, message.from_user.first_name, True) user_id, group_id = du.get_user_id(message.from_user.id), du.get_group_id(message.chat.id) group = dg.stat_user(user_id, group_id) month = dm.stat_user(user_id, group_id) namer = "".join(re.sub(r'[^\w\s]', '', message.from_user.first_name).split()) name = morph.parse(namer)[0].inflect({"gent"}) if name is None: name = namer else: name = name.word await message.answer( text=f'*| СТАТИСТИКА {plain(name.upper())} |*\n\n' f'*- За всё время* / *За месяц -*\n' f'*>>* Сообщений в базе *[ {group[0]} / {month[0]} ]*\n\n' f'*>* Ответов *- [ {group[1]} / {month[1]} ]*\n' f'*>* Команд *- [ {group[2]} / {month[2]} ]*\n' f'*>* Ссылок *- [ {group[3]} / {month[3]} ]*\n' f'*>* Стикеров *- [ {group[5]} / {month[5]} ]*\n' f'*>* Медиа файлов *- [ {group[4]} / {month[4]} ]*\n' f'*>* Голос/Кружочки *- [ {group[6]} / {month[6]} ]*' ) # РЕДАКТИРОВАНИЕ ИМЕНИ @router.message(Command('edit')) async def update(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 3, message.from_user.first_name, True) user_id, group_id = du.get_user_id(message.from_user.id), du.get_group_id(message.chat.id) text_edit = re.sub(r'[^\w\s]', '', message.text.lower()).split() if len(text_edit) == 2: name = morph.parse(text_edit[1])[0] if name is None: name = text_edit[1] else: name = name.normal_form if name not in list(map(lambda x: x[0], dg.all_names(group_id))): if not db.edit_user_exists(user_id): db.add_edit_user(user_id) dg.update_name(user_id, group_id, name) await message.reply(f'{md(name.title())}, ваше имя было успешно изменено)') else: await message.reply(f'{md(message.from_user.first_name.lower().title())}, такое имя уже присутствует в чате!') else: await message.reply('Вы не правильно ввели имя! Имя должно быть ' 'из одного слова и идти сразу после команды!') # ВОЗВРАТ К ДИНАМИЧЕСКОМУ ИМЕНИ @router.message(Command('back_edit')) async def update_return(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 3, message.from_user.first_name, True) user_id, group_id = du.get_user_id(message.from_user.id), du.get_group_id(message.chat.id) name = re.sub(r'[^\w\s]', '', message.from_user.first_name.lower()) if db.edit_user_exists(user_id): db.del_edit_user(user_id) dg.update_name(user_id, group_id, name) await message.reply(f'{md(name.title())}, вы успешно вернулись к динамическому изменению имени)') else: await message.reply(f'{md(name.title())}, вы не устанавливали постоянное имя!') # ВКЛЮЧЕНИЕ ТЕКСТОВЫХ ИВЕНТОВ @router.message(Command('start_bot')) async def opening(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 3, message.from_user.first_name, True) group_id = du.get_group_id(message.chat.id) if db.check_status(group_id): await message.answer("У вас уже включены текстовые ивенты!") else: db.update_status(group_id) await message.answer("Текстовые ивенты включены!") # ОТКЛЮЧЕНИЕ ТЕКСТОВЫХ ИВЕНТОВ @router.message(Command('stop_bot')) async def closing(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 3, message.from_user.first_name, True) group_id = du.get_group_id(message.chat.id) if db.check_status(group_id): db.update_status(group_id) await message.answer("Текстовые ивенты отключены!") else: await message.answer("У вас уже отключены текстовые ивенты!") # УДАЛЕНИЕ ПОЛЬЗОВАТЕЛЕЙ АВТОМАТИЧЕСКИ @router.message(F.content_type == ContentType.LEFT_CHAT_MEMBER) async def delete(message: Message): if du.user_exists(message.left_chat_member.id): user_id, group_id = du.get_user_id(message.left_chat_member.id), du.get_group_id(message.chat.id) dg.del_user(group_id, user_id) if db.group_exists_month(group_id): if dm.user_exists(user_id, group_id): dm.del_user(group_id, user_id) try: await message.delete() except Exception: return # УДАЛЕНИЕ ТЕХНИЧЕСКИХ СООБЩЕНИЙ @router.message(F.content_type.in_([ ContentType.NEW_CHAT_TITLE, ContentType.NEW_CHAT_PHOTO, ContentType.PINNED_MESSAGE, ContentType.VIDEO_CHAT_ENDED, ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED ])) async def chat_events(message: Message): try: await message.delete() except Exception: return # МЕДИА ФАЙЛЫ @router.message(F.content_type.in_([ ContentType.LOCATION, ContentType.CONTACT, ContentType.VIDEO, ContentType.PHOTO, ContentType.AUDIO, ContentType.DOCUMENT ])) async def media(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 5, message.from_user.first_name, True) # ГОЛОСОВЫЕ СООБЩЕНИЯ И КРУЖОЧКИ @router.message(F.content_type.in_([ContentType.VOICE, ContentType.VIDEO_NOTE])) 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 and message.voice.duration <= 60: query = await recognize_voice(message.voice.file_id) if not query: return 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] 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) async def stick(message: Message): if message.chat.id < 0: upd_stat(message.from_user.id, message.chat.id, 6, message.from_user.first_name, True) # ТЕКСТОВЫЕ ИВЕНТЫ @router.message(F.content_type == ContentType.TEXT) async def send_events(message: Message): group_id, user_id, name = message.chat.id, message.from_user.id, message.from_user.first_name # ОБНОВЛЕНИЕ СТАТИСТИКИ if message.chat.id < 0: upd_stat(user_id, group_id, 1, name) # СООБЩЕНИЙ if len(message.text) > 1 and message.text[0] == '/': upd_stat(user_id, group_id, 3, name) # КОМАНД if message.reply_to_message: upd_stat(user_id, group_id, 2, name) # ОТВЕТОВ НА СООБЩЕНИЯ # ПЕРЕМЕННЫЕ low_mes = message.text.lower() words = message.text.split() unsigned = re.sub(r'[^\w\s]', '', low_mes).split() first_form = [morph.parse(i)[0].normal_form for i in unsigned] 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))))] # сообщение только из знаков препинания: дальше нужны слова (раньше — IndexError на unsigned[0]) if len(words) >= 2 and unsigned: # ПОЛЕЗНЫЕ ФУНКЦИИ if len(words) == 5 and "число от" in low_mes: try: num1, num2 = int(low_mes.split()[2]), int(low_mes.split()[4]) await message.answer( text=f"Число *[ {random.randint(num1, num2)} ]*" ) except ValueError: pass return if unsigned[0] == 'переведи': if "переведи - " 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(md(lang_form([words[_] for _ in range(len(words)) if _ > 2], unsigned[1]))) return if unsigned[0] == 'переверни': if "переверни - " in low_mes: await message.answer(md(revers(message.text[12:], True))) elif "переверни полностью - " in low_mes: await message.answer(md(revers(message.text[22:], False))) return if unsigned[0] == 'озвучь' and "озвучь - " in low_mes: text_to_voice = message.text[9:] # синтез — в отдельном потоке и во временный файл (раньше общий 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 # РАНДОМ ИВЕНТЫ if "подраться" == words[0].lower() and "с" == words[1].lower(): text = " ".join([words[i] for i in range(len(words)) if i > 1]) if random.randint(0, 1) == 0: await message.answer_photo( photo=FSInputFile(f"../data/fight/({random.randint(1, 8)}).jpg"), 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"{md(message.from_user.first_name)}, ты победил в драке " f"с {md(text.title())}, {base.VAR_WIN[random.randint(0, 1)]}" ) # ИВЕНТ ВЗАИМОДЕЙСТВИЯ for word in base.TMOK_LIST: if word in low_mes: lst = [words[i] for i in range(len(words)) if i != 0] text = " ".join(lst) await message.answer_photo( photo=FSInputFile(f"../data/tmok/({random.randint(1, 4)}).jpg"), caption=f"{md(message.from_user.first_name)} " f"{inflect_past(words[0])} {md(text)}" ) for word in base.KILL_LIST: if word in low_mes: lst = [words[i] for i in range(len(words)) if i != 0] text = " ".join(lst) await message.answer_photo( photo=FSInputFile(f"../data/kill/({random.randint(1, 6)}).jpg"), caption=f"{md(message.from_user.first_name)} " f"{inflect_past(words[0])} {md(text)}" ) 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(md(resp.json()["text"])) except Exception: logging.warning("Сервис советов недоступен") return if message.chat.id < 0: # УПОМИНАНИЯ ПО ИМЕНАМ if names: 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( # раньше tg://user_id?id= — такой ссылки нет, упоминание не работало f"[{plain(message.from_user.first_name)}](tg://user?id={user_id}) *>* {md(translator(words))}" ) return