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.
9123f57968
...
ef4af56486
26 changed files with 610 additions and 868 deletions
|
|
@ -1,5 +0,0 @@
|
||||||
.git
|
|
||||||
.env
|
|
||||||
db
|
|
||||||
tests
|
|
||||||
__pycache__
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# Токен бота от @BotFather
|
|
||||||
BOT_TOKEN=123456:your-token
|
|
||||||
5
.gitattributes
vendored
5
.gitattributes
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
* text=auto eol=lf
|
|
||||||
*.jpg binary
|
|
||||||
*.ogg binary
|
|
||||||
*.oga binary
|
|
||||||
*.db binary
|
|
||||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
|
@ -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
|
|
||||||
42
.github/workflows/docker.yml
vendored
42
.github/workflows/docker.yml
vendored
|
|
@ -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
5
.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
.env
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
.pytest_cache/
|
|
||||||
db/*.db
|
|
||||||
23
Dockerfile
23
Dockerfile
|
|
@ -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
21
LICENSE
|
|
@ -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.
|
|
||||||
103
README.en.md
103
README.en.md
|
|
@ -1,103 +0,0 @@
|
||||||
# AboBot
|
|
||||||
|
|
||||||
[Русский](README.md) · **English**
|
|
||||||
|
|
||||||
[](https://github.com/EDeev/chatping_abobot/actions/workflows/ci.yml)
|
|
||||||
[](https://github.com/EDeev/chatping_abobot/actions/workflows/docker.yml)
|
|
||||||
[](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
199
README.md
|
|
@ -1,100 +1,153 @@
|
||||||
# AboBot
|
# 🤖 ABOBOT - Telegram Group Management Bot
|
||||||
|
|
||||||
**Русский** · [English](README.en.md)
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
[](https://github.com/EDeev/chatping_abobot/actions/workflows/ci.yml)
|
Многофункциональный Telegram-бот для управления групповыми чатами с расширенной аналитикой и интерактивными возможностями.
|
||||||
[](https://github.com/EDeev/chatping_abobot/actions/workflows/docker.yml)
|
|
||||||
[](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>
|
||||||
|
|
|
||||||
54
code/base.py
54
code/base.py
|
|
@ -1,28 +1,26 @@
|
||||||
import os
|
# ДАННЫЕ БОТА
|
||||||
|
TOKEN = "XXXXXXXXXX" # @chat_abobot
|
||||||
# ДАННЫЕ БОТА (из переменных окружения)
|
DEBAG = "XXXXXXXXXX" # Технический чат
|
||||||
TOKEN = os.getenv("BOT_TOKEN", "XXXXXXXXXX") # @chat_abobot
|
|
||||||
DEBAG = os.getenv("DEBUG_CHAT_ID", "") # Технический чат
|
TEX_GROUP = "XXXXXXXXXX"
|
||||||
|
|
||||||
TEX_GROUP = os.getenv("TECH_GROUP_ID", "")
|
# ОБРАЩЕНИЯ К ПОЛЬЗОВАТЕЛЯМ
|
||||||
|
KILL_LIST = ["побить", "отмудохать", "избить", "уебать", "отметелить"]
|
||||||
# ОБРАЩЕНИЯ К ПОЛЬЗОВАТЕЛЯМ
|
TMOK_LIST = ["поцеловать", "чмокнуть", "соснуть", "лайкнуть"]
|
||||||
KILL_LIST = ["побить", "отмудохать", "избить", "уебать", "отметелить"]
|
QUAT_LIST = [['дай', 'дайте'], ['ахуенный', 'ахуеный', 'охуеный', 'охуенный', 'блять', 'сука'], ['совет', 'совета']]
|
||||||
TMOK_LIST = ["поцеловать", "чмокнуть", "соснуть", "лайкнуть"]
|
|
||||||
QUAT_LIST = [['дай', 'дайте'], ['ахуенный', 'ахуеный', 'охуеный', 'охуенный', 'блять', 'сука'], ['совет', 'совета']]
|
# ФРАЗЫ ИВЕНТ ДРАКИ
|
||||||
|
VAR_WIN = ["опустив его ниже плинтуса!", "поставив его на место!"]
|
||||||
# ФРАЗЫ ИВЕНТ ДРАКИ
|
VAR_LOSE = ["Кунг - Фу!", "магии!", "резинового члена!", "банды цапель!"]
|
||||||
VAR_WIN = ["опустив его ниже плинтуса!", "поставив его на место!"]
|
|
||||||
VAR_LOSE = ["Кунг - Фу!", "магии!", "резинового члена!", "банды цапель!"]
|
# ПЕРЕВОДЧИК СЛОВ
|
||||||
|
ERR = ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';',
|
||||||
# ПЕРЕВОДЧИК СЛОВ
|
"'", 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', '`']
|
||||||
ERR = ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';',
|
ERR_ = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':',
|
||||||
"'", 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', '`']
|
'"', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', '~']
|
||||||
ERR_ = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':',
|
TRU = ['й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж',
|
||||||
'"', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', '~']
|
'э', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', '.']
|
||||||
TRU = ['й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж',
|
TRU_ = ['Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж',
|
||||||
'э', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', '.']
|
'Э', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю', ',']
|
||||||
TRU_ = ['Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж',
|
|
||||||
'Э', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю', ',']
|
ALB = ['а', 'у', 'о', 'ы', 'и', 'э', 'я', 'ю', 'ё', 'е', 'А', 'У', 'О', 'Ы', 'И', 'Э', 'Я', 'Ю', 'Ё', 'Е']
|
||||||
|
|
||||||
ALB = ['а', 'у', 'о', 'ы', 'и', 'э', 'я', 'ю', 'ё', 'е', 'А', 'У', 'О', 'Ы', 'И', 'Э', 'Я', 'Ю', 'Ё', 'Е']
|
|
||||||
|
|
|
||||||
33
code/bot.py
33
code/bot.py
|
|
@ -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__':
|
||||||
|
|
|
||||||
238
code/handlers.py
238
code/handlers.py
|
|
@ -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
|
num = random.randint(1000, 9999)
|
||||||
|
audio = f"../data/voices/{user_id}_{num}.oga"
|
||||||
|
|
||||||
group_id = du.get_group_id(message.chat.id)
|
file_id = message.voice.file_id
|
||||||
unsigned = re.sub(r'[^\w\s]', '', query.lower()).split()
|
file = await bot.get_file(file_id)
|
||||||
first_form = [morph.parse(i)[0].normal_form for i in unsigned]
|
file_path = file.file_path
|
||||||
chat_names = [x[0] for x in dg.all_names(group_id)]
|
await bot.download_file(file_path, audio)
|
||||||
names = [_ for _ in first_form if _ in chat_names]
|
|
||||||
|
|
||||||
text = notice(names, False, group_id, message.from_user.id) if names else None
|
data, samplerate = sf.read(audio)
|
||||||
if text:
|
os.remove(audio)
|
||||||
await message.reply(text)
|
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
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
357
code/script.py
357
code/script.py
|
|
@ -1,189 +1,170 @@
|
||||||
import re
|
from base import ERR, ERR_, TRU, TRU_, ALB
|
||||||
from string import digits, punctuation
|
from string import digits, punctuation
|
||||||
|
import re, sql
|
||||||
from base import ERR, ERR_, TRU, TRU_, ALB
|
|
||||||
# одни и те же соединения с базами, что и у обработчиков (раньше здесь открывались вторые)
|
# инициализируем соединение с БД
|
||||||
from init import db, du, dg, dm
|
db = sql.Base('../db/base.db')
|
||||||
|
du = sql.User('../db/users.db')
|
||||||
MD_SPECIAL = "_*`["
|
dg = sql.Group('../db/groups.db')
|
||||||
|
dm = sql.Month('../db/month.db')
|
||||||
|
|
||||||
def md(text):
|
|
||||||
"""Экранирование для Markdown вне разметки: «_» или «*» в имени или тексте иначе ломают отправку"""
|
def upd_stat(user_id, group_id, var_id, name, mes=False):
|
||||||
return "".join("\\" + ch if ch in MD_SPECIAL else ch for ch in str(text))
|
name = re.sub(r'[^\w\s]', '', name.lower())
|
||||||
|
if not du.user_exists(user_id):
|
||||||
|
du.add_user(user_id)
|
||||||
def plain(text):
|
user_id = du.get_user_id(user_id)
|
||||||
"""Текст внутри *жирного* или [ссылки]: экранирование там не работает, спецсимволы убираются"""
|
|
||||||
return "".join(ch for ch in str(text) if ch not in MD_SPECIAL + "]")
|
if not du.group_exists(group_id):
|
||||||
|
du.add_group(group_id)
|
||||||
|
group_id = du.get_group_id(group_id)
|
||||||
def join_names(items):
|
|
||||||
"""«а», «а и б», «а, б и в»"""
|
if not db.group_exists(group_id):
|
||||||
if len(items) <= 1:
|
db.add_group(group_id)
|
||||||
return "".join(items)
|
dg.created_group(group_id)
|
||||||
return ", ".join(items[:-1]) + " и " + items[-1]
|
|
||||||
|
if not dg.user_exists(user_id, group_id):
|
||||||
|
dg.add_user(group_id, user_id, name)
|
||||||
def upd_stat(user_id, group_id, var_id, name, mes=False):
|
|
||||||
name = re.sub(r'[^\w\s]', '', name.lower())
|
db.update_stat(group_id, var_id)
|
||||||
if not du.user_exists(user_id):
|
dg.update_stat(user_id, group_id, var_id)
|
||||||
du.add_user(user_id)
|
|
||||||
user_id = du.get_user_id(user_id)
|
if not db.group_exists_month(group_id):
|
||||||
|
db.add_group_month(group_id)
|
||||||
if not du.group_exists(group_id):
|
dm.created_group(group_id)
|
||||||
du.add_group(group_id)
|
dm.add_user(group_id, user_id)
|
||||||
group_id = du.get_group_id(group_id)
|
else:
|
||||||
|
if not dm.user_exists(user_id, group_id):
|
||||||
if not db.group_exists(group_id):
|
dm.add_user(group_id, user_id)
|
||||||
db.add_group(group_id)
|
|
||||||
dg.created_group(group_id)
|
db.update_month_stat(group_id, var_id)
|
||||||
|
dm.update_stat(user_id, group_id, var_id)
|
||||||
if not dg.user_exists(user_id, group_id):
|
|
||||||
dg.add_user(group_id, user_id, name)
|
if mes:
|
||||||
|
db.update_month_stat(group_id, 1)
|
||||||
db.update_stat(group_id, var_id)
|
dg.update_stat(user_id, group_id, 1)
|
||||||
dg.update_stat(user_id, group_id, var_id)
|
dm.update_stat(user_id, group_id, 1)
|
||||||
|
db.update_stat(group_id, 1)
|
||||||
if not db.group_exists_month(group_id):
|
|
||||||
db.add_group_month(group_id)
|
if not db.edit_user_exists(user_id):
|
||||||
dm.created_group(group_id)
|
dg.update_name(user_id, group_id, name)
|
||||||
dm.add_user(group_id, user_id)
|
|
||||||
else:
|
|
||||||
if not dm.user_exists(user_id, group_id):
|
def lang_form(text, smbl='г'):
|
||||||
dm.add_user(group_id, user_id)
|
for i in range(len(text)):
|
||||||
|
word = [str(j) + str(smbl) + str(j).lower() if j in ALB else str(j) for j in text[i]]
|
||||||
db.update_month_stat(group_id, var_id)
|
text[i] = ''.join(word)
|
||||||
dm.update_stat(user_id, group_id, var_id)
|
return ' '.join(text)
|
||||||
|
|
||||||
if mes:
|
|
||||||
db.update_month_stat(group_id, 1)
|
def notice(name, all_users, group_id, author):
|
||||||
dg.update_stat(user_id, group_id, 1)
|
names = [i[0] for i in dg.all_names(group_id)]
|
||||||
dm.update_stat(user_id, group_id, 1)
|
ids = [du.get_first_user_id(i[0]) for i in dg.all_ids(group_id)]
|
||||||
db.update_stat(group_id, 1)
|
|
||||||
|
if all_users:
|
||||||
if not db.edit_user_exists(user_id):
|
name = dg.user_name(name, group_id)
|
||||||
dg.update_name(user_id, group_id, name)
|
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]
|
||||||
def lang_form(text, smbl='г'):
|
return f'{", ".join(usr)} вас вызывает {name.title()}'
|
||||||
for i in range(len(text)):
|
else:
|
||||||
word = [str(j) + str(smbl) + str(j).lower() if j in ALB else str(j) for j in text[i]]
|
no_copy = []
|
||||||
text[i] = ''.join(word)
|
for i in name:
|
||||||
return ' '.join(text)
|
if i not in no_copy:
|
||||||
|
no_copy.append(i)
|
||||||
|
|
||||||
def notice(name, all_users, group_id, author):
|
if len(no_copy) > 1: # ПЕРЕПИСАТЬ КОД ГАВНА КУСОК
|
||||||
names = [i[0] for i in dg.all_names(group_id)]
|
usr = [f"[{_.title()}](tg://user?id={str(ids[names.index(_)])})" for _ in no_copy if int(author) != ids[names.index(_)]]
|
||||||
ids = [du.get_first_user_id(i[0]) for i in dg.all_ids(group_id)]
|
usr.append(f'{usr[-2]} и {usr[-1]}')
|
||||||
|
del usr[-2], usr[-2]
|
||||||
if all_users:
|
return f"{', '.join(usr)} вас упомянули)"
|
||||||
name = dg.user_name(name, group_id)
|
else:
|
||||||
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"[{no_copy[0].title()}](tg://user?id={str(ids[names.index(no_copy[0])])})"
|
||||||
if len(usr) < 2:
|
return f"{usr}, тебя упомянули)"
|
||||||
raise ValueError("в группе меньше трёх участников")
|
|
||||||
return f'{join_names(usr)} вас вызывает {md(name.title())}'
|
|
||||||
else:
|
def checker(signs, words, group_id, user_id, name):
|
||||||
no_copy = []
|
if len([z for z in signs if (z in digits) or (z in punctuation)]) == len(signs) or signs[0] == '/':
|
||||||
for i in name:
|
return int('-1')
|
||||||
if i not in no_copy:
|
|
||||||
no_copy.append(i)
|
if len(signs) > 4:
|
||||||
|
for x in range(3, len(signs)):
|
||||||
if len(no_copy) > 1:
|
http = "".join([signs[x - 3], signs[x - 2], signs[x - 1], signs[x]])
|
||||||
# раньше при двух именах, одно из которых — автор, падало на usr[-2]
|
if 'http' in http:
|
||||||
usr = [f"[{plain(_.title())}](tg://user?id={str(ids[names.index(_)])})" for _ in no_copy if int(author) != ids[names.index(_)]]
|
upd_stat(user_id, group_id, 4, name)
|
||||||
if len(usr) > 1:
|
return int('-1')
|
||||||
return f"{join_names(usr)} вас упомянули)"
|
|
||||||
if usr:
|
count, raw = 0, 0
|
||||||
return f"{usr[0]}, тебя упомянули)"
|
for word in words:
|
||||||
return None
|
for symbol in word:
|
||||||
else:
|
raw += 1
|
||||||
usr = f"[{plain(no_copy[0].title())}](tg://user?id={str(ids[names.index(no_copy[0])])})"
|
if (symbol in ERR) or (symbol in ERR_) or (symbol in digits):
|
||||||
return f"{usr}, тебя упомянули)"
|
count += 1
|
||||||
|
return len(signs) - raw + count
|
||||||
|
|
||||||
def checker(signs, words, group_id, user_id, name):
|
|
||||||
if len([z for z in signs if (z in digits) or (z in punctuation)]) == len(signs) or signs[0] == '/':
|
def translator(words):
|
||||||
return int('-1')
|
itg = []
|
||||||
|
for word in words:
|
||||||
if len(signs) > 4:
|
raw_word = []
|
||||||
for x in range(3, len(signs)):
|
for symbol in word:
|
||||||
http = "".join([signs[x - 3], signs[x - 2], signs[x - 1], signs[x]])
|
if symbol in ERR:
|
||||||
if 'http' in http:
|
count = 0
|
||||||
upd_stat(user_id, group_id, 4, name)
|
for _ in ERR:
|
||||||
return int('-1')
|
if symbol == _:
|
||||||
|
raw_word.append(TRU[count])
|
||||||
count, raw = 0, 0
|
break
|
||||||
for word in words:
|
count += 1
|
||||||
for symbol in word:
|
elif symbol in ERR_:
|
||||||
raw += 1
|
count = 0
|
||||||
if (symbol in ERR) or (symbol in ERR_) or (symbol in digits):
|
for _ in ERR_:
|
||||||
count += 1
|
if symbol == _:
|
||||||
return len(signs) - raw + count
|
raw_word.append(TRU_[count])
|
||||||
|
break
|
||||||
|
count += 1
|
||||||
def translator(words):
|
itg.append("".join(raw_word))
|
||||||
itg = []
|
return " ".join(itg)
|
||||||
for word in words:
|
|
||||||
raw_word = []
|
|
||||||
for symbol in word:
|
def revers(message, var):
|
||||||
if symbol in ERR:
|
no_pct = re.sub(r'[^\w\s]', '', message)
|
||||||
count = 0
|
|
||||||
for _ in ERR:
|
if var:
|
||||||
if symbol == _:
|
sml, pnc, pct, prf = [i for i in message] + [str(0)], [], [], False
|
||||||
raw_word.append(TRU[count])
|
for i in sml:
|
||||||
break
|
if i in punctuation or i == ' ':
|
||||||
count += 1
|
pnc.append(i)
|
||||||
elif symbol in ERR_:
|
prf, flag = True if sml.index(i) == 0 else False, False
|
||||||
count = 0
|
else:
|
||||||
for _ in ERR_:
|
flag = True
|
||||||
if symbol == _:
|
|
||||||
raw_word.append(TRU_[count])
|
if flag and len(pnc) > 0:
|
||||||
break
|
pct.append(''.join(pnc))
|
||||||
count += 1
|
pnc = []
|
||||||
itg.append("".join(raw_word))
|
|
||||||
return " ".join(itg)
|
wrd, rev, lst, txt = no_pct.split(), [], [], []
|
||||||
|
for i in wrd:
|
||||||
|
word, up, itg = [i[-1 - l].lower() for l in range(len(i))], [], []
|
||||||
def revers(message, var):
|
|
||||||
no_pct = re.sub(r'[^\w\s]', '', message)
|
for _ in i:
|
||||||
|
up.append(True if _ in TRU_ else False)
|
||||||
if var:
|
for j in range(len(up)):
|
||||||
sml, pnc, pct, prf = [i for i in message] + [str(0)], [], [], False
|
itg.append(word[j].upper() if up[j] else word[j])
|
||||||
for i in sml:
|
|
||||||
if i in punctuation or i == ' ':
|
rev.append(''.join(itg))
|
||||||
pnc.append(i)
|
|
||||||
prf, flag = True if sml.index(i) == 0 else False, False
|
if prf:
|
||||||
else:
|
for i in range(len(pct)):
|
||||||
flag = True
|
txt = (txt + [pct[i]]) if (i + 1) == len(pct) and len(pct) > len(rev) else (txt + [pct[i], rev[i]])
|
||||||
|
return ''.join(txt)
|
||||||
if flag and len(pnc) > 0:
|
else:
|
||||||
pct.append(''.join(pnc))
|
for i in range(len(rev)):
|
||||||
pnc = []
|
if i > 0:
|
||||||
|
txt.append(pct[i - 1])
|
||||||
wrd, rev, txt = no_pct.split(), [], []
|
txt.append(rev[i])
|
||||||
for i in wrd:
|
if (i + 1) == len(rev) and len(pct) == len(rev):
|
||||||
word, up, itg = [i[-1 - l].lower() for l in range(len(i))], [], []
|
txt.append(pct[i])
|
||||||
|
return ''.join(txt)
|
||||||
for _ in i:
|
else:
|
||||||
up.append(True if _ in TRU_ else False)
|
sml = [i for i in message]
|
||||||
for j in range(len(up)):
|
|
||||||
itg.append(word[j].upper() if up[j] else word[j])
|
|
||||||
|
|
||||||
rev.append(''.join(itg))
|
|
||||||
|
|
||||||
if prf:
|
|
||||||
for i in range(len(pct)):
|
|
||||||
txt = (txt + [pct[i]]) if (i + 1) == len(pct) and len(pct) > len(rev) else (txt + [pct[i], rev[i]])
|
|
||||||
return ''.join(txt)
|
|
||||||
else:
|
|
||||||
for i in range(len(rev)):
|
|
||||||
if i > 0:
|
|
||||||
txt.append(pct[i - 1])
|
|
||||||
txt.append(rev[i])
|
|
||||||
if (i + 1) == len(rev) and len(pct) == len(rev):
|
|
||||||
txt.append(pct[i])
|
|
||||||
return ''.join(txt)
|
|
||||||
else:
|
|
||||||
sml = [i for i in message]
|
|
||||||
return ''.join([sml[-1 - i] for i in range(len(sml))])
|
return ''.join([sml[-1 - i] for i in range(len(sml))])
|
||||||
235
code/sql.py
235
code/sql.py
|
|
@ -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):
|
||||||
|
|
|
||||||
11
compose.yaml
11
compose.yaml
|
|
@ -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
BIN
data/voices/1_7047.oga
Normal file
Binary file not shown.
BIN
db/base.db
Normal file
BIN
db/base.db
Normal file
Binary file not shown.
0
db/month.db
Normal file
0
db/month.db
Normal file
BIN
db/users.db
Normal file
BIN
db/users.db
Normal file
Binary file not shown.
|
|
@ -1,3 +0,0 @@
|
||||||
-r requirements.txt
|
|
||||||
pytest==8.4.2
|
|
||||||
ruff==0.14.0
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
|
||||||
|
|
@ -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 # статистика за всё время не трогается
|
|
||||||
Loading…
Add table
Reference in a new issue