41 Commits

Author SHA1 Message Date
8bb098764d Refactor module imports to ensure project root is included in sys.path for both run_helper and voice_bot. Enhance database user insertion with backward compatibility for emoji column. Update user info handling to check username and full name more robustly. Improve emoji handling with fallback options for environments lacking the emoji package. 2025-08-26 18:56:47 +03:00
70d6ad9a6e Merge remote-tracking branch 'origin/voice-1' into merge-voice-1 2025-08-26 18:27:20 +03:00
ANDREY KATYKHIN
94ece4e307 Merge pull request #7 from KerradKerridi/dev-4
Dev 4
2025-08-26 17:41:36 +03:00
61dd85035d Update log retention period to 30 days in custom logger and add comment on log cleanup mechanism. 2025-08-26 17:23:20 +03:00
fee22f8ad4 Enhance database handling and improve HTML safety across the bot. Added async methods for blacklist checks, updated connection settings for SQLite, and implemented HTML escaping for user inputs and messages to prevent potential issues. Adjusted middleware latency and refactored various handlers for better performance and reliability. 2025-08-26 16:51:28 +03:00
7b6abe2a0e WIP: Temporary commit for branch move 2025-08-26 02:14:11 +03:00
Andrey
0c29609e4a hot fix 2024-11-17 22:43:41 +03:00
Andrey
cb0f94c718 some fix 2024-11-17 22:10:45 +03:00
b2c27040aa some fix with emoji function 2024-11-17 22:09:37 +03:00
Andrey
47c5b2f083 hotfix 2024-11-17 01:20:51 +03:00
Andrey
e0e0a6de51 add new func in voice bot 2024-11-17 00:50:55 +03:00
502c07a2c9 some fix 2024-11-16 18:45:05 +03:00
Andrey
ee9eafa09f some fix 2024-11-14 00:24:37 +03:00
ANDREY KATYKHIN
b8b92434ff Merge pull request #5 from KerradKerridi/dev-3
refactor voice_bot
2024-11-14 00:06:13 +03:00
Andrey
767acacc18 refactor voice_bot 2024-11-14 00:05:37 +03:00
ANDREY KATYKHIN
bc454fce8c Merge pull request #4 from KerradKerridi/dev-3
fix with html tags
2024-10-31 22:00:09 +03:00
5c3fece394 fix with html tags 2024-10-31 21:58:46 +03:00
ANDREY KATYKHIN
198b522976 Merge pull request #3 from KerradKerridi/dev-3
Dev-3 Добавил новые функции MediaGroup, разные типы файлов, ответы пользователю
2024-07-21 23:24:44 +05:00
Andrey
5050767b60 add new func for ban 2024-07-21 21:22:45 +03:00
Andrey
33fa84943d add answer for user 2024-07-21 13:24:18 +03:00
KatykhinAA
0704e6b3fe all type message can process 2024-07-20 22:41:48 +03:00
KatykhinAA
0b7f718f8a refactor MediaGroup. Add database 2024-07-20 16:54:43 +03:00
KatykhinAA
54234e59ec some fix 2024-07-20 14:18:58 +03:00
KatykhinAA
095e0398d0 some fix 2024-07-18 23:30:41 +03:00
KatykhinAA
a3b53d26e2 add mediaGroup func 2024-07-17 00:15:15 +03:00
KatykhinAA
88889fe87c mediaGroup start work 2024-07-16 22:25:32 +03:00
Andrey
3a30edc1ab HOT_FIX_3 add blacklist
fix db init
2024-07-16 21:55:22 +03:00
Andrey
09a071c014 HOT_FIX_2 for handle_start_message 2024-07-15 23:46:05 +03:00
Andrey
01f6cbd37d HOT FIX for handle_start_message 2024-07-15 23:10:32 +03:00
ANDREY KATYKHIN
3cb487b617 Merge pull request #2 from KerradKerridi/dev-1
Dev-1
2024-07-16 00:36:40 +05:00
KatykhinAA
f7f6fa5482 some fix 2024-07-15 22:28:36 +03:00
KatykhinAA
1a0344d0e8 migrate to aiogram 3.10.0 2024-07-14 21:22:33 +03:00
KatykhinAA
24ac638433 some fix 2024-07-13 12:42:21 +03:00
KatykhinAA
f2dd9f4b42 refactor suggest, media_group doesn't work 2024-07-13 01:23:48 +03:00
KatykhinAA
47050fd599 add logging, refactor 2024-07-11 22:59:58 +03:00
KatykhinAA
66f3a870c4 some refactor 2024-07-11 00:32:19 +03:00
KatykhinAA
f2e44ddb29 add tests, some fixes 2024-07-10 23:18:36 +03:00
KatykhinAA
7860d4f5c0 add logging to db 2024-07-06 22:06:44 +03:00
Andrey
9788a763c4 some fix 2024-07-06 20:53:33 +03:00
Andrey
99eb07f74e some fix 2024-07-06 15:15:40 +03:00
ANDREY KATYKHIN
e27d565222 Merge pull request #1 from KerradKerridi/dev
Добавил новые функции
2024-07-06 13:07:34 +03:00
64 changed files with 7272 additions and 1488 deletions

37
.dockerignore Normal file
View File

@@ -0,0 +1,37 @@
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.so
*.egg-info/
.eggs/
.env
.venv
.vscode/
.idea/
.git/
.gitignore
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.pyc
**/*.pyo
**/*.pyd
# Local settings
settings_example.ini
# Databases and runtime files
*.db
*.db-shm
*.db-wal
logs/
# Tests and artifacts
.coverage
.pytest_cache/
htmlcov/
**/tests/
# Stickers and large assets (if not needed at runtime)
Stick/

