1
0
Fork 0
mirror of https://github.com/EDeev/chatping_abobot.git synced 2026-10-08 04:59:42 +03:00

Исправлены упоминания, экранирование и блокировка бота

- ответ на текст в неправильной раскладке упоминал автора ссылкой tg://user_id?id= —
  такой нет, упоминание не работало; теперь tg://user?id=;
- имена и текст пользователей подставлялись в Markdown как есть: «_» или «*» в имени
  ломали отправку статистики, ивентов и упоминаний;
- каждое голосовое в группе распознавалось через Google прямо в обработчике, и бот на это
  время не отвечал никому; распознавание, озвучка и запрос совета — в отдельном потоке,
  у запроса совета есть таймаут;
- временные файлы голосовых и озвучки — во временной папке на запрос (раньше общий
  voice.ogg и файлы со случайным номером, остававшиеся при ошибке);
- падения: упоминание двух имён, одно из которых — автор; сообщение из одних знаков
  препинания; глагол, который pymorphy3 не может поставить в прошедшее время;
- script.py берёт соединения с базами из init.py, а не открывает вторые;
- токен — из BOT_TOKEN.
This commit is contained in:
Деев Егор Викторович 2026-10-06 01:10:52 +00:00
parent e4786d8b68
commit f5441bdde6
4 changed files with 165 additions and 145 deletions

View file

@ -1,8 +1,10 @@
# ДАННЫЕ БОТА import os
TOKEN = "XXXXXXXXXX" # @chat_abobot
DEBAG = "XXXXXXXXXX" # Технический чат
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 = ["побить", "отмудохать", "избить", "уебать", "отметелить"] KILL_LIST = ["побить", "отмудохать", "избить", "уебать", "отметелить"]

View file

@ -3,17 +3,60 @@ from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKe
from aiogram.filters import Command, CommandStart from aiogram.filters import Command, CommandStart
from aiogram.enums import ContentType from aiogram.enums import ContentType
import logging, random, requests, os, re import asyncio
import speech_recognition as sr import logging
import os
import random
import re
import tempfile
import requests
import soundfile as sf import soundfile as sf
import speech_recognition as sr
from gtts import gTTS from gtts import gTTS
from init import *
from script import *
import base 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() 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/<id>_<случайное число>, при ошибке файлы оставались)"""
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) @router.message(F.content_type == ContentType.NEW_CHAT_MEMBERS)
async def notification(message: Message): async def notification(message: Message):
@ -26,7 +69,7 @@ async def notification(message: Message):
await bot.send_message( await bot.send_message(
message.chat.id, message.chat.id,
f'*Привет группа {message.chat.title}!*\n\n' f'*Привет группа {plain(message.chat.title)}!*\n\n'
f'Все функции вы можете узнать по команде /help! Для того, чтобы ' f'Все функции вы можете узнать по команде /help! Для того, чтобы '
f'уведомления по имени и команда /all нормально функционировали, ' f'уведомления по имени и команда /all нормально функционировали, '
f'необходимо чтобы каждый участник группы написал хотя бы одно ' f'необходимо чтобы каждый участник группы написал хотя бы одно '
@ -147,47 +190,20 @@ async def every(message: Message):
# КОМАНДА RECOGNIZE # КОМАНДА RECOGNIZE
@router.message(Command("recognize")) @router.message(Command("recognize"))
async def recognise(message: Message): 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: mes = await bot.send_message(
num = random.randint(1000, 9999) chat_id=message.chat.id,
audio = f"../data/voices/{user_id}_{num}.oga" text="Распознавание.....",
reply_to_message_id=message.reply_to_message.message_id
file_id = message.reply_to_message.voice.file_id )
file = await bot.get_file(file_id) query = await recognize_voice(message.reply_to_message.voice.file_id)
file_path = file.file_path if query:
await bot.download_file(file_path, audio) await mes.edit_text(f'*{plain(message.reply_to_message.from_user.first_name)} сказал(a)* "{md(query)}"')
else:
data, samplerate = sf.read(audio) await mes.edit_text("Распознать сообщение не удалось!")
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
# СТАТИСТИКА ГРУППЫ # СТАТИСТИКА ГРУППЫ
@router.message(Command('stat_group')) @router.message(Command('stat_group'))
@ -231,7 +247,7 @@ async def stat_user(message: Message):
name = name.word name = name.word
await message.answer( await message.answer(
text=f'*| СТАТИСТИКА {name.upper()} |*\n\n' text=f'*| СТАТИСТИКА {plain(name.upper())} |*\n\n'
f'*- За всё время* / *За месяц -*\n' f'*- За всё время* / *За месяц -*\n'
f'*>>* Сообщений в базе *[ {group[0]} / {month[0]} ]*\n\n' f'*>>* Сообщений в базе *[ {group[0]} / {month[0]} ]*\n\n'
f'*>* Ответов *- [ {group[1]} / {month[1]} ]*\n' f'*>* Ответов *- [ {group[1]} / {month[1]} ]*\n'
@ -263,9 +279,9 @@ async def update(message: Message):
db.add_edit_user(user_id) db.add_edit_user(user_id)
dg.update_name(user_id, group_id, name) dg.update_name(user_id, group_id, name)
await message.reply(f'{name.title()}, ваше имя было успешно изменено)') await message.reply(f'{md(name.title())}, ваше имя было успешно изменено)')
else: 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: else:
await message.reply('Вы не правильно ввели имя! Имя должно быть ' await message.reply('Вы не правильно ввели имя! Имя должно быть '
'из одного слова и идти сразу после команды!') 'из одного слова и идти сразу после команды!')
@ -282,9 +298,9 @@ async def update_return(message: Message):
db.del_edit_user(user_id) db.del_edit_user(user_id)
dg.update_name(user_id, group_id, name) dg.update_name(user_id, group_id, name)
await message.reply(f'{name.title()}, вы успешно вернулись к динамическому изменению имени)') await message.reply(f'{md(name.title())}, вы успешно вернулись к динамическому изменению имени)')
else: else:
await message.reply(f'{name.title()}, вы не устанавливали постоянное имя!') await message.reply(f'{md(name.title())}, вы не устанавливали постоянное имя!')
# ВКЛЮЧЕНИЕ ТЕКСТОВЫХ ИВЕНТОВ # ВКЛЮЧЕНИЕ ТЕКСТОВЫХ ИВЕНТОВ
@router.message(Command('start_bot')) @router.message(Command('start_bot'))
@ -361,47 +377,21 @@ async def voice(message: Message):
if message.chat.id < 0: if message.chat.id < 0:
upd_stat(message.from_user.id, message.chat.id, 7, message.from_user.first_name, True) 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: if message.voice and message.chat.id < 0 and message.voice.duration <= 60:
user_id = du.get_user_id(message.from_user.id) query = await recognize_voice(message.voice.file_id)
if not query:
return
num = random.randint(1000, 9999) group_id = du.get_group_id(message.chat.id)
audio = f"../data/voices/{user_id}_{num}.oga" 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]
file_id = message.voice.file_id text = notice(names, False, group_id, message.from_user.id) if names else None
file = await bot.get_file(file_id) if text:
file_path = file.file_path await message.reply(text)
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:
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
# СТИКЕРЫ # СТИКЕРЫ
@router.message(F.content_type == ContentType.STICKER) @router.message(F.content_type == ContentType.STICKER)
@ -431,7 +421,8 @@ async def send_events(message: Message):
if message.chat.id < 0: 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))))] 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: if len(words) == 5 and "число от" in low_mes:
try: try:
@ -445,29 +436,32 @@ async def send_events(message: Message):
if unsigned[0] == 'переведи': if unsigned[0] == 'переведи':
if "переведи - " in low_mes: if "переведи - " in low_mes:
await message.answer(lang_form([words[_] for _ in range(len(words)) if _ > 1])) await message.answer(md(lang_form([words[_] for _ in range(len(words)) if _ > 1])))
elif f"переведи ({unsigned[1]}) - " in low_mes: elif len(unsigned) > 1 and f"переведи ({unsigned[1]}) - " in low_mes:
if len(unsigned[1]) == 1: 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 return
if unsigned[0] == 'переверни': if unsigned[0] == 'переверни':
if "переверни - " in low_mes: 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: elif "переверни полностью - " in low_mes:
await message.answer(revers(message.text[22:], False)) await message.answer(md(revers(message.text[22:], False)))
return return
if unsigned[0] == 'озвучь' and "озвучь - " in low_mes: if unsigned[0] == 'озвучь' and "озвучь - " in low_mes:
text_to_voice = message.text[9:] text_to_voice = message.text[9:]
tts = gTTS(text_to_voice, lang='ru') # синтез — в отдельном потоке и во временный файл (раньше общий voice.ogg:
tts.save('../data/voices/voice.ogg') # одновременные запросы перезаписывали друг друга)
await bot.send_voice( with tempfile.TemporaryDirectory() as tmp:
chat_id=group_id, path = os.path.join(tmp, "voice.mp3")
voice=FSInputFile('../data/voices/voice.ogg'), try:
caption=f"*{text_to_voice}*" await asyncio.to_thread(gTTS(text_to_voice, lang='ru').save, path)
) except Exception:
os.remove('../data/voices/voice.ogg') 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 return
# РАНДОМ ИВЕНТЫ # РАНДОМ ИВЕНТЫ
@ -477,14 +471,14 @@ async def send_events(message: Message):
if random.randint(0, 1) == 0: if random.randint(0, 1) == 0:
await message.answer_photo( await message.answer_photo(
photo=FSInputFile(f"../data/fight/({random.randint(1, 8)}).jpg"), 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)]}" f", с помощью {base.VAR_LOSE[random.randint(0, 3)]}"
) )
else: else:
await message.answer_photo( await message.answer_photo(
photo=FSInputFile(f"../data/fight/({random.randint(1, 8)}).jpg"), photo=FSInputFile(f"../data/fight/({random.randint(1, 8)}).jpg"),
caption=f"{message.from_user.first_name}, ты победил в драке " caption=f"{md(message.from_user.first_name)}, ты победил в драке "
f"с {text.title()}, {base.VAR_WIN[random.randint(0, 1)]}" 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] lst = [words[i] for i in range(len(words)) if i != 0]
text = " ".join(lst) text = " ".join(lst)
slv = morph.parse(words[0].lower())[0]
await message.answer_photo( await message.answer_photo(
photo=FSInputFile(f"../data/tmok/({random.randint(1, 4)}).jpg"), photo=FSInputFile(f"../data/tmok/({random.randint(1, 4)}).jpg"),
caption=f"{message.from_user.first_name} " caption=f"{md(message.from_user.first_name)} "
f"{slv.inflect({'past', 'sing', 'indc'}).word} {text}" f"{inflect_past(words[0])} {md(text)}"
) )
for word in base.KILL_LIST: 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] lst = [words[i] for i in range(len(words)) if i != 0]
text = " ".join(lst) text = " ".join(lst)
slv = morph.parse(words[0].lower())[0]
await message.answer_photo( await message.answer_photo(
photo=FSInputFile(f"../data/kill/({random.randint(1, 6)}).jpg"), photo=FSInputFile(f"../data/kill/({random.randint(1, 6)}).jpg"),
caption=f"{message.from_user.first_name} " caption=f"{md(message.from_user.first_name)} "
f"{slv.inflect({'past', 'sing', 'indc'}).word} {text}" 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[1] in base.QUAT_LIST[1] and
unsigned[2] in base.QUAT_LIST[2]): unsigned[2] in base.QUAT_LIST[2]):
try: try:
word = requests.get('http://fucking-great-advice.ru/api/random').json() # без таймаута и в обработчике запрос мог надолго заморозить бота
await message.reply(word["text"]) resp = await asyncio.to_thread(requests.get, 'http://fucking-great-advice.ru/api/random', timeout=5)
except: await message.reply(md(resp.json()["text"]))
pass except Exception:
logging.warning("Сервис советов недоступен")
return return
if message.chat.id < 0: if message.chat.id < 0:
# УПОМИНАНИЯ ПО ИМЕНАМ # УПОМИНАНИЯ ПО ИМЕНАМ
if names: if names:
try: text = notice(names, False, du.get_group_id(group_id), user_id)
await message.reply(notice(names, False, du.get_group_id(group_id), user_id)) if text:
except Exception: await message.reply(text)
pass
return return
# ПЕРЕВОДЧИК СЛОВ # ПЕРЕВОДЧИК СЛОВ
if (checker([i for i in low_mes], words, group_id, user_id, name.lower()) == len(low_mes) and 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])): not any([engl_dict.check(i) for i in unsigned if len(i) > 1])):
await message.reply( 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 return

View file

@ -4,7 +4,11 @@ from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.bot import DefaultBotProperties 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)) bot = Bot(token=base.TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.MARKDOWN))
dp = Dispatcher(storage=MemoryStorage()) dp = Dispatcher(storage=MemoryStorage())
@ -15,6 +19,7 @@ engl_dict = enchant.Dict("en_US")
import sql import sql
os.makedirs('../db', exist_ok=True)
db = sql.Base('../db/base.db') db = sql.Base('../db/base.db')
du = sql.User('../db/users.db') du = sql.User('../db/users.db')
dg = sql.Group('../db/groups.db') dg = sql.Group('../db/groups.db')

View file

@ -1,12 +1,28 @@
from base import ERR, ERR_, TRU, TRU_, ALB import re
from string import digits, punctuation from string import digits, punctuation
import re, sql
# инициализируем соединение с БД from base import ERR, ERR_, TRU, TRU_, ALB
db = sql.Base('../db/base.db') # одни и те же соединения с базами, что и у обработчиков (раньше здесь открывались вторые)
du = sql.User('../db/users.db') from init import db, du, dg, dm
dg = sql.Group('../db/groups.db')
dm = sql.Month('../db/month.db') 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): 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: if all_users:
name = dg.user_name(name, group_id) 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 = [f"[{plain(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]}') if len(usr) < 2:
del usr[-2], usr[-2] raise ValueError("в группе меньше трёх участников")
return f'{", ".join(usr)} вас вызывает {name.title()}' return f'{join_names(usr)} вас вызывает {md(name.title())}'
else: else:
no_copy = [] no_copy = []
for i in name: for i in name:
if i not in no_copy: if i not in no_copy:
no_copy.append(i) no_copy.append(i)
if len(no_copy) > 1: # ПЕРЕПИСАТЬ КОД ГАВНА КУСОК 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[-2]
usr.append(f'{usr[-2]} и {usr[-1]}') usr = [f"[{plain(_.title())}](tg://user?id={str(ids[names.index(_)])})" for _ in no_copy if int(author) != ids[names.index(_)]]
del usr[-2], usr[-2] if len(usr) > 1:
return f"{', '.join(usr)} вас упомянули)" return f"{join_names(usr)} вас упомянули)"
if usr:
return f"{usr[0]}, тебя упомянули)"
return None
else: 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}, тебя упомянули)" return f"{usr}, тебя упомянули)"
@ -142,7 +161,7 @@ def revers(message, var):
pct.append(''.join(pnc)) pct.append(''.join(pnc))
pnc = [] pnc = []
wrd, rev, lst, txt = no_pct.split(), [], [], [] wrd, rev, txt = no_pct.split(), [], []
for i in wrd: for i in wrd:
word, up, itg = [i[-1 - l].lower() for l in range(len(i))], [], [] word, up, itg = [i[-1 - l].lower() for l in range(len(i))], [], []