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

Compare commits

..

No commits in common. "9123f579685c8e2b07550d2e996f27a923d19891" and "ef4af56486bad46d103511ca5af5d650376d5a2f" have entirely different histories.

26 changed files with 610 additions and 868 deletions

View file

@ -1,5 +0,0 @@
.git
.env
db
tests
__pycache__

View file

@ -1,2 +0,0 @@
# Токен бота от @BotFather
BOT_TOKEN=123456:your-token

5
.gitattributes vendored
View file

@ -1,5 +0,0 @@
* text=auto eol=lf
*.jpg binary
*.ogg binary
*.oga binary
*.db binary

View file

@ -1,19 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: sudo apt-get update -qq && sudo apt-get install -y -qq libenchant-2-2 hunspell-en-us
- run: pip install -r requirements-dev.txt
- run: ruff check --select E9,F,B code tests
- run: pytest -q

View file

@ -1,42 +0,0 @@
name: Docker
on:
push:
tags: ["v*"]
workflow_dispatch:
jobs:
image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/login-action@v3
with:
registry: dcr.deev.su
username: ${{ secrets.ZOT_USERNAME }}
password: ${{ secrets.ZOT_PASSWORD }}
- id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/edeev/chatping_abobot
dcr.deev.su/edeev/chatping_abobot
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

5
.gitignore vendored
View file

@ -1,5 +0,0 @@
.env
__pycache__/
*.pyc
.pytest_cache/
db/*.db

View file

@ -1,23 +0,0 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# словарь английского для распознавания текста в неправильной раскладке
RUN apt-get update \
&& apt-get install -y --no-install-recommends libenchant-2-2 hunspell-en-us \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY code/ code/
COPY data/ data/
RUN useradd --create-home --uid 1000 app && mkdir -p db && chown -R app:app /app
USER app
VOLUME ["/app/db"]
# пути к базам и картинкам в коде — относительно папки code/ (../db, ../data)
WORKDIR /app/code
CMD ["python", "bot.py"]

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Egor Deev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,103 +0,0 @@
# AboBot
[Русский](README.md) · **English**
[![CI](https://github.com/EDeev/chatping_abobot/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/chatping_abobot/actions/workflows/ci.yml)
[![Docker](https://github.com/EDeev/chatping_abobot/actions/workflows/docker.yml/badge.svg)](https://github.com/EDeev/chatping_abobot/actions/workflows/docker.yml)
[![License](https://img.shields.io/github/license/EDeev/chatping_abobot)](LICENSE)
A Telegram bot for group chats. It:
- pings a member when their name comes up in a message or a voice note;
- keeps statistics for the chat and every member;
- fixes text typed in the wrong keyboard layout;
- transcribes and voices messages;
- runs playful events.
The bot speaks Russian.
**Status:** personal project, running since 2021 · bot [@chat_abobot](https://t.me/chat_abobot) · over
20,000 users from 45 chats in the bot's database (October 2026)
**Stack:** Python 3.12 · aiogram 3 · pymorphy3 · pyenchant · SpeechRecognition · gTTS · SQLite · Docker
## Features
- **Name mentions.** Words of a message are reduced to their base form (pymorphy3) and compared with
members' names, so any grammatical case of a name mentions the right person. A custom name can be set with `/edit`. Names are also found
in voice notes up to a minute long.
- **`/all`** mentions every member.
- **All-time and monthly statistics** (`/stat_group`, `/stat_user`): messages, replies, commands, links,
media, stickers, voice and video notes. Monthly numbers reset at the start of each month.
- **Wrong keyboard layout.** A message like `ghbdtn` is repeated as «привет». English words are recognized
with a dictionary (pyenchant) and left alone.
- **Voice.** `/recognize` as a reply to a voice note transcribes it; «Озвучь - текст» turns text into a
voice note.
- **Events:**
- random numbers;
- reversing text;
- a language game;
- mock fights;
- picture reactions.
Turned off with `/stop_bot`.
- Deleting service messages (joins, leaves, title and photo changes) when the bot is an admin.
## Running
```bash
git clone https://github.com/EDeev/chatping_abobot.git && cd chatping_abobot
cp .env.example .env # BOT_TOKEN from @BotFather
docker compose up -d
```
Prebuilt image: `docker pull ghcr.io/edeev/chatping_abobot` or `docker pull dcr.deev.su/edeev/chatping_abobot`.
The SQLite databases are created on first start.
Without Docker you need Python 3.12, the enchant system library and an English dictionary
(`apt install libenchant-2-2 hunspell-en-us`). Then: `pip install -r requirements.txt` and
`cd code && BOT_TOKEN=… python bot.py`.
## How it works
```
code/bot.py entry point and monthly statistics reset
code/handlers.py message, command, voice and event handlers
code/script.py statistics, name lookup, keyboard layout, text reversal
code/sql.py four SQLite databases: groups, users, all-time and monthly statistics
data/ event pictures and the greeting
```
Member statistics live in a separate table per chat — that is how the bot's production databases are
built. Counters are incremented in a single query, so simultaneous messages are not lost. Speech recognition
and synthesis (Google Web Speech and gTTS) run in a separate thread, so the bot never freezes.
## Development
```bash
pip install -r requirements-dev.txt
ruff check --select E9,F,B code tests && pytest
```
What the tests cover:
- statistics counters;
- mentions and `/all`;
- the monthly reset;
- keyboard layout, reversal and escaping;
- loading of the bot's handlers.
The Docker image is built on `v*` tags and published to GitHub Packages and `dcr.deev.su`.
## License
MIT — see [LICENSE](LICENSE).
## Author
**Egor Deev** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space)
---
<div align="center">
<sub>⭐ If you find this project useful, give it a star on GitHub!</sub>
<p><sub>Made with ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
</div>

199
README.md
View file

@ -1,100 +1,153 @@
# AboBot # 🤖 ABOBOT - Telegram Group Management Bot
**Русский** · [English](README.en.md) ![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)
![Aiogram](https://img.shields.io/badge/Aiogram-3.x-green.svg)
![SQLite](https://img.shields.io/badge/SQLite-3-orange.svg)
![License](https://img.shields.io/badge/License-MIT-yellow.svg)
[![CI](https://github.com/EDeev/chatping_abobot/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/chatping_abobot/actions/workflows/ci.yml) Многофункциональный Telegram-бот для управления групповыми чатами с расширенной аналитикой и интерактивными возможностями.
[![Docker](https://github.com/EDeev/chatping_abobot/actions/workflows/docker.yml/badge.svg)](https://github.com/EDeev/chatping_abobot/actions/workflows/docker.yml)
[![License](https://img.shields.io/github/license/EDeev/chatping_abobot)](LICENSE)
Telegram-бот для групповых чатов. Он: ## 🎯 Основные возможности
- зовёт участника, когда его имя звучит в сообщении или в голосовом;
- ведёт статистику чата и каждого участника;
- исправляет текст, набранный в неправильной раскладке;
- распознаёт и озвучивает голосовые;
- устраивает шуточные ивенты.
**Статус:** личный проект, работает с 2021 года · бот [@chat_abobot](https://t.me/chat_abobot) · в базе ### 📊 Система аналитики
бота больше 20 000 пользователей из 45 чатов (октябрь 2026) - **Комплексная статистика** - детальный учёт активности участников
- **Временная сегментация** - анализ данных за месяц и весь период
- **Многомерные метрики** - сообщения, ответы, команды, медиафайлы, голосовые
**Стек:** Python 3.12 · aiogram 3 · pymorphy3 · pyenchant · SpeechRecognition · gTTS · SQLite · Docker ### 👥 Управление участниками
- **Интеллектуальные упоминания** - автоматическое обнаружение имён в сообщениях
- **Динамическое именование** - морфологическая обработка и нормализация
- **Групповые вызовы** - команда `/all` для оповещения всех участников
## Возможности ### 🔧 Утилиты и инструменты
- **Транслитерация** - автоматический перевод с латиницы на кириллицу
- **Транскрипция** - конвертация голосовых сообщений в текст (gTTS)
- **Текстовые трансформации** - реверс слов и предложений
- **Генерация голоса** - озвучивание текстовых сообщений
- **Упоминания по имени.** Слова сообщения приводятся к начальной форме (pymorphy3) и сравниваются с ### 🎮 Интерактивные функции
именами участников, поэтому участницу Машу позовут и «Маша», и «позови Машу», и «с Машей». Своё имя - **Игровые механики** - симуляция драк и взаимодействий
для упоминаний можно задать командой `/edit`. Имена ищутся и в голосовых до минуты. - **Рандомизация** - генерация случайных чисел в диапазоне
- **`/all`** — упомянуть всех участников. - **Советник** - интеграция с API для получения рандомных советов
- **Статистика за всё время и за месяц** (`/stat_group`, `/stat_user`): сообщения, ответы, команды,
ссылки, медиа, стикеры, голосовые и кружочки. Месячная обнуляется в начале месяца.
- **Неправильная раскладка.** Сообщение вида `ghbdtn` бот повторит как «привет». Английские слова он
отличает по словарю (pyenchant) и не трогает.
- **Голос.** `/recognize` ответом на голосовое — расшифровка; «Озвучь - текст» — голосовое из текста.
- **Ивенты:**
- «Число от 1 до 100»;
- «Переверни - …»;
- «Переведи - …» — на кирпичный язык;
- «Подраться с …»;
- «Чмокнуть» и другие действия с картинками.
Отключаются командой `/stop_bot`. ## 🏗️ Архитектура системы
- Удаление служебных сообщений (вход, выход, смена названия и фото) — если у бота права администратора.
## Запуск ### Технологический стек
```
Backend Framework: aiogram (Async Telegram Bot API)
Database Engine: SQLite (многотабличная структура)
NLP Processing: pymorphy3 (морфологический анализ)
Speech Recognition: speech_recognition + gTTS
Async Framework: asyncio (асинхронная обработка)
```
### Структура базы данных
```
📁 Databases
├── base.db # Основная статистика групп
├── users.db # Маппинг пользователей
├── groups.db # Индивидуальная статистика участников
└── month.db # Временная сегментация данных
```
### Модульная организация
```
📁 Project Structure
├── bot.py # Точка входа и инициализация
├── handlers.py # Обработчики событий и команд
├── script.py # Бизнес-логика и алгоритмы
├── sql.py # Абстракция базы данных
├── init.py # Конфигурация и зависимости
└── base.py # Константы и настройки
```
## 🚀 Быстрый старт
### Установка зависимостей
```bash ```bash
git clone https://github.com/EDeev/chatping_abobot.git && cd chatping_abobot pip install -r requirements.txt
cp .env.example .env # BOT_TOKEN от @BotFather
docker compose up -d
``` ```
Готовый образ: `docker pull ghcr.io/edeev/chatping_abobot` или `docker pull dcr.deev.su/edeev/chatping_abobot`. ### Конфигурация
Базы SQLite создаются при первом запуске. 1. Получите токен бота у [@BotFather](https://t.me/BotFather)
2. Обновите `TOKEN` в `code/base.py`
Без Docker нужны Python 3.12, системная библиотека enchant и английский словарь 3. Настройте права администратора для автоудаления системных сообщений
(`apt install libenchant-2-2 hunspell-en-us`). Затем: `pip install -r requirements.txt` и
`cd code && BOT_TOKEN=… python bot.py`.
## Как устроено
```
code/bot.py запуск и ежемесячное обнуление статистики
code/handlers.py обработчики сообщений, команд, голосовых и ивентов
code/script.py учёт статистики, поиск имён, раскладка, переворот текста
code/sql.py четыре базы SQLite: группы, пользователи, статистика за всё время и за месяц
data/ картинки ивентов и приветствие
```
Статистика участников хранится в отдельной таблице на каждый чат — так устроены рабочие базы бота.
Счётчики увеличиваются одним запросом, поэтому одновременные сообщения не теряются. Распознавание и синтез
речи (Google Web Speech и gTTS) идут в отдельном потоке, чтобы бот не замирал.
## Разработка
### Запуск
```bash ```bash
pip install -r requirements-dev.txt cd code
ruff check --select E9,F,B code tests && pytest python bot.py
``` ```
Что проверяют тесты: ## 📋 Команды
- счётчики статистики;
- упоминания и `/all`;
- обнуление месячной статистики;
- раскладку, переворот и экранирование;
- что обработчики бота загружаются.
Docker-образ собирается по тегу `v*` и публикуется в GitHub Packages и `dcr.deev.su`. | Команда | Функционал |
|---------|------------|
| `/start`, `/help` | Интерактивное меню с описанием возможностей |
| `/all` | Упоминание всех участников группы |
| `/stat_group` | Детальная аналитика группы |
| `/stat_user` | Персональная статистика пользователя |
| `/recognize` | Транскрипция голосового сообщения |
| `/edit <имя>` | Установка кастомного имени для упоминаний |
| `/back_edit` | Возврат к динамическому именованию |
| `/start_bot` | Активация текстовых событий |
| `/stop_bot` | Деактивация текстовых событий |
## Лицензия ## 🎲 Текстовые события
MIT — см. [LICENSE](LICENSE). ### Интерактивные команды
- **"Чмокнуть [имя]"** - позитивное взаимодействие
- **"Отмудохать [имя]"** - игровая агрессия
- **"Подраться с [имя]"** - симуляция боя с рандомным исходом
- **"Число от X до Y"** - генерация случайного числа
- **"Переведи (символ) - текст"** - трансформация в "кирпичный" язык
- **"Переверни - текст"** - реверс слов с сохранением пунктуации
- **"Озвучь - текст"** - генерация голосового сообщения
## Автор ## 🔍 Технические особенности
**Деев Егор Викторович** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space) ### Алгоритмы обработки
- **Морфологический анализ** - приведение имён к начальной форме
- **Детекция транслита** - эвристический алгоритм определения раскладки
- **Парсинг голоса** - интеграция Google Speech Recognition
- **Обработка пунктуации** - сохранение форматирования при трансформациях
### Оптимизации производительности
- **Асинхронная архитектура** - неблокирующая обработка запросов
- **Кэширование соединений** - переиспользование подключений к БД
- **Ленивая загрузка** - подгрузка данных по требованию
## 📈 Метрики и аналитика
Система ведёт учёт следующих параметров:
- Общее количество сообщений
- Количество ответов на сообщения
- Использование команд бота
- Отправка медиафайлов и стикеров
- Голосовые сообщения и видеокружки
- Размещение ссылок
## 🛠️ Требования к системе
- Python 3.8+
- Операционная система: Linux/Windows/macOS
- RAM: минимум 512MB
- Свободное место: 100MB для логов и медиафайлов
## 📄 Лицензия
Проект распространяется под лицензией MIT.
## 👨‍💻 Автор
**Деев Егор Викторович** - Backend Developer
- GitHub: [@EDeev](https://github.com/EDeev)
- Email: egor@deev.space
- Telegram: [@Egor_Deev](https://t.me/Egor_Deev)
--- ---
<div align="center"> <div align="center">
<sub>⭐ Если проект оказался полезным, поставьте звёздочку на GitHub!</sub> <sub>⭐ Если проект оказался полезным, поставьте звезду на GitHub!</sub>
<p><sub>Сделано с ❤️ — <a href="https://deev.space">deev.space</a></sub></p> <p><sub>Создано с ❤️ от вашего дорогого - deev.space ©</sub></p>
</div> </div>

View file

@ -1,10 +1,8 @@
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

@ -1,43 +1,14 @@
import asyncio import asyncio
import logging import logging
from datetime import date
from init import bot, dp, db, dm from init import bot, dp
from handlers import router from handlers import router
def reset_month_if_needed(today=None):
"""Статистика «за месяц» раньше нигде не обнулялась и копилась с момента создания таблиц.
При первом запуске месяц только запоминается — обнуление со следующей смены месяца"""
key = (today or date.today()).strftime("%Y-%m")
saved = db.get_meta("month")
if saved == key:
return False
if saved is not None:
db.reset_month()
dm.reset_all()
logging.info("Месячная статистика обнулена: %s → %s", saved, key)
db.set_meta("month", key)
return saved is not None
async def month_watcher():
while True:
try:
reset_month_if_needed()
except Exception:
logging.exception("Не удалось обнулить месячную статистику")
await asyncio.sleep(600)
async def main() -> None: async def main() -> None:
dp.include_router(router) dp.include_router(router)
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
watcher = asyncio.create_task(month_watcher()) await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types(), skip_updates=True)
try:
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
finally:
watcher.cancel()
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -3,60 +3,17 @@ 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 asyncio import logging, random, requests, os, re
import logging
import os
import random
import re
import tempfile
import requests
import soundfile as sf
import speech_recognition as sr import speech_recognition as sr
import soundfile as sf
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):
@ -69,7 +26,7 @@ async def notification(message: Message):
await bot.send_message( await bot.send_message(
message.chat.id, message.chat.id,
f'*Привет группа {plain(message.chat.title)}!*\n\n' f'*Привет группа {message.chat.title}!*\n\n'
f'Все функции вы можете узнать по команде /help! Для того, чтобы ' f'Все функции вы можете узнать по команде /help! Для того, чтобы '
f'уведомления по имени и команда /all нормально функционировали, ' f'уведомления по имени и команда /all нормально функционировали, '
f'необходимо чтобы каждый участник группы написал хотя бы одно ' f'необходимо чтобы каждый участник группы написал хотя бы одно '
@ -190,20 +147,47 @@ 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):
if not (message.reply_to_message and message.reply_to_message.voice): user_id = du.get_user_id(message.from_user.id)
await message.reply("Ответьте этой командой на голосовое сообщение!")
return
mes = await bot.send_message( if message.reply_to_message and message.reply_to_message.voice:
chat_id=message.chat.id, num = random.randint(1000, 9999)
text="Распознавание.....", audio = f"../data/voices/{user_id}_{num}.oga"
reply_to_message_id=message.reply_to_message.message_id
) file_id = message.reply_to_message.voice.file_id
query = await recognize_voice(message.reply_to_message.voice.file_id) file = await bot.get_file(file_id)
if query: file_path = file.file_path
await mes.edit_text(f'*{plain(message.reply_to_message.from_user.first_name)} сказал(a)* "{md(query)}"') await bot.download_file(file_path, audio)
else:
await mes.edit_text("Распознать сообщение не удалось!") 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
# СТАТИСТИКА ГРУППЫ # СТАТИСТИКА ГРУППЫ
@router.message(Command('stat_group')) @router.message(Command('stat_group'))
@ -247,7 +231,7 @@ async def stat_user(message: Message):
name = name.word name = name.word
await message.answer( await message.answer(
text=f'*| СТАТИСТИКА {plain(name.upper())} |*\n\n' text=f'*| СТАТИСТИКА {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'
@ -279,9 +263,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'{md(name.title())}, ваше имя было успешно изменено)') await message.reply(f'{name.title()}, ваше имя было успешно изменено)')
else: else:
await message.reply(f'{md(message.from_user.first_name.lower().title())}, такое имя уже присутствует в чате!') await message.reply(f'{message.from_user.first_name.lower().title()}, такое имя уже присутствует в чате!')
else: else:
await message.reply('Вы не правильно ввели имя! Имя должно быть ' await message.reply('Вы не правильно ввели имя! Имя должно быть '
'из одного слова и идти сразу после команды!') 'из одного слова и идти сразу после команды!')
@ -298,9 +282,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'{md(name.title())}, вы успешно вернулись к динамическому изменению имени)') await message.reply(f'{name.title()}, вы успешно вернулись к динамическому изменению имени)')
else: else:
await message.reply(f'{md(name.title())}, вы не устанавливали постоянное имя!') await message.reply(f'{name.title()}, вы не устанавливали постоянное имя!')
# ВКЛЮЧЕНИЕ ТЕКСТОВЫХ ИВЕНТОВ # ВКЛЮЧЕНИЕ ТЕКСТОВЫХ ИВЕНТОВ
@router.message(Command('start_bot')) @router.message(Command('start_bot'))
@ -377,21 +361,47 @@ 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 and message.chat.id < 0 and message.voice.duration <= 60: if message.voice.duration <= 60:
query = await recognize_voice(message.voice.file_id) user_id = du.get_user_id(message.from_user.id)
if not query:
return
group_id = du.get_group_id(message.chat.id) num = random.randint(1000, 9999)
unsigned = re.sub(r'[^\w\s]', '', query.lower()).split() audio = f"../data/voices/{user_id}_{num}.oga"
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 file_id = message.voice.file_id
if text: file = await bot.get_file(file_id)
await message.reply(text) 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:
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)
@ -421,8 +431,7 @@ 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))))]
# сообщение только из знаков препинания: дальше нужны слова (раньше — IndexError на unsigned[0]) if len(words) >= 2:
if len(words) >= 2 and unsigned:
# ПОЛЕЗНЫЕ ФУНКЦИИ # ПОЛЕЗНЫЕ ФУНКЦИИ
if len(words) == 5 and "число от" in low_mes: if len(words) == 5 and "число от" in low_mes:
try: try:
@ -436,32 +445,29 @@ async def send_events(message: Message):
if unsigned[0] == 'переведи': if unsigned[0] == 'переведи':
if "переведи - " in low_mes: if "переведи - " in low_mes:
await message.answer(md(lang_form([words[_] for _ in range(len(words)) if _ > 1]))) await message.answer(lang_form([words[_] for _ in range(len(words)) if _ > 1]))
elif len(unsigned) > 1 and f"переведи ({unsigned[1]}) - " in low_mes: elif f"переведи ({unsigned[1]}) - " in low_mes:
if len(unsigned[1]) == 1: if len(unsigned[1]) == 1:
await message.answer(md(lang_form([words[_] for _ in range(len(words)) if _ > 2], unsigned[1]))) await message.answer(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(md(revers(message.text[12:], True))) await message.answer(revers(message.text[12:], True))
elif "переверни полностью - " in low_mes: elif "переверни полностью - " in low_mes:
await message.answer(md(revers(message.text[22:], False))) await message.answer(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:]
# синтез — в отдельном потоке и во временный файл (раньше общий voice.ogg: tts = gTTS(text_to_voice, lang='ru')
# одновременные запросы перезаписывали друг друга) tts.save('../data/voices/voice.ogg')
with tempfile.TemporaryDirectory() as tmp: await bot.send_voice(
path = os.path.join(tmp, "voice.mp3") chat_id=group_id,
try: voice=FSInputFile('../data/voices/voice.ogg'),
await asyncio.to_thread(gTTS(text_to_voice, lang='ru').save, path) caption=f"*{text_to_voice}*"
except Exception: )
logging.exception("gTTS") os.remove('../data/voices/voice.ogg')
await message.reply("Озвучить не получилось, попробуйте позже")
return
await bot.send_voice(chat_id=group_id, voice=FSInputFile(path), caption=f"*{plain(text_to_voice)}*")
return return
# РАНДОМ ИВЕНТЫ # РАНДОМ ИВЕНТЫ
@ -471,14 +477,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"{md(message.from_user.first_name)}, ты был унижен {md(text.title())}" caption=f"{message.from_user.first_name}, ты был унижен {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"{md(message.from_user.first_name)}, ты победил в драке " caption=f"{message.from_user.first_name}, ты победил в драке "
f"с {md(text.title())}, {base.VAR_WIN[random.randint(0, 1)]}" f"с {text.title()}, {base.VAR_WIN[random.randint(0, 1)]}"
) )
# ИВЕНТ ВЗАИМОДЕЙСТВИЯ # ИВЕНТ ВЗАИМОДЕЙСТВИЯ
@ -487,10 +493,11 @@ 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"{md(message.from_user.first_name)} " caption=f"{message.from_user.first_name} "
f"{inflect_past(words[0])} {md(text)}" f"{slv.inflect({'past', 'sing', 'indc'}).word} {text}"
) )
for word in base.KILL_LIST: for word in base.KILL_LIST:
@ -498,37 +505,36 @@ 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"{md(message.from_user.first_name)} " caption=f"{message.from_user.first_name} "
f"{inflect_past(words[0])} {md(text)}" f"{slv.inflect({'past', 'sing', 'indc'}).word} {text}"
) )
if (len(unsigned) >= 3 and if (unsigned[0] in base.QUAT_LIST[0] 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()
resp = await asyncio.to_thread(requests.get, 'http://fucking-great-advice.ru/api/random', timeout=5) await message.reply(word["text"])
await message.reply(md(resp.json()["text"])) except:
except Exception: pass
logging.warning("Сервис советов недоступен")
return return
if message.chat.id < 0: if message.chat.id < 0:
# УПОМИНАНИЯ ПО ИМЕНАМ # УПОМИНАНИЯ ПО ИМЕНАМ
if names: if names:
text = notice(names, False, du.get_group_id(group_id), user_id) try:
if text: await message.reply(notice(names, False, du.get_group_id(group_id), user_id))
await message.reply(text) except Exception:
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(
# раньше tg://user_id?id= — такой ссылки нет, упоминание не работало f"[{message.from_user.first_name}](tg://user_id?id={user_id}) *>* {translator(words)}"
f"[{plain(message.from_user.first_name)}](tg://user?id={user_id}) *>* {md(translator(words))}"
) )
return return

View file

@ -4,11 +4,7 @@ from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.bot import DefaultBotProperties from aiogram.client.bot import DefaultBotProperties
import os import base, enchant, pymorphy3
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())
@ -19,7 +15,6 @@ 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,28 +1,12 @@
import re
from string import digits, punctuation
from base import ERR, ERR_, TRU, TRU_, ALB from base import ERR, ERR_, TRU, TRU_, ALB
# одни и те же соединения с базами, что и у обработчиков (раньше здесь открывались вторые) from string import digits, punctuation
from init import db, du, dg, dm import re, sql
MD_SPECIAL = "_*`[" # инициализируем соединение с БД
db = sql.Base('../db/base.db')
du = sql.User('../db/users.db')
def md(text): dg = sql.Group('../db/groups.db')
"""Экранирование для Markdown вне разметки: «_» или «*» в имени или тексте иначе ломают отправку""" dm = sql.Month('../db/month.db')
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):
@ -79,26 +63,23 @@ 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"[{plain(names[i].title())}](tg://user?id={str(ids[i])})" for i in range(len(names)) if names[i] != name.lower()] usr = [f"[{names[i].title()}](tg://user?id={str(ids[i])})" for i in range(len(names)) if names[i] != name.lower()]
if len(usr) < 2: usr.append(f'{usr[-2]} и {usr[-1]}')
raise ValueError("в группе меньше трёх участников") del usr[-2], usr[-2]
return f'{join_names(usr)} вас вызывает {md(name.title())}' return f'{", ".join(usr)} вас вызывает {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[-2] usr = [f"[{_.title()}](tg://user?id={str(ids[names.index(_)])})" for _ in no_copy if int(author) != ids[names.index(_)]]
usr = [f"[{plain(_.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]}')
if len(usr) > 1: del usr[-2], usr[-2]
return f"{join_names(usr)} вас упомянули)" return f"{', '.join(usr)} вас упомянули)"
if usr:
return f"{usr[0]}, тебя упомянули)"
return None
else: else:
usr = f"[{plain(no_copy[0].title())}](tg://user?id={str(ids[names.index(no_copy[0])])})" usr = f"[{no_copy[0].title()}](tg://user?id={str(ids[names.index(no_copy[0])])})"
return f"{usr}, тебя упомянули)" return f"{usr}, тебя упомянули)"
@ -161,7 +142,7 @@ def revers(message, var):
pct.append(''.join(pnc)) pct.append(''.join(pnc))
pnc = [] pnc = []
wrd, rev, txt = no_pct.split(), [], [] wrd, rev, lst, 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))], [], []

View file

@ -1,16 +1,5 @@
import logging
import sqlite3 import sqlite3
# номер счётчика в обработчиках → колонка таблицы статистики
COLUMNS = {1: "mes", 2: "rep", 3: "com", 4: "url", 5: "med", 6: "sti", 7: "voi"}
# Таблица на каждый чат — так устроены рабочие базы бота; имя таблицы — внутренний числовой id чата,
# поэтому перед подстановкой в SQL он приводится к int
def table(group_id):
return f"[{int(group_id)}]"
class Base: class Base:
def __init__(self, database): def __init__(self, database):
@ -18,66 +7,55 @@ class Base:
self.connection = sqlite3.connect(database) self.connection = sqlite3.connect(database)
self.cursor = self.connection.cursor() self.cursor = self.connection.cursor()
# на новой установке таблиц ещё нет
stat_columns = ", ".join(f"{c} INTEGER" for c in COLUMNS.values())
with self.connection:
self.cursor.execute("CREATE TABLE IF NOT EXISTS work (group_id INTEGER NOT NULL, "
"state BOOLEAN NOT NULL DEFAULT (True))")
self.cursor.execute(f"CREATE TABLE IF NOT EXISTS stat (group_id INTEGER NOT NULL, {stat_columns})")
self.cursor.execute(f"CREATE TABLE IF NOT EXISTS month (group_id INTEGER NOT NULL, {stat_columns})")
self.cursor.execute("CREATE TABLE IF NOT EXISTS edit (user_id INTEGER NOT NULL)")
self.cursor.execute("CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)")
def get_meta(self, key):
with self.connection:
row = self.cursor.execute("SELECT value FROM meta WHERE key = ?", (key,)).fetchone()
return row[0] if row else None
def set_meta(self, key, value):
with self.connection:
self.cursor.execute("INSERT INTO meta (key, value) VALUES (?, ?) "
"ON CONFLICT(key) DO UPDATE SET value = excluded.value", (key, value))
def reset_month(self):
"""Обнуляем месячную статистику групп"""
with self.connection:
self.cursor.execute("UPDATE month SET " + ", ".join(f"{c} = 0" for c in COLUMNS.values()))
def increment(self, name, group_id, var_id):
"""Счётчик +1 одним запросом: раньше SELECT и UPDATE с ручным +1 теряли сообщения,
пришедшие одновременно"""
column = COLUMNS[var_id]
try:
with self.connection:
self.cursor.execute(f"UPDATE {name} SET {column} = {column} + 1 WHERE group_id = ?", (group_id,))
except sqlite3.Error:
# статистика не должна мешать остальной обработке сообщения
logging.exception("Не удалось обновить статистику %s", name)
# КОМАНДЫ # КОМАНДЫ
def add_group(self, group_id): def add_group(self, group_id):
"""Добавляем нового пользователя""" """Добавляем нового пользователя"""
with self.connection: with self.connection:
self.cursor.execute("INSERT INTO `work` (`group_id`) VALUES(?)", (group_id,)) self.cursor.execute(f"INSERT INTO `work` (`group_id`) VALUES(?)", (group_id,))
self.cursor.execute("INSERT INTO `stat` (`group_id`, `mes`, `rep`, `com`, `url`, `med`, `sti`, " self.cursor.execute(f"INSERT INTO `stat` (`group_id`, `mes`, `rep`, `com`, `url`, `med`, `sti`, "
"`voi`) VALUES(?,?,?,?,?,?,?,?)", (group_id, 0, 0, 0, 0, 0, 0, 0)) f"`voi`) VALUES(?,?,?,?,?,?,?,?)", (group_id, 0, 0, 0, 0, 0, 0, 0))
return return
def group_exists(self, group_id): def group_exists(self, group_id):
"""Проверяем, есть ли уже пользователь в базе""" """Проверяем, есть ли уже пользователь в базе"""
with self.connection: with self.connection:
result = self.cursor.execute('SELECT * FROM `work` WHERE `group_id` = ?', (group_id,)).fetchall() result = self.cursor.execute(f'SELECT * FROM `work` WHERE `group_id` = ?', (group_id,)).fetchall()
return bool(len(result)) return bool(len(result))
# ТАБЛИЦА STAT # ТАБЛИЦА STAT
def update_stat(self, group_id, var_id): def update_stat(self, group_id, var_id):
"""Обновляем статистику""" """Обновляем статистику"""
self.increment("stat", group_id, var_id) try:
with self.connection:
self.cursor.execute(f"SELECT * FROM `stat` WHERE `group_id` = ?", (group_id,))
if var_id == 1:
return self.cursor.execute(f"UPDATE `stat` SET `mes` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[1] + 1), group_id))
elif var_id == 2:
return self.cursor.execute(f"UPDATE `stat` SET `rep` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[2] + 1), group_id))
elif var_id == 3:
return self.cursor.execute(f"UPDATE `stat` SET `com` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[3] + 1), group_id))
elif var_id == 4:
return self.cursor.execute(f"UPDATE `stat` SET `url` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[4] + 1), group_id))
elif var_id == 5:
return self.cursor.execute(f"UPDATE `stat` SET `med` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[5] + 1), group_id))
elif var_id == 6:
return self.cursor.execute(f"UPDATE `stat` SET `sti` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[6] + 1), group_id))
elif var_id == 7:
return self.cursor.execute(f"UPDATE `stat` SET `voi` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[7] + 1), group_id))
except Exception as e:
print(repr(e))
def stat_group(self, group_id): def stat_group(self, group_id):
"""Получение данных пользователя""" """Получение данных пользователя"""
with self.connection: with self.connection:
self.cursor.execute("SELECT * FROM `stat` WHERE `group_id` = ?", (group_id,)) self.cursor.execute(f"SELECT * FROM `stat` WHERE `group_id` = ?", (group_id,))
data = self.cursor.fetchone() data = self.cursor.fetchone()
return data[1:] return data[1:]
@ -96,12 +74,37 @@ class Base:
def update_month_stat(self, group_id, var_id): def update_month_stat(self, group_id, var_id):
"""Обновляем статистику""" """Обновляем статистику"""
self.increment("month", group_id, var_id) try:
with self.connection:
self.cursor.execute(f"SELECT * FROM `month` WHERE `group_id` = ?", (group_id,))
if var_id == 1:
return self.cursor.execute(f"UPDATE `month` SET `mes` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[1] + 1), group_id))
elif var_id == 2:
return self.cursor.execute(f"UPDATE `month` SET `rep` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[2] + 1), group_id))
elif var_id == 3:
return self.cursor.execute(f"UPDATE `month` SET `com` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[3] + 1), group_id))
elif var_id == 4:
return self.cursor.execute(f"UPDATE `month` SET `url` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[4] + 1), group_id))
elif var_id == 5:
return self.cursor.execute(f"UPDATE `month` SET `med` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[5] + 1), group_id))
elif var_id == 6:
return self.cursor.execute(f"UPDATE `month` SET `sti` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[6] + 1), group_id))
elif var_id == 7:
return self.cursor.execute(f"UPDATE `month` SET `voi` = ? WHERE `group_id` = ?",
((self.cursor.fetchone()[7] + 1), group_id))
except Exception as e:
print(repr(e))
def month_stat_group(self, group_id): def month_stat_group(self, group_id):
"""Получение данных пользователя""" """Получение данных пользователя"""
with self.connection: with self.connection:
self.cursor.execute("SELECT * FROM `month` WHERE `group_id` = ?", (group_id,)) self.cursor.execute(f"SELECT * FROM `month` WHERE `group_id` = ?", (group_id,))
data = self.cursor.fetchone() data = self.cursor.fetchone()
return data[1:] return data[1:]
@ -120,19 +123,19 @@ class Base:
def del_edit_user(self, user_id): def del_edit_user(self, user_id):
"""Удаление пользователя""" """Удаление пользователя"""
with self.connection: with self.connection:
return self.cursor.execute('DELETE FROM `edit` WHERE `user_id` = ?', (user_id,)) return self.cursor.execute(f'DELETE FROM `edit` WHERE `user_id` = ?', (user_id,))
# ТАБЛИЦА WORK # ТАБЛИЦА WORK
def check_status(self, group_id): def check_status(self, group_id):
"""Получаем статус""" """Получаем статус"""
with self.connection: with self.connection:
return self.cursor.execute('SELECT `state` FROM `work` WHERE `group_id` = ?', (group_id,)).fetchone()[0] return self.cursor.execute(f'SELECT `state` FROM `work` WHERE `group_id` = ?', (group_id,)).fetchone()[0]
def update_status(self, group_id): def update_status(self, group_id):
"""Обновляем статус""" """Обновляем статус"""
with self.connection: with self.connection:
state = self.cursor.execute('SELECT `state` FROM `work` WHERE `group_id` = ?', (group_id,)).fetchone()[0] state = self.cursor.execute(f'SELECT `state` FROM `work` WHERE `group_id` = ?', (group_id,)).fetchone()[0]
return self.cursor.execute("UPDATE `work` SET `state` = ? WHERE `group_id` = ?", (not state, group_id)) return self.cursor.execute(f"UPDATE `work` SET `state` = ? WHERE `group_id` = ?", (not state, group_id))
# ЗАКРЫТИЕ ВЫЗОВА # ЗАКРЫТИЕ ВЫЗОВА
def close(self): def close(self):
@ -146,33 +149,27 @@ class User:
self.connection = sqlite3.connect(database) self.connection = sqlite3.connect(database)
self.cursor = self.connection.cursor() self.cursor = self.connection.cursor()
with self.connection:
self.cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, "
"user_id INTEGER NOT NULL)")
self.cursor.execute("CREATE TABLE IF NOT EXISTS \"groups\" (id INTEGER PRIMARY KEY AUTOINCREMENT, "
"group_id INTEGER NOT NULL)")
# СВЯЗКА ПОЛЬЗОВАТЕЛЯ # СВЯЗКА ПОЛЬЗОВАТЕЛЯ
def user_exists(self, user_id): def user_exists(self, user_id):
"""Проверяем, есть ли уже пользователь в базе""" """Проверяем, есть ли уже пользователь в базе"""
with self.connection: with self.connection:
result = self.cursor.execute('SELECT * FROM `users` WHERE `user_id` = ?', (user_id,)).fetchall() result = self.cursor.execute(f'SELECT * FROM `users` WHERE `user_id` = ?', (user_id,)).fetchall()
return bool(len(result)) return bool(len(result))
def add_user(self, user_id): def add_user(self, user_id):
"""Добавляем нового пользователя""" """Добавляем нового пользователя"""
with self.connection: with self.connection:
return self.cursor.execute("INSERT INTO `users` (`user_id`) VALUES(?)", (user_id,)) return self.cursor.execute(f"INSERT INTO `users` (`user_id`) VALUES(?)", (user_id,))
def get_user_id(self, user_id): def get_user_id(self, user_id):
"""Получаем короткое айди юзера""" """Получаем короткое айди юзера"""
with self.connection: with self.connection:
return self.cursor.execute('SELECT `id` FROM `users` WHERE `user_id` = ?', (user_id,)).fetchone()[0] return self.cursor.execute(f'SELECT `id` FROM `users` WHERE `user_id` = ?', (user_id,)).fetchone()[0]
def get_first_user_id(self, user_id): def get_first_user_id(self, user_id):
"""Получаем длинное айди юзера""" """Получаем длинное айди юзера"""
with self.connection: with self.connection:
return self.cursor.execute('SELECT `user_id` FROM `users` WHERE `id` = ?', (user_id,)).fetchone()[0] return self.cursor.execute(f'SELECT `user_id` FROM `users` WHERE `id` = ?', (user_id,)).fetchone()[0]
# СВЯЗКА ГРУППЫ # СВЯЗКА ГРУППЫ
def group_exists(self, group_id): def group_exists(self, group_id):
@ -189,12 +186,12 @@ class User:
def get_group_id(self, group_id): def get_group_id(self, group_id):
"""Получаем короткое айди юзера""" """Получаем короткое айди юзера"""
with self.connection: with self.connection:
return self.cursor.execute('SELECT `id` FROM `groups` WHERE `group_id` = ?', (group_id,)).fetchone()[0] return self.cursor.execute(f'SELECT `id` FROM `groups` WHERE `group_id` = ?', (group_id,)).fetchone()[0]
def get_first_group_id(self, group_id): def get_first_group_id(self, group_id):
"""Получаем длинное айди юзера""" """Получаем длинное айди юзера"""
with self.connection: with self.connection:
return self.cursor.execute('SELECT `group_id` FROM `groups` WHERE `id` = ?', (group_id,)).fetchone()[0] return self.cursor.execute(f'SELECT `group_id` FROM `groups` WHERE `id` = ?', (group_id,)).fetchone()[0]
def update_group_id(self, from_id, to_id): def update_group_id(self, from_id, to_id):
"""Заменяем на новый айди""" """Заменяем на новый айди"""
@ -217,7 +214,7 @@ class Group:
def created_group(self, group_id): def created_group(self, group_id):
"""Создаём новую таблицу""" """Создаём новую таблицу"""
with self.connection: with self.connection:
return self.cursor.execute(f"""CREATE TABLE IF NOT EXISTS {table(group_id)} ( return self.cursor.execute(f"""CREATE TABLE IF NOT EXISTS [{group_id}] (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
first_name STRING, first_name STRING,
mes INTEGER, mes INTEGER,
@ -231,57 +228,76 @@ class Group:
def add_user(self, group_id, user_id, name): def add_user(self, group_id, user_id, name):
"""Добавляем нового пользователя""" """Добавляем нового пользователя"""
with self.connection: with self.connection:
return self.cursor.execute(f"INSERT INTO {table(group_id)} (`user_id`, `first_name`, `mes`, `rep`, `com`, `url`, " return self.cursor.execute(f"INSERT INTO `{group_id}` (`user_id`, `first_name`, `mes`, `rep`, `com`, `url`, "
f"`med`, `sti`, `voi`) VALUES(?,?,?,?,?,?,?,?,?)", (user_id, name, 0, 0, 0, 0, 0, f"`med`, `sti`, `voi`) VALUES(?,?,?,?,?,?,?,?,?)", (user_id, name, 0, 0, 0, 0, 0,
0, 0)) 0, 0))
def all_names(self, group_id): def all_names(self, group_id):
"""Список имён""" """Список имён"""
with self.connection: with self.connection:
return self.cursor.execute(f'SELECT `first_name` FROM {table(group_id)}').fetchall() return self.cursor.execute(f'SELECT `first_name` FROM `{group_id}`').fetchall()
def all_ids(self, group_id): def all_ids(self, group_id):
"""Список айди""" """Список айди"""
with self.connection: with self.connection:
return self.cursor.execute(f'SELECT `user_id` FROM {table(group_id)}').fetchall() return self.cursor.execute(f'SELECT `user_id` FROM `{group_id}`').fetchall()
def update_stat(self, user_id, group_id, var_id): def update_stat(self, user_id, group_id, var_id):
"""Обновляем статистику""" """Обновляем статистику"""
column = COLUMNS[var_id]
try: try:
with self.connection: with self.connection:
self.cursor.execute(f"UPDATE {table(group_id)} SET {column} = {column} + 1 WHERE user_id = ?", self.cursor.execute(f"SELECT * FROM `{group_id}` WHERE `user_id` = ?", (user_id,))
(user_id,)) if var_id == 1:
except sqlite3.Error: return self.cursor.execute(f"UPDATE `{group_id}` SET `mes` = ? WHERE `user_id` = ?",
logging.exception("Не удалось обновить статистику участника") ((self.cursor.fetchone()[2] + 1), user_id))
elif var_id == 2:
return self.cursor.execute(f"UPDATE `{group_id}` SET `rep` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[3] + 1), user_id))
elif var_id == 3:
return self.cursor.execute(f"UPDATE `{group_id}` SET `com` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[4] + 1), user_id))
elif var_id == 4:
return self.cursor.execute(f"UPDATE `{group_id}` SET `url` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[5] + 1), user_id))
elif var_id == 5:
return self.cursor.execute(f"UPDATE `{group_id}` SET `med` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[6] + 1), user_id))
elif var_id == 6:
return self.cursor.execute(f"UPDATE `{group_id}` SET `sti` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[7] + 1), user_id))
elif var_id == 7:
return self.cursor.execute(f"UPDATE `{group_id}` SET `voi` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[8] + 1), user_id))
except Exception as e:
print(repr(e))
def user_exists(self, user_id, group_id): def user_exists(self, user_id, group_id):
"""Проверяем, есть ли уже пользователь в базе""" """Проверяем, есть ли уже пользователь в базе"""
with self.connection: with self.connection:
result = self.cursor.execute(f'SELECT * FROM {table(group_id)} WHERE `user_id` = ?', (user_id,)).fetchall() result = self.cursor.execute(f'SELECT * FROM `{group_id}` WHERE `user_id` = ?', (user_id,)).fetchall()
return bool(len(result)) return bool(len(result))
def update_name(self, user_id, group_id, name): def update_name(self, user_id, group_id, name):
"""Обновляем имя пользователя""" """Обновляем имя пользователя"""
with self.connection: with self.connection:
return self.cursor.execute(f"UPDATE {table(group_id)} SET `first_name` = ? WHERE `user_id` = ?", (name, user_id)) return self.cursor.execute(f"UPDATE `{group_id}` SET `first_name` = ? WHERE `user_id` = ?", (name, user_id))
def user_name(self, user_id, group_id): def user_name(self, user_id, group_id):
"""Получаем имя пользователя по айди""" """Получаем имя пользователя по айди"""
with self.connection: with self.connection:
return self.cursor.execute(f"SELECT `first_name` FROM {table(group_id)} WHERE `user_id` = ?", (user_id,)).fetchone()[0] return self.cursor.execute(f"SELECT `first_name` FROM `{group_id}` WHERE `user_id` = ?", (user_id,)).fetchone()[0]
def stat_user(self, user_id, group_id): def stat_user(self, user_id, group_id):
"""Получение данных пользователя""" """Получение данных пользователя"""
with self.connection: with self.connection:
self.cursor.execute(f"SELECT * FROM {table(group_id)} WHERE `user_id` = ?", (user_id,)) self.cursor.execute(f"SELECT * FROM `{group_id}` WHERE `user_id` = ?", (user_id,))
data = self.cursor.fetchone() data = self.cursor.fetchone()
return data[2:] return data[2:]
def del_user(self, group_id, user_id): def del_user(self, group_id, user_id):
"""Удаление пользователя""" """Удаление пользователя"""
with self.connection: with self.connection:
return self.cursor.execute(f'DELETE FROM {table(group_id)} WHERE `user_id` = ?', (user_id,)) return self.cursor.execute(f'DELETE FROM `{group_id}` WHERE `user_id` = ?', (user_id,))
# ЗАКРЫТИЕ ВЫЗОВА # ЗАКРЫТИЕ ВЫЗОВА
def close(self): def close(self):
@ -295,29 +311,40 @@ class Month:
self.connection = sqlite3.connect(database) self.connection = sqlite3.connect(database)
self.cursor = self.connection.cursor() self.cursor = self.connection.cursor()
def reset_all(self):
"""Обнуляем месячную статистику участников во всех чатах"""
with self.connection:
tables = [r[0] for r in self.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table'")]
for name in tables:
if name.lstrip("-").isdigit():
self.cursor.execute(f"UPDATE {table(name)} SET " + ", ".join(f"{c} = 0" for c in COLUMNS.values()))
# КОМАНДЫ # КОМАНДЫ
def update_stat(self, user_id, group_id, var_id): def update_stat(self, user_id, group_id, var_id):
"""Обновляем статистику""" """Обновляем статистику"""
column = COLUMNS[var_id]
try: try:
with self.connection: with self.connection:
self.cursor.execute(f"UPDATE {table(group_id)} SET {column} = {column} + 1 WHERE user_id = ?", self.cursor.execute(f"SELECT * FROM `{group_id}` WHERE `user_id` = ?", (user_id,))
(user_id,)) if var_id == 1:
except sqlite3.Error: return self.cursor.execute(f"UPDATE `{group_id}` SET `mes` = ? WHERE `user_id` = ?",
logging.exception("Не удалось обновить статистику участника") ((self.cursor.fetchone()[1] + 1), user_id))
elif var_id == 2:
return self.cursor.execute(f"UPDATE `{group_id}` SET `rep` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[2] + 1), user_id))
elif var_id == 3:
return self.cursor.execute(f"UPDATE `{group_id}` SET `com` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[3] + 1), user_id))
elif var_id == 4:
return self.cursor.execute(f"UPDATE `{group_id}` SET `url` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[4] + 1), user_id))
elif var_id == 5:
return self.cursor.execute(f"UPDATE `{group_id}` SET `med` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[5] + 1), user_id))
elif var_id == 6:
return self.cursor.execute(f"UPDATE `{group_id}` SET `sti` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[6] + 1), user_id))
elif var_id == 7:
return self.cursor.execute(f"UPDATE `{group_id}` SET `voi` = ? WHERE `user_id` = ?",
((self.cursor.fetchone()[7] + 1), user_id))
except Exception as e:
print(repr(e))
def created_group(self, group_id): def created_group(self, group_id):
"""Создаём новую таблицу""" """Создаём новую таблицу"""
with self.connection: with self.connection:
return self.cursor.execute(f"""CREATE TABLE IF NOT EXISTS {table(group_id)} ( return self.cursor.execute(f"""CREATE TABLE IF NOT EXISTS [{group_id}] (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
mes INTEGER, mes INTEGER,
rep INTEGER, rep INTEGER,
@ -330,31 +357,31 @@ class Month:
def add_user(self, group_id, user_id): def add_user(self, group_id, user_id):
"""Добавляем нового пользователя""" """Добавляем нового пользователя"""
with self.connection: with self.connection:
return self.cursor.execute(f"INSERT INTO {table(group_id)} (`user_id`, `mes`, `rep`, `com`, `url`, `med`, " return self.cursor.execute(f"INSERT INTO `{group_id}` (`user_id`, `mes`, `rep`, `com`, `url`, `med`, "
f"`sti`, `voi`) VALUES(?,?,?,?,?,?,?,?)", (user_id, 0, 0, 0, 0, 0, 0, 0)) f"`sti`, `voi`) VALUES(?,?,?,?,?,?,?,?)", (user_id, 0, 0, 0, 0, 0, 0, 0))
def all_ids(self, group_id): def all_ids(self, group_id):
"""Список айди""" """Список айди"""
with self.connection: with self.connection:
return self.cursor.execute(f'SELECT `user_id` FROM {table(group_id)}').fetchall() return self.cursor.execute(f'SELECT `user_id` FROM `{group_id}`').fetchall()
def user_exists(self, user_id, group_id): def user_exists(self, user_id, group_id):
"""Проверяем, есть ли уже пользователь в базе""" """Проверяем, есть ли уже пользователь в базе"""
with self.connection: with self.connection:
result = self.cursor.execute(f'SELECT * FROM {table(group_id)} WHERE `user_id` = ?', (user_id,)).fetchall() result = self.cursor.execute(f'SELECT * FROM `{group_id}` WHERE `user_id` = ?', (user_id,)).fetchall()
return bool(len(result)) return bool(len(result))
def stat_user(self, user_id, group_id): def stat_user(self, user_id, group_id):
"""Получение данных пользователя""" """Получение данных пользователя"""
with self.connection: with self.connection:
self.cursor.execute(f"SELECT * FROM {table(group_id)} WHERE `user_id` = ?", (user_id,)) self.cursor.execute(f"SELECT * FROM `{group_id}` WHERE `user_id` = ?", (user_id,))
data = self.cursor.fetchone() data = self.cursor.fetchone()
return data[1:] return data[1:]
def del_user(self, group_id, user_id): def del_user(self, group_id, user_id):
"""Удаление пользователя""" """Удаление пользователя"""
with self.connection: with self.connection:
return self.cursor.execute(f'DELETE FROM {table(group_id)} WHERE `user_id` = ?', (user_id,)) return self.cursor.execute(f'DELETE FROM `{group_id}` WHERE `user_id` = ?', (user_id,))
# ЗАКРЫТИЕ ВЫЗОВА # ЗАКРЫТИЕ ВЫЗОВА
def close(self): def close(self):

View file

@ -1,11 +0,0 @@
services:
bot:
build: .
image: ghcr.io/edeev/chatping_abobot:latest
env_file: .env
volumes:
- db:/app/db
restart: unless-stopped
volumes:
db:

BIN
data/voices/1_7047.oga Normal file

Binary file not shown.

BIN
db/base.db Normal file

Binary file not shown.

0
db/month.db Normal file
View file

BIN
db/users.db Normal file

Binary file not shown.

View file

@ -1,3 +0,0 @@
-r requirements.txt
pytest==8.4.2
ruff==0.14.0

View file

@ -1,8 +1,34 @@
aiogram==3.23.0 # Core Framework Dependencies
pymorphy3==2.0.6 aiogram==3.10.0
asyncio-mqtt==0.16.1
# Database Management
sqlite3
# Natural Language Processing
pymorphy3==1.2.1
pymorphy3-dicts-ru==2.4.417150.4580142 pymorphy3-dicts-ru==2.4.417150.4580142
pyenchant==3.2.2 enchant==1.6.6
SpeechRecognition==3.14.4
gTTS==2.5.4 # Speech Recognition & Synthesis
soundfile==0.13.1 SpeechRecognition==3.10.4
requests==2.32.5 gTTS==2.5.1
soundfile==0.12.1
# HTTP Client for API Integration
requests==2.31.0
# Audio Processing Dependencies
pyaudio==0.2.14
pydub==0.25.1
# System Dependencies (if needed)
# Note: Some packages may require system-level installation
# Ubuntu/Debian: sudo apt-get install python3-enchant libenchant-2-2
# macOS: brew install enchant portaudio
# Windows: Download pyaudio wheel from unofficial binaries
# Development Dependencies (Optional)
# pytest==8.2.2
# black==24.4.2
# flake8==7.1.0

View file

@ -1,11 +0,0 @@
import os
import sys
# бот открывает базы по путям ../db относительно папки code — запускаем как в проде, но во временной папке
import tempfile
ROOT = tempfile.mkdtemp()
os.makedirs(os.path.join(ROOT, "code"))
os.chdir(os.path.join(ROOT, "code"))
os.environ.setdefault("BOT_TOKEN", "123456:TEST")
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "code"))

View file

@ -1,65 +0,0 @@
import script
from init import du, dg
def test_md_and_plain():
assert script.md("ivan_petrov *x*") == "ivan\\_petrov \\*x\\*"
assert script.plain("[a]_b*") == "ab"
def test_join_names():
assert script.join_names(["а"]) == "а"
assert script.join_names(["а", "б"]) == "а и б"
assert script.join_names(["а", "б", "в"]) == "а, б и в"
def test_translator_fixes_wrong_layout():
assert script.translator(["ghbdtn", "vbh"]) == "привет мир"
def test_revers_and_lang_form():
assert script.revers("Привет, мир!", True) == "Тевирп, рим!"
assert script.lang_form(["кот"]) == "когот"
def test_stats_counters_and_mentions():
chat, author, other, third = -100500, 1, 2, 3
script.upd_stat(author, chat, 1, "Автор")
script.upd_stat(other, chat, 1, "Иван_")
script.upd_stat(other, chat, 1, "Иван_")
script.upd_stat(third, chat, 2, "Пётр")
group_id = du.get_group_id(chat)
ivan = du.get_user_id(other)
assert dg.stat_user(ivan, group_id)[0] == 2 # два сообщения — счётчик атомарный
# упоминание: спецсимволы Markdown из имени внутри ссылки убираются, автор себя не упоминает
text = script.notice(["иван_", "автор"], False, group_id, author)
assert text == "[Иван](tg://user?id=2), тебя упомянули)"
assert script.notice(["автор"], False, group_id, author).startswith("[Автор]")
every = script.notice(du.get_user_id(author), True, group_id, author)
assert "[Иван](tg://user?id=2) и [Пётр](tg://user?id=3) вас вызывает Автор" == every
def test_bot_imports():
import bot # noqa: F401 — обработчики и роутер собираются без ошибок
def test_month_reset():
from datetime import date
import bot
from init import db, dm
chat, user = -200300, 10
script.upd_stat(user, chat, 1, "Маша")
group_id = du.get_group_id(chat)
member = du.get_user_id(user)
assert bot.reset_month_if_needed(date(2026, 10, 6)) is False # первый запуск — только запомнить
assert dm.stat_user(member, group_id)[0] == 1
assert bot.reset_month_if_needed(date(2026, 10, 20)) is False # тот же месяц
assert bot.reset_month_if_needed(date(2026, 11, 1)) is True
assert dm.stat_user(member, group_id)[0] == 0 and db.month_stat_group(group_id)[0] == 0
assert dg.stat_user(member, group_id)[0] == 1 # статистика за всё время не трогается