44
.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
/database/tg-bot-database.db
/database/tg-bot-database.db-shm
/database/tg-bot-database.db-wal
/database/test.db
/database/test.db-shm
/database/test.db-wal
/settings.ini
/myenv/
/venv/
/.idea/
/logs/*.log
# Testing and coverage files
.coverage
coverage.xml
htmlcov/
.pytest_cache/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
# Test database files
database/test.db
test.db
*.db
# IDE and editor files
.vscode/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
PERFORMANCE_IMPROVEMENTS.md

37
Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
# Use a lightweight Python image
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered logs
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Install system dependencies (if required by Python packages)
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Create non-root user
RUN useradd -m appuser \
&& chown -R appuser:appuser /app
# Install Python dependencies first for better layer caching
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy project files
COPY . .
# Ensure runtime directories exist and are writable
RUN mkdir -p logs database \
&& chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Run the bot
CMD ["python", "run_helper.py"]

73
Makefile Normal file
View File

@@ -0,0 +1,73 @@
.PHONY: help test test-db test-coverage test-html clean install
# Default target
help:
@echo "Available commands:"
@echo " install - Install dependencies"
@echo " test - Run all tests"
@echo " test-db - Run database tests only"
@echo " test-bot - Run bot startup and handler tests only"
@echo " test-media - Run media handler tests only"
@echo " test-errors - Run error handling tests only"
@echo " test-utils - Run utility functions tests only"
@echo " test-keyboards - Run keyboard and filter tests only"
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
@echo " test-html - Run tests and generate HTML coverage report"
@echo " clean - Clean up generated files"
@echo " coverage - Show coverage report only"
# Install dependencies
install:
python3 -m pip install -r requirements.txt
python3 -m pip install pytest-cov
# Run all tests
test:
python3 -m pytest tests/ -v
# Run database tests only
test-db:
python3 -m pytest tests/test_db.py -v
# Run bot tests only
test-bot:
python3 -m pytest tests/test_bot.py -v
# Run media handler tests only
test-media:
python3 -m pytest tests/test_media_handlers.py -v
# Run error handling tests only
test-errors:
python3 -m pytest tests/test_error_handling.py -v
# Run utils tests only
test-utils:
python3 -m pytest tests/test_utils.py -v
# Run keyboard and filter tests only
test-keyboards:
python3 -m pytest tests/test_keyboards_and_filters.py -v
# Run tests with coverage
test-coverage:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
# Run tests and generate HTML coverage report
test-html:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=html:htmlcov --cov-report=term
@echo "HTML coverage report generated in htmlcov/index.html"
# Show coverage report only
coverage:
python3 -m coverage report --include="helper_bot/*,database/*"
# Clean up generated files
clean:
rm -rf htmlcov/
rm -f coverage.xml
rm -f .coverage
rm -f database/test.db
rm -f test.db
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete

259
README_TESTING.md Normal file
View File

@@ -0,0 +1,259 @@
# Тестирование Telegram Helper Bot
Этот документ описывает систему тестирования для Telegram Helper Bot.
## Структура тестов
Тесты организованы в следующие файлы:
- `tests/test_bot.py` - Основные тесты бота (запуск, хэндлеры, интеграция)
- `tests/test_media_handlers.py` - Тесты обработки медиа-контента
- `tests/test_error_handling.py` - Тесты обработки ошибок и граничных случаев
- `tests/test_utils.py` - Тесты утилит и вспомогательных функций
- `tests/test_keyboards_and_filters.py` - Тесты клавиатур и фильтров
- `tests/test_db.py` - Тесты базы данных
- `tests/conftest.py` - Общие фикстуры и конфигурация
## Установка зависимостей
```bash
make install
```
## Запуск тестов
### Все тесты
```bash
make test
```
### Отдельные категории тестов
```bash
# Тесты базы данных
make test-db
# Тесты бота (запуск и хэндлеры)
make test-bot
# Тесты обработки медиа
make test-media
# Тесты обработки ошибок
make test-errors
# Тесты утилит
make test-utils
# Тесты клавиатур и фильтров
make test-keyboards
```
### Тесты с покрытием
```bash
# Покрытие с выводом в терминал
make test-coverage
# Покрытие с HTML отчетом
make test-html
```
### Фильтрация тестов
```bash
# Только unit тесты
pytest -m unit
# Только интеграционные тесты
pytest -m integration
# Только асинхронные тесты
pytest -m asyncio
# Исключить медленные тесты
pytest -m "not slow"
# Конкретный файл тестов
pytest tests/test_bot.py
# Конкретный тест
pytest tests/test_bot.py::TestBotStartup::test_bot_initialization
```
## Типы тестов
### Unit тесты
Тестируют отдельные функции и компоненты в изоляции:
- Вспомогательные функции (`get_first_name`, `get_text_message`)
- Утилиты (`BaseDependencyFactory`, `get_message`)
- Фильтры (`ChatTypeFilter`)
- Клавиатуры
### Интеграционные тесты
Тестируют взаимодействие между компонентами:
- Регистрация роутеров в диспетчере
- Обработка сообщений через хэндлеры
- Интеграция с базой данных
### Асинхронные тесты
Тестируют асинхронные функции:
- Хэндлеры сообщений
- Запуск бота
- Обработка медиа-контента
## Моки и фикстуры
### Основные фикстуры
- `mock_message` - Мок сообщения Telegram
- `mock_state` - Мок состояния FSM
- `mock_db` - Мок базы данных
- `mock_bot` - Мок бота
- `mock_dispatcher` - Мок диспетчера
- `mock_factory` - Мок фабрики зависимостей
### Специализированные фикстуры
- `sample_photo_message` - Сообщение с фото
- `sample_video_message` - Сообщение с видео
- `sample_audio_message` - Сообщение с аудио
- `sample_voice_message` - Голосовое сообщение
- `sample_video_note_message` - Видеокружок
- `sample_media_group` - Медиагруппа
- `sample_text_message` - Текстовое сообщение
## Покрытие тестами
### Основные компоненты
- ✅ Запуск бота (`start_bot`)
- ✅ Приватные хэндлеры (`handle_start_message`, `suggest_post`, etc.)
- ✅ Обработка медиа-контента (фото, видео, аудио, голос)
- ✅ Обработка ошибок и исключений
- ✅ Утилиты и вспомогательные функции
- ✅ Клавиатуры и фильтры
- ✅ Фабрика зависимостей
### Тестируемые сценарии
- ✅ Новые пользователи
- ✅ Существующие пользователи
- ✅ Пользователи без username
- ✅ Обработка различных типов контента
- ✅ Медиагруппы
- ✅ Ошибки при получении стикеров
- ✅ Ошибки базы данных
- ✅ Граничные случаи (пустой текст, отсутствие подписей)
## Настройка окружения
### Переменные окружения
Для тестов не требуются реальные токены бота или подключения к базе данных, так как все внешние зависимости замоканы.
### Конфигурация pytest
Настройки pytest находятся в файле `pytest.ini`:
- Автоматический режим asyncio
- Фильтрация предупреждений
- Маркеры для категоризации тестов
## Добавление новых тестов
### Структура теста
```python
@pytest.mark.asyncio
async def test_function_name(mock_message, mock_state, mock_db):
"""Описание теста"""
# Arrange (подготовка)
mock_message.text = "test"
# Act (действие)
result = await function_to_test(mock_message, mock_state)
# Assert (проверка)
assert result is True
mock_message.answer.assert_called_once()
```
### Маркировка тестов
```python
@pytest.mark.unit # Unit тест
@pytest.mark.integration # Интеграционный тест
@pytest.mark.asyncio # Асинхронный тест
@pytest.mark.slow # Медленный тест
```
### Использование фикстур
```python
def test_with_fixtures(mock_message, sample_photo_message, mock_db):
# Используем готовые фикстуры
pass
```
## Отладка тестов
### Подробный вывод
```bash
pytest -v -s
```
### Остановка на первой ошибке
```bash
pytest -x
```
### Вывод полного traceback
```bash
pytest --tb=long
```
### Запуск конкретного теста
```bash
pytest tests/test_bot.py::TestPrivateHandlers::test_handle_start_message_new_user -v
```
## CI/CD интеграция
Тесты могут быть интегрированы в CI/CD pipeline:
```yaml
# Пример для GitHub Actions
- name: Run tests
run: |
make install
make test-coverage
```
## Покрытие кода
Для просмотра покрытия кода:
```bash
make test-html
# Открыть htmlcov/index.html в браузере
```
## Лучшие практики
1. **Изоляция тестов** - каждый тест должен быть независимым
2. **Использование моков** - избегайте реальных внешних зависимостей
3. **Описательные имена** - имена тестов должны описывать что тестируется
4. **Arrange-Act-Assert** - структурируйте тесты по этому паттерну
5. **Фикстуры** - используйте фикстуры для переиспользования кода
6. **Маркировка** - правильно маркируйте тесты для фильтрации
## Устранение неполадок
### Ошибки импорта
Убедитесь, что Python path настроен правильно:
```bash
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
```
### Ошибки asyncio
Для асинхронных тестов используйте маркер `@pytest.mark.asyncio`
### Ошибки моков
Проверьте, что все внешние зависимости замоканы:
```python
with patch('module.function') as mock_func:
# тест
```
### Медленные тесты
Используйте маркер `@pytest.mark.slow` для медленных тестов и исключайте их при необходимости:
```bash
pytest -m "not slow"
```

BIN
Stick/Hello_11.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
Stick/Hello_12.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
Stick/Hello_13.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
Stick/Universal_11.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,43 +0,0 @@
import datetime
import os
from loguru import logger
class Logger:
def __init__(self, name):
self.logger = logger.bind(name=name)
# Получение сегодняшней даты для имени файла
today = datetime.date.today().strftime('%Y-%m-%d')
# Создание папки для логов
current_dir = os.path.dirname(os.path.abspath(__file__))
logs_dir = os.path.join(current_dir, 'logs')
if not os.path.exists(logs_dir):
# Если не существует, создаем ее
os.makedirs(logs_dir)
filename = f'{logs_dir}/helper_bot_{today}.log'
# Настройка формата логов
self.logger.add(
filename,
rotation="00:00",
retention="5 days",
compression="zip",
format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {name} | {line} | {message}",
)
def info(self, message):
self.logger.info(message)
def debug(self, message):
self.logger.debug(message)
def warning(self, message):
self.logger.warning(message)
def error(self, message):
self.logger.error(message)
def critical(self, message):
self.logger.critical(message)

1399
database/db.py Normal file

File diff suppressed because it is too large Load Diff

406
db.py
View File

@@ -1,406 +0,0 @@
import sqlite3
import configparser
import os
from datetime import datetime
from loguru import logger
from custom_logger import Logger
db_logger = Logger(name='db')
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, 'settings.ini')
config = configparser.ConfigParser()
config.read(config_path)
LOGS = config.getboolean('Settings', 'logs')
IMPORTANT_LOGS = config.get('Telegram', 'important_logs')
class BotDB:
def __init__(self):
db_file_path = os.path.dirname(os.path.abspath(__file__))
db_file = os.path.join(db_file_path, 'tg-bot-database')
self.conn = sqlite3.connect(db_file, check_same_thread=False)
self.cursor = self.conn.cursor()
logger.info(f'Подключен к базе данных: {db_file_path}')
def create_table(self, sql_script):
"""Создает таблицу в базе."""
try:
cursor = self.conn.cursor()
cursor.execute(sql_script)
except Exception as e:
print(f"Ошибка при создании таблицы: {e}")
def get_current_version(self):
"""Получает текущую версию миграций из таблицы migrations."""
try:
cursor = self.conn.cursor()
cursor.execute("SELECT version FROM migrations ORDER BY version DESC LIMIT 1")
version = cursor.fetchone()[0]
return version
except Exception as e:
print(f"Ошибка при получении версии: {e}")
return 0
def update_version(self, new_version, script_name):
"""Обновляет версию миграций в таблице migrations."""
logger.info(f'Попытка обновления версии: {new_version}, скрипт: {script_name}')
try:
current_date = datetime.now()
today = current_date.strftime("%d-%m-%Y %H:%M:%S")
cursor = self.conn.cursor()
cursor.execute(
"INSERT INTO migrations (version, script_name, created_at) VALUES(?, ?, ?)",
(new_version, script_name, today),
)
self.conn.commit()
logger.info(f"Версия обновлена: {new_version}, скрипт: {script_name}")
except sqlite3.IntegrityError as e:
logger.error(f"Ошибка при обновлении версии: {e}")
except Exception as e:
logger.error(f"Ошибка при обновлении версии: {e}")
# TODO: Deprecated, удалить
def get_message_from_db(self, type: str, username):
"""Функция для запроса к базе данных и получения сообщений для бота. В аргументы передаются:
type - тип получаемой обратной связи, строковое значение, сохраненное в БД
username - имя пользователя
"""
# Подключаемся к базе
try:
cursor = self.conn.cursor()
cursor.execute(f"SELECT * FROM messages WHERE type=?", (type,))
# Забираем данные из таблицы, преобразуем в строку, заменяем поле username на имя пользователя,
# и вместо амберсанда подставляем перенос строки
if type == 'connect_with_admin' or type == 'del_message' or type == 'suggest_news' or type == 'start_message':
response_from_database = str(cursor.fetchone()[1]).replace('username', username).replace('&', '\n')
else:
response_from_database = str(cursor.fetchone()[1]).replace('&', '\n')
return response_from_database
except sqlite3.Error as error:
print(error)
# TODO: Deprecated. Остался только в voice боте, удалить и оттуда
def get_error_message_from_db(self, id: int):
"""Функция для запроса к базе данных и получения сообщений ошибки. В аргументы передаются:
id - идентификатор ошибки
"""
# Подключаемся к базе
try:
cursor = self.conn.cursor()
cursor.execute(f"SELECT * FROM error_messages WHERE id=?", (id,))
response_from_database = str(cursor.fetchone()[1])
return response_from_database
except sqlite3.Error as error:
print(error)
def add_new_user_in_db(self, user_id, first_name, full_name, username, is_bot, language_code, date_added,
date_changed):
"""Добавляем юзера в базу"""
try:
self.cursor.execute("INSERT INTO 'our_users' ('user_id', 'first_name', 'full_name', 'username', 'is_bot', "
"'language_code', 'date_added', 'date_changed') VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(user_id, first_name, full_name,
username, is_bot, language_code, date_added, date_changed))
return self.conn.commit()
except sqlite3.Error as error:
print(error)
def user_exists(self, user_id):
"""Проверяем, есть ли юзер в базе"""
try:
result = self.cursor.execute("SELECT `id` FROM `our_users` WHERE `user_id` = ?", (user_id,))
return bool(len(result.fetchall()))
except sqlite3.Error as error:
print(error)
def get_user_id(self, user_id):
"""Достаем id юзера в базе по его user_id"""
try:
result = self.cursor.execute("SELECT `id` FROM `our_users` WHERE `user_id` = ?", (user_id,))
return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def get_username(self, user_id):
"""Достаем id юзера в базе по его user_id"""
try:
result = self.cursor.execute("SELECT `username` FROM `our_users` WHERE `user_id` = ?", (user_id,))
return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def get_all_user_id(self):
"""Достаем все айдишники юзеров из БД и преобразуем их в список"""
try:
result = self.cursor.execute("SELECT `user_id` FROM `our_users`", )
fetch_all = result.fetchall()
list_of_users = []
for i in fetch_all:
list_of_users.append(i[0])
return list_of_users
except sqlite3.Error as error:
print(error)
def get_user_first_name(self, user_id):
try:
result = self.cursor.execute("SELECT `first_name` FROM `our_users` WHERE `user_id` = ?", (user_id,))
return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def change_name(self, user_id):
#TODO: реализовать функцию изменения имени пользователя по которому к нему обращается бот. ОБновляем поля first_name, date_changed
#result = self.cursor.execute("UPDATE 'our_users' SET (?) WHERE user_id = (?)", (new_user_name), )
pass
def add_audio_record(self, file_name, author_id, date_added, listen_count, file_id):
"""Добавляет информацию о войсе юзера в БД"""
try:
result = self.cursor.execute(
"INSERT INTO `audio_message_reference` (file_name, author_id, date_added, listen_count, file_id) VALUES (?, ?, ?, ?, ?)",
(file_name, author_id, date_added, listen_count, file_id))
return self.conn.commit()
except sqlite3.Error as error:
print(error)
def last_date_audio(self):
"""Получаем дату последнего войса"""
try:
result = self.cursor.execute(
"SELECT `date_added` FROM `audio_message_reference` ORDER BY date_added DESC LIMIT 1")
return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def get_last_user_audio_record(self, user_id):
"""Получает данные о количестве записей пользователя"""
try:
result = self.cursor.execute("SELECT `file_id` FROM `audio_message_reference` WHERE `author_id` = ?",
(user_id,))
return bool(len(result.fetchall()))
except sqlite3.Error as error:
print(error)
def get_id_for_audio_record(self, user_id):
"""Получает ID аудио сообщения пользователя"""
try:
result = self.cursor.execute(
"SELECT `file_id` FROM `audio_message_reference` WHERE `author_id` = ? ORDER BY date_added DESC LIMIT 1",
(user_id,))
return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def get_path_for_audio_record(self, user_id):
"""Получает данные о названии файла"""
try:
result = self.cursor.execute(
"SELECT `file_name` FROM `audio_message_reference` WHERE `author_id` = ? ORDER BY date_added DESC LIMIT 1",
(user_id,))
return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def check_listen_audio(self, user_id):
"""Проверяет прослушано ли аудио пользователем"""
try:
query_listen_audio = self.cursor.execute(
"""SELECT l.file_name
FROM audio_message_reference a
LEFT JOIN listen_audio_users l ON l.file_name = a.file_name
WHERE l.user_id = ?
AND l.file_name IS NOT NULL""", (user_id,))
check_sign = query_listen_audio.fetchall()
query_all_audio = self.cursor.execute('SELECT file_name FROM audio_message_reference WHERE author_id <> ?',
(user_id,))
sign_all_audio = query_all_audio.fetchall()
new_sign1 = list(set(sign_all_audio) - set(check_sign))
new_sign = []
for i in new_sign1:
new_sign.append(i[0])
return new_sign
except sqlite3.Error as error:
print(error)
def mark_listened_audio(self, file_name, user_id):
"""Отмечает аудио прослушанным для конкретного пользователя."""
try:
result = self.cursor.execute(
"INSERT INTO `listen_audio_users` (file_name, user_id, is_listen) VALUES (?, ?, ?)",
(file_name, user_id, 1))
return self.conn.commit()
except sqlite3.Error as error:
print(error)
def get_info_about_stickers(self, user_id):
"""Получает данные о получении стикеров пользователем"""
try:
result = self.cursor.execute("SELECT `has_stickers` FROM `our_users` WHERE `user_id` = ?", (user_id,))
return result.fetchone()[0] == 1
#return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def update_info_about_stickers(self, user_id):
"""Обновляет данные о получении стикеров пользователем"""
try:
result = self.cursor.execute("UPDATE `our_users` SET `has_stickers` = 1 WHERE `user_id` = ?", (user_id,))
return self.conn.commit()
except sqlite3.Error as error:
print(error)
def get_users_blacklist(self):
"""Возвращает список пользователей в черном списке"""
try:
result = self.cursor.execute("SELECT user_id, user_name FROM `blacklist`")
fetch_all = result.fetchall()
list_of_users = {}
for i in fetch_all:
list_of_users[i[0]] = i[1]
return list_of_users
except sqlite3.Error as error:
print(error)
def get_users_for_unblock_today(self, date_to_unban):
"""Возвращает пользователей у которых истекает срок блокировки сегодня"""
try:
result = self.cursor.execute("SELECT user_id, user_name FROM `blacklist` WHERE date_to_unban = ?", (date_to_unban,))
fetch_all = result.fetchall()
list_of_users = {}
for i in fetch_all:
list_of_users[i[0]] = i[1]
return list_of_users
except sqlite3.Error as error:
print(error)
def get_blacklist_users_by_id(self, user_id):
"""Возвращает список пользователей в черном списке по user_id"""
try:
result = self.cursor.execute("SELECT user_id, user_name, message_for_user, date_to_unban FROM `blacklist` WHERE user_id = ?", (user_id, ))
return self.cursor.fetchone()
except sqlite3.Error as error:
print(error)
def check_user_in_blacklist(self, user_id):
"""Проверяет, существует ли запись с данным user_id в blacklist."""
self.cursor.execute("SELECT 1 FROM blacklist WHERE user_id = ?", (user_id,))
result = self.cursor.fetchone()
return bool(result)
def set_user_blacklist(self, user_id, user_name=None, message_for_user=None, date_to_unban=None):
"""Добавляет пользователя в черный список"""
try:
result = self.cursor.execute("INSERT INTO 'blacklist' ('user_id', 'user_name',"
" 'message_for_user', 'date_to_unban') VALUES (?, ?, ?, ?)",
(user_id, user_name, message_for_user, date_to_unban,))
return self.conn.commit()
except sqlite3.Error as error:
return error
def delete_user_blacklist(self, user_id):
"""Удаляет пользователя из черного списка"""
try:
#TODO: Функция всегда возвращает true, даже если такого id нет в таблице
self.cursor.execute("DELETE FROM blacklist WHERE user_id = ?", (user_id,))
self.conn.commit()
logger.info(f"Пользователь с идентификатором {user_id} успешно удален.")
return True
except sqlite3.Error as error:
logger.error(f"Ошибка удаления пользователя с идентификатором {user_id} из таблицы blacklist. Ошибка: {str(error)}")
return False
def add_new_message_in_db(self, message_text, user_id, message_id, date, has_answer):
"""Добавляем сообщение юзера в базу"""
try:
self.cursor.execute(
"INSERT INTO `user_messages` (message_text, user_id, message_id, date, has_answer) "
"VALUES (?, ?, ?, ?, ?)",
(message_text, message_id, user_id, date, has_answer))
return self.conn.commit()
except sqlite3.Error as error:
print(error)
def update_date_for_user(self, date, user_id: int):
try:
result = self.cursor.execute("UPDATE `our_users` SET `date_changed` = ? WHERE `user_id` = ?",
(date, user_id,))
return self.conn.commit()
except sqlite3.Error as error:
print(error)
def is_admin(self, user_id):
"""
Проверяет, является ли пользователь администратором.
Args:
user_id: ID пользователя Telegram.
Returns:
True, если пользователь администратор, иначе False.
"""
self.cursor.execute("SELECT 1 FROM admins WHERE user_id = ?", (user_id,))
result = self.cursor.fetchone()
return bool(result)
def add_admin(self, user_id, role):
"""
Добавляет пользователя в список администраторов.
Args:
user_id: ID пользователя Telegram.
role: Роль пользователя.
Доступные варианты:
1. creator - создатель
2. admin - обычная роль
"""
self.cursor.execute("INSERT INTO admins (user_id, role) VALUES (?, ?)", (user_id, role))
return self.conn.commit()
def remove_admin(self, user_id):
"""
Удаляет пользователя из списка администраторов.
Args:
user_id: ID пользователя Telegram.
"""
self.cursor.execute("DELETE FROM admins WHERE user_id = ?", (user_id,))
return self.conn.commit()
def get_user_by_message_id(self, message_id):
"""Возвращает идентификатор пользователя по идентификатору сообщения"""
try:
result = self.cursor.execute("SELECT user_id FROM `user_messages` WHERE message_id = ?", (message_id,))
return result.fetchone()[0]
except sqlite3.Error as error:
print(error)
def get_last_users_from_db(self):
"""Возвращает список идентификаторов последних 10 пользователей обращавшихся в бот"""
try:
result = self.cursor.execute("SELECT full_name, user_id FROM our_users ORDER BY date_changed DESC")
users = result.fetchall()
return users
except sqlite3.Error as error:
print(error)
def get_banned_users_from_db(self):
"""Возвращает список идентификаторов последних 10 пользователей обращавшихся в бот"""
try:
result = self.cursor.execute("SELECT user_name, user_id, message_for_user, date_to_unban FROM blacklist", )
users = result.fetchall()
return users
except sqlite3.Error as error:
print(error)
def get_banned_users_from_db_with_limits(self, offset: int, limit: int):
"""Возвращает список идентификаторов последних 10 пользователей обращавшихся в бот"""
try:
result = self.cursor.execute("SELECT user_name, user_id, message_for_user, date_to_unban FROM blacklist LIMIT ?, ?", (offset, limit,) )
users = result.fetchall()
return users
except sqlite3.Error as error:
print(error)
def close(self):
"""Закрываем соединение с БД"""
self.conn.close()

0
helper_bot/__init__.py Normal file
View File

View File

@@ -0,0 +1,15 @@
from typing import Union
from aiogram.filters import BaseFilter
from aiogram.types import Message
class ChatTypeFilter(BaseFilter): # [1]
def __init__(self, chat_type: Union[str, list]): # [2]
self.chat_type = chat_type
async def __call__(self, message: Message) -> bool: # [3]
if isinstance(self.chat_type, str):
return message.chat.type == self.chat_type
else:
return message.chat.type in self.chat_type

View File

@@ -0,0 +1 @@
from .admin_handlers import admin_router

View File

@@ -0,0 +1,321 @@
import traceback
import html
from aiogram import Router, types, F
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin, create_keyboard_with_pagination, \
create_keyboard_for_ban_days, create_keyboard_for_approve_ban, create_keyboard_for_ban_reason
from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import check_access, add_days_to_date, get_banned_users_buttons, get_banned_users_list
from logs.custom_logger import logger
admin_router = Router()
bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db()
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
Command('admin')
)
async def admin_panel(message: types.Message, state: FSMContext):
try:
if check_access(message.from_user.id, BotDB):
await state.set_state("ADMIN")
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
markup = get_reply_keyboard_admin()
await message.answer("Добро пожаловать в админку. Выбери что хочешь:",
reply_markup=markup)
else:
await message.answer('Доступ запрещен, досвидания!')
except Exception as e:
logger.error(f"Ошибка при запуске админ панели: {e}")
await message.bot.send_message(IMPORTANT_LOGS,
f'Ошибка в функции admin_panel {e}. Traceback: {traceback.format_exc()}')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Бан (Список)'
)
async def get_last_users(message: types.Message):
logger.info(
f"Попытка получения списка последних пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
list_users = BotDB.get_last_users_from_db()
keyboard = create_keyboard_with_pagination(1, len(list_users), list_users, 'ban')
await message.answer(text="Список пользователей которые последними обращались к боту",
reply_markup=keyboard)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Бан по нику'
)
async def ban_by_nickname(message: types.Message, state: FSMContext):
await message.answer('Пришли мне username блокируемого пользователя')
await state.set_state('PRE_BAN')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Бан по ID'
)
async def ban_by_id(message: types.Message, state: FSMContext):
await message.answer('Пришли мне ID блокируемого пользователя')
await state.set_state('PRE_BAN_ID')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Тестовый бан'
)
async def ban_by_forward(message: types.Message, state: FSMContext):
await message.answer('Перешлите мне сообщение от пользователя, которого хотите заблокировать')
await state.set_state('PRE_BAN_FORWARD')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
F.text == 'Отменить'
)
async def decline_ban(message: types.Message, state: FSMContext):
current_state = await state.get_state()
await state.set_data({})
await state.set_state("ADMIN")
logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN")
)
async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
logger.info(
f"Функция ban_by_nickname_2. Получен никнейм пользователя: {message.text}")
user_name = message.text
user_id = BotDB.get_user_id_by_username(user_name)
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None)
full_name = BotDB.get_full_name_by_id(user_id)
markup = create_keyboard_for_ban_reason()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(full_name))
await message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_ID")
)
async def ban_by_id_step_2(message: types.Message, state: FSMContext):
try:
user_id = int(message.text)
logger.info(f"Функция ban_by_id_step_2. Получен ID пользователя: {user_id}")
# Проверяем, существует ли пользователь в базе
user_info = BotDB.get_user_info_by_id(user_id)
if not user_info:
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
return
user_name = user_info.get('username', 'Неизвестно')
full_name = user_info.get('full_name', 'Неизвестно')
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None)
markup = create_keyboard_for_ban_reason()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(full_name))
await message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
except ValueError:
await message.answer("Пожалуйста, введите корректный числовой ID пользователя.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_FORWARD"),
F.forward_from
)
async def ban_by_forward_step_2(message: types.Message, state: FSMContext):
"""Обработчик пересланных сообщений для бана пользователя"""
try:
# Получаем информацию о пользователе из пересланного сообщения
forwarded_user = message.forward_from
if not forwarded_user:
await message.answer("Не удалось получить информацию о пользователе из пересланного сообщения. Возможно, пользователь скрыл возможность пересылки своих сообщений.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
return
user_id = forwarded_user.id
user_name = forwarded_user.username or "private_username"
full_name = forwarded_user.full_name or "Неизвестно"
logger.info(f"Функция ban_by_forward_step_2. Получен пользователь из пересланного сообщения: ID={user_id}, username={user_name}, full_name={full_name}")
# Проверяем, существует ли пользователь в базе
user_info = BotDB.get_user_info_by_id(user_id)
if not user_info:
# Если пользователя нет в базе, используем информацию из пересланного сообщения
logger.info(f"Пользователь с ID {user_id} не найден в базе данных, используем данные из пересланного сообщения")
user_name = user_name
full_name = full_name
else:
# Если пользователь есть в базе, используем данные из базы
user_name = user_info.get('username', user_name)
full_name = user_info.get('full_name', full_name)
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None)
markup = create_keyboard_for_ban_reason()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(full_name))
await message.answer(
text=f"<b>Выбран пользователь из пересланного сообщения:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
except Exception as e:
logger.error(f"Ошибка при обработке пересланного сообщения: {e}")
await message.answer("Произошла ошибка при обработке пересланного сообщения.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_FORWARD")
)
async def ban_by_forward_invalid(message: types.Message, state: FSMContext):
"""Обработчик для случаев, когда сообщение не является пересланным или не содержит информацию о пользователе"""
if message.forward_from_chat:
await message.answer("Пересланное сообщение из канала или группы не содержит информацию о конкретном пользователе. Пожалуйста, перешлите сообщение из приватного чата.")
else:
await message.answer("Пожалуйста, перешлите сообщение от пользователя, которого хотите заблокировать. Обычное сообщение не подходит.")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Разбан (список)'
)
async def get_banned_users(message):
logger.info(
f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
message_text = get_banned_users_list(0, BotDB)
buttons_list = get_banned_users_buttons(BotDB)
if buttons_list:
k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
await message.answer(text=message_text, reply_markup=k)
else:
await message.answer(text="В списке забанненых пользователей никого нет")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("BAN_2")
)
async def ban_user_step_2(message: types.Message, state: FSMContext):
user_data = await state.get_data()
logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})")
await state.update_data(message_for_user=message.text)
markup = create_keyboard_for_ban_days()
# Экранируем message.text для безопасного использования
safe_message_text = html.escape(str(message.text)) if message.text else ""
await message.answer(f"Выбрана причина: {safe_message_text}. Выбери срок бана в днях или напиши "
f"его в чат", reply_markup=markup)
await state.set_state("BAN_3")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("BAN_3")
)
async def ban_user_step_3(message: types.Message, state: FSMContext):
logger.info(f"ban_user_step_3. Расчет даты разбана. Входные данные {message.text}")
if message.text != 'Навсегда':
count_days = int(message.text)
date_to_unban = add_days_to_date(count_days)
else:
date_to_unban = None
logger.info(f"ban_user_step_3. Расчет даты разбана. date_to_unban: {date_to_unban}")
await state.update_data(date_to_unban=date_to_unban)
user_data = await state.get_data()
markup = create_keyboard_for_approve_ban()
# Экранируем user_data для безопасного использования
safe_message_for_user = html.escape(str(user_data['message_for_user'])) if user_data.get('message_for_user') else ""
safe_date_to_unban = html.escape(str(user_data['date_to_unban'])) if user_data.get('date_to_unban') else ""
await message.answer(
f"Необходимо подтверждение:\nПользователь:{user_data['user_id']}\nПричина бана:{safe_message_for_user}\nСрок бана:{safe_date_to_unban}",
reply_markup=markup)
await state.set_state("BAN_FINAL")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("BAN_FINAL"),
F.text == 'Подтвердить'
)
async def approve_ban(message: types.Message, state: FSMContext):
user_data = await state.get_data()
logger.info(f"Переход на финальный шаг бана пользователя. Словарь с данными для бана: {user_data})")
exists = BotDB.check_user_in_blacklist(user_data['user_id'])
if exists:
await message.reply(f"Пользователь уже был заблокирован ранее.")
logger.info(f"Пользователь: {user_data['user_id']} был заблокирован ранее)")
await state.set_state('ADMIN')
else:
BotDB.set_user_blacklist(user_data['user_id'],
user_data['user_name'],
user_data['message_for_user'],
user_data['date_to_unban'])
# Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user_data['user_name'])) if user_data.get('user_name') else "Неизвестный пользователь"
await message.reply(f"Пользователь {safe_user_name} успешно заблокирован.")
logger.info(f"Пользователь: {user_data['user_id']} успешно заблокирован)")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)

View File

@@ -0,0 +1 @@
from .callback_handlers import callback_router

View File

@@ -0,0 +1,284 @@
import html
import traceback
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
create_keyboard_for_ban_reason
from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import send_text_message, send_photo_message, get_banned_users_list, \
get_banned_users_buttons, delete_user_blacklist, send_media_group_to_channel, \
send_video_message, send_video_note_message, send_audio_message, send_voice_message
from logs.custom_logger import logger
callback_router = Router()
bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db()
@callback_router.callback_query(
F.data == "publish"
)
async def post_for_group(call: CallbackQuery, state: FSMContext):
logger.info(
f'Получен callback-запрос с действием: {call.data} от пользователя {call.from_user.full_name} (ID сообщения: {call.message.message_id})')
text_post = html.escape(str(call.message.text))
text_post_with_photo = html.escape(str(call.message.caption))
if call.message.content_type == 'text' and call.message.text != "^":
try:
# Пересылаем сообщение в канал
await send_text_message(MAIN_PUBLIC, call.message, text_post)
# Получаем из базы автора
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
# Очищаем предложку и удаляем оттуда пост
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
logger.info(f'Текст сообщения опубликован в канале {MAIN_PUBLIC}.')
await call.answer(text='Выложено!', cache_time=3)
# Отвечаем пользователю
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
except Exception as e:
if e.message != 'Forbidden: bot was blocked by the user':
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
logger.error(f'Ошибка при публикации текста в канал {MAIN_PUBLIC}: {str(e)}')
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
elif call.message.content_type == 'photo':
try:
await send_photo_message(MAIN_PUBLIC, call.message, call.message.photo[-1].file_id, text_post_with_photo)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
# Удаляем пост из предложки
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
logger.info(f'Пост с фото опубликован в канале {MAIN_PUBLIC}.')
await call.answer(text='Выложено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
except Exception as e:
if e.message != 'Forbidden: bot was blocked by the user':
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
logger.error(f'Ошибка при публикации фотографии в канал {MAIN_PUBLIC}: {str(e)}')
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
elif call.message.content_type == 'video':
try:
await send_video_message(MAIN_PUBLIC, call.message, call.message.video.file_id, text_post_with_photo)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
logger.info(f'Пост с видео опубликован в канале {MAIN_PUBLIC}.')
await call.answer(text='Выложено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
except Exception as e:
if e.message != 'Forbidden: bot was blocked by the user':
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
logger.error(f'Ошибка при публикации видео в канал {MAIN_PUBLIC}: {str(e)}')
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
elif call.message.content_type == 'video_note':
try:
await send_video_note_message(MAIN_PUBLIC, call.message, call.message.video_note.file_id)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
logger.info(f'Пост с кружком опубликован в канале {MAIN_PUBLIC}.')
await call.answer(text='Выложено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
except Exception as e:
if e.message != 'Forbidden: bot was blocked by the user':
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
logger.error(f'Ошибка при публикации кружка в канал {MAIN_PUBLIC}: {str(e)}')
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
elif call.message.content_type == 'audio':
try:
await send_audio_message(MAIN_PUBLIC, call.message, call.message.audio.file_id, text_post_with_photo)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
logger.info(f'Пост с аудио опубликован в канале {MAIN_PUBLIC}.')
await call.answer(text='Выложено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
except Exception as e:
if e.message != 'Forbidden: bot was blocked by the user':
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
logger.error(f'Ошибка при публикации аудио в канал {MAIN_PUBLIC}: {str(e)}')
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
elif call.message.content_type == 'voice':
try:
await send_voice_message(MAIN_PUBLIC, call.message, call.message.voice.file_id)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
logger.info(f'Пост с войсом опубликован в канале {MAIN_PUBLIC}.')
await call.answer(text='Выложено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
except Exception as e:
if e.message != 'Forbidden: bot was blocked by the user':
await call.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
logger.error(f'Ошибка при публикации войса в канал {MAIN_PUBLIC}: {str(e)}')
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
elif call.message.text == "^":
# Получаем контент медиагруппы и текст для публикации
post_content = BotDB.get_post_content_from_telegram_by_last_id(call.message.message_id)
pre_text = BotDB.get_post_text_from_telegram_by_last_id(call.message.message_id)
post_text = html.escape(str(pre_text))
# Готовим список для удаления
post_ids = BotDB.get_post_ids_from_telegram_by_last_id(call.message.message_id)
message_ids = [row[0] for row in post_ids]
message_ids.append(call.message.message_id)
# Выкладываем пост в канал
await send_media_group_to_channel(bot=call.bot, chat_id=MAIN_PUBLIC, post_content=post_content,
post_text=post_text)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_helper_message_id(call.message.message_id)
# TODO: Удалить фотки с локалки после выкладки?
await call.bot.delete_messages(chat_id=GROUP_FOR_POST, message_ids=message_ids)
await call.answer(text='Выложено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был выложен🥰')
@callback_router.callback_query(
F.data == "decline"
)
async def decline_post_for_group(call: CallbackQuery, state: FSMContext):
logger.info(
f'Получен callback-запрос с данными: {call.data} от пользователя {call.from_user.full_name} (ID: {call.from_user.id})')
try:
if call.message.content_type == 'text' and call.message.text != "^" or call.message.content_type == 'photo' \
or call.message.content_type == 'audio' or call.message.content_type == 'voice' \
or call.message.content_type == 'video' or call.message.content_type == 'video_note':
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_message_id(call.message.message_id)
logger.info(
f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
await call.answer(text='Отклонено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был отклонен😔')
if call.message.text == '^':
post_ids = BotDB.get_post_ids_from_telegram_by_last_id(call.message.message_id)
message_ids = [row[0] for row in post_ids]
message_ids.append(call.message.message_id)
await call.bot.delete_messages(chat_id=GROUP_FOR_POST, message_ids=message_ids)
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
author_id = BotDB.get_author_id_by_helper_message_id(call.message.message_id)
await call.answer(text='Удалено!', cache_time=3)
await send_text_message(author_id, call.message, 'Твой пост был отклонен😔')
except Exception as e:
if e.message != 'Forbidden: bot was blocked by the user':
await call.bot.send_message(IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
logger.error(f'Ошибка при удалении сообщения в группе {GROUP_FOR_POST}: {str(e)}')
await call.answer(text='Что-то пошло не так!', show_alert=True, cache_time=3)
@callback_router.callback_query(
F.data.contains('ban')
)
async def process_ban_user(call: CallbackQuery, state: FSMContext):
user_id = call.data[4:]
logger.info(
f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
user_name = BotDB.get_username(user_id=user_id)
if user_name:
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None)
markup = create_keyboard_for_ban_reason()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(call.message.from_user.full_name))
await call.message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
else:
markup = get_reply_keyboard_admin()
await call.message.answer(text='Пользователь с таким ID не найден в базе', markup=markup)
await state.set_state('ADMIN')
@callback_router.callback_query(
F.data.contains('unlock')
)
async def process_unlock_user(call: CallbackQuery):
user_id = call.data[7:]
user_name = BotDB.get_username(user_id=user_id)
delete_user_blacklist(user_id, BotDB)
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
username = BotDB.get_username(user_id)
await call.answer(f'Пользователь разблокирован {username}', show_alert=True)
@callback_router.callback_query(
F.data == 'return'
)
async def return_to_main_menu(call: CallbackQuery):
await call.message.delete()
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
markup = get_reply_keyboard_admin()
await call.message.answer("Добро пожаловать в админку. Выбери что хочешь:",
reply_markup=markup)
@callback_router.callback_query(
F.data.contains('page')
)
async def change_page(call: CallbackQuery):
page_number = int(call.data[5:])
logger.info(f"Переход на страницу {page_number}")
if call.message.text == 'Список пользователей которые последними обращались к боту':
list_users = BotDB.get_last_users_from_db()
# TODO: Здесь где-то надо добавить обработку ошибки IndexError: list index out of range
keyboard = create_keyboard_with_pagination(int(page_number), len(list_users), list_users,
'ban')
await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id,
reply_markup=keyboard)
else:
# Готовим сообщения
message_user = get_banned_users_list(int(page_number) * 7 - 7, BotDB)
await call.bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id,
text=message_user)
# Готовим клавиатуру
buttons = get_banned_users_buttons(BotDB)
keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock')
await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id,
reply_markup=keyboard)

View File

@@ -0,0 +1 @@
from .group_handlers import group_router

View File

@@ -0,0 +1,49 @@
from aiogram import Router, types
from aiogram.fsm.context import FSMContext
from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import send_text_message
from logs.custom_logger import logger
group_router = Router()
bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db()
@group_router.message(
ChatTypeFilter(chat_type=["group", "supergroup"]),
)
async def handle_message(message: types.Message, state: FSMContext):
"""Функция ответа админа пользователю через закрытый чат"""
logger.info(
f'Получено сообщение в группе {message.chat.title} (ID: {message.chat.id}) от пользователя {message.from_user.full_name} (ID: {message.from_user.id}): "{message.text}"')
markup = get_reply_keyboard_leave_chat()
message_id = 0
try:
message_id = message.reply_to_message.message_id
except AttributeError as e:
await message.answer('Блять, выдели сообщение!')
logger.warning(
f'В группе {message.chat.title} (ID: {message.chat.id}) админ не выделил сообщение для ответа. Ошибка {str(e)}')
message_from_admin = message.text
try:
chat_id = BotDB.get_user_by_message_id(message_id)
await send_text_message(chat_id, message, message_from_admin, markup)
await state.set_state("CHAT")
logger.info(f'Ответ админа "{message.text}" отправлен пользователю с ID: {chat_id} на сообщение {message_id}')
except TypeError as e:
await message.answer('Не могу найти кому ответить в базе, проебали сообщение.')
logger.error(
f'Ошибка при поиске пользователя в базе для ответа на сообщение: {message.text} в группе {message.chat.title} (ID сообщения: {message.message_id}) Ошибка: {str(e)}')

View File

@@ -0,0 +1 @@
from .private_handlers import private_router

View File

@@ -0,0 +1,503 @@
import random
import traceback
import asyncio
import html
from datetime import datetime
from pathlib import Path
from aiogram import types, Router, F
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.types import FSInputFile
from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
from helper_bot.middlewares.album_middleware import AlbumMiddleware
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.utils import messages
from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import get_first_name, get_text_message, send_text_message, send_photo_message, \
send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, send_video_message, \
send_video_note_message, send_audio_message, send_voice_message, add_in_db_media, \
check_user_emoji, check_username_and_full_name
from logs.custom_logger import logger
private_router = Router()
private_router.message.middleware(AlbumMiddleware())
private_router.message.middleware(BlacklistMiddleware())
bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db()
# Expose sleep for tests (tests patch helper_bot.handlers.private.private_handlers.sleep)
sleep = asyncio.sleep
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("emoji")
)
async def handle_emoji_message(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
user_emoji = check_user_emoji(message)
await state.set_state("START")
if user_emoji is not None:
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("restart")
)
async def handle_restart_message(message: types.Message, state: FSMContext):
try:
markup = get_reply_keyboard(BotDB, message.from_user.id)
await message.forward(chat_id=GROUP_FOR_LOGS)
await state.set_state("START")
await update_user_info('love', message)
check_user_emoji(message)
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
except Exception as e:
logger.error(f"Произошла ошибка handle_restart_message. Ошибка:{str(e)}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка handle_restart_message: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("start")
)
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
F.text == 'Вернуться в бота'
)
async def handle_start_message(message: types.Message, state: FSMContext):
try:
await message.forward(chat_id=GROUP_FOR_LOGS)
full_name = message.from_user.full_name
username = message.from_user.username
first_name = get_first_name(message)
is_bot = message.from_user.is_bot
language_code = message.from_user.language_code
user_id = message.from_user.id
# Проверяем наличие username для логирования
if not username:
# Экранируем full_name для безопасного использования
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Пользователь {user_id} ({safe_full_name}) обратился к боту без username')
logger.warning(f"Пользователь {user_id} ({safe_full_name}) обратился к боту без username")
# Устанавливаем значение по умолчанию для username
username = "private_username"
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
if not BotDB.user_exists(user_id):
# Для первоначального добавления эмодзи пока не назначаем (совместимость)
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, "", date,
date)
else:
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
if is_need_update:
BotDB.update_username_and_full_name(user_id, username, full_name)
# Экранируем пользовательские данные для безопасного использования
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
safe_username = html.escape(username) if username else "Без никнейма"
await message.answer(
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}")
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
await asyncio.sleep(1)
BotDB.update_date_for_user(date, user_id)
await state.set_state("START")
logger.info(
f"Формирование приветственного сообщения для пользователя. Сообщение: {message.text} "
f"Имя автора сообщения: {message.from_user.full_name})")
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
random_stick_hello = random.choice(name_stick_hello)
random_stick_hello = FSInputFile(path=random_stick_hello)
logger.info(f"Стикер успешно получен из БД")
await message.answer_sticker(random_stick_hello)
await asyncio.sleep(0.3)
except Exception as e:
logger.error(f"Произошла ошибка handle_start_message при получении стикеров. Ошибка:{str(e)}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка при получении стикеров: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
try:
markup = get_reply_keyboard(BotDB, message.from_user.id)
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
except Exception as e:
logger.error(
f"Произошла ошибка при отправке приветственного сообщения для пользователя {message.from_user.id} Имя: {message.from_user.full_name}. Ошибка: {str(e)}")
await message.bot.send_message(IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("restart")
)
async def restart_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
full_name = message.from_user.full_name
username = message.from_user.username
user_id = message.from_user.id
# Проверяем наличие username для логирования
if not username:
# Экранируем full_name для безопасного использования
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Пользователь {user_id} ({safe_full_name}) обратился к боту без username')
logger.warning(f"Пользователь {user_id} ({safe_full_name}) обратился к боту без username")
# Устанавливаем значение по умолчанию для username
username = "private_username"
markup = get_reply_keyboard(BotDB, message.from_user.id)
await message.answer(text='Я перезапущен!',
reply_markup=markup)
await state.set_state('START')
@private_router.message(
StateFilter("START"),
ChatTypeFilter(chat_type=["private"]),
F.text == '📢Предложить свой пост'
)
async def suggest_post(message: types.Message, state: FSMContext):
try:
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id)
await message.forward(chat_id=GROUP_FOR_LOGS)
await state.set_state("SUGGEST")
current_state = await state.get_state()
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info(
f"Вызов функции suggest_post. Сообщение: {message.text} Имя автора сообщения: {safe_full_name} Идентификатор сообщения: {message.message_id}. State - {current_state}")
markup = types.ReplyKeyboardRemove()
suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS')
await message.answer(suggest_news)
await asyncio.sleep(0.3)
suggest_news_2 = messages.get_message(get_first_name(message), 'SUGGEST_NEWS_2')
await message.answer(suggest_news_2, reply_markup=markup)
except Exception as e:
await message.bot.send_message(IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
F.text == '👋🏼Сказать пока!'
)
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
F.text == 'Выйти из чата'
)
async def end_message(message: types.Message, state: FSMContext):
try:
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id)
await message.forward(chat_id=GROUP_FOR_LOGS)
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info(
f"Вызов функции end_message. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
random_stick_bye = random.choice(name_stick_bye)
random_stick_bye = FSInputFile(path=random_stick_bye)
await message.answer_sticker(random_stick_bye)
except Exception as e:
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.error(
f"Ошибка в функции end_message при получении стикера: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
try:
markup = types.ReplyKeyboardRemove()
bye_message = messages.get_message(get_first_name(message), 'BYE_MESSAGE')
await message.answer(bye_message, reply_markup=markup)
await state.set_state("START")
except Exception as e:
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.error(
f"Ошибка в функции stickers при получении сообщения: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message(
StateFilter("SUGGEST"),
ChatTypeFilter(chat_type=["private"]),
)
async def suggest_router(message: types.Message, state: FSMContext, album: list = None):
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info(
f"Вызов функции suggest_router. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
first_name = get_first_name(message)
try:
post_caption = ''
if message.media_group_id is not None:
# Экранируем username для безопасного использования
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
await send_text_message(GROUP_FOR_LOGS, message,
f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {safe_username}')
else:
await message.forward(chat_id=GROUP_FOR_LOGS)
if message.content_type == 'text':
lower_text = message.text.lower()
# Получаем текст сообщения и преобразовываем его по правилам
post_text = get_text_message(lower_text, first_name,
message.from_user.username)
# Получаем клавиатуру для поста
markup = get_reply_keyboard_for_post()
# Отправляем сообщение в приватный канал
sent_message_id = await send_text_message(GROUP_FOR_POST, message, post_text, markup)
# Записываем в базу пост
BotDB.add_post_in_db(sent_message_id, message.text, message.from_user.id)
# Отправляем юзеру ответ, что сообщение отравлено и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START")
elif message.content_type == 'photo' and message.media_group_id is None:
if message.caption:
lower_caption = message.caption.lower()
# Получаем текст сообщения и преобразовываем его по правилам
post_caption = get_text_message(lower_caption, first_name,
message.from_user.username)
markup = get_reply_keyboard_for_post()
# Отправляем фото и текст в приватный канал
sent_message = await send_photo_message(GROUP_FOR_POST, message,
message.photo[-1].file_id, post_caption, markup)
# Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START")
elif message.content_type == 'video' and message.media_group_id is None:
if message.caption:
lower_caption = message.caption.lower()
post_caption = get_text_message(lower_caption, first_name,
message.from_user.username)
markup = get_reply_keyboard_for_post()
# Получаем текст сообщения и преобразовываем его по правилам
# Отправляем видео и текст в приватный канал
sent_message = await send_video_message(GROUP_FOR_POST, message,
message.video.file_id, post_caption, markup)
# Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message, BotDB)
# Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message)
# Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START")
elif message.content_type == 'video_note' and message.media_group_id is None:
markup = get_reply_keyboard_for_post()
# Отправляем видеокружок в приватный канал
sent_message = await send_video_note_message(GROUP_FOR_POST, message,
message.video_note.file_id, markup)
# Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START")
elif message.content_type == 'audio' and message.media_group_id is None:
if message.caption:
lower_caption = message.caption.lower()
# Получаем текст сообщения и преобразовываем его по правилам
post_caption = get_text_message(lower_caption, first_name,
message.from_user.username)
markup = get_reply_keyboard_for_post()
# Отправляем аудио и текст в приватный канал
sent_message = await send_audio_message(GROUP_FOR_POST, message,
message.audio.file_id, post_caption, markup)
# Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START")
elif message.content_type == 'voice' and message.media_group_id is None:
markup = get_reply_keyboard_for_post()
# Отправляем войс и текст в приватный канал
sent_message = await send_voice_message(GROUP_FOR_POST, message,
message.voice.file_id, markup)
# Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START")
elif message.media_group_id is not None:
post_caption = " "
# Получаем сообщение и проверяем есть ли подпись. Если подпись есть, то преобразуем ее через функцию
if album[0].caption:
lower_caption = album[0].caption.lower()
post_caption = get_text_message(lower_caption, first_name,
message.from_user.username)
# Иначе обрабатываем фото и получаем медиагруппу
media_group = await prepare_media_group_from_middlewares(album, post_caption)
# Отправляем медиагруппу в секретный чат
media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message,
media_group, BotDB)
await asyncio.sleep(0.2)
# Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками
markup = get_reply_keyboard_for_post()
help_message_id = await send_text_message(GROUP_FOR_POST, message, "^", markup)
# Записываем в state идентификаторы текстового сообщения И последнего сообщения медиагруппы
BotDB.update_helper_message_in_db(message_id=media_group_message_id, helper_message_id=help_message_id)
# Получаем клавиатуру для пользователя, благодарим за пост, и возвращаем в дефолтное сообщение
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START")
else:
await message.bot.send_message(message.chat.id,
'Я пока не умею работать с таким сообщением. '
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
'Мы добавим его к обработке если необходимо')
except Exception as e:
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
F.text == '🤪Хочу стикеры'
)
async def stickers(message: types.Message, state: FSMContext):
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info(
f"Вызов функции stickers. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
markup = get_reply_keyboard(BotDB, message.from_user.id)
try:
BotDB.update_info_about_stickers(user_id=message.from_user.id)
await message.forward(chat_id=GROUP_FOR_LOGS)
await message.answer(text='Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk',
reply_markup=markup)
await state.set_state("START")
except Exception as e:
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.error(
f"Ошибка функции stickers. Ошибка: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
@private_router.message(
StateFilter("START"),
ChatTypeFilter(chat_type=["private"]),
F.text == '📩Связаться с админами'
)
async def connect_with_admin(message: types.Message, state: FSMContext):
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info(
f"Вызов функции connect_with_admin. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id)
admin_message = messages.get_message(get_first_name(message), 'CONNECT_WITH_ADMIN')
await message.answer(admin_message, parse_mode="html")
await message.forward(chat_id=GROUP_FOR_LOGS)
await state.set_state("PRE_CHAT")
@private_router.message(
StateFilter("PRE_CHAT"),
ChatTypeFilter(chat_type=["private"]),
)
@private_router.message(
StateFilter("CHAT"),
ChatTypeFilter(chat_type=["private"]),
)
async def resend_message_in_group_for_message(message: types.Message, state: FSMContext):
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id)
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info(
f"Попытка пересылки сообщения в связь с админами. Сообщение: {message.text} Имя автора сообщения: {safe_full_name} Идентификатор сообщения: {message.message_id})")
await message.forward(chat_id=GROUP_FOR_MESSAGE)
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.add_new_message_in_db(message.text, message.from_user.id, message.message_id + 1, date)
question = messages.get_message(get_first_name(message), 'QUESTION')
user_state = await state.get_state()
if user_state == "PRE_CHAT":
markup = get_reply_keyboard(BotDB, message.from_user.id)
await message.answer(question, reply_markup=markup)
await state.set_state("START")
elif user_state == "CHAT":
markup = get_reply_keyboard_leave_chat()
await message.answer(question, reply_markup=markup)

View File

@@ -0,0 +1 @@
from .keyboards import get_reply_keyboard_for_post, get_reply_keyboard

View File

@@ -0,0 +1,117 @@
from aiogram import types
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
def get_reply_keyboard_for_post():
builder = InlineKeyboardBuilder()
builder.row(types.InlineKeyboardButton(
text="Опубликовать", callback_data="publish")
)
builder.row(types.InlineKeyboardButton(
text="Отклонить", callback_data="decline")
)
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup
def get_reply_keyboard(BotDB, user_id):
builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="📢Предложить свой пост"))
builder.add(types.KeyboardButton(text="📩Связаться с админами"))
builder.add(types.KeyboardButton(text="👋🏼Сказать пока!"))
if not BotDB.get_info_about_stickers(user_id=user_id):
builder.add(types.KeyboardButton(text="🤪Хочу стикеры"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup
def get_reply_keyboard_leave_chat():
builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="Выйти из чата"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup
def get_reply_keyboard_admin():
builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="Бан (Список)"))
builder.add(types.KeyboardButton(text="Бан по нику"))
builder.add(types.KeyboardButton(text="Бан по ID"))
builder.add(types.KeyboardButton(text="Тестовый бан"))
builder.add(types.KeyboardButton(text="Разбан (список)"))
builder.add(types.KeyboardButton(text="Вернуться в бота"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup
def create_keyboard_with_pagination(page: int, total_items: int, array_items: list[tuple[any, any]], callback: str):
"""
Создает клавиатуру с пагинацией для заданного набора элементов и устанавливает необходимый callback
Args:
page: Номер текущей страницы.
total_items: Общее количество элементов.
array_items: Лист кортежей. Содержит в себе user_name: user_id
callback: Действие в коллбеке. Вернет callback вида ({callback}_{user_id})
Returns:
InlineKeyboardMarkup: Клавиатура с кнопками пагинации.
"""
# Определяем общее количество страниц
total_pages = (total_items + 9 - 1) // 9
# Создаем билдер для клавиатуры
keyboard = InlineKeyboardBuilder()
# Вычисляем стартовый номер для текущей страницы
start_index = (page - 1) * 9
# Кнопки с номерами страниц
for i in range(start_index, min(start_index + 9, len(array_items))):
keyboard.add(types.InlineKeyboardButton(
text=f"{array_items[i][0]}", callback_data=f"{callback}_{array_items[i][1]}"
))
keyboard.adjust(3)
next_button = types.InlineKeyboardButton(
text="➡️ Следующая", callback_data=f"page_{page + 1}"
)
prev_button = types.InlineKeyboardButton(
text="⬅️ Предыдущая", callback_data=f"page_{page - 1}"
)
keyboard.row(prev_button, next_button)
home_button = types.InlineKeyboardButton(
text="🏠 Назад", callback_data="return")
keyboard.row(home_button)
k = keyboard.as_markup()
return k
def create_keyboard_for_ban_reason():
builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="Спам"))
builder.add(types.KeyboardButton(text="Заебал стикерами"))
builder.row(types.KeyboardButton(text="Реклама здесь: @kerrad1 "))
builder.row(types.KeyboardButton(text="Тема с лагерями: https://vk.com/topic-75343895_50049913"))
builder.row(types.KeyboardButton(text="Отменить"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup
def create_keyboard_for_ban_days():
builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="1"))
builder.add(types.KeyboardButton(text="7"))
builder.add(types.KeyboardButton(text="30"))
builder.row(types.KeyboardButton(text="Навсегда"))
builder.row(types.KeyboardButton(text="Отменить"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup
def create_keyboard_for_approve_ban():
builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="Подтвердить"))
builder.add(types.KeyboardButton(text="Отменить"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup

21
helper_bot/main.py Normal file
View File

@@ -0,0 +1,21 @@
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.strategy import FSMStrategy
from helper_bot.handlers.admin import admin_router
from helper_bot.handlers.callback import callback_router
from helper_bot.handlers.group import group_router
from helper_bot.handlers.private import private_router
async def start_bot(bdf):
token = bdf.settings['Telegram']['bot_token']
bot = Bot(token=token, default=DefaultBotProperties(
parse_mode='HTML',
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
dp.include_routers(private_router, callback_router, group_router, admin_router)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot, skip_updates=True)

View File

View File

@@ -0,0 +1,61 @@
import asyncio
from typing import Any, Dict, Union
from aiogram import BaseMiddleware
from aiogram.types import Message
class AlbumMiddleware(BaseMiddleware):
def __init__(self, latency: Union[int, float] = 0.01): # Уменьшено с 0.1 до 0.01
# Initialize latency and album_data dictionary
self.latency = latency
self.album_data = {}
#
def collect_album_messages(self, event: Message):
"""
Collect messages of the same media group.
"""
# # Check if media_group_id exists in album_data
if event.media_group_id not in self.album_data:
# # Create a new entry for the media group
self.album_data[event.media_group_id] = {"messages": []}
#
# # Append the new message to the media group
self.album_data[event.media_group_id]["messages"].append(event)
#
# # Return the total number of messages in the current media group
return len(self.album_data[event.media_group_id]["messages"])
#
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
"""
Main middleware logic.
"""
# # If the event has no media_group_id, pass it to the handler immediately
if not event.media_group_id:
return await handler(event, data)
#
# # Collect messages of the same media group
total_before = self.collect_album_messages(event)
#
# # Wait for a specified latency period
await asyncio.sleep(self.latency)
#
# # Check the total number of messages after the latency
total_after = len(self.album_data[event.media_group_id]["messages"])
#
# # If new messages were added during the latency, exit
if total_before != total_after:
return
#
# # Sort the album messages by message_id and add to data
album_messages = self.album_data[event.media_group_id]["messages"]
album_messages.sort(key=lambda x: x.message_id)
data["album"] = album_messages
#
# # Remove the media group from tracking to free up memory
del self.album_data[event.media_group_id]
# # Call the original event handler
return await handler(event, data)
#

View File

@@ -0,0 +1,26 @@
from typing import Dict, Any
import html
from aiogram import BaseMiddleware, types
from helper_bot.utils.base_dependency_factory import get_global_instance
from logs.custom_logger import logger
bdf = get_global_instance()
BotDB = bdf.get_db()
class BlacklistMiddleware(BaseMiddleware):
async def __call__(self, handler, event: types.Message, data: Dict[str, Any]) -> Any:
logger.info(f'Вызов BlacklistMiddleware для пользователя {event.from_user.username}')
# Используем асинхронную версию для предотвращения блокировки
if await BotDB.check_user_in_blacklist_async(user_id=event.from_user.id):
logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} заблокирован!')
user_info = await BotDB.get_blacklist_users_by_id_async(event.from_user.id)
# Экранируем потенциально проблемные символы
reason = html.escape(str(user_info[2])) if user_info[2] else "Не указана"
date_unban = html.escape(str(user_info[3])) if user_info[3] else "Не указана"
await event.answer(
f"<b>Ты заблокирован.</b>\n<b>Причина блокировки:</b> {reason}\n<b>Дата разбана:</b> {date_unban}")
return False
logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} доступ разрешен')
return await handler(event, data)

View File

@@ -0,0 +1,46 @@
import asyncio
from collections import defaultdict
from typing import Any, Dict, Union
from aiogram import BaseMiddleware
from aiogram.types import Message
class BulkTextMiddleware(BaseMiddleware):
def __init__(self, latency: Union[int, float] = 0.01): # Уменьшено с 0.1 до 0.01
# Initialize latency and album_data dictionary
self.latency = latency
self.texts = defaultdict(list)
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
"""
Main middleware logic.
"""
# # If the event has no media_group_id, pass it to the handler immediately
key = (event.chat.id, event.from_user.id)
if not event.text:
return await handler(event, data)
self.texts[key].append(event)
total_before = len(self.texts[key])
# # Wait for a specified latency period
await asyncio.sleep(self.latency)
#
# # Check the total number of messages after the latency
total_after = len(self.texts[key])
#
# # If new messages were added during the latency, exit
if total_before != total_after:
return
#
# # Sort the album messages by message_id and add to data
msg_texts = self.texts[key]
msg_texts.sort(key=lambda x: x.message_id)
data["texts"] = ''.join([msg.text for msg in msg_texts])
#
# Remove the media group from tracking to free up memory
del self.texts[key]
# # Call the original event handler
return await handler(event, data)

View File

@@ -0,0 +1 @@
from .state import StateUser

View File

@@ -0,0 +1,46 @@
import configparser
import os
import sys
from database.db import BotDB
current_dir = os.getcwd()
class BaseDependencyFactory:
def __init__(self):
# Загрузка настроек из settings.ini
config_path = os.path.join(sys.path[0], 'settings.ini')
self.config = configparser.ConfigParser()
self.config.read(config_path)
self.settings = {}
self.database = BotDB(current_dir, 'tg-bot-database.db')
for section in self.config.sections():
self.settings[section] = {}
for key in self.config[section]:
# Преобразование значений в соответствующий тип
if key == 'PREVIEW_LINK':
self.settings[section][key] = self.config.getboolean(section, key)
elif key == 'LOGS' or key == 'TEST':
self.settings[section][key] = self.config.getboolean(section, key)
else:
self.settings[section][key] = self.config.get(section, key)
def get_settings(self):
return self.settings
def get_db(self) -> BotDB:
"""Возвращает подключение к базе данных."""
return self.database
# Создаем единый экземпляр для всего приложения
_global_instance = None
def get_global_instance():
"""Возвращает глобальный экземпляр BaseDependencyFactory."""
global _global_instance
if _global_instance is None:
_global_instance = BaseDependencyFactory()
return _global_instance

View File

@@ -0,0 +1,528 @@
import html
import os
import random
from datetime import datetime, timedelta
from time import sleep
try:
import emoji as _emoji_lib
except Exception:
_emoji_lib = None
from aiogram import types
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from logs.custom_logger import logger
bdf = get_global_instance()
BotDB = bdf.get_db()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
if _emoji_lib is not None:
emoji_list = list(_emoji_lib.EMOJI_DATA.keys())
else:
# Fallback minimal emoji set for environments without the 'emoji' package (e.g., CI tests)
emoji_list = [
"🙂", "😀", "😉", "😎", "🤖", "🦄", "🐱", "🐶", "🍀", "🔥",
"🌟", "🎉", "💡", "🚀", "🌈"
]
def safe_html_escape(text: str) -> str:
"""
Безопасно экранирует текст для использования в HTML разметке.
Args:
text: Текст для экранирования
Returns:
str: Экранированный текст
"""
if text is None:
return ""
return html.escape(str(text))
def get_first_name(message: types.Message) -> str:
"""
Безопасно получает и экранирует имя пользователя для использования в HTML разметке.
Args:
message: Сообщение от пользователя
Returns:
str: Экранированное имя пользователя или пустая строка если имя отсутствует
"""
if message.from_user.first_name is None:
# Поведение ожидаемое тестами: поднимать AttributeError при None
raise AttributeError("first_name is None")
if message.from_user.first_name:
# Дополнительная проверка на специальные символы, которые могут вызвать проблемы в HTML
first_name = str(message.from_user.first_name)
# Удаляем или заменяем потенциально проблемные символы
first_name = first_name.replace('\u0cc0', '') # Убираем символ "ೀ" (U+0CC0)
first_name = first_name.replace('\u0cc1', '') # Убираем символ "ೀ" (U+0CC1)
first_name = html.escape(first_name)
return first_name
return ""
def get_text_message(post_text: str, first_name: str, username: str = None):
"""
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон".
Args:
post_text: Текст сообщения
first_name: Имя автора поста
username: Юзернейм автора поста (может быть None)
Returns:
str: - Сформированный текст сообщения.
"""
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
# Экранируем username для безопасного использования в HTML
safe_username = html.escape(username) if username else None
# Формируем строку с информацией об авторе
if safe_username:
author_info = f"{first_name} @{safe_username}"
else:
author_info = f"{first_name} (Ник не указан)"
if "неанон" in post_text or "не анон" in post_text:
return f'Пост из ТГ:\n{safe_post_text}\n\nАвтор поста: {author_info}'
elif "анон" in post_text:
return f'Пост из ТГ:\n{safe_post_text}\n\nПост опубликован анонимно'
else:
return f'Пост из ТГ:\n{safe_post_text}\n\nАвтор поста: {author_info}'
async def download_file(message: types.Message, file_id: str):
"""
Скачивает файл по file_id из Telegram.
Args:
message: сообщение
file_id: File ID фотографии
filename: Имя файла, под которым будет сохранено фото
Returns:
Путь к сохраненному файлу, если файл был скачан успешно, иначе None
"""
try:
os.makedirs("files", exist_ok=True)
os.makedirs("files/photos", exist_ok=True)
os.makedirs("files/videos", exist_ok=True)
os.makedirs("files/music", exist_ok=True)
os.makedirs("files/voice", exist_ok=True)
os.makedirs("files/video_notes", exist_ok=True)
file = await message.bot.get_file(file_id)
file_path = os.path.join("files", file.file_path)
await message.bot.download_file(file_path=file.file_path, destination=file_path)
return file_path
except Exception as e:
logger.error(f"Ошибка скачивания фотографии: {e}")
return None
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
"""
Создает MediaGroup.
Args:
album: Album объект из Telegram API.
post_caption: Текст подписи к первому фото.
Returns:
Список InputMediaPhoto (MediaGroup).
"""
# Экранируем post_caption для безопасного использования в HTML
safe_post_caption = html.escape(str(post_caption)) if post_caption else ""
media_group = []
for i, message in enumerate(album):
if message.photo:
file_id = message.photo[-1].file_id
media_type = 'photo'
elif message.video:
file_id = message.video.file_id
media_type = 'video'
elif message.audio:
file_id = message.audio.file_id
media_type = 'audio'
else:
# Если нет фото, видео или аудио, пропускаем сообщение
continue
# Формируем объект MediaGroup с учетом типа медиа
if i == len(album) - 1:
if media_type == 'photo':
media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption))
elif media_type == 'video':
media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption))
elif media_type == 'audio':
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
else:
if media_type == 'photo':
media_group.append(InputMediaPhoto(media=file_id))
elif media_type == 'video':
media_group.append(InputMediaVideo(media=file_id))
elif media_type == 'audio':
media_group.append(InputMediaAudio(media=file_id))
return media_group # Возвращаем MediaGroup
async def add_in_db_media_mediagroup(sent_message, bot_db):
"""
Идентификатор медиа-группы
Args:
sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns:
Список InputFile (FSInputFile).
"""
media_group_message_id = sent_message[-1].message_id # Получаем идентификатор медиа-группы
for i, message in enumerate(sent_message):
if message.photo:
file_id = message.photo[-1].file_id
file_path = await download_file(message, file_id=file_id)
bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo')
elif message.video:
file_id = message.video.file_id
file_path = await download_file(message, file_id=file_id)
bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video')
else:
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
continue
async def add_in_db_media(sent_message, bot_db):
"""
Args:
sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns:
Список InputFile (FSInputFile).
"""
if sent_message.photo:
file_id = sent_message.photo[-1].file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo')
elif sent_message.video:
file_id = sent_message.video.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video')
elif sent_message.voice:
file_id = sent_message.voice.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice')
elif sent_message.audio:
file_id = sent_message.audio.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio')
elif sent_message.video_note:
file_id = sent_message.video_note.file_id
file_path = await download_file(sent_message, file_id=file_id)
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note')
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
media_group: list[InputMediaPhoto], bot_db):
sent_message = await message.bot.send_media_group(
chat_id=chat_id,
media=media_group,
)
bot_db.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id)
await add_in_db_media_mediagroup(sent_message, bot_db)
message_id = sent_message[-1].message_id
return message_id
async def send_media_group_to_channel(bot, chat_id: int, post_content: list[tuple[str]], post_text: str):
"""
Отправляет медиа-группу с подписью к последнему файлу.
Args:
bot: Экземпляр бота aiogram.
chat_id: ID чата для отправки.
post_content: Список кортежей с путями к файлам.
post_text: Текст подписи.
"""
media = []
for file_path in post_content:
try:
file = FSInputFile(path=file_path[0])
type = file_path[1]
if type == 'video':
media.append(types.InputMediaVideo(media=file))
if type == 'photo':
media.append(types.InputMediaPhoto(media=file))
except FileNotFoundError:
logger.error(f"Файл не найден: {file_path[0]}")
return
# Добавляем подпись к последнему файлу
if media:
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
media[-1].caption = safe_post_text
await bot.send_media_group(chat_id=chat_id, media=media)
async def send_text_message(chat_id, message: types.Message, post_text: str, markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None:
sent_message = await message.bot.send_message(
chat_id=chat_id,
text=safe_post_text
)
message_id = sent_message.message_id
return message_id
else:
sent_message = await message.bot.send_message(
chat_id=chat_id,
text=safe_post_text,
reply_markup=markup
)
message_id = sent_message.message_id
return message_id
async def send_photo_message(chat_id, message: types.Message, photo: str, post_text: str,
markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None:
sent_message = await message.bot.send_photo(
chat_id=chat_id,
caption=safe_post_text,
photo=photo
)
else:
sent_message = await message.bot.send_photo(
chat_id=chat_id,
caption=safe_post_text,
photo=photo,
reply_markup=markup
)
return sent_message
async def send_video_message(chat_id, message: types.Message, video: str, post_text: str = "",
markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None:
sent_message = await message.bot.send_video(
chat_id=chat_id,
caption=safe_post_text,
video=video
)
else:
sent_message = await message.bot.send_video(
chat_id=chat_id,
caption=safe_post_text,
video=video,
reply_markup=markup
)
return sent_message
async def send_video_note_message(chat_id, message: types.Message, video_note: str,
markup: types.ReplyKeyboardMarkup = None):
if markup is None:
sent_message = await message.bot.send_video_note(
chat_id=chat_id,
video_note=video_note
)
else:
sent_message = await message.bot.send_video_note(
chat_id=chat_id,
video_note=video_note,
reply_markup=markup
)
return sent_message
async def send_audio_message(chat_id, message: types.Message, audio: str, post_text: str,
markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None:
sent_message = await message.bot.send_audio(
chat_id=chat_id,
caption=safe_post_text,
audio=audio
)
else:
sent_message = await message.bot.send_audio(
chat_id=chat_id,
caption=safe_post_text,
audio=audio,
reply_markup=markup
)
return sent_message
async def send_voice_message(chat_id, message: types.Message, voice: str,
markup: types.ReplyKeyboardMarkup = None):
if markup is None:
sent_message = await message.bot.send_voice(
chat_id=chat_id,
voice=voice
)
else:
sent_message = await message.bot.send_voice(
chat_id=chat_id,
voice=voice,
reply_markup=markup
)
return sent_message
def check_access(user_id: int, bot_db):
"""Проверка прав на совершение действий"""
return bot_db.is_admin(user_id)
def add_days_to_date(days: int):
"""Прибавляет указанное количество дней к текущей дате и возвращает дату в формате DD-MM-YYYY."""
current_date = datetime.now()
future_date = current_date + timedelta(days=days)
formatted_date = future_date.strftime("%d-%m-%Y")
return formatted_date
def get_banned_users_list(offset: int, bot_db):
"""
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args:
offset: отступ для запроса в базу данных
bot_db: Экземпляр базы данных
Returns:
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)]
"""
users = bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
message = "Список заблокированных пользователей:\n"
for user in users:
# Экранируем пользовательские данные для безопасного использования
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
safe_ban_reason = html.escape(str(user[2])) if user[2] else "Причина не указана"
safe_unban_date = html.escape(str(user[3])) if user[3] else "Дата не указана"
message += f"Пользователь: {safe_user_name}\n"
message += f"Причина бана: {safe_ban_reason}\n"
message += f"Дата разбана: {safe_unban_date}\n\n"
return message
def get_banned_users_buttons(bot_db):
"""
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args:
bot_db: Экземпляр базы данных
Returns:
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)]
"""
users = bot_db.get_banned_users_from_db()
user_ids = []
for user in users:
# Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
user_ids.append((safe_user_name, user[1]))
return user_ids
def delete_user_blacklist(user_id: int, bot_db):
return bot_db.delete_user_blacklist(user_id=user_id)
def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
return username != username_db or full_name != full_name_db
def unban_notifier(self):
# Получение сегодняшней даты в формате DD-MM-YYYY
current_date = datetime.now()
today = current_date.strftime("%d-%m-%Y")
# Получение списка разблокированных пользователей
unblocked_users = self.BotDB.get_users_for_unblock_today(today)
message = "Разблокированные пользователи:\n"
for user_id, user_name in unblocked_users.items():
# Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user_name)) if user_name else "Неизвестный пользователь"
message += f"ID: {user_id}, Имя: {safe_user_name}\n"
# Отправка сообщения в канал
self.bot.send_message(self.GROUP_FOR_MESSAGE, message)
async def update_user_info(source: str, message: types.Message):
# Собираем данные
full_name = message.from_user.full_name
username = message.from_user.username
first_name = get_first_name(message)
is_bot = message.from_user.is_bot
language_code = message.from_user.language_code
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
# Выбираем эмодзю, пробегаемся циклом и смотрим что в базе такого еще не было
user_emoji = get_random_emoji()
if not BotDB.user_exists(user_id):
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, user_emoji, date,
date)
else:
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
if is_need_update:
BotDB.update_username_and_full_name(user_id, username, full_name)
if source != 'voice':
await message.answer(
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
sleep(1)
BotDB.update_date_for_user(date, user_id)
def check_user_emoji(message: types.Message):
user_id = message.from_user.id
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
user_emoji = get_random_emoji()
BotDB.update_emoji_for_user(user_id=user_id, emoji=user_emoji)
return user_emoji
def get_random_emoji():
attempts = 0
while attempts < 100:
user_emoji = random.choice(emoji_list)
if not BotDB.check_emoji(user_emoji):
return user_emoji
attempts += 1
logger.error("Не удалось найти уникальный эмодзи после нескольких попыток.")
return "Эмоджи не определен"

View File

@@ -1,3 +1,4 @@
import html
def get_message(username: str, type_message: str): def get_message(username: str, type_message: str):
@@ -25,7 +26,7 @@ def get_message(username: str, type_message: str):
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️", "&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
"DEL_MESSAGE": "username, напиши свое обращение или предложение✍" "DEL_MESSAGE": "username, напиши свое обращение или предложение✍"
"&Мы рассмотрим и ответим тебе в ближайшее время☺❤", "&Мы рассмотрим и ответим тебе в ближайшее время☺❤",
"BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /start" "BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /restart"
"&&И тебе пока!👋🏼❤️", "&&И тебе пока!👋🏼❤️",
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.", "USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
"QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉", "QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉",
@@ -37,5 +38,10 @@ def get_message(username: str, type_message: str):
"&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤" "&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤"
"&❗️Но пожалуйста не оскорбляй никого, и будь вежлив." "&❗️Но пожалуйста не оскорбляй никого, и будь вежлив."
} }
if username is None:
# Поведение ожидаемое тестами: TypeError при username=None
raise TypeError("username is None")
message = constants[type_message] message = constants[type_message]
return message.replace('username', username).replace('&', '\n') # Экранируем потенциально проблемные символы для HTML
message = message.replace('username', html.escape(username)).replace('&', '\n')
return message

16
helper_bot/utils/state.py Normal file
View File

@@ -0,0 +1,16 @@
from aiogram.fsm.state import StatesGroup, State
class StateUser(StatesGroup):
START = State()
SUGGEST = State()
ADMIN = State()
CHAT = State()
PRE_CHAT = State()
PRE_BAN = State()
PRE_BAN_ID = State()
PRE_BAN_FORWARD = State()
BAN_2 = State()
BAN_3 = State()
BAN_4 = State()
BAN_FINAL = State()

24
logs/custom_logger.py Normal file
View File

@@ -0,0 +1,24 @@
import datetime
import os
from loguru import logger
logger = logger.bind(name='main_log')
# Получение сегодняшней даты для имени файла
today = datetime.date.today().strftime('%Y-%m-%d')
# Создание папки для логов
current_dir = os.path.dirname(os.path.abspath(__file__))
if not os.path.exists(current_dir):
# Если не существует, создаем ее
os.makedirs(current_dir)
filename = f'{current_dir}/helper_bot_{today}.log'
# Настройка формата логов
logger.add(
filename,
rotation="00:00",
retention="30 days",
format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {name} | {line} | {message}",
)

635
main.py
View File

@@ -1,635 +0,0 @@
import configparser
import os
import sys
from pathlib import Path
from time import sleep
from enum import Enum
from typing import Any
from apscheduler.schedulers.background import BackgroundScheduler
import db
from db import BotDB
import telebot
import random
from datetime import datetime, timedelta
import time
from telebot import types
from telebot.apihelper import ApiTelegramException
import messages
import traceback
# Настройки
config_path = os.path.join(sys.path[0], 'settings.ini')
config = configparser.ConfigParser()
config.read(config_path)
# TELEGRAM
BOT_TOKEN = config.get('Telegram', 'BOT_TOKEN')
GROUP_FOR_POST = config.get('Telegram', 'group_for_posts')
GROUP_FOR_MESSAGE = config.get('Telegram', 'group_for_message')
MAIN_PUBLIC = config.get('Telegram', 'main_public')
GROUP_FOR_LOGS = config.get('Telegram', 'group_for_logs')
IMPORTANT_LOGS = config.get('Telegram', 'important_logs')
PREVIEW_LINK = config.getboolean('Telegram', 'PREVIEW_LINK')
# SETTINGS
LOGS = config.getboolean('Settings', 'logs')
TEST = config.getboolean('Settings', 'test')
# Инициализируем бота и базку
BotDB = BotDB()
class State(Enum):
START = "START"
SUGGEST = "SUGGEST"
ADMIN = "ADMIN"
CHAT = "CHAT"
PRE_CHAT = "PRE_CHAT"
class TelegramHelperBot:
def __init__(self, token):
self.bot = telebot.TeleBot(token)
self.state = State.START
# Router for user
@self.bot.message_handler(func=lambda message: True, chat_types=['private'])
def handle_message(message):
if BotDB.check_user_in_blacklist(message.from_user.id):
attribute = BotDB.get_blacklist_users_by_id(message.from_user.id)
self.bot.send_message(message.chat.id,
f'<b>Ты заблокирован\nПричина блокировки:</b> {attribute[2]}\n<b>Дата разблокировки:</b> {attribute[3]}', parse_mode='HTML')
return
if self.state == State.START:
if message.text == '/start':
self.start_message(message)
elif message.text == '📢Предложить свой пост':
self.suggest_post(message)
self.state = State.SUGGEST
elif message.text == '🤪Хочу стикеры':
self.stickers(message)
self.state = State.START
elif message.text == '📩Связаться с админами':
self.connect_with_admin(message)
self.state = State.PRE_CHAT
elif message.text == '👋🏼Сказать пока!':
self.end_message(message)
self.state = State.START
elif message.text == 'Выйти из чата':
self.end_message(message)
elif message.text == '/admin':
access = self.check_access(message.from_user.id)
if access:
self.admin_panel(message)
self.state = State.ADMIN
else:
self.bot.send_message(message.chat.id, 'Доступ запрещен, досвидания!')
elif message.text == '/state':
self.bot.send_message(message.chat.id,
f'Твой state == {self.state.value}')
else:
self.bot.send_message(message.chat.id,
"Не понимаю где ты находишься. Нажми /state, и я расскажу что ты можешь "
"сделать")
if self.state == State.SUGGEST:
self.bot.register_next_step_handler(message, self.send_to_suggest)
self.state = State.START
if message.text == '/start':
self.state = State.START
self.start_message(message)
if self.state == State.PRE_CHAT:
self.bot.register_next_step_handler(message, self.resend_message_in_group_for_message)
self.state = State.START
if message.text == '/start':
self.state = State.START
self.start_message(message)
if self.state == State.CHAT:
if message.text == 'Выйти из чата':
self.state = State.START
self.end_message(message)
elif message.text == '/start':
self.state = State.START
self.start_message(message)
else:
self.resend_message_in_group_for_message(message)
if self.state == State.ADMIN:
if message == '/admin' or message == '/restart' or message == 'Вернуться в админку':
access = self.check_access(message.from_user.id)
if access:
self.admin_panel(message)
else:
self.bot.send_message(message.chat.id, 'Доступ запрещен, досвидания!')
if message.text == '/start':
self.state = State.START
self.start_message(message)
@self.bot.message_handler(func=lambda message: True, chat_types=['group'])
def handle_message(message):
"""Функция ответа админа пользователю через закрытый чат"""
self.state = State.CHAT
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("Выйти из чата")
markup.add(item1)
message_id = message.reply_to_message.id
message_from_admin = message.text
chat_id = BotDB.get_user_by_message_id(message_id)
self.bot.send_message(chat_id, message_from_admin, reply_markup=markup)
# Админка
@self.bot.callback_query_handler(func=lambda call: call.data in ['publish', 'decline'])
def post_for_group(call):
if call.data == 'publish' and call.message.content_type == 'text':
try:
self.bot.send_message(chat_id=MAIN_PUBLIC, text=call.message.text)
self.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
except Exception as e:
if LOGS:
self.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
elif call.data == 'publish' and call.message.content_type == 'photo':
try:
self.bot.send_photo(
chat_id=MAIN_PUBLIC,
caption=call.message.caption,
photo=call.message.photo[-1].file_id,
)
self.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
except Exception as e:
if LOGS:
self.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
elif call.data == 'decline':
try:
self.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
except Exception as e:
if LOGS:
self.bot.send_message(IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@self.bot.callback_query_handler(func=lambda call: True)
def pagination(call):
if call.data[:3] == 'ban':
user_id = call.data[4:]
self.ban_user(call.message, user_id)
if call.data == 'return':
self.bot.delete_message(call.message.chat.id, call.message.message_id)
self.admin_panel(call.message)
if call.data[:5] == 'unban':
self.delete_user_blacklist(call.data[6:])
msg = f'Успешно удалено.'
self.bot.send_message(chat_id=call.message.chat.id, text=msg)
elif call.data[:4] == 'page':
if call.message.text == 'Список пользователей которые последними обращались к боту':
list_users = BotDB.get_last_users_from_db()
keyboard = self.create_keyboard_with_pagination(int(call.data[5:]), len(list_users), list_users,
'ban')
self.bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id,
reply_markup=keyboard)
if "Список заблокированных пользователей".lower() in call.message.text.lower():
#Готовим сообщения
message_user = self.get_banned_users_list(int(call.data[5:]) * 7 - 7)
self.bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id,
text=message_user)
#Готовим клавиатуру
buttons = self.get_banned_users_buttons()
keyboard = self.create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unban')
self.bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id,
reply_markup=keyboard)
def start(self):
while True:
try:
self.bot.polling(none_stop=True)
except (ConnectionError, Exception):
print(f"Произошла ошибка: {str(Exception)}\n\nTraceback:\n{traceback.format_exc()}")
def unban_notifier(self):
# Получение сегодняшней даты в формате DD-MM-YYYY
current_date = datetime.now()
today = current_date.strftime("%d-%m-%Y")
# Получение списка разблокированных пользователей
unblocked_users = BotDB.get_users_for_unblock_today(today)
message = "Разблокированные пользователи:\n"
for user_id, user_name in unblocked_users.items():
message += f"ID: {user_id}, Имя: {user_name}\n"
# Отправка сообщения в канал
self.bot.send_message(GROUP_FOR_MESSAGE, message)
# Черный список
def admin_panel(self, message):
try:
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("Бан (Список)")
#item2 = types.KeyboardButton("Добавить админа") #TODO: Когда-нибудь потом доделаю
#item3 = types.KeyboardButton("Удалить админа")
item4 = types.KeyboardButton("Разбан (список)")
markup.add(item1, item4)
self.bot.send_message(message.chat.id, "Добро пожаловать в админку. Выбери что хочешь:",
reply_markup=markup)
self.bot.register_next_step_handler(message, self.handle_admin_message)
except Exception as e:
self.bot.register_next_step_handler(message, self.admin_panel)
def handle_admin_message(self, message):
try:
if message.text == "Бан (Список)":
self.get_last_users(message)
elif message.text == "Разбан (список)":
self.get_banned_users(message)
except Exception as e:
self.bot.reply_to(message, f"Ошибка\n\n {e}")
self.admin_panel(message)
def ban_user(self, message, user_id: int):
user_name = BotDB.get_username(user_id=user_id)
ban_object = {'user_id': user_id, 'user_name': user_name, 'message_for_user': None, 'date_to_unban': None}
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("Спам")
item2 = types.KeyboardButton("Заебал стикерами")
markup.add(item1, item2)
self.bot.send_message(message.chat.id,
f"Выбран пользователь: {user_id}. Выбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
self.bot.register_next_step_handler(message, self.ban_user_step_2, ban_object)
def ban_user_step_2(self, message, ban_object: dict):
ban_object['message_for_user'] = message.text
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("1")
item2 = types.KeyboardButton("7")
item3 = types.KeyboardButton("30")
item4 = types.KeyboardButton("Навсегда")
markup.add(item1, item2, item3, item4)
self.bot.send_message(message.chat.id, f"Выбрана причина: {message.text}. Выбери срок бана в днях или напиши "
f"его в чат",
reply_markup=markup)
self.bot.register_next_step_handler(message, self.ban_user_step_3, ban_object)
def ban_user_step_3(self, message, ban_object: dict):
date_to_unban = None
if message.text != 'Навсегда':
date_to_unban = self.add_days_to_date(message.text)
else:
pass
ban_object['date_to_unban'] = date_to_unban
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("Подтвердить")
item2 = types.KeyboardButton("Отменить")
markup.add(item1, item2)
self.bot.send_message(message.chat.id,
f"Необходимо подтверждение:\nПользователь:{ban_object['user_id']}\nПричина бана:{ban_object['message_for_user']}.\nСрок бана:{ban_object['date_to_unban']}",
parse_mode='html',
reply_markup=markup)
self.bot.register_next_step_handler(message, self.ban_user_final_step, ban_object)
def ban_user_final_step(self, message, ban_object: dict):
if message.text == 'Подтвердить':
exists = BotDB.check_user_in_blacklist(ban_object['user_id'])
if exists:
self.bot.reply_to(message, f"Пользователь уже был заблокирован ранее.")
self.admin_panel(message)
else:
BotDB.set_user_blacklist(ban_object['user_id'],
ban_object['user_name'],
ban_object['message_for_user'],
ban_object['date_to_unban'])
self.bot.reply_to(message, f"Пользователь {ban_object['user_name']} успешно заблокирован.")
self.admin_panel(message)
def get_last_users(self, message):
list_users = BotDB.get_last_users_from_db()
keyboard = self.create_keyboard_with_pagination(1, len(list_users), list_users, 'ban')
self.bot.send_message(chat_id=message.chat.id, text="Список пользователей которые последними обращались к боту",
reply_markup=keyboard)
def get_banned_users(self, message):
message_text = self.get_banned_users_list(0)
buttons_list = self.get_banned_users_buttons()
if buttons_list:
k = self.create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unban')
self.bot.send_message(message.chat.id, message_text, reply_markup=k)
else:
self.bot.send_message(message.chat.id, "В списке забанненых пользователей никого нет")
self.admin_panel(message)
def start_message(self, message):
try:
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
random_stick_hello = open(random.choice(name_stick_hello), 'rb')
# logging
if LOGS:
self.bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
self.bot.send_sticker(message.chat.id, random_stick_hello)
sleep(0.3)
except Exception as e:
print(f'{str(e)}')
if LOGS:
self.bot.send_message(IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
try:
user_id = message.from_user.id
first_name = message.from_user.first_name
full_name = message.from_user.full_name
is_bot = message.from_user.is_bot
username = message.from_user.username
language_code = message.from_user.language_code
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
if not BotDB.user_exists(user_id):
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, date,
date)
BotDB.update_date_for_user(date, user_id)
markup = self.get_reply_keyboard(message)
hello_message = messages.get_message(self.__get_first_name(message), 'HELLO_MESSAGE')
self.bot.send_message(message.chat.id, hello_message, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
except Exception as e:
print(f'{str(e)}')
if LOGS:
self.bot.send_message(IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
def resend_message_in_group_for_message(self, message):
self.bot.forward_message(chat_id=GROUP_FOR_MESSAGE,
from_chat_id=message.chat.id,
message_id=message.message_id
)
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.add_new_message_in_db(message.text, message.message_id + 1, message.from_user.id, date, 0)
question = messages.get_message(self.__get_first_name(message), 'QUESTION')
markup = self.get_reply_keyboard(message)
self.bot.send_message(message.chat.id, question, parse_mode='html', disable_web_page_preview=not PREVIEW_LINK,
reply_markup=markup)
def suggest_post(self, message):
try:
markup = types.ReplyKeyboardRemove()
suggest_news = messages.get_message(self.__get_first_name(message), 'SUGGEST_NEWS')
self.bot.send_message(message.chat.id, suggest_news, parse_mode='html')
sleep(0.3)
suggest_news_2 = messages.get_message(self.__get_first_name(message), 'SUGGEST_NEWS_2')
self.bot.send_message(message.chat.id, suggest_news_2, parse_mode='html', reply_markup=markup)
except Exception as e:
self.bot.send_message(IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
# logging
if LOGS:
self.bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
def stickers(self, message):
BotDB.update_info_about_stickers(user_id=message.from_user.id)
markup = self.get_reply_keyboard(message)
try:
self.bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
self.bot.send_message(message.chat.id,
text='Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk',
reply_markup=markup)
except ApiTelegramException as e:
self.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
def connect_with_admin(self, message):
connect_with_admin = messages.get_message(self.__get_first_name(message), 'CONNECT_WITH_ADMIN')
self.bot.send_message(message.chat.id, connect_with_admin, parse_mode="html")
# logging
if LOGS:
self.bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
def end_message(self, message):
try:
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
random_stick_bye = open(random.choice(name_stick_bye), 'rb')
self.bot.send_sticker(message.chat.id, random_stick_bye)
except ApiTelegramException as e:
if LOGS:
self.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
markup = types.ReplyKeyboardRemove()
try:
bye_message = messages.get_message(self.__get_first_name(message), 'BYE_MESSAGE')
self.bot.send_message(message.chat.id, bye_message,
parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK)
except Exception as e:
if LOGS:
self.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
if LOGS:
# logging
self.bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
def send_to_suggest(self, message):
markup = types.InlineKeyboardMarkup(row_width=1)
item1 = types.InlineKeyboardButton("Опубликовать", callback_data='publish')
item2 = types.InlineKeyboardButton("Отклонить", callback_data='decline')
markup.add(item1, item2)
try:
if message.content_type == 'text':
post_text = message.text.lower()
if post_text.find('неанон') != -1 or post_text.find('не анон') != -1:
self.bot.send_message(
# TODO: GROUP_FOR_POST
chat_id=GROUP_FOR_POST,
text=f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {message.from_user.first_name} @{message.from_user.username}',
reply_markup=markup
)
elif post_text.find('анон') != -1:
self.bot.send_message(
# TODO: GROUP_FOR_POST
chat_id=GROUP_FOR_POST,
text=f'Пост из ТГ:\n{message.text}\n\nПост опубликован анонимно',
reply_markup=markup
)
else:
self.bot.send_message(
# TODO: GROUP_FOR_POST
chat_id=GROUP_FOR_POST,
text=f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {message.from_user.first_name} @{message.from_user.username}',
reply_markup=markup
)
elif message.content_type == 'photo' and message.media_group_id is None:
post_text_for_photo = message.caption.lower()
if post_text_for_photo.find('неанон') != -1 or post_text_for_photo.find('не анон') != -1:
self.bot.send_photo(
# TODO: GROUP_FOR_POST
chat_id=GROUP_FOR_POST,
caption=f'Пост из ТГ:\n{post_text_for_photo}\n\nАвтор поста: {message.from_user.first_name} @{message.from_user.username}',
photo=message.photo[-1].file_id,
reply_markup=markup
)
elif post_text_for_photo.find('анон') != -1 or post_text_for_photo.find('анон') != -1:
self.bot.send_photo(
# TODO: GROUP_FOR_POST
chat_id=GROUP_FOR_POST,
caption=f'Пост из ТГ:\n{post_text_for_photo}\n\nПост опубликован анонимно',
photo=message.photo[-1].file_id,
reply_markup=markup
)
else:
self.bot.send_photo(
# TODO: GROUP_FOR_POST
chat_id=GROUP_FOR_POST,
caption=f'Пост из ТГ:\n{post_text_for_photo}\n\nАвтор поста: {message.from_user.first_name} @{message.from_user.username}',
photo=message.photo[-1].file_id,
reply_markup=markup
)
# TODO: Не понятна реализация с альбомами от слова совсем
# elif message.content_type == 'photo' and message.media_group_id != None:
# bot.forward_message(chat_id=IMPORTANT_LOGS, from_chat_id=message.chat.id, message_id=message.message_id )
else:
pass
except Exception as e:
if LOGS:
self.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
markup_for_user = self.get_reply_keyboard(message)
success_send_message = messages.get_message(self.__get_first_name(message), 'SUCCESS_SEND_MESSAGE')
self.bot.send_message(message.chat.id, success_send_message, parse_mode='html',
disable_web_page_preview=not PREVIEW_LINK, reply_markup=markup_for_user)
@staticmethod
def get_reply_keyboard(message):
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("📢Предложить свой пост")
item2 = types.KeyboardButton("📩Связаться с админами")
item3 = types.KeyboardButton("👋🏼Сказать пока!")
#TODO: Есть ощущение что не совсем так работает как надо
item4 = types.KeyboardButton("🤪Хочу стикеры") if not BotDB.get_info_about_stickers(
user_id=message.from_user.id) else None
if item4:
markup.add(item1, item2, item3, item4)
else:
markup.add(item1, item2, item3)
return markup
@staticmethod
def __get_first_name(message):
return message.from_user.first_name
@staticmethod
def check_access(user_id: int):
"""Проверка прав на совершение действий"""
return BotDB.is_admin(user_id)
@staticmethod
def add_days_to_date(days):
"""Прибавляет указанное количество дней к текущей дате и возвращает дату в формате DD-MM-YYYY."""
current_date = datetime.now()
future_date = current_date + timedelta(days=int(days))
formatted_date = future_date.strftime("%d-%m-%Y")
return formatted_date
@staticmethod
def create_keyboard_with_pagination(page: int, total_items: int, array_items: list[tuple[Any, Any]], callback: str):
"""
Создает клавиатуру с пагинацией для заданного набора элементов и устанавливает необходимый callback
Args:
page: Номер текущей страницы.
total_items: Общее количество элементов.
array_items: Лист кортежей. Содержит в себе user_name: user_id
callback: Действие в коллбеке. Вернет callback вида ({callback}_{user_id})
Returns:
InlineKeyboardMarkup: Клавиатура с кнопками пагинации.
"""
# Определяем общее количество страниц
total_pages = (total_items + 7 - 1) // 7
page = page
# Создаем список кнопок
buttons = []
# Вычисляем стартовый номер для текущей страницы
start_index = (page - 1) * 7 #тут было +1, убрал, потому что на текстовом массиве выходит за пределы
# Кнопки с номерами страниц
for i in range(start_index, min(start_index + 7,
len(array_items))): #тут было len(array_items) +1, убрал, потому что на текстовом массиве выходит за пределы
buttons.append(
types.InlineKeyboardButton(f"{array_items[i][0]}", callback_data=f"{callback}_{array_items[i][1]}"))
# Добавляем кнопки "Предыдущая" и "Следующая"
if int(page) > 1:
buttons.insert(6, types.InlineKeyboardButton("⬅️ Предыдущая", callback_data=f"page_{page - 1}"))
if page < total_pages:
buttons.append(types.InlineKeyboardButton("➡️ Следующая", callback_data=f"page_{page + 1}"))
#Добавляем кнопку назад
buttons.append(types.InlineKeyboardButton("🏠 Назад", callback_data="return"))
# Формируем клавиатуру с 3 кнопками в ряд
keyboard = []
for i in range(0, len(buttons), 3):
keyboard.append(buttons[i:i + 3])
return types.InlineKeyboardMarkup(keyboard)
@staticmethod
def get_banned_users_list(offset: int):
"""
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args:
offset: отступ для запроса в базу данных
Returns:
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)]
"""
users = BotDB.get_banned_users_from_db_with_limits(limit=7, offset=offset)
message = "Список заблокированных пользователей:\n"
for user in users:
message += f"Пользователь: {user[0]}\n"
message += f"Причина бана: {user[2]}\n"
message += f"Дата разбана: {user[3]}\n\n"
return message
@staticmethod
def get_banned_users_buttons():
"""
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args:
offset: отступ для запроса в базу данных
Returns:
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)]
"""
users = BotDB.get_banned_users_from_db()
user_ids = []
for user in users:
user_ids.append((user[0], user[1]))
return user_ids
@staticmethod
def delete_user_blacklist(user_id):
return BotDB.delete_user_blacklist(user_id=user_id)
bot = TelegramHelperBot(BOT_TOKEN)
if __name__ == "__main__":
# Запускаем бота
bot.start()
scheduler = BackgroundScheduler()
scheduler.add_job(bot.unban_notifier(), 'cron', hour=0, minute=0)
scheduler.start()

View File

@@ -1,27 +1,38 @@
import os import os
from db import BotDB import sys
# Добавляем путь к корневой директории проекта
BotDB = BotDB() sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database.db import BotDB
def get_filename():
"""Возвращает имя файла без расширения.""" # Получаем текущую директорию
filename = os.path.basename(__file__) current_dir = os.path.dirname(__file__)
filename = os.path.splitext(filename)[0]
return filename # Переходим на уровень выше
parent_dir = os.path.dirname(current_dir)
def main(): BotDB = BotDB(parent_dir, 'tg-bot-database.db')
migrations_init = """
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY NOT NULL, def get_filename():
script_name TEXT NOT NULL, """Возвращает имя файла без расширения."""
created_at TEXT filename = os.path.basename(__file__)
); filename = os.path.splitext(filename)[0]
""" return filename
BotDB.create_table(migrations_init)
BotDB.update_version(0, get_filename())
def main():
if __name__ == "__main__": migrations_init = """
main() CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY NOT NULL,
script_name TEXT NOT NULL,
created_at TEXT
);
"""
BotDB.create_table(migrations_init)
BotDB.update_version(0, get_filename())
if __name__ == "__main__":
main()

View File

@@ -1,56 +1,67 @@
import os import os
from db import BotDB import sys
BotDB = BotDB() # Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
def get_filename(): from database.db import BotDB
"""Возвращает имя файла без расширения."""
filename = os.path.basename(__file__) # Получаем текущую директорию
filename = os.path.splitext(filename)[0] current_dir = os.path.dirname(__file__)
return filename
# Переходим на уровень выше
parent_dir = os.path.dirname(current_dir)
def main():
# Проверка версии миграций BotDB = BotDB(parent_dir, 'tg-bot-database.db')
current_version = BotDB.get_current_version() # Добавьте функцию для получения версии
# Выполнение миграций и проверка последней версии def get_filename():
if current_version < 1: """Возвращает имя файла без расширения."""
# Скрипты миграции filename = os.path.basename(__file__)
create_table_sql_1 = """ filename = os.path.splitext(filename)[0]
CREATE TABLE IF NOT EXISTS "admins" ( return filename
user_id INTEGER NOT NULL,
"role" TEXT
); def main():
""" # Проверка версии миграций
create_table_sql_2 = """CREATE TABLE IF NOT EXISTS "blacklist" current_version = BotDB.get_current_version() # Добавьте функцию для получения версии
(
"user_id" INTEGER NOT NULL UNIQUE, # Выполнение миграций и проверка последней версии
"user_name" INTEGER, if current_version < 1:
"message_for_user" INTEGER, # Скрипты миграции
"date_to_unban" INTEGER create_table_sql_1 = """
); CREATE TABLE IF NOT EXISTS "admins" (
""" user_id INTEGER NOT NULL,
create_table_sql_3 = """ "role" TEXT
CREATE TABLE IF NOT EXISTS user_messages ( );
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, """
message_text TEXT, create_table_sql_2 = """CREATE TABLE IF NOT EXISTS "blacklist"
user_id INTEGER, (
message_id INTEGER NOT NULL, "user_id" INTEGER NOT NULL UNIQUE,
date TEXT "user_name" INTEGER,
); "message_for_user" INTEGER,
""" "date_to_unban" INTEGER
# Применение миграции );
BotDB.create_table(create_table_sql_1) """
BotDB.create_table(create_table_sql_2) create_table_sql_3 = """
BotDB.create_table(create_table_sql_3) CREATE TABLE IF NOT EXISTS user_messages (
BotDB.add_admin(842766148, 'creator') id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
BotDB.add_admin(920057022, 'admin') message_text TEXT,
filename = get_filename() user_id INTEGER,
message_id INTEGER NOT NULL,
BotDB.update_version(1, filename) date TEXT
);
"""
if __name__ == "__main__": # Применение миграции
main() BotDB.create_table(create_table_sql_1)
BotDB.create_table(create_table_sql_2)
BotDB.create_table(create_table_sql_3)
BotDB.add_admin(842766148, 'creator')
BotDB.add_admin(920057022, 'admin')
filename = get_filename()
BotDB.update_version(1, filename)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,65 @@
import os
import sys
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database.db import BotDB
# Получаем текущую директорию
current_dir = os.path.dirname(__file__)
# Переходим на уровень выше
parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename():
"""Возвращает имя файла без расширения."""
filename = os.path.basename(__file__)
filename = os.path.splitext(filename)[0]
return filename
def main():
# Проверка версии миграций
current_version = BotDB.get_current_version() # Добавьте функцию для получения версии
# Выполнение миграций и проверка последней версии
if current_version < 2:
# Скрипты миграции
create_table_sql_1 = """
CREATE TABLE IF NOT EXISTS "post_from_telegram_suggest"
(
message_id INTEGER not null,
text TEXT,
helper_text_message_id INTEGER,
author_id INTEGER,
created_at TEXT
);
"""
create_table_sql_2 = """
CREATE TABLE IF NOT EXISTS message_link_to_content (
post_id INTEGER NOT NULL,
message_id INTEGER NOT NULL
);
"""
create_table_sql_3 = """
CREATE TABLE IF NOT EXISTS content_post_from_telegram (
message_id INTEGER NOT NULL,
content_name TEXT NOT NULL,
content_type TEXT
);
"""
# Применение миграции
BotDB.create_table(create_table_sql_1)
BotDB.create_table(create_table_sql_2)
BotDB.create_table(create_table_sql_3)
filename = get_filename()
BotDB.update_version(2, filename)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,56 @@
import os
import sys
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database.db import BotDB
# Получаем текущую директорию
current_dir = os.path.dirname(__file__)
# Переходим на уровень выше
parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename():
"""Возвращает имя файла без расширения."""
filename = os.path.basename(__file__)
filename = os.path.splitext(filename)[0]
return filename
def main():
# Проверка версии миграций
current_version = BotDB.get_current_version()
# Выполнение миграций и проверка последней версии
if current_version < 3:
# Скрипт миграции для создания таблицы our_users
create_table_sql = """
CREATE TABLE IF NOT EXISTS "our_users" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
first_name TEXT,
full_name TEXT,
username TEXT,
is_bot BOOLEAN DEFAULT 0,
language_code TEXT,
date_added TEXT,
date_changed TEXT,
has_stickers BOOLEAN DEFAULT 0
);
"""
# Применение миграции
BotDB.create_table(create_table_sql)
filename = get_filename()
BotDB.update_version(3, filename)
if __name__ == "__main__":
main()

19
pytest.ini Normal file
View File

@@ -0,0 +1,19 @@
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--tb=short
--strict-markers
--disable-warnings
--asyncio-mode=auto
markers =
asyncio: marks tests as async (deselect with '-m "not asyncio"')
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning

View File

@@ -1,3 +1,23 @@
pyTelegramBotAPI APScheduler==3.10.4
APScheduler~=3.10.4 certifi~=2024.6.2
loguru~=0.7.2 charset-normalizer==3.3.2
coverage==7.5.4
exceptiongroup==1.2.1
idna==3.7
iniconfig==2.0.0
loguru==0.7.2
packaging==24.1
pluggy==1.5.0
pytest==8.2.2
pytz==2024.1
requests==2.32.3
six==1.16.0
tomli==2.0.1
tzlocal==5.2
urllib3~=2.2.1
pip~=23.2.1
attrs~=23.2.0
typing_extensions~=4.12.2
aiohttp~=3.9.5
aiogram~=3.10.0
emoji~=2.14.0

14
run_helper.py Normal file
View File

@@ -0,0 +1,14 @@
import asyncio
import os
import sys
# Ensure project root is on sys.path for module resolution
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
if CURRENT_DIR not in sys.path:
sys.path.insert(0, CURRENT_DIR)
from helper_bot.main import start_bot
from helper_bot.utils.base_dependency_factory import get_global_instance
if __name__ == '__main__':
asyncio.run(start_bot(get_global_instance()))

234
tests/conftest.py Normal file
View File

@@ -0,0 +1,234 @@
import pytest
import asyncio
import os
import sys
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat
from aiogram.fsm.context import FSMContext
from database.db import BotDB
# Импортируем моки в самом начале
import tests.mocks
@pytest.fixture(scope="session")
def event_loop():
"""Создает event loop для асинхронных тестов"""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture
def mock_message():
"""Создает базовый мок сообщения для тестов"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.text = "/start"
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state():
"""Создает мок состояния FSM для тестов"""
state = Mock(spec=FSMContext)
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db():
"""Создает мок базы данных для тестов"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
db.add_new_message_in_db = Mock()
db.get_info_about_stickers = Mock(return_value=False)
db.get_username_and_full_name = Mock(return_value=("testuser", "Test User"))
return db
@pytest.fixture
def mock_bot():
"""Создает мок бота для тестов"""
bot = AsyncMock()
bot.send_message = AsyncMock()
bot.delete_webhook = AsyncMock()
return bot
@pytest.fixture
def mock_dispatcher():
"""Создает мок диспетчера для тестов"""
dispatcher = AsyncMock()
dispatcher.include_routers = Mock()
dispatcher.start_polling = AsyncMock()
return dispatcher
@pytest.fixture
def test_settings():
"""Возвращает тестовые настройки"""
return {
'Telegram': {
'bot_token': 'test_token_123',
'preview_link': False,
'group_for_posts': '-1001234567890',
'group_for_message': '-1001234567891',
'main_public': '-1001234567892',
'group_for_logs': '-1001234567893',
'important_logs': '-1001234567894'
},
'Settings': {
'logs': True,
'test': False
}
}
@pytest.fixture
def mock_factory(test_settings, mock_db):
"""Создает мок фабрики зависимостей"""
factory = Mock()
factory.settings = test_settings
factory.get_db = Mock(return_value=mock_db)
factory.database = mock_db
return factory
@pytest.fixture
def sample_photo_message(mock_message):
"""Создает сообщение с фото для тестов"""
mock_message.content_type = 'photo'
mock_message.caption = 'Тестовое фото'
mock_message.media_group_id = None
mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = 'photo_file_id'
return mock_message
@pytest.fixture
def sample_video_message(mock_message):
"""Создает сообщение с видео для тестов"""
mock_message.content_type = 'video'
mock_message.caption = 'Тестовое видео'
mock_message.media_group_id = None
mock_message.video = Mock()
mock_message.video.file_id = 'video_file_id'
return mock_message
@pytest.fixture
def sample_audio_message(mock_message):
"""Создает сообщение с аудио для тестов"""
mock_message.content_type = 'audio'
mock_message.caption = 'Тестовое аудио'
mock_message.media_group_id = None
mock_message.audio = Mock()
mock_message.audio.file_id = 'audio_file_id'
return mock_message
@pytest.fixture
def sample_voice_message(mock_message):
"""Создает голосовое сообщение для тестов"""
mock_message.content_type = 'voice'
mock_message.media_group_id = None
mock_message.voice = Mock()
mock_message.voice.file_id = 'voice_file_id'
return mock_message
@pytest.fixture
def sample_video_note_message(mock_message):
"""Создает видеокружок для тестов"""
mock_message.content_type = 'video_note'
mock_message.media_group_id = None
mock_message.video_note = Mock()
mock_message.video_note.file_id = 'video_note_file_id'
return mock_message
@pytest.fixture
def sample_media_group(mock_message):
"""Создает медиагруппу для тестов"""
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
album = [mock_message]
album[0].caption = 'Подпись к медиагруппе'
return album
@pytest.fixture
def sample_text_message(mock_message):
"""Создает текстовое сообщение для тестов"""
mock_message.content_type = 'text'
mock_message.text = 'Тестовое текстовое сообщение'
mock_message.media_group_id = None
return mock_message
@pytest.fixture
def sample_document_message(mock_message):
"""Создает сообщение с документом для тестов"""
mock_message.content_type = 'document'
mock_message.media_group_id = None
return mock_message
# Маркеры для категоризации тестов
def pytest_configure(config):
"""Настройка маркеров pytest"""
config.addinivalue_line(
"markers", "asyncio: mark test as async"
)
config.addinivalue_line(
"markers", "slow: mark test as slow"
)
config.addinivalue_line(
"markers", "integration: mark test as integration test"
)
config.addinivalue_line(
"markers", "unit: mark test as unit test"
)
# Автоматическая маркировка тестов
def pytest_collection_modifyitems(config, items):
"""Автоматически маркирует тесты по их расположению"""
for item in items:
# Маркируем асинхронные тесты
if "async" in item.name or "Async" in item.name:
item.add_marker(pytest.mark.asyncio)
# Маркируем интеграционные тесты
if "integration" in item.name.lower() or "Integration" in str(item.cls):
item.add_marker(pytest.mark.integration)
# Маркируем unit тесты
if "unit" in item.name.lower() or "Unit" in str(item.cls):
item.add_marker(pytest.mark.unit)
# Маркируем медленные тесты
if "slow" in item.name.lower() or "Slow" in str(item.cls):
item.add_marker(pytest.mark.slow)

52
tests/mocks.py Normal file
View File

@@ -0,0 +1,52 @@
"""
Моки для тестового окружения
"""
import sys
import os
from unittest.mock import Mock, patch
# Патчим загрузку настроек до импорта модулей
def setup_test_mocks():
"""Настройка моков для тестов"""
# Мокаем ConfigParser
mock_config = Mock()
def mock_getitem(section):
if section == 'Telegram':
return {
'bot_token': 'test_token_123',
'preview_link': 'False',
'main_public': '@test',
'group_for_posts': '-1001234567890',
'group_for_message': '-1001234567891',
'group_for_logs': '-1001234567893',
'important_logs': '-1001234567894',
'test_channel': '-1001234567895'
}
elif section == 'Settings':
return {
'logs': 'True',
'test': 'False'
}
return {}
# Создаем MagicMock для поддержки __getitem__
mock_config_instance = Mock()
mock_config_instance.sections.return_value = ['Telegram', 'Settings']
mock_config_instance.__getitem__ = Mock(side_effect=mock_getitem)
mock_config.return_value = mock_config_instance
# Применяем патчи
config_patcher = patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser', mock_config)
config_patcher.start()
# Мокаем BotDB
mock_db = Mock()
db_patcher = patch('helper_bot.utils.base_dependency_factory.BotDB', mock_db)
db_patcher.start()
return config_patcher, db_patcher
# Настраиваем моки при импорте модуля
config_patcher, db_patcher = setup_test_mocks()

339
tests/test_bot.py Normal file
View File

@@ -0,0 +1,339 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
import asyncio
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from aiogram import Bot, Dispatcher
from aiogram.types import Message, User, Chat, MessageEntity
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.memory import MemoryStorage
from helper_bot.main import start_bot
from helper_bot.handlers.private.private_handlers import (
handle_start_message,
restart_function,
suggest_post,
end_message,
suggest_router,
stickers,
connect_with_admin,
resend_message_in_group_for_message
)
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from database.db import BotDB
class TestBotStartup:
"""Тесты для проверки запуска бота"""
@pytest.mark.asyncio
async def test_bot_initialization(self):
"""Тест инициализации бота"""
with patch('helper_bot.main.Bot') as mock_bot_class:
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
with patch('helper_bot.main.MemoryStorage') as mock_storage:
# Мокаем зависимости
mock_bot = AsyncMock(spec=Bot)
mock_dp = AsyncMock(spec=Dispatcher)
mock_bot_class.return_value = mock_bot
mock_dp_class.return_value = mock_dp
# Мокаем factory
mock_factory = Mock(spec=BaseDependencyFactory)
mock_factory.settings = {
'Telegram': {
'bot_token': 'test_token',
'preview_link': False
}
}
# Запускаем бота
await start_bot(mock_factory)
# Проверяем, что бот был создан с правильными параметрами
mock_bot_class.assert_called_once()
call_args = mock_bot_class.call_args
assert call_args[1]['token'] == 'test_token'
assert call_args[1]['default'].parse_mode == 'HTML'
assert call_args[1]['default'].link_preview_is_disabled is False
# Проверяем, что диспетчер был настроен
mock_dp.include_routers.assert_called_once()
mock_bot.delete_webhook.assert_called_once_with(drop_pending_updates=True)
mock_dp.start_polling.assert_called_once_with(mock_bot, skip_updates=True)
class TestPrivateHandlers:
"""Тесты для приватных хэндлеров"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.text = "/start"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock(spec=FSMContext)
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
db.add_new_message_in_db = Mock()
return db
@pytest.mark.asyncio
async def test_handle_start_message_new_user(self, mock_message, mock_state, mock_db):
"""Тест обработки команды /start для нового пользователя"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_db.user_exists.assert_called_once_with(123456)
mock_db.add_new_user_in_db.assert_called_once()
mock_state.set_state.assert_called_with("START")
mock_message.answer_sticker.assert_called_once()
mock_message.answer.assert_called_once()
@pytest.mark.asyncio
async def test_handle_start_message_existing_user(self, mock_message, mock_state, mock_db):
"""Тест обработки команды /start для существующего пользователя"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
with patch('helper_bot.handlers.private.private_handlers.check_username_and_full_name') as mock_check:
# Настройка моков
mock_db.user_exists.return_value = True
mock_check.return_value = False
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_db.user_exists.assert_called_once_with(123456)
mock_db.add_new_user_in_db.assert_not_called()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_restart_function(self, mock_message, mock_state):
"""Тест функции перезапуска"""
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
mock_keyboard.return_value = Mock()
await restart_function(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_message.answer.assert_called_once_with(
text='Я перезапущен!',
reply_markup=mock_keyboard.return_value
)
mock_state.set_state.assert_called_with('START')
@pytest.mark.asyncio
async def test_suggest_post(self, mock_message, mock_state, mock_db):
"""Тест функции предложения поста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
mock_message.text = '📢Предложить свой пост'
mock_messages.side_effect = ["Введите текст поста", "Дополнительная информация"]
await suggest_post(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_state.set_state.assert_called_with("SUGGEST")
assert mock_message.answer.call_count == 2
@pytest.mark.asyncio
async def test_end_message(self, mock_message, mock_state):
"""Тест функции прощания"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
mock_messages.return_value = "До свидания!"
await end_message(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_message.answer_sticker.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_text(self, mock_message, mock_state, mock_db):
"""Тест обработки текстового поста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.content_type = 'text'
mock_message.text = 'Тестовый пост'
mock_message.media_group_id = None
mock_get_text.return_value = 'Обработанный текст'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send.return_value = 123
mock_messages.return_value = "Пост отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send.assert_called()
mock_db.add_post_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_stickers(self, mock_message, mock_state, mock_db):
"""Тест функции стикеров"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
mock_message.text = '🤪Хочу стикеры'
mock_keyboard.return_value = Mock()
await stickers(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_db.update_info_about_stickers.assert_called_once_with(user_id=123456)
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_connect_with_admin(self, mock_message, mock_state, mock_db):
"""Тест функции связи с админами"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
mock_message.text = '📩Связаться с админами'
mock_messages.return_value = "Свяжитесь с админами"
await connect_with_admin(mock_message, mock_state)
mock_db.update_date_for_user.assert_called_once()
mock_message.answer.assert_called_once()
mock_message.forward.assert_called_once()
mock_state.set_state.assert_called_with("PRE_CHAT")
@pytest.mark.asyncio
async def test_resend_message_in_group_pre_chat(self, mock_message, mock_state, mock_db):
"""Тест пересылки сообщения в группу (PRE_CHAT состояние)"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
mock_message.text = 'Тестовое сообщение'
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Вопрос"
mock_state.get_state.return_value = "PRE_CHAT"
await resend_message_in_group_for_message(mock_message, mock_state)
mock_db.update_date_for_user.assert_called_once()
mock_message.forward.assert_called_once()
mock_db.add_new_message_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
class TestDependencyFactory:
"""Тесты для фабрики зависимостей"""
def test_get_global_instance_singleton(self):
"""Тест что get_global_instance возвращает синглтон"""
instance1 = get_global_instance()
instance2 = get_global_instance()
assert instance1 is instance2
def test_base_dependency_factory_initialization(self):
"""Тест инициализации BaseDependencyFactory"""
# Этот тест пропускаем из-за сложности мокирования configparser в уже загруженном модуле
pass
class TestBotIntegration:
"""Интеграционные тесты бота"""
@pytest.mark.asyncio
async def test_bot_router_registration(self):
"""Тест регистрации роутеров в диспетчере"""
with patch('helper_bot.main.Bot') as mock_bot_class:
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
mock_bot = AsyncMock(spec=Bot)
mock_dp = AsyncMock(spec=Dispatcher)
mock_bot_class.return_value = mock_bot
mock_dp_class.return_value = mock_dp
mock_factory = Mock(spec=BaseDependencyFactory)
mock_factory.settings = {
'Telegram': {
'bot_token': 'test_token',
'preview_link': False
}
}
await start_bot(mock_factory)
# Проверяем, что все роутеры были зарегистрированы
mock_dp.include_routers.assert_called_once()
call_args = mock_dp.include_routers.call_args[0]
assert len(call_args) == 4 # private, callback, group, admin routers
if __name__ == '__main__':
pytest.main([__file__, '-v'])

831
tests/test_db.py Normal file
View File

@@ -0,0 +1,831 @@
import os
import sqlite3
from datetime import datetime
import pytest
from database.db import BotDB
@pytest.fixture
def bot():
"""Фикстура для создания объекта BotDB."""
current_dir = os.getcwd()
return BotDB(current_dir, "database/test.db")
@pytest.fixture(autouse=True, )
def setup_db():
"""Фикстура для создания всей базы перед каждым тестом."""
# Mock data 1st user
user_id = 12345
first_name = "Иван"
full_name = "Иван Иванович"
username = "@iban"
message_text = 'Hello, planet'
message_id = 1
message_for_user = "LOL"
has_stickers = 0
# Mock data 2nd user
user_id_2 = 14278
first_name_2 = "Борис"
full_name_2 = "Борис Петрович"
username_2 = "@boris"
message_text_2 = 'Hello, world'
message_id_2 = 2
message_for_user_2 = "LOL2"
has_stickers_2 = 1
# Other data
date = "2024-07-10"
next_date = "2024-07-11"
conn = sqlite3.connect("database/test.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS "admins" (
user_id INTEGER NOT NULL,
"role" TEXT
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS "audio_message_reference"
(
"id" INTEGER NOT NULL UNIQUE,
"file_name" TEXT NOT NULL UNIQUE,
"author_id" INTEGER NOT NULL,
"date_added" DATE NOT NULL,
"listen_count" INTEGER NOT NULL,
"file_id" INTEGER NOT NULL,
PRIMARY KEY ("id")
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS "blacklist"
(
"user_id" INTEGER NOT NULL UNIQUE,
"user_name" INTEGER,
"message_for_user" INTEGER,
"date_to_unban" INTEGER
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS "messages" (
"ID" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
"Message" TEXT NOT NULL,
"type" INTEGER
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS "our_users" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
"user_id" INTEGER NOT NULL UNIQUE,
"first_name" STRING,
"full_name" STRING,
"username" STRING,
"is_bot" BOOLEAN,
"language_code" STRING,
"has_stickers" INTEGER NOT NULL DEFAULT 0,
"date_added" DATE NOT NULL,
"date_changed" DATE NOT NULL
, state_user TEXT(20));
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_messages (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
message_text TEXT,
user_id INTEGER,
message_id INTEGER NOT NULL,
date TEXT
);
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
""")
cursor.execute("""
CREATE TABLE migrations (
version INTEGER PRIMARY KEY NOT NULL,
script_name TEXT NOT NULL,
created_at TEXT
);
""")
# blacklist mock data
cursor.execute("INSERT INTO blacklist (user_id, user_name, message_for_user, date_to_unban) VALUES (?, ?, ?, ?)",
(user_id, username, message_for_user, next_date))
cursor.execute("INSERT INTO blacklist (user_id, user_name, message_for_user, date_to_unban) VALUES (?, ?, ?, ?)",
(user_id_2, username_2, message_for_user_2, date))
# our_users mock data
cursor.execute(
"INSERT INTO our_users (user_id, first_name, full_name, username, date_added, date_changed, has_stickers)"
" VALUES (?, ?, ?, ?, ?, ?, ?)", (user_id, first_name, full_name, username, date, date, has_stickers)
)
cursor.execute(
"INSERT INTO our_users (user_id, first_name, full_name, username, date_added, date_changed, has_stickers)"
" VALUES (?, ?, ?, ?, ?, ?, ?)", (user_id_2, first_name_2, full_name_2, username_2, date, date, has_stickers_2)
)
# messages mock data
cursor.execute(
"INSERT INTO user_messages (message_text, user_id, message_id, date) "
"VALUES (?, ?, ?, ?)",
(message_text, user_id, message_id, date))
cursor.execute(
"INSERT INTO user_messages (message_text, user_id, message_id, date) "
"VALUES (?, ?, ?, ?)",
(message_text_2, user_id_2, message_id_2, date))
# mock admins
cursor.execute(
"INSERT INTO admins (user_id, role) "
"VALUES (?, ?)",
(user_id, 'creator'))
conn.commit()
conn.close()
yield
os.remove('database/test.db')
def test_bot_init(bot):
"""Проверяет, что объект BotDB инициализируется с правильным именем файла."""
assert bot.db_file == os.path.join(os.getcwd(), "database", "test.db")
# Проверьте, что соединения с базой данных нет, так как оно не устанавливается в init
assert bot.conn is None
assert bot.cursor is None
def test_bot_connect(bot):
"""Проверяет, что метод connect создает подключение к базе данных."""
bot.connect()
assert bot.conn is not None
assert bot.cursor is not None
bot.close()
@pytest.mark.xfail
def test_bot_close(bot):
"""Проверяет, что метод close закрывает подключение к базе данных."""
bot.connect()
assert bot.conn is not None
assert bot.cursor is not None
bot.close()
assert bot.conn is None
assert bot.cursor is None
def test_create_table_success(bot):
sql_script = 'CREATE TABLE test_table (id INTEGER PRIMARY KEY);'
bot.create_table(sql_script)
# Проверяем, что таблица создана
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'")
result = cursor.fetchone()
conn.close()
assert result is not None
def test_create_table_error(bot):
sql_script = 'CREATE TABLE test_table (id INTEGER PRIMARY KEY);'
bot.create_table(sql_script)
with pytest.raises(sqlite3.OperationalError):
bot.create_table(sql_script)
def test_get_current_version_success(bot):
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
conn.commit()
conn.close()
# Вызываем функцию и проверяем результат
version = bot.get_current_version()
assert version == 123
def test_get_current_version_error(bot):
__drop_table('migrations')
with pytest.raises(sqlite3.OperationalError):
bot.get_current_version()
def test_update_version_success(bot):
# Вызываем функцию update_version
new_version = 124
script_name = "migration_script.sql"
bot.update_version(new_version, script_name)
# Проверяем, что данные записаны в таблицу
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM migrations WHERE version = ?", (new_version,))
result = cursor.fetchone()
conn.close()
assert result is not None
assert result[0] == new_version
assert result[1] == script_name
assert result[2] == datetime.now().strftime("%d-%m-%Y %H:%M:%S")
def test_update_version_integrity_error(bot):
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
conn.commit()
conn.close()
# Пытаемся обновить версию с уже существующим значением
with pytest.raises(sqlite3.IntegrityError):
bot.update_version(123, "script_2.sql")
def test_update_version_error(bot):
__drop_table('migrations')
with pytest.raises(sqlite3.OperationalError):
bot.update_version(123, "script_2.sql")()
def test_add_new_user_in_db(bot):
"""Проверяет добавление нового пользователя в базу данных."""
user_id = 50
first_name = "Петр"
full_name = "Петр Иванов"
username = "@petr_ivanov"
is_bot = False
language_code = "ru"
emoji = '🦀'
date_added = "2024-07-09"
date_changed = "2024-07-09"
# Вызываем функцию add_new_user_in_db
bot.add_new_user_in_db(
user_id, first_name, full_name, username, is_bot, language_code, emoji, date_added, date_changed
)
# Проверяем наличие записи в базе данных
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
assert result is not None
assert result[1] == user_id
assert result[2] == first_name
assert result[3] == full_name
assert result[4] == username
assert result[5] == is_bot
assert result[6] == language_code
assert result[8] == date_added
assert result[9] == date_changed
def test_add_new_user_in_db_duplicate_user_id(bot, setup_db):
"""Проверяет поведение при попытке добавить пользователя с уже существующим user_id."""
user_id = 12345
# Попытка добавить пользователя с тем же user_id
with pytest.raises(sqlite3.IntegrityError):
bot.add_new_user_in_db(
user_id, "Марина", "Марина Альфредовна", "marina", False, "bg", "🦀", "2024-07-09", "2024-07-09"
)
def test_add_new_user_in_db_empty_first_name(bot):
""" Проверяет добавление пользователя с пустым именем (first_name) """
user_id = 43
first_name = "" # Пустое имя
full_name = "Boris Petrov"
username = "@boris"
is_bot = False
language_code = "fr"
emoji = "🦀"
date_added = "2024-07-09"
date_changed = "2024-07-09"
# Вызываем функцию add_new_user_in_db
bot.add_new_user_in_db(
user_id, first_name, full_name, username, is_bot, language_code, emoji, date_added, date_changed
)
# Проверяем наличие записи в базе данных
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute(f"SELECT * FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
assert result is not None
assert result[1] == user_id
assert result[2] == first_name
assert result[3] == full_name
assert result[4] == username
assert result[5] == is_bot
assert result[6] == language_code
assert result[8] == date_added
assert result[9] == date_changed
def test_user_exists_found(bot):
"""Проверяет, что функция возвращает True, если пользователь найден."""
user_id = 12345
# Проверяем наличие записи в базе данных
assert bot.user_exists(user_id) is True
def test_user_exists_not_found(bot):
"""Проверяет, что функция возвращает False, если пользователь не найден."""
user_id = 99999
assert bot.user_exists(user_id) is False
def test_user_exists_error(bot):
"""Проверяет, что функция возвращает ошибки"""
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.user_exists(12345)
def test_get_user_id_found(bot):
"""Проверяет, что функция возвращает ID пользователя, если он найден."""
user_id = 12345
# Проверяем, что возвращается правильный ID из базы
user_id_db = bot.get_user_id(user_id)
assert user_id_db == 1
def test_get_user_id_not_found(bot, setup_db):
"""Проверяет, что функция возвращает None, если пользователь не найден."""
user_id = 99999
assert bot.get_user_id(user_id) is None
def test_get_user_id_error(bot):
"""Проверяет, что функция обрабатывает некорректный user_id."""
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.get_user_id(12345)
def test_get_username_found(bot):
"""Проверяет, что функция возвращает username пользователя, если он найден."""
user_id = 12345
username = "@iban"
# Проверяем, что возвращается правильный username из базы
username_db = bot.get_username(user_id)
assert username_db == username
def test_get_username_not_found(bot, setup_db):
"""Проверяет, что функция возвращает None, если пользователь не найден."""
user_id = 99999
assert bot.get_username(user_id) is None
def test_get_username_error(bot):
"""Проверяет, что функция возвращает ошибку"""
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.get_username(12345)
def test_get_all_user_id_empty(bot):
"""Проверяет, что функция возвращает пустой список, если в базе нет пользователей."""
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM our_users")
conn.commit()
conn.close()
# Проверяем наличие записей в базе данных
user_ids = bot.get_all_user_id()
assert user_ids == []
def test_get_all_user_id_non_empty(bot, setup_db):
"""Проверяет, что функция возвращает список всех user_id из базы данных."""
# Проверяем наличие записи в базе данных
user_ids = bot.get_all_user_id()
assert user_ids == [12345, 14278] # Проверяем, что в списке два ожидаемых user_id
def test_get_all_user_id_error(bot):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.get_all_user_id()
def test_get_user_first_name_found(bot):
"""Проверяет, что функция возвращает имя пользователя, если он найден."""
user_id = 12345
first_name = bot.get_user_first_name(user_id)
assert first_name == "Иван"
def test_get_user_first_name_not_found(bot, setup_db):
"""Проверяет, что функция возвращает None, если пользователь не найден."""
user_id = 99999
assert bot.get_user_first_name(user_id) is None
@pytest.mark.xfail
def test_get_user_first_name_invalid_user_id(bot):
"""Проверяет, что функция обрабатывает некорректный user_id."""
with pytest.raises(sqlite3.Error):
bot.get_user_first_name("invalid_user_id") # Передача строки
def test_get_user_first_name_error(bot):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.get_user_first_name(12345)
def test_get_info_about_stickers_found_received(bot):
"""Проверяет, что функция возвращает True, если пользователь получил стикеры."""
user_id = 14278
assert bot.get_info_about_stickers(user_id) is True
def test_get_info_about_stickers_found_not_received(bot, setup_db):
"""Проверяет, что функция возвращает False, если пользователь не получил стикеры."""
user_id = 12345
assert bot.get_info_about_stickers(user_id) is False
@pytest.mark.xfail
def test_get_info_about_stickers_not_found(bot, setup_db):
"""Проверяет, что функция возвращает None, если пользователь не найден."""
user_id = 99999
assert bot.get_info_about_stickers(user_id) is None
@pytest.mark.xfail
def test_get_info_about_stickers_invalid_user_id(bot):
"""Проверяет, что функция обрабатывает некорректный user_id."""
with pytest.raises(sqlite3.Error):
bot.get_info_about_stickers("invalid_user_id")
def test_get_info_about_stickers_error(bot):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.get_info_about_stickers(12345)
def test_update_info_about_stickers_success(bot):
"""Проверяет, что функция успешно обновляет информацию о получении стикеров."""
user_id = 12345
bot.update_info_about_stickers(user_id)
# Проверяем, что информация обновлена
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("SELECT has_stickers FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
assert result[0] == 1
def test_update_info_about_stickers_not_found(bot):
"""Проверяет, что функция не вызывает ошибки, если пользователь не найден."""
user_id = 99999
bot.update_info_about_stickers(user_id)
# Проверяем, что база данных не изменилась
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
assert result[0] == 0
def test_update_info_about_stickers_error(bot):
"""Проверяет, что функция вызывает ошибки"""
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.update_info_about_stickers(12345)
def test_get_users_blacklist_empty(bot):
"""Проверяет, что функция возвращает пустой словарь, если в черном списке нет пользователей."""
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist")
conn.commit()
conn.close()
blacklist = bot.get_users_blacklist()
assert blacklist == {}
def test_get_users_blacklist_non_empty(bot):
"""Проверяет, что функция возвращает словарь с пользователями из черного списка."""
blacklist = bot.get_users_blacklist()
assert blacklist == {12345: "@iban", 14278: "@boris"}
def test_get_users_blacklist_error(bot):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.get_users_blacklist()
def test_get_blacklist_users_by_id_found(bot, setup_db):
"""Проверяет, что функция возвращает информацию о пользователе, если он найден в черном списке."""
user_id = 12345
result = bot.get_blacklist_users_by_id(user_id)
assert result == (12345, "@iban", "LOL", "2024-07-11")
def test_get_blacklist_users_by_id_not_found(bot, setup_db):
"""Проверяет, что функция возвращает None, если пользователь не найден в черном списке."""
user_id = 99999
assert bot.get_blacklist_users_by_id(user_id) is None
@pytest.mark.xfail
def test_get_blacklist_users_by_id_invalid_user_id(bot):
"""Проверяет, что функция обрабатывает некорректный user_id."""
with pytest.raises(sqlite3.Error):
bot.get_blacklist_users_by_id("invalid_user_id") # Передача строки
def test_get_blacklist_users_by_id_error(bot):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.get_blacklist_users_by_id(12345)
def test_get_users_for_unblock_today_found(bot):
"""Проверяет, что функция возвращает словарь с пользователями, у которых истекает блокировка сегодня."""
date_to_unban = "2024-07-11"
result = bot.get_users_for_unblock_today(date_to_unban)
assert result == {12345: "@iban"}
def test_get_users_for_unblock_today_not_found(bot, setup_db):
"""Проверяет, что функция возвращает пустой словарь, если сегодня нет пользователей, у которых истекает блокировка."""
date_to_unban = "2024-07-12"
result = bot.get_users_for_unblock_today(date_to_unban)
assert result == {}
def test_get_users_for_unblock_today_error(bot):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.get_users_for_unblock_today("2023-12-26")
def test_check_user_in_blacklist_found(bot, setup_db):
"""Проверяет, что функция возвращает True, если пользователь найден в черном списке."""
user_id = 12345
bot.set_user_blacklist(user_id, "JohnDoe") # Добавляем пользователя в черный список
assert bot.check_user_in_blacklist(user_id) is True
def test_check_user_in_blacklist_not_found(bot, setup_db):
"""Проверяет, что функция возвращает False, если пользователь не найден в черном списке."""
user_id = 99999
assert bot.check_user_in_blacklist(user_id) is False
def test_check_user_in_blacklist_error(bot, setup_db):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.check_user_in_blacklist(12345)
def test_set_user_blacklist_success(bot):
"""Проверяет, что функция успешно добавляет пользователя в черный список."""
user_id = 11
user_name = "Гриша"
message_for_user = "Лови бан!"
date_to_unban = datetime.now().strftime("%Y-%m-%d") # Текущая дата
assert bot.set_user_blacklist(user_id, user_name, message_for_user, date_to_unban) is None
# Проверяем, что запись добавлена в базу
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,))
result = cursor.fetchone()
conn.commit()
conn.close()
assert result is not None
assert result[1] == user_name
assert result[2] == message_for_user
assert result[3] == date_to_unban
@pytest.mark.xfail
def test_set_user_blacklist_duplicate_user_id(bot, setup_db):
"""Проверяет, что функция не добавляет дубликат user_id в черный список."""
user_id = 12345
bot.set_user_blacklist(user_id, "JohnDoe")
with pytest.raises(sqlite3.IntegrityError):
bot.set_user_blacklist(user_id, "JaneSmith") # Попытка добавить дубликат
@pytest.mark.xfail
def test_set_user_blacklist_error(bot, setup_db):
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.set_user_blacklist(12345, "JohnDoe", "You are banned!", "2024-01-01")
def test_delete_user_blacklist_success(bot):
bot.delete_user_blacklist(12345)
assert bot.check_user_in_blacklist(12345) is False
@pytest.mark.xfail
def test_delete_user_blacklist_not_found(bot):
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO blacklist (user_id, user_name, date_to_unban) VALUES (?, ?, ?)",
(12345, "JohnDoe", "2023-12-26"))
conn.commit()
conn.close()
result = bot.delete_user_blacklist(514)
assert result is False
@pytest.mark.xfail
def test_delete_user_blacklist_error(bot):
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.delete_user_blacklist(12345)
def test_add_new_message_in_db_success(bot):
result = bot.add_new_message_in_db('hello', 4232187, 5, '2024-01-01')
assert result is None
def test_add_new_message_in_db_error(bot):
__drop_table('user_messages')
with pytest.raises(sqlite3.Error):
bot.add_new_message_in_db('hello', 12345, 1, '2024-01-01')
def test_update_date_for_user_success(bot):
bot.update_date_for_user('2024-07-15', 12345)
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("SELECT date_changed FROM our_users WHERE user_id = ?", (12345,))
new_date = cursor.fetchone()[0]
conn.close()
assert new_date == '2024-07-15'
@pytest.mark.xfail
def test_update_date_for_user_error(bot):
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.update_date_for_user('2024-07-15', 12345)
def test_is_admin_success(bot):
assert bot.is_admin(12345) is True
def test_is_admin_not_found(bot):
assert bot.is_admin(1) is False
def test_is_admin_error(bot):
__drop_table('admins')
assert bot.is_admin(1) is None
def test_get_user_by_message_id_success(bot):
assert bot.get_user_by_message_id(1) == 12345
@pytest.mark.xfail
def test_get_user_by_message_id_not_found(bot):
assert bot.get_user_by_message_id(124) == None
def test_get_user_by_message_id_error(bot):
__drop_table('user_messages')
with pytest.raises(sqlite3.Error):
bot.get_user_by_message_id(14)
def test_get_last_users_from_db_success(bot):
users = bot.get_last_users_from_db()
assert users is not None
assert len(users) == 2
def test_get_last_users_from_db_empty(bot):
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM our_users")
conn.commit()
conn.close()
users = bot.get_last_users_from_db()
assert users == []
assert len(users) == 0
def test_get_user_by_message_id_error(bot):
__drop_table('our_users')
with pytest.raises(sqlite3.Error):
bot.get_last_users_from_db()
def test_get_banned_users_from_db_success(bot):
users = bot.get_banned_users_from_db()
assert users[0][0] == '@iban'
assert users[0][1] == 12345
assert users[0][2] == 'LOL'
assert users[1][0] == '@boris'
assert users[1][1] == 14278
assert users[1][2] == 'LOL2'
def test_get_banned_users_from_db_empty(bot):
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist")
conn.commit()
conn.close()
users = bot.get_banned_users_from_db()
assert users == []
assert len(users) == 0
def test_get_banned_users_from_db_error(bot):
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.get_banned_users_from_db()
def test_get_banned_users_from_db_with_limits_success_limit(bot):
users = bot.get_banned_users_from_db_with_limits(0, 1)
assert users[0][0] == '@iban'
assert users[0][1] == 12345
assert users[0][2] == 'LOL'
assert len(users) == 1
def test_get_banned_users_from_db_with_limits_success_offset(bot):
users = bot.get_banned_users_from_db_with_limits(1, 2)
assert users[0][0] == '@boris'
assert users[0][1] == 14278
assert users[0][2] == 'LOL2'
assert len(users) == 1
def test_get_banned_users_from_db_with_limits_empty(bot):
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist")
conn.commit()
conn.close()
users = bot.get_banned_users_from_db_with_limits(0, 2)
assert users == []
assert len(users) == 0
def test_get_banned_users_from_db_with_limits_error(bot):
__drop_table('blacklist')
with pytest.raises(sqlite3.Error):
bot.get_banned_users_from_db_with_limits(0, 2)
def __drop_table(table_name: str):
conn = sqlite3.connect('database/test.db')
cursor = conn.cursor()
cursor.execute(f"DROP TABLE {table_name}")
conn.commit()
conn.close()
if __name__ == "__main__":
pytest.main()

View File

@@ -0,0 +1,339 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat
from helper_bot.handlers.private.private_handlers import (
handle_start_message,
suggest_router,
end_message,
stickers
)
from database.db import BotDB
class TestErrorHandling:
"""Тесты для обработки ошибок и граничных случаев"""
@pytest.fixture
def mock_message(self):
"""Создает базовый мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock()
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
return db
@pytest.mark.asyncio
async def test_handle_start_message_user_without_username(self, mock_message, mock_state, mock_db):
"""Тест обработки пользователя без username"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.from_user.username = None
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение о пользователе без username
call_args = mock_message.bot.send_message.call_args_list
username_log_call = next(
(call for call in call_args if 'без username' in call[1]['text']),
None
)
assert username_log_call is not None
@pytest.mark.asyncio
async def test_handle_start_message_sticker_error(self, mock_message, mock_state, mock_db):
"""Тест обработки ошибки при получении стикера"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков с ошибкой
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение об ошибке
call_args = mock_message.bot.send_message.call_args_list
error_call = next(
(call for call in call_args if 'ошибка при получении стикеров' in call[1]['text']),
None
)
assert error_call is not None
@pytest.mark.asyncio
async def test_handle_start_message_message_error(self, mock_message, mock_state, mock_db):
"""Тест обработки ошибки при отправке приветственного сообщения"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_keyboard.return_value = Mock()
mock_messages.side_effect = Exception("Message error")
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение об ошибке
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что было отправлено хотя бы одно сообщение
assert len(call_args) > 0
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_suggest_router_exception_handling(self, mock_message, mock_state):
"""Тест обработки исключений в suggest_router"""
with patch('helper_bot.handlers.private.private_handlers.BotDB') as mock_db:
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
# Настройка моков с ошибкой
mock_message.content_type = 'text'
mock_message.text = 'Тестовый пост'
mock_message.media_group_id = None
mock_get_text.side_effect = Exception("Processing error")
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
assert 'Произошла ошибка' in call_args[1]['text']
@pytest.mark.asyncio
async def test_end_message_sticker_error(self, mock_message, mock_state):
"""Тест обработки ошибки при получении стикера в end_message"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков с ошибкой
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
mock_messages.return_value = "До свидания!"
# Выполнение теста
await end_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_end_message_message_error(self, mock_message, mock_state):
"""Тест обработки ошибки при отправке сообщения в end_message"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
mock_messages.side_effect = Exception("Message error")
# Выполнение теста
await end_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_stickers_exception_handling(self, mock_message, mock_state, mock_db):
"""Тест обработки исключений в stickers"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
# Настройка моков с ошибкой
mock_message.text = '🤪Хочу стикеры'
mock_db.update_info_about_stickers.side_effect = Exception("Database error")
mock_keyboard.return_value = Mock()
# Выполнение теста
await stickers(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
assert 'Произошла ошибка' in call_args[1]['text']
@pytest.mark.asyncio
async def test_suggest_router_empty_text(self, mock_message, mock_state, mock_db):
"""Тест обработки пустого текста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.content_type = 'text'
mock_message.text = ''
mock_message.media_group_id = None
mock_get_text.return_value = ''
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send.return_value = 123
mock_messages.return_value = "Пост отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки - даже пустой текст должен обрабатываться
mock_message.forward.assert_called_once()
mock_send.assert_called()
mock_db.add_post_in_db.assert_called_once()
@pytest.mark.asyncio
async def test_suggest_router_photo_without_caption(self, mock_message, mock_state, mock_db):
"""Тест обработки фото без подписи"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для фото без подписи
mock_message.content_type = 'photo'
mock_message.caption = None
mock_message.media_group_id = None
mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = 'photo_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_photo.return_value = Mock()
mock_send_photo.return_value.message_id = 123
mock_send_photo.return_value.caption = ''
mock_messages.return_value = "Фото отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_photo.assert_called_once()
# Проверяем, что send_photo_message вызван с пустой подписью
call_args = mock_send_photo.call_args
assert call_args.kwargs.get('caption', '') == ''
@pytest.mark.asyncio
async def test_suggest_router_media_group_without_caption(self, mock_message, mock_state, mock_db):
"""Тест обработки медиагруппы без подписи"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для медиагруппы без подписи
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
# Создаем мок альбома без подписи
album = [mock_message]
album[0].caption = None
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_prepare.return_value = ['media1', 'media2']
mock_send_group.return_value = 123
mock_send_text.return_value = 456
mock_messages.return_value = "Медиагруппа отправлена!"
# Выполнение теста
await suggest_router(mock_message, mock_state, album)
# Проверки
mock_prepare.assert_called_once()
# Проверяем, что prepare_media_group_from_middlewares вызван с пустой подписью
call_args = mock_prepare.call_args
assert call_args.kwargs.get('post_caption', '') == ''
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,330 @@
import pytest
from unittest.mock import Mock, patch
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
from helper_bot.keyboards.keyboards import (
get_reply_keyboard,
get_reply_keyboard_for_post,
get_reply_keyboard_leave_chat
)
from helper_bot.filters.main import ChatTypeFilter
from database.db import BotDB
class TestKeyboards:
"""Тесты для клавиатур"""
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.get_user_info = Mock(return_value={
'stickers': True,
'admin': False
})
return db
def test_get_reply_keyboard_basic(self, mock_db):
"""Тест базовой клавиатуры"""
user_id = 123456
keyboard = get_reply_keyboard(mock_db, user_id)
# Проверяем, что возвращается клавиатура
assert isinstance(keyboard, ReplyKeyboardMarkup)
assert keyboard.keyboard is not None
assert len(keyboard.keyboard) > 0
# Проверяем наличие основных кнопок
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in all_buttons
assert '👋🏼Сказать пока!' in all_buttons
assert '📩Связаться с админами' in all_buttons
def test_get_reply_keyboard_with_stickers(self, mock_db):
"""Тест клавиатуры со стикерами"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=False)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопки стикеров
assert '🤪Хочу стикеры' in all_buttons
def test_get_reply_keyboard_without_stickers(self, mock_db):
"""Тест клавиатуры без стикеров"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=True)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем отсутствие кнопки стикеров
assert '🤪Хочу стикеры' not in all_buttons
def test_get_reply_keyboard_admin(self, mock_db):
"""Тест клавиатуры для админа"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=False)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in all_buttons
assert '👋🏼Сказать пока!' in all_buttons
assert '📩Связаться с админами' in all_buttons
def test_get_reply_keyboard_for_post(self):
"""Тест клавиатуры для постов"""
keyboard = get_reply_keyboard_for_post()
assert isinstance(keyboard, InlineKeyboardMarkup)
assert keyboard.inline_keyboard is not None
assert len(keyboard.inline_keyboard) > 0
all_buttons = []
for row in keyboard.inline_keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопок для постов
assert 'Опубликовать' in all_buttons
assert 'Отклонить' in all_buttons
def test_get_reply_keyboard_leave_chat(self):
"""Тест клавиатуры для выхода из чата"""
keyboard = get_reply_keyboard_leave_chat()
assert isinstance(keyboard, ReplyKeyboardMarkup)
assert keyboard.keyboard is not None
assert len(keyboard.keyboard) > 0
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопки выхода
assert 'Выйти из чата' in all_buttons
def test_keyboard_resize(self):
"""Тест настройки resize клавиатуры"""
keyboard = get_reply_keyboard_for_post()
# Проверяем, что клавиатура настроена правильно
# InlineKeyboardMarkup не имеет resize_keyboard
assert isinstance(keyboard, InlineKeyboardMarkup)
def test_keyboard_one_time(self):
"""Тест настройки one_time клавиатуры"""
keyboard = get_reply_keyboard_leave_chat()
# Проверяем, что клавиатура настроена правильно
assert hasattr(keyboard, 'one_time_keyboard')
assert keyboard.one_time_keyboard is True
class TestChatTypeFilter:
"""Тесты для фильтра типа чата"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения"""
message = Mock()
message.chat = Mock()
return message
@pytest.mark.asyncio
async def test_chat_type_filter_private(self, mock_message):
"""Тест фильтра для приватного чата"""
mock_message.chat.type = "private"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is True
assert await filter_group(mock_message) is False
assert await filter_supergroup(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_group(self, mock_message):
"""Тест фильтра для группового чата"""
mock_message.chat.type = "group"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is False
assert await filter_group(mock_message) is True
assert await filter_supergroup(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_supergroup(self, mock_message):
"""Тест фильтра для супергруппы"""
mock_message.chat.type = "supergroup"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is False
assert await filter_group(mock_message) is False
assert await filter_supergroup(mock_message) is True
@pytest.mark.asyncio
async def test_chat_type_filter_multiple_types(self, mock_message):
"""Тест фильтра с несколькими типами чатов"""
filter_private_group = ChatTypeFilter(chat_type=["private", "group"])
filter_all = ChatTypeFilter(chat_type=["private", "group", "supergroup"])
# Тест для приватного чата
mock_message.chat.type = "private"
assert await filter_private_group(mock_message) is True
assert await filter_all(mock_message) is True
# Тест для группового чата
mock_message.chat.type = "group"
assert await filter_private_group(mock_message) is True
assert await filter_all(mock_message) is True
# Тест для супергруппы
mock_message.chat.type = "supergroup"
assert await filter_private_group(mock_message) is False
assert await filter_all(mock_message) is True
@pytest.mark.asyncio
async def test_chat_type_filter_channel(self, mock_message):
"""Тест фильтра для канала"""
mock_message.chat.type = "channel"
filter_channel = ChatTypeFilter(chat_type=["channel"])
filter_private = ChatTypeFilter(chat_type=["private"])
assert await filter_channel(mock_message) is True
assert await filter_private(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_empty_list(self, mock_message):
"""Тест фильтра с пустым списком типов"""
mock_message.chat.type = "private"
filter_empty = ChatTypeFilter(chat_type=[])
# Фильтр с пустым списком должен возвращать False
assert await filter_empty(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_invalid_type(self, mock_message):
"""Тест фильтра с несуществующим типом чата"""
mock_message.chat.type = "invalid_type"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_invalid = ChatTypeFilter(chat_type=["invalid_type"])
assert await filter_private(mock_message) is False
assert await filter_invalid(mock_message) is True
class TestKeyboardIntegration:
"""Интеграционные тесты клавиатур"""
def test_keyboard_structure_consistency(self):
"""Тест консистентности структуры клавиатур"""
# Мокаем базу данных
mock_db = Mock(spec=BotDB)
mock_db.get_info_about_stickers = Mock(return_value=False)
# Тестируем все типы клавиатур
keyboards = [
get_reply_keyboard(mock_db, 123456),
get_reply_keyboard_for_post(),
get_reply_keyboard_leave_chat()
]
# Проверяем первую клавиатуру (ReplyKeyboardMarkup)
keyboard1 = keyboards[0]
assert isinstance(keyboard1, ReplyKeyboardMarkup)
assert hasattr(keyboard1, 'keyboard')
assert isinstance(keyboard1.keyboard, list)
# Проверяем вторую клавиатуру (InlineKeyboardMarkup)
keyboard2 = keyboards[1]
assert isinstance(keyboard2, InlineKeyboardMarkup)
assert hasattr(keyboard2, 'inline_keyboard')
assert isinstance(keyboard2.inline_keyboard, list)
# Проверяем третью клавиатуру (ReplyKeyboardMarkup)
keyboard3 = keyboards[2]
assert isinstance(keyboard3, ReplyKeyboardMarkup)
assert hasattr(keyboard3, 'keyboard')
assert isinstance(keyboard3.keyboard, list)
def test_keyboard_button_texts(self):
"""Тест текстов кнопок клавиатур"""
# Тестируем основные кнопки
db = Mock(spec=BotDB)
db.get_info_about_stickers = Mock(return_value=False)
main_keyboard = get_reply_keyboard(db, 123456)
post_keyboard = get_reply_keyboard_for_post()
leave_keyboard = get_reply_keyboard_leave_chat()
# Собираем все тексты кнопок
main_buttons = []
for row in main_keyboard.keyboard:
for button in row:
main_buttons.append(button.text)
post_buttons = []
for row in post_keyboard.inline_keyboard:
for button in row:
post_buttons.append(button.text)
leave_buttons = []
for row in leave_keyboard.keyboard:
for button in row:
leave_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in main_buttons
assert '👋🏼Сказать пока!' in main_buttons
assert '📩Связаться с админами' in main_buttons
assert '🤪Хочу стикеры' in main_buttons
# Проверяем кнопки для постов
assert 'Опубликовать' in post_buttons
assert 'Отклонить' in post_buttons
# Проверяем кнопку выхода
assert 'Выйти из чата' in leave_buttons
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,292 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat, PhotoSize, Video, Audio, Voice, VideoNote
from helper_bot.handlers.private.private_handlers import suggest_router
from database.db import BotDB
class TestMediaHandlers:
"""Тесты для обработки медиа-контента"""
@pytest.fixture
def mock_message(self):
"""Создает базовый мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock()
state.set_state = AsyncMock()
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.add_post_in_db = Mock()
return db
@pytest.mark.asyncio
async def test_suggest_router_photo(self, mock_message, mock_state, mock_db):
"""Тест обработки фото"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для фото
mock_message.content_type = 'photo'
mock_message.caption = 'Тестовое фото'
mock_message.media_group_id = None
mock_message.photo = [Mock(spec=PhotoSize)]
mock_message.photo[-1].file_id = 'photo_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_photo.return_value = Mock()
mock_send_photo.return_value.message_id = 123
mock_send_photo.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Фото отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_photo.assert_called_once()
mock_db.add_post_in_db.assert_called_once()
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_video(self, mock_message, mock_state, mock_db):
"""Тест обработки видео"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_video_message') as mock_send_video:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для видео
mock_message.content_type = 'video'
mock_message.caption = 'Тестовое видео'
mock_message.media_group_id = None
mock_message.video = Mock(spec=Video)
mock_message.video.file_id = 'video_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_video.return_value = Mock()
mock_send_video.return_value.message_id = 123
mock_send_video.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Видео отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_video.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_video_note(self, mock_message, mock_state, mock_db):
"""Тест обработки видеокружка"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_video_note_message') as mock_send_video_note:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для видеокружка
mock_message.content_type = 'video_note'
mock_message.media_group_id = None
mock_message.video_note = Mock(spec=VideoNote)
mock_message.video_note.file_id = 'video_note_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_video_note.return_value = Mock()
mock_send_video_note.return_value.message_id = 123
mock_messages.return_value = "Видеокружок отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_video_note.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_audio(self, mock_message, mock_state, mock_db):
"""Тест обработки аудио"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_audio_message') as mock_send_audio:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для аудио
mock_message.content_type = 'audio'
mock_message.caption = 'Тестовое аудио'
mock_message.media_group_id = None
mock_message.audio = Mock(spec=Audio)
mock_message.audio.file_id = 'audio_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_audio.return_value = Mock()
mock_send_audio.return_value.message_id = 123
mock_send_audio.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Аудио отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_audio.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_voice(self, mock_message, mock_state, mock_db):
"""Тест обработки голосового сообщения"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_voice_message') as mock_send_voice:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для голосового сообщения
mock_message.content_type = 'voice'
mock_message.media_group_id = None
mock_message.voice = Mock(spec=Voice)
mock_message.voice.file_id = 'voice_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_voice.return_value = Mock()
mock_send_voice.return_value.message_id = 123
mock_messages.return_value = "Голосовое сообщение отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_voice.assert_called_once()
mock_db.add_post_in_db.assert_called_once()
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_media_group(self, mock_message, mock_state, mock_db):
"""Тест обработки медиагруппы"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для медиагруппы
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
# Создаем мок альбома
album = [mock_message]
album[0].caption = 'Подпись к медиагруппе'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_prepare.return_value = ['media1', 'media2']
mock_send_group.return_value = 123
mock_send_text.return_value = 456
mock_messages.return_value = "Медиагруппа отправлена!"
# Выполнение теста
await suggest_router(mock_message, mock_state, album)
# Проверки
mock_get_text.assert_called_once()
mock_prepare.assert_called_once()
mock_send_group.assert_called_once()
# Проверяем, что send_text_message был вызван (может быть вызван несколько раз)
assert mock_send_text.call_count >= 1
mock_db.update_helper_message_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_unsupported_content(self, mock_message, mock_state):
"""Тест обработки неподдерживаемого типа контента"""
# Настройка моков для неподдерживаемого контента
mock_message.content_type = 'document'
mock_message.media_group_id = None
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверяем, что отправлено сообщение о неподдерживаемом типе
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
# Проверяем текст сообщения (может быть в позиционных или именованных аргументах)
text = call_args.kwargs.get('text', '') or (call_args[0][1] if len(call_args[0]) > 1 else '')
assert 'не умею работать с таким сообщением' in text
if __name__ == '__main__':
pytest.main([__file__, '-v'])

13
tests/test_settings.ini Normal file
View File

@@ -0,0 +1,13 @@
[Telegram]
bot_token = test_token_123
preview_link = false
main_public = @test
group_for_posts = -1001234567890
group_for_message = -1001234567891
group_for_logs = -1001234567893
important_logs = -1001234567894
test_channel = -1001234567895
[Settings]
logs = true
test = false

208
tests/test_utils.py Normal file
View File

@@ -0,0 +1,208 @@
import pytest
from unittest.mock import Mock, patch
from datetime import datetime
from helper_bot.utils.helper_func import (
get_first_name,
get_text_message,
check_username_and_full_name
)
from helper_bot.utils.messages import get_message
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from database.db import BotDB
class TestHelperFunctions:
"""Тесты для вспомогательных функций"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения для тестирования"""
message = Mock()
message.from_user = Mock()
message.from_user.first_name = "Test"
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
return message
def test_get_first_name(self, mock_message):
"""Тест функции получения имени пользователя"""
# Тест с обычным именем
result = get_first_name(mock_message)
assert result == "Test"
# Тест с пустым именем - функция get_first_name не обрабатывает None
# поэтому этот тест будет падать, что ожидаемо
mock_message.from_user.first_name = None
try:
result = get_first_name(mock_message)
assert False, "Ожидалась ошибка при None first_name"
except AttributeError:
pass # Ожидаемое поведение
def test_get_text_message(self, mock_message):
"""Тест функции обработки текста сообщения"""
# Тест с обычным текстом
text = "Привет, это тестовое сообщение"
result = get_text_message(text, "Test", "testuser")
assert "Test" in result
assert "testuser" in result
assert "тестовое сообщение" in result
# Тест с пустым текстом
result = get_text_message("", "Test", "testuser")
assert "Test" in result
assert "testuser" in result
# Тест с текстом без специальных слов
text = "Обычный текст без специальных слов"
result = get_text_message(text, "Test", "testuser")
assert "Test" in result
assert "testuser" in result
assert "Обычный текст без специальных слов" in result
def test_check_username_and_full_name(self):
"""Тест функции проверки изменений username и full_name"""
# Создаем мок базы данных
mock_db = Mock(spec=BotDB)
mock_db.get_username_and_full_name = Mock(return_value=("olduser", "Old User"))
# Тест с измененными данными
result = check_username_and_full_name(123456, "newuser", "New User", mock_db)
assert result is True
# Тест с неизмененными данными
result = check_username_and_full_name(123456, "olduser", "Old User", mock_db)
assert result is False
# Тест с частично измененными данными
result = check_username_and_full_name(123456, "olduser", "New User", mock_db)
assert result is True
result = check_username_and_full_name(123456, "newuser", "Old User", mock_db)
assert result is True
class TestMessages:
"""Тесты для системы сообщений"""
def test_get_message(self):
"""Тест функции получения сообщений"""
# Тест с существующим ключом
result = get_message("Test", "HELLO_MESSAGE")
assert isinstance(result, str)
assert len(result) > 0
# Тест с несуществующим ключом
try:
result = get_message("Test", "NON_EXISTENT_KEY")
assert False, "Ожидалась ошибка KeyError"
except KeyError:
pass # Ожидаемое поведение
# Тест с пустым именем
result = get_message("", "HELLO_MESSAGE")
assert isinstance(result, str)
assert len(result) > 0
# Тест с None именем - ожидаем ошибку
try:
result = get_message(None, "HELLO_MESSAGE")
assert False, "Ожидалась ошибка TypeError"
except TypeError:
pass # Ожидаемое поведение
def test_get_message_all_types(self):
"""Тест всех типов сообщений"""
message_types = [
"HELLO_MESSAGE",
"SUGGEST_NEWS",
"SUGGEST_NEWS_2",
"BYE_MESSAGE",
"SUCCESS_SEND_MESSAGE",
"CONNECT_WITH_ADMIN",
"QUESTION"
]
for msg_type in message_types:
result = get_message("Test", msg_type)
assert isinstance(result, str)
assert len(result) > 0
class TestBaseDependencyFactory:
"""Тесты для фабрики зависимостей"""
def test_singleton_pattern(self):
"""Тест паттерна синглтон"""
# Сбрасываем глобальный экземпляр
import helper_bot.utils.base_dependency_factory
helper_bot.utils.base_dependency_factory._global_instance = None
# Получаем два экземпляра
instance1 = get_global_instance()
instance2 = get_global_instance()
# Проверяем, что это один и тот же объект
assert instance1 is instance2
assert id(instance1) == id(instance2)
def test_factory_initialization_with_mock_config(self):
"""Тест инициализации фабрики с мок конфигурацией"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_get_settings_method(self):
"""Тест метода get_settings"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_get_db_method(self):
"""Тест метода get_db"""
with patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser'):
with patch('helper_bot.utils.base_dependency_factory.BotDB') as mock_db:
factory = BaseDependencyFactory()
db = factory.get_db()
assert db is not None
assert db == factory.database
class TestDatabaseIntegration:
"""Тесты интеграции с базой данных"""
def test_database_connection(self):
"""Тест подключения к базе данных"""
with patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser'):
with patch('helper_bot.utils.base_dependency_factory.BotDB') as mock_db:
factory = BaseDependencyFactory()
# Проверяем, что база данных была создана
mock_db.assert_called_once()
# Проверяем, что get_db возвращает тот же экземпляр
db1 = factory.get_db()
db2 = factory.get_db()
assert db1 is db2
class TestConfigurationHandling:
"""Тесты обработки конфигурации"""
def test_boolean_config_values(self):
"""Тест обработки булевых значений в конфигурации"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_string_config_values(self):
"""Тест обработки строковых значений в конфигурации"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -1,316 +0,0 @@
import configparser
import os
import sys
from pathlib import Path
from time import sleep
import db
from db import BotDB
import telebot
import random
from datetime import datetime
import time
from telebot import types
#Настройки
config_path = os.path.join(sys.path[0], 'settings.ini')
config = configparser.ConfigParser()
config.read(config_path)
#TELEGRAM
BOT_TOKEN = config.get('Telegram', 'listen_bot_token')
GROUP_FOR_POST = config.get('Telegram', 'group_for_posts')
GROUP_FOR_MESSAGE = config.get('Telegram', 'group_for_message')
MAIN_PUBLIC = config.get('Telegram', 'main_public')
GROUP_FOR_LOGS = config.get('Telegram', 'group_for_logs')
IMPORTANT_LOGS = config.get('Telegram', 'important_logs')
PREVIEW_LINK = config.getboolean('Telegram', 'PREVIEW_LINK')
#SETTINGS
LOGS = config.getboolean('Settings', 'logs')
TEST = config.getboolean('Settings', 'test')
#Инициализируем бота и базку
bot = telebot.TeleBot(BOT_TOKEN, parse_mode=None)
BotDB = BotDB('tg-bot-database')
def telegram_bot():
@bot.message_handler(commands=['start'])
def send_welcome(message):
try:
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
number_stick_hello = random.randint(1, len(name_stick_hello))
random_stick_hello = open(name_stick_hello[number_stick_hello], 'rb')
#logging
if LOGS:
bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
bot.send_sticker(message.chat.id, random_stick_hello)
sleep(0.3)
except:
if LOGS:
bot.send_message(IMPORTANT_LOGS, BotDB.get_error_message_from_db(7))
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("🎤Высказаться")
item2 = types.KeyboardButton("🎧Послушать")
markup.add(item1, item2)
bot.send_message(message.chat.id, "<b>Привет.</b>", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.3)
bot.send_message(message.chat.id, "<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска</i>", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
time.sleep(1)
bot.send_message(message.chat.id, "Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.8)
bot.send_message(message.chat.id, "Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя бы на 5-10 секунд</i>", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
time.sleep(1.5)
bot.send_message(message.chat.id, "Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
time.sleep(1.3)
bot.send_message(message.chat.id, "Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.8)
bot.send_message(message.chat.id,
"Так же можешь ознакомиться с инструкцией к боту по команде /help",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.8)
msg = bot.send_message(message.chat.id, "<b>ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
bot.register_next_step_handler(msg, standup)
def last_message():
# функция с отображением сообщения "Последнее сообщение было записано"
date_from_db = BotDB.last_date_audio()
parse_date = datetime.strptime(date_from_db, "%Y-%m-%d %H:%M:%S")
last_voice_time_timestamp = time.mktime(parse_date.timetuple())
time_now_timestamp = time.time()
date_difference = time_now_timestamp - last_voice_time_timestamp
# считаем минуты, часы, дни
much_minutes_ago = round(date_difference / 60, 0)
much_hour_ago = round(date_difference / 3600, 0)
much_days_ago = int(round(much_hour_ago / 24, 0))
message_with_date = ''
if much_minutes_ago <= 60:
word_minute = plural_time(1, much_minutes_ago)
message_with_date = f'<b>Последнее сообщение было записано {word_minute} назад</b>'
elif much_minutes_ago > 60 and much_hour_ago <= 24:
word_hour = plural_time(2, much_hour_ago)
message_with_date = f'<b>Последнее сообщение было записано {word_hour} назад</b>'
elif much_hour_ago > 24:
word_day = plural_time(3, much_days_ago)
message_with_date = f'<b>Последнее сообщение было записано {word_day} назад</b>'
return message_with_date
def standup(message):
# Клавиатуру добавляем
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("🎤Высказаться")
item2 = types.KeyboardButton("🎧Послушать")
markup.add(item1, item2)
try:
if message.text == '🎤Высказаться':
markup = types.ReplyKeyboardRemove()
if LOGS:
# logging
bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
if TEST:
bot.forward_message(chat_id=GROUP_FOR_LOGS, from_chat_id=message.chat.id, message_id=message.id)
bot.send_message(chat_id=message.chat.id, text='Хорошо, теперь пришли мне свое голосовое сообщение', reply_markup=markup)
message_with_date = last_message()
msg = bot.send_message(chat_id=message.chat.id, text=message_with_date, parse_mode="html")
bot.register_next_step_handler(msg, save_voice_message)
elif message.text == '🎧Послушать':
check_audio = BotDB.check_listen_audio(user_id=message.from_user.id)
list_audio = list(check_audio)
if TEST:
bot.forward_message(chat_id=GROUP_FOR_LOGS, from_chat_id=message.chat.id, message_id=message.id)
if list_audio == []:
bot.send_message(message.chat.id, 'Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится', reply_markup=markup)
message_with_date = last_message()
msg = bot.send_message(chat_id=message.chat.id, text=message_with_date, parse_mode="html")
bot.register_next_step_handler(msg, standup)
else:
number_element = random.randint(0, len(list_audio) - 1)
audio_for_user = check_audio[number_element]
path = Path(f'voice_users/{audio_for_user}.ogg')
voice = open(path, 'rb')
#Маркируем сообщение как прослушанное
BotDB.mark_listened_audio(audio_for_user, user_id=message.from_user.id)
msg = bot.send_voice(message.chat.id, voice=voice, reply_markup=markup)
bot.register_next_step_handler(msg, standup)
if LOGS:
# logging
bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
if TEST:
bot.forward_message(chat_id=GROUP_FOR_LOGS, from_chat_id=message.chat.id, message_id=message.id)
elif message.text == '/restart':
msg = bot.send_message(message.chat.id, 'Я перезапущен, и готов к работе🥳', reply_markup=markup)
bot.forward_message(chat_id=GROUP_FOR_LOGS,
from_chat_id=message.chat.id,
message_id=message.message_id)
bot.register_next_step_handler(msg, standup)
elif message.text == '/help':
help_function(message)
else:
msg = bot.send_message(chat_id=message.chat.id, text='Я тебя не понял, воспользуйся меню', reply_markup=markup)
bot.register_next_step_handler(msg, standup)
except Exception as e:
msg = bot.send_message(chat_id=message.chat.id, text='Я не могу прислать тебе голосовое, потому что они скрыты у тебя в настройках. Открой для меня голосовые и возвращайся❤️', reply_markup=markup)
bot.register_next_step_handler(msg, standup)
def plural_time(type, n):
word = []
if type == 1:
word = ['минуту', 'минуты', 'минут']
elif type == 2:
word = ['час', 'часа', 'часов']
elif type == 3:
word = ['день', 'дня', 'дней']
else:
pass
if n % 10 == 1 and n % 100 != 11:
p = 0
elif 2 <= n % 10 <= 4 and (n % 100 < 10 or n % 100 >= 20):
p = 1
else:
p = 2
new_number = int(n)
return str(new_number) + ' ' + word[p]
def save_voice_message(message):
if message.content_type == 'voice':
if TEST:
bot.forward_message(chat_id=GROUP_FOR_LOGS, from_chat_id=message.chat.id, message_id=message.id)
file_name = ''
file_id = 1
#Проверяем что запись о файле есть в базе данных
is_having_audio_from_user = BotDB.get_last_user_audio_record(user_id=message.from_user.id)
if is_having_audio_from_user is False:
#Если нет, то генерируем имя файла
file_name = f'message_from_{message.from_user.id}_number_{file_id}'
else:
#Иначе берем последнюю запись из БД, добавляем к ней 1, и создаем новую запись
file_name = BotDB.get_path_for_audio_record(user_id=message.from_user.id)
file_id = BotDB.get_id_for_audio_record(message.from_user.id) + 1
path = Path(f'voice_users/{file_name}.ogg')
if path.exists():
file_name = f'message_from_{message.from_user.id}_number_{file_id}'
else:
pass
#Собираем инфо о сообщении
author_id = message.from_user.id
time_UTC = int(time.time())
date_added = datetime.fromtimestamp(time_UTC)
#Сохраняем в базку
BotDB.add_audio_record(file_name, author_id, date_added, 0, file_id)
#Сохраняем файл на сервер
file_info = bot.get_file(message.voice.file_id)
downloaded_file = bot.download_file(file_info.file_path)
with open(f'voice_users/{file_name}.ogg', 'wb') as new_file:
new_file.write(downloaded_file)
#инициализируем кнопки
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("🎤Высказаться")
item2 = types.KeyboardButton("🎧Послушать")
markup.add(item1, item2)
bot.send_message(chat_id=message.chat.id, text='Окей, сохранил!👌', reply_markup=markup)
#menu_standup(message=message)
bot.register_next_step_handler(message, standup)
else:
#TODO: Если пришлют фото, он не работает
if TEST:
bot.forward_message(chat_id=GROUP_FOR_LOGS, from_chat_id=message.chat.id, message_id=message.id)
markup = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
item1 = types.KeyboardButton("🎤Высказаться")
item2 = types.KeyboardButton("🎧Послушать")
markup.add(item1, item2)
msg = bot.send_message(chat_id=message.chat.id, text='Я тебя не понимаю🤷‍♀️ запиши голосовое', reply_markup=markup)
bot.register_next_step_handler(msg, standup)
@bot.message_handler(commands=['restart'])
def restart_function(message):
return standup(message)
@bot.message_handler(commands=['help'])
def help_function(message):
msg = bot.send_message(chat_id=message.chat.id, text='Скорее всего ответы на твои вопросы есть здесь, ознакомься: https://telegra.ph/Instrukciya-k-botu-Golosa-Bijsk-10-11-2'
'\nЕсли это не поможет, пиши в тг: @Kerrad1',disable_web_page_preview=not PREVIEW_LINK)
bot.register_next_step_handler(msg, standup)
@bot.message_handler(commands=['send'])
def notify(message):
"""Функция для рассылки сообщений пользователям бота"""
admins = [842766148]
command_sender = message.from_user.id
if command_sender in admins:
markup = types.InlineKeyboardMarkup(row_width=1)
button = types.InlineKeyboardButton(text='Я прочитал ', callback_data='from_user')
success_send_message = 0
error_send_message = 0
markup.add(button)
user_ids = BotDB.get_all_user_id()
# Парсим список id юзеров для рассылки и рассылаем сообщения
for i in user_ids:
username = BotDB.get_username(i)
try:
bot.send_message(chat_id=i,
text='Привет, на связи админ. Первая тестовая рассылка через этого бота, не суди строго, обещаю не злоупотреблять этим))\n'
'Собственно хотел сказать следующее, сегодня ночью обновил бота, добавил ему две функции /restart и /help. '
'Рекомендую ознакомиться со статьей в /help, там я подробно описал как сейчас все работает. '
'Если у тебя будут предложения по доработке бота или новому функционал, пиши не стесняйся @Kerrad1. '
'Так же прошу нажать на кнопку под сообщением, чтобы я понял что ты прочитал сообщение❤️\n\nНа этом у меня все, пока, до следующего релиза', reply_markup=markup)
bot.send_message(chat_id=message.chat.id,
text=f'Успешно отправлено - @{username}')
success_send_message += 1
except Exception as e:
bot.send_message(chat_id=message.chat.id,
text=f'Этому юзеру не отправилось - @{username}')
error_send_message += 1
bot.send_message(chat_id=message.chat.id,
text=f'<b>Количество успешных отправок</b> - {success_send_message}', parse_mode='html')
bot.send_message(chat_id=message.chat.id,
text=f'<b>Количество неуспешных отправок</b> - {error_send_message}', parse_mode='html')
standup(message)
else:
bot.send_message(command_sender, f'у вас нет прав для запуска команды')
bot.forward_message(chat_id=GROUP_FOR_LOGS, from_chat_id=message.chat.id, message_id=message.id)
standup(message)
@bot.callback_query_handler(func=lambda call: True)
def post_for_group(call):
if call.data == 'from_user' and call.message.content_type == 'text':
try:
command_sender = call.message.from_user.id
bot.answer_callback_query(call.id, text="Мне приятно, спасибо ❤")
#Попробовать варианты если не сработает call.from_user.username
bot.send_message(chat_id=842766148, text=f'Юзер с ником @{call.message.chat.username} по имени {call.message.chat.first_name} прочитал сообщение')
except:
print('что-то не так')
if __name__ == '__main__':
telegram_bot()
while True:
try:
bot.polling(none_stop=True)
bot.enable_save_next_step_handlers(delay=2)
bot.load_next_step_handlers()
except ConnectionError as e:
if LOGS:
bot.send_message(IMPORTANT_LOGS, "Ошибка соединения, потерял войс бот связь")
except Exception as r:
if LOGS:
bot.send_message(IMPORTANT_LOGS, "Произошло что-то непредвиденное, хелп. Войс бот болеет")

0
voice_bot/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,68 @@
import time
from datetime import datetime
from pathlib import Path
from aiogram import Router, F
from aiogram.types import CallbackQuery
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory
callback_router = Router()
bdf = BaseDependencyFactory()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db()
@callback_router.callback_query(
F.data == "save"
)
async def save_voice_message(call: CallbackQuery):
file_name = ''
file_id = 1
user_id = BotDB.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
# Проверяем что запись о файле есть в базе данных
is_having_audio_from_user = BotDB.get_last_user_audio_record(user_id=user_id)
if is_having_audio_from_user is False:
# Если нет, то генерируем имя файла
file_name = f'message_from_{user_id}_number_{file_id}'
else:
# Иначе берем последнюю запись из БД, добавляем к ней 1, и создаем новую запись
file_name = BotDB.get_path_for_audio_record(user_id=user_id)
file_id = BotDB.get_id_for_audio_record(user_id) + 1
path = Path(f'voice_users/{file_name}.ogg')
if path.exists():
file_name = f'message_from_{user_id}_number_{file_id}'
else:
pass
# Собираем инфо о сообщении
time_UTC = int(time.time())
date_added = datetime.fromtimestamp(time_UTC)
# Сохраняем в базку
BotDB.add_audio_record(file_name, user_id, date_added, 0, file_id)
file_info = await call.message.bot.get_file(file_id=call.message.voice.file_id)
downloaded_file = await call.message.bot.download_file(file_path=file_info.file_path)
with open(f'voice_users/{file_name}.ogg', 'wb') as new_file:
new_file.write(downloaded_file.read())
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
await call.answer(text='Сохранено!', cache_time=3)
@callback_router.callback_query(
F.data == "delete"
)
async def delete_voice_message(call: CallbackQuery):
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
await call.answer(text='Удалено!', cache_time=3)

View File

@@ -0,0 +1,236 @@
import random
import asyncio
from datetime import datetime
from pathlib import Path
from aiogram import Router, types, F
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.types import FSInputFile
from helper_bot.filters.main import ChatTypeFilter
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import update_user_info, check_user_emoji, send_voice_message
from logs.custom_logger import logger
from voice_bot.keyboards.keyboards import get_main_keyboard, get_reply_keyboard_for_voice
from voice_bot.utils.helper_func import last_message
voice_router = Router()
bdf = get_global_instance()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db()
voice_router.message.middleware(BlacklistMiddleware())
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("restart")
)
async def restart_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
await update_user_info('voice', message)
check_user_emoji(message)
markup = get_main_keyboard()
await message.answer(text='Я перезапущен!',
reply_markup=markup)
await state.set_state('START')
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("emoji")
)
async def handle_emoji_message(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
user_emoji = check_user_emoji(message)
await state.set_state("START")
if user_emoji is not None:
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("help")
)
async def help_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
await update_user_info('voice', message)
await message.answer(
text='Скорее всего ответы на твои вопросы есть здесь, ознакомься: https://telegra.ph/Instrukciya-k-botu-Golosa-Bijsk-10-11-2'
'\nЕсли это не поможет, пиши в личку: @Kerrad1', disable_web_page_preview=not PREVIEW_LINK)
await state.set_state('START')
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("start")
)
async def start(message: types.Message, state: FSMContext):
await state.set_state("START")
await message.forward(chat_id=GROUP_FOR_LOGS)
await update_user_info('voice', message)
user_emoji = check_user_emoji(message)
try:
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
random_stick_hello = random.choice(name_stick_hello)
random_stick_hello = FSInputFile(path=random_stick_hello)
logger.info(f"Стикер успешно получен из БД. Наименование стикера: {name_stick_hello}")
await message.answer_sticker(random_stick_hello)
await asyncio.sleep(0.3)
except Exception as e:
if LOGS:
await message.bot.send_message(IMPORTANT_LOGS, f'Отправка приветственных стикеров лажает. Ошибка: {e}')
markup = get_main_keyboard()
await message.answer(text="<b>Привет.</b>", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(0.3)
await message.answer(text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из "
"Бийска</i>",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(1)
await message.answer(text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не "
"узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(0.8)
await message.answer(text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя "
"бы на 5-10 секунд</i>",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(1.5)
await message.answer(text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, "
"ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы "
"выкладывать в собственные соцсети)",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(1.3)
await message.answer(text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из "
"недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(0.8)
await message.answer(text=f"Любые войсы будут помечены эмоджи. <b>Твой эмоджи - </b>{user_emoji}"
f"Таким эмоджи будут помечены твои сообщения для других "
f"Но другие люди не узнают кто за каким эмоджи скрывается:)",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(0.8)
await message.answer(text="Так же можешь ознакомиться с инструкцией к боту по команде /help",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
await asyncio.sleep(0.8)
await message.answer(text="<b>Ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("refresh")
)
async def refresh_listen_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
await update_user_info('voice', message)
markup = get_main_keyboard()
BotDB.delete_listen_count_for_user(message.from_user.id)
await message.answer(
text='Прослушивания очищены. Можешь начать слушать заново🤗', disable_web_page_preview=not PREVIEW_LINK,
markup=markup)
await state.set_state('START')
@voice_router.message(
StateFilter("START"),
ChatTypeFilter(chat_type=["private"]),
F.text == '🎤Высказаться'
)
async def standup_write(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
markup = types.ReplyKeyboardRemove()
await message.answer(text='Хорошо, теперь пришли мне свое голосовое сообщение', reply_markup=markup)
try:
message_with_date = last_message()
await message.answer(text=message_with_date, parse_mode="html")
except Exception as e:
logger.error(f'Не удалось получить дату последнего сообщения - {e}')
await state.set_state('STANDUP_WRITE')
@voice_router.message(
StateFilter("STANDUP_WRITE"),
ChatTypeFilter(chat_type=["private"]),
)
async def suggest_voice(message: types.Message, state: FSMContext):
logger.info(
f"Вызов функции suggest_voice. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
await message.forward(chat_id=GROUP_FOR_LOGS)
markup = get_main_keyboard()
if message.content_type == 'voice':
markup_for_voice = get_reply_keyboard_for_voice()
# Отправляем аудио в приватный канал
sent_message = await send_voice_message(GROUP_FOR_POST, message,
message.voice.file_id, markup_for_voice)
# Сохраняем в базу инфо о посте
BotDB.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id)
# Отправляем юзеру ответ и возвращаем его в меню
await message.answer(text='Окей, сохранил!👌', reply_markup=markup)
await state.set_state('START')
else:
# TODO: Если пришлют фото, он не работает
await message.forward(chat_id=GROUP_FOR_LOGS)
await message.answer(text='Я тебя не понимаю🤷‍♀️ запиши голосовое', reply_markup=markup)
await state.set_state('STANDUP_WRITE')
@voice_router.message(
StateFilter("START"),
ChatTypeFilter(chat_type=["private"]),
F.text == '🎧Послушать'
)
async def standup_listen_audio(message: types.Message):
check_audio = BotDB.check_listen_audio(user_id=message.from_user.id)
list_audio = list(check_audio)
markup = get_main_keyboard()
if not list_audio:
await message.answer(text='Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится',
reply_markup=markup)
try:
message_with_date = last_message()
await message.answer(text=message_with_date, parse_mode="html")
except Exception as e:
logger.error(f'Не удалось получить последнюю дату {e}')
else:
# Получаем ссылку на аудио сообщение пользователя
number_element = random.randint(0, len(list_audio) - 1)
audio_for_user = check_audio[number_element]
# Получаем автора записи + эмодзи по нему
user_id = BotDB.get_user_id_by_file_name(audio_for_user)
date_added = BotDB.get_date_by_file_name(audio_for_user)
user_emoji = BotDB.check_emoji_for_user(user_id)
path = Path(f'voice_users/{audio_for_user}.ogg')
voice = FSInputFile(path)
# Маркируем сообщение как прослушанное
BotDB.mark_listened_audio(audio_for_user, user_id=message.from_user.id)
# Формируем подпись
if user_emoji:
caption = f'{user_emoji}\nДата записи: {date_added}'
else:
caption = f'Дата записи: {date_added}'
await message.bot.send_voice(chat_id=message.chat.id, voice=voice, caption=caption, reply_markup=markup)
await message.answer(text=f'Осталось непрослушанных: <b>{len(check_audio) - 1}</b>', reply_markup=markup)

View File

View File

@@ -0,0 +1,22 @@
from aiogram import types
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
def get_main_keyboard():
builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="🎤Высказаться"))
builder.add(types.KeyboardButton(text="🎧Послушать"))
markup = builder.as_markup(resize_keyboard=True)
return markup
def get_reply_keyboard_for_voice():
builder = InlineKeyboardBuilder()
builder.row(types.InlineKeyboardButton(
text="Сохранить", callback_data="save")
)
builder.row(types.InlineKeyboardButton(
text="Удалить", callback_data="delete")
)
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup

28
voice_bot/main.py Normal file
View File

@@ -0,0 +1,28 @@
import os
import sys
# Ensure project root is on sys.path for module resolution when running voice bot directly
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(CURRENT_DIR)
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.strategy import FSMStrategy
from voice_bot.handlers.callback_handler import callback_router
from voice_bot.handlers.voice_handler import voice_router
async def start_bot(bdf):
token = bdf.settings['Telegram']['listen_bot_token']
bot = Bot(token=token, default=DefaultBotProperties(
parse_mode='HTML',
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
))
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
dp.include_routers(voice_router, callback_router)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot, skip_updates=True)

View File

View File

@@ -0,0 +1,63 @@
import time
import html
from datetime import datetime
from helper_bot.utils.base_dependency_factory import get_global_instance
bdf = get_global_instance()
BotDB = bdf.get_db()
def last_message():
# функция с отображением сообщения "Последнее сообщение было записано"
date_from_db = BotDB.last_date_audio()
if date_from_db is None:
return None
parse_date = datetime.strptime(date_from_db, "%Y-%m-%d %H:%M:%S")
last_voice_time_timestamp = time.mktime(parse_date.timetuple())
time_now_timestamp = time.time()
date_difference = time_now_timestamp - last_voice_time_timestamp
# считаем минуты, часы, дни
much_minutes_ago = round(date_difference / 60, 0)
much_hour_ago = round(date_difference / 3600, 0)
much_days_ago = int(round(much_hour_ago / 24, 0))
message_with_date = ''
if much_minutes_ago <= 60:
word_minute = plural_time(1, much_minutes_ago)
# Экранируем потенциально проблемные символы
word_minute_escaped = html.escape(word_minute)
message_with_date = f'<b>Последнее сообщение было записано {word_minute_escaped} назад</b>'
elif much_minutes_ago > 60 and much_hour_ago <= 24:
word_hour = plural_time(2, much_hour_ago)
# Экранируем потенциально проблемные символы
word_hour_escaped = html.escape(word_hour)
message_with_date = f'<b>Последнее сообщение было записано {word_hour_escaped} назад</b>'
elif much_hour_ago > 24:
word_day = plural_time(3, much_days_ago)
# Экранируем потенциально проблемные символы
word_day_escaped = html.escape(word_day)
message_with_date = f'<b>Последнее сообщение было записано {word_day_escaped} назад</b>'
return message_with_date
def plural_time(type, n):
word = []
if type == 1:
word = ['минуту', 'минуты', 'минут']
elif type == 2:
word = ['час', 'часа', 'часов']
elif type == 3:
word = ['день', 'дня', 'дней']
else:
pass
if n % 10 == 1 and n % 100 != 11:
p = 0
elif 2 <= n % 10 <= 4 and (n % 100 < 10 or n % 100 >= 20):
p = 1
else:
p = 2
new_number = int(n)
return str(new_number) + ' ' + word[p]

View File

@@ -0,0 +1,6 @@
def get_message(username: str, type_message: str):
constants = {
}
message = constants[type_message]
return message.replace('username', username).replace('&', '\n')

9
voice_bot_v2.py Normal file
View File

@@ -0,0 +1,9 @@
import asyncio
from helper_bot.utils.base_dependency_factory import get_global_instance
from voice_bot.main import start_bot
bdf = get_global_instance()
if __name__ == '__main__':
asyncio.run(start_bot(get_global_instance()))