Compare commits
41 Commits
dev
...
merge-voic
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bb098764d | |||
| 70d6ad9a6e | |||
|
|
94ece4e307 | ||
| 61dd85035d | |||
| fee22f8ad4 | |||
| 7b6abe2a0e | |||
|
|
0c29609e4a | ||
|
|
cb0f94c718 | ||
| b2c27040aa | |||
|
|
47c5b2f083 | ||
|
|
e0e0a6de51 | ||
| 502c07a2c9 | |||
|
|
ee9eafa09f | ||
|
|
b8b92434ff | ||
|
|
767acacc18 | ||
|
|
bc454fce8c | ||
| 5c3fece394 | |||
|
|
198b522976 | ||
|
|
5050767b60 | ||
|
|
33fa84943d | ||
|
|
0704e6b3fe | ||
|
|
0b7f718f8a | ||
|
|
54234e59ec | ||
|
|
095e0398d0 | ||
|
|
a3b53d26e2 | ||
|
|
88889fe87c | ||
|
|
3a30edc1ab | ||
|
|
09a071c014 | ||
|
|
01f6cbd37d | ||
|
|
3cb487b617 | ||
|
|
f7f6fa5482 | ||
|
|
1a0344d0e8 | ||
|
|
24ac638433 | ||
|
|
f2dd9f4b42 | ||
|
|
47050fd599 | ||
|
|
66f3a870c4 | ||
|
|
f2e44ddb29 | ||
|
|
7860d4f5c0 | ||
|
|
9788a763c4 | ||
|
|
99eb07f74e | ||
|
|
e27d565222 |
37
.dockerignore
Normal file
37
.dockerignore
Normal 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
44
.gitignore
vendored
Normal 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
37
Dockerfile
Normal 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
73
Makefile
Normal 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
259
README_TESTING.md
Normal 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
BIN
Stick/Hello_11.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
Stick/Hello_12.webp
Normal file
BIN
Stick/Hello_12.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
Stick/Hello_13.webp
Normal file
BIN
Stick/Hello_13.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
Stick/Universal_11.webp
Normal file
BIN
Stick/Universal_11.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -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
1399
database/db.py
Normal file
File diff suppressed because it is too large
Load Diff
406
db.py
406
db.py
@@ -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
0
helper_bot/__init__.py
Normal file
15
helper_bot/filters/main.py
Normal file
15
helper_bot/filters/main.py
Normal 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
|
||||
1
helper_bot/handlers/admin/__init__.py
Normal file
1
helper_bot/handlers/admin/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .admin_handlers import admin_router
|
||||
321
helper_bot/handlers/admin/admin_handlers.py
Normal file
321
helper_bot/handlers/admin/admin_handlers.py
Normal 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)
|
||||
1
helper_bot/handlers/callback/__init__.py
Normal file
1
helper_bot/handlers/callback/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .callback_handlers import callback_router
|
||||
284
helper_bot/handlers/callback/callback_handlers.py
Normal file
284
helper_bot/handlers/callback/callback_handlers.py
Normal 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)
|
||||
1
helper_bot/handlers/group/__init__.py
Normal file
1
helper_bot/handlers/group/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .group_handlers import group_router
|
||||
49
helper_bot/handlers/group/group_handlers.py
Normal file
49
helper_bot/handlers/group/group_handlers.py
Normal 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)}')
|
||||
1
helper_bot/handlers/private/__init__.py
Normal file
1
helper_bot/handlers/private/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .private_handlers import private_router
|
||||
503
helper_bot/handlers/private/private_handlers.py
Normal file
503
helper_bot/handlers/private/private_handlers.py
Normal 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)
|
||||
1
helper_bot/keyboards/__init__.py
Normal file
1
helper_bot/keyboards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .keyboards import get_reply_keyboard_for_post, get_reply_keyboard
|
||||
117
helper_bot/keyboards/keyboards.py
Normal file
117
helper_bot/keyboards/keyboards.py
Normal 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
21
helper_bot/main.py
Normal 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)
|
||||
0
helper_bot/middlewares/__init__.py
Normal file
0
helper_bot/middlewares/__init__.py
Normal file
61
helper_bot/middlewares/album_middleware.py
Normal file
61
helper_bot/middlewares/album_middleware.py
Normal 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)
|
||||
#
|
||||
26
helper_bot/middlewares/blacklist_middleware.py
Normal file
26
helper_bot/middlewares/blacklist_middleware.py
Normal 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)
|
||||
46
helper_bot/middlewares/text_middleware.py
Normal file
46
helper_bot/middlewares/text_middleware.py
Normal 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)
|
||||
|
||||
1
helper_bot/utils/__init__.py
Normal file
1
helper_bot/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .state import StateUser
|
||||
46
helper_bot/utils/base_dependency_factory.py
Normal file
46
helper_bot/utils/base_dependency_factory.py
Normal 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
|
||||
528
helper_bot/utils/helper_func.py
Normal file
528
helper_bot/utils/helper_func.py
Normal 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 "Эмоджи не определен"
|
||||
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
|
||||
|
||||
def get_message(username: str, type_message: str):
|
||||
@@ -25,7 +26,7 @@ def get_message(username: str, type_message: str):
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
|
||||
"DEL_MESSAGE": "username, напиши свое обращение или предложение✍"
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺❤",
|
||||
"BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /start"
|
||||
"BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /restart"
|
||||
"&&И тебе пока!👋🏼❤️",
|
||||
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
|
||||
"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]
|
||||
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
16
helper_bot/utils/state.py
Normal 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
24
logs/custom_logger.py
Normal 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
635
main.py
@@ -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()
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
import os
|
||||
from db import BotDB
|
||||
|
||||
|
||||
BotDB = BotDB()
|
||||
|
||||
|
||||
def get_filename():
|
||||
"""Возвращает имя файла без расширения."""
|
||||
filename = os.path.basename(__file__)
|
||||
filename = os.path.splitext(filename)[0]
|
||||
return filename
|
||||
|
||||
|
||||
def main():
|
||||
migrations_init = """
|
||||
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()
|
||||
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():
|
||||
migrations_init = """
|
||||
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()
|
||||
|
||||
@@ -1,56 +1,67 @@
|
||||
import os
|
||||
from db import BotDB
|
||||
|
||||
BotDB = BotDB()
|
||||
|
||||
|
||||
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 < 1:
|
||||
# Скрипты миграции
|
||||
create_table_sql_1 = """
|
||||
CREATE TABLE IF NOT EXISTS "admins" (
|
||||
user_id INTEGER NOT NULL,
|
||||
"role" TEXT
|
||||
);
|
||||
"""
|
||||
create_table_sql_2 = """CREATE TABLE IF NOT EXISTS "blacklist"
|
||||
(
|
||||
"user_id" INTEGER NOT NULL UNIQUE,
|
||||
"user_name" INTEGER,
|
||||
"message_for_user" INTEGER,
|
||||
"date_to_unban" INTEGER
|
||||
);
|
||||
"""
|
||||
create_table_sql_3 = """
|
||||
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
|
||||
);
|
||||
"""
|
||||
# Применение миграции
|
||||
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()
|
||||
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 < 1:
|
||||
# Скрипты миграции
|
||||
create_table_sql_1 = """
|
||||
CREATE TABLE IF NOT EXISTS "admins" (
|
||||
user_id INTEGER NOT NULL,
|
||||
"role" TEXT
|
||||
);
|
||||
"""
|
||||
create_table_sql_2 = """CREATE TABLE IF NOT EXISTS "blacklist"
|
||||
(
|
||||
"user_id" INTEGER NOT NULL UNIQUE,
|
||||
"user_name" INTEGER,
|
||||
"message_for_user" INTEGER,
|
||||
"date_to_unban" INTEGER
|
||||
);
|
||||
"""
|
||||
create_table_sql_3 = """
|
||||
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
|
||||
);
|
||||
"""
|
||||
# Применение миграции
|
||||
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()
|
||||
|
||||
65
migrations/002_create_tables_media_group.py
Normal file
65
migrations/002_create_tables_media_group.py
Normal 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()
|
||||
56
migrations/003_create_our_users_table.py
Normal file
56
migrations/003_create_our_users_table.py
Normal 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
19
pytest.ini
Normal 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
|
||||
@@ -1,3 +1,23 @@
|
||||
pyTelegramBotAPI
|
||||
APScheduler~=3.10.4
|
||||
loguru~=0.7.2
|
||||
APScheduler==3.10.4
|
||||
certifi~=2024.6.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
14
run_helper.py
Normal 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
234
tests/conftest.py
Normal 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
52
tests/mocks.py
Normal 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
339
tests/test_bot.py
Normal 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
831
tests/test_db.py
Normal 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()
|
||||
339
tests/test_error_handling.py
Normal file
339
tests/test_error_handling.py
Normal 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'])
|
||||
330
tests/test_keyboards_and_filters.py
Normal file
330
tests/test_keyboards_and_filters.py
Normal 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'])
|
||||
292
tests/test_media_handlers.py
Normal file
292
tests/test_media_handlers.py
Normal 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
13
tests/test_settings.ini
Normal 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
208
tests/test_utils.py
Normal 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'])
|
||||
316
voice_bot.py
316
voice_bot.py
@@ -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
0
voice_bot/__init__.py
Normal file
0
voice_bot/handlers/__init__.py
Normal file
0
voice_bot/handlers/__init__.py
Normal file
68
voice_bot/handlers/callback_handler.py
Normal file
68
voice_bot/handlers/callback_handler.py
Normal 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)
|
||||
236
voice_bot/handlers/voice_handler.py
Normal file
236
voice_bot/handlers/voice_handler.py
Normal 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)
|
||||
0
voice_bot/keyboards/__init__.py
Normal file
0
voice_bot/keyboards/__init__.py
Normal file
22
voice_bot/keyboards/keyboards.py
Normal file
22
voice_bot/keyboards/keyboards.py
Normal 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
28
voice_bot/main.py
Normal 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)
|
||||
0
voice_bot/utils/__init__.py
Normal file
0
voice_bot/utils/__init__.py
Normal file
63
voice_bot/utils/helper_func.py
Normal file
63
voice_bot/utils/helper_func.py
Normal 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]
|
||||
6
voice_bot/utils/messages.py
Normal file
6
voice_bot/utils/messages.py
Normal 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
9
voice_bot_v2.py
Normal 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()))
|
||||
Reference in New Issue
Block a user