1
0
Fork 0
mirror of https://github.com/EDeev/chatping_abobot.git synced 2026-10-07 20:49:45 +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 @@
# ДАННЫЕ БОТА
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 = ["побить", "отмудохать", "избить", "уебать", "отметелить"]

View file

@ -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/<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)
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

View file

@ -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')

View file

@ -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))], [], []