diff --git a/.gitignore b/.gitignore
index 060656b..4997c47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,3 +89,6 @@ env/
ENV/
env.bak/
venv.bak/
+
+# Other files
+voice_users/
diff --git a/database/async_db.py b/database/async_db.py
index e0bb693..145ffb1 100644
--- a/database/async_db.py
+++ b/database/async_db.py
@@ -725,15 +725,15 @@ class AsyncBotDB:
await conn.close()
# Методы для работы с аудио
- async def add_audio_record(self, file_name: str, author_id: int, file_id: str):
+ async def add_audio_record(self, file_name: str, author_id: int):
"""Добавление аудио записи."""
conn = None
try:
date_added = datetime.now().strftime("%d-%m-%Y %H:%M:%S")
conn = await self._get_connection()
await conn.execute(
- "INSERT INTO audio_message_reference (file_name, author_id, date_added, file_id) VALUES (?, ?, ?, ?)",
- (file_name, author_id, date_added, file_id)
+ "INSERT INTO audio_message_reference (file_name, author_id, date_added) VALUES (?, ?, ?)",
+ (file_name, author_id, date_added)
)
await conn.commit()
except Exception as e:
@@ -778,23 +778,7 @@ class AsyncBotDB:
if conn:
await conn.close()
- async def get_audio_file_id(self, user_id: int) -> Optional[str]:
- """Получение file_id последнего аудио пользователя."""
- conn = None
- try:
- conn = await self._get_connection()
- async with conn.execute(
- "SELECT file_id FROM audio_message_reference WHERE author_id = ? ORDER BY date_added DESC LIMIT 1",
- (user_id,)
- ) as cursor:
- result = await cursor.fetchone()
- return result[0] if result else None
- except Exception as e:
- self.logger.error(f"Ошибка при получении file_id аудио: {e}")
- raise
- finally:
- if conn:
- await conn.close()
+
async def get_audio_file_name(self, user_id: int) -> Optional[str]:
"""Получение имени файла последнего аудио пользователя."""
diff --git a/database/db.py b/database/db.py
index 481c4e4..9ee11f6 100644
--- a/database/db.py
+++ b/database/db.py
@@ -1337,21 +1337,20 @@ class BotDB:
self.close()
def get_id_for_audio_record(self, user_id):
- """Получает ID аудио сообщения пользователя"""
+ """Получает следующий номер аудио сообщения пользователя"""
self.logger.info(
f"Запуск функции get_id_for_audio_record. user_id={user_id}")
try:
self.connect()
r = self.cursor.execute(
- "SELECT `file_id` FROM `audio_message_reference` WHERE `author_id` = ? "
- "ORDER BY date_added DESC LIMIT 1",
+ "SELECT COUNT(*) FROM `audio_message_reference` WHERE `author_id` = ?",
(user_id,))
result = r.fetchone()[0]
self.logger.info(
f"Результат функции get_id_for_audio_record: {result}")
return result
except sqlite3.Error as error:
- self.logger.error(f"Ошибка получения последней даты войса: {error}")
+ self.logger.error(f"Ошибка получения количества аудио: {error}")
raise
finally:
self.close()
diff --git a/database/schema.sql b/database/schema.sql
index 5224dc9..a02cf5c 100644
--- a/database/schema.sql
+++ b/database/schema.sql
@@ -17,8 +17,7 @@ CREATE TABLE IF NOT EXISTS audio_message_reference (
file_name TEXT NOT NULL UNIQUE,
author_id INTEGER NOT NULL,
date_added DATE NOT NULL,
- listen_count INTEGER NOT NULL DEFAULT 0,
- file_id INTEGER NOT NULL
+ listen_count INTEGER NOT NULL DEFAULT 0
);
-- Database migrations tracking
diff --git a/helper_bot/handlers/callback/callback_handlers.py b/helper_bot/handlers/callback/callback_handlers.py
index e18f359..53b0222 100644
--- a/helper_bot/handlers/callback/callback_handlers.py
+++ b/helper_bot/handlers/callback/callback_handlers.py
@@ -1,6 +1,16 @@
import html
import traceback
+import time
+from datetime import datetime
+
+from aiogram import Router, F
+from aiogram.types import CallbackQuery
+
+from helper_bot.handlers.voice.constants import CALLBACK_SAVE, CALLBACK_DELETE
+from helper_bot.handlers.voice.services import AudioFileService
+from logs.custom_logger import logger
+
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
@@ -185,3 +195,60 @@ async def change_page(
message_id=call.message.message_id,
reply_markup=keyboard
)
+
+
+@callback_router.callback_query(F.data == CALLBACK_SAVE)
+async def save_voice_message(
+ call: CallbackQuery,
+ bot_db: MagicData("bot_db")
+ ):
+ try:
+ # Создаем сервис для работы с аудио файлами
+ audio_service = AudioFileService(bot_db)
+
+ # Получаем ID пользователя из базы
+ user_id = bot_db.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
+
+ # Генерируем имя файла
+ file_name = audio_service.generate_file_name(user_id)
+
+ # Собираем инфо о сообщении
+ time_UTC = int(time.time())
+ date_added = datetime.fromtimestamp(time_UTC)
+
+ # Сохраняем в базу данных
+ audio_service.save_audio_file(file_name, user_id, date_added)
+
+ # Скачиваем и сохраняем файл
+ await audio_service.download_and_save_audio(call.bot, call.message.message_id, file_name)
+
+ # Удаляем сообщение из предложки
+ await call.bot.delete_message(
+ chat_id=bot_db.settings['Telegram']['group_for_posts'],
+ message_id=call.message.message_id
+ )
+
+ await call.answer(text='Сохранено!', cache_time=3)
+
+ except Exception as e:
+ logger.error(f"Ошибка при сохранении голосового сообщения: {e}")
+ await call.answer(text='Ошибка при сохранении!', cache_time=3)
+
+
+@callback_router.callback_query(F.data == CALLBACK_DELETE)
+async def delete_voice_message(
+ call: CallbackQuery,
+ bot_db: MagicData("bot_db")
+ ):
+ try:
+ # Удаляем сообщение из предложки
+ await call.bot.delete_message(
+ chat_id=bot_db.settings['Telegram']['group_for_posts'],
+ message_id=call.message.message_id
+ )
+
+ await call.answer(text='Удалено!', cache_time=3)
+
+ except Exception as e:
+ logger.error(f"Ошибка при удалении голосового сообщения: {e}")
+ await call.answer(text='Ошибка при удалении!', cache_time=3)
diff --git a/helper_bot/handlers/private/constants.py b/helper_bot/handlers/private/constants.py
index 8255e92..344e69b 100644
--- a/helper_bot/handlers/private/constants.py
+++ b/helper_bot/handlers/private/constants.py
@@ -17,7 +17,8 @@ BUTTON_TEXTS: Final[Dict[str, str]] = {
"LEAVE_CHAT": "Выйти из чата",
"RETURN_TO_BOT": "Вернуться в бота",
"WANT_STICKERS": "🤪Хочу стикеры",
- "CONNECT_ADMIN": "📩Связаться с админами"
+ "CONNECT_ADMIN": "📩Связаться с админами",
+ "VOICE_BOT": "🎤Голосовой бот"
}
# Button to command mapping for metrics
@@ -27,7 +28,8 @@ BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
"Выйти из чата": "leave_chat",
"Вернуться в бота": "return_to_bot",
"🤪Хочу стикеры": "want_stickers",
- "📩Связаться с админами": "connect_admin"
+ "📩Связаться с админами": "connect_admin",
+ "🎤Голосовой бот": "voice_bot"
}
# Error messages
diff --git a/helper_bot/handlers/private/private_handlers.py b/helper_bot/handlers/private/private_handlers.py
index fdf2214..dc6443c 100644
--- a/helper_bot/handlers/private/private_handlers.py
+++ b/helper_bot/handlers/private/private_handlers.py
@@ -72,12 +72,13 @@ class PrivateHandlers:
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["LEAVE_CHAT"])
self.router.message.register(self.stickers, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["WANT_STICKERS"])
self.router.message.register(self.connect_with_admin, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["CONNECT_ADMIN"])
+
# State handlers
self.router.message.register(self.suggest_router, StateFilter(FSM_STATES["SUGGEST"]), ChatTypeFilter(chat_type=["private"]))
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["PRE_CHAT"]), ChatTypeFilter(chat_type=["private"]))
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["CHAT"]), ChatTypeFilter(chat_type=["private"]))
-
+
@error_handler
async def handle_emoji_message(self, message: types.Message, state: FSMContext, **kwargs):
"""Handle emoji command"""
diff --git a/helper_bot/handlers/voice/__init__.py b/helper_bot/handlers/voice/__init__.py
new file mode 100644
index 0000000..4ce27cd
--- /dev/null
+++ b/helper_bot/handlers/voice/__init__.py
@@ -0,0 +1,3 @@
+from .voice_handler import VoiceHandlers
+
+__all__ = ["VoiceHandlers"]
diff --git a/helper_bot/handlers/voice/constants.py b/helper_bot/handlers/voice/constants.py
new file mode 100644
index 0000000..07c1f9f
--- /dev/null
+++ b/helper_bot/handlers/voice/constants.py
@@ -0,0 +1,56 @@
+from typing import Final, Dict
+
+# Voice bot constants
+VOICE_BOT_NAME = "voice"
+
+# States
+STATE_START = "START"
+STATE_STANDUP_WRITE = "STANDUP_WRITE"
+
+# Commands
+CMD_START = "start"
+CMD_HELP = "help"
+CMD_RESTART = "restart"
+CMD_EMOJI = "emoji"
+CMD_REFRESH = "refresh"
+
+# Command to command mapping for metrics
+COMMAND_MAPPING: Final[Dict[str, str]] = {
+ "start": "voice_start",
+ "help": "voice_help",
+ "restart": "voice_restart",
+ "emoji": "voice_emoji",
+ "refresh": "voice_refresh"
+}
+
+# Button texts
+BTN_SPEAK = "🎤Высказаться"
+BTN_LISTEN = "🎧Послушать"
+
+# Button to command mapping for metrics
+BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
+ "🎤Высказаться": "voice_speak",
+ "🎧Послушать": "voice_listen"
+}
+
+# Callback data
+CALLBACK_SAVE = "save"
+CALLBACK_DELETE = "delete"
+
+# Callback to command mapping for metrics
+CALLBACK_COMMAND_MAPPING: Final[Dict[str, str]] = {
+ "save": "voice_save",
+ "delete": "voice_delete"
+}
+
+# File paths
+VOICE_USERS_DIR = "voice_users"
+STICK_DIR = "Stick"
+STICK_PATTERN = "Hello_*"
+
+# Time delays
+STICKER_DELAY = 0.3
+MESSAGE_DELAY_1 = 1.0
+MESSAGE_DELAY_2 = 1.5
+MESSAGE_DELAY_3 = 1.3
+MESSAGE_DELAY_4 = 0.8
diff --git a/voice_bot/handlers/exceptions.py b/helper_bot/handlers/voice/exceptions.py
similarity index 100%
rename from voice_bot/handlers/exceptions.py
rename to helper_bot/handlers/voice/exceptions.py
diff --git a/voice_bot/handlers/services.py b/helper_bot/handlers/voice/services.py
similarity index 97%
rename from voice_bot/handlers/services.py
rename to helper_bot/handlers/voice/services.py
index 5d41a44..966da5f 100644
--- a/voice_bot/handlers/services.py
+++ b/helper_bot/handlers/voice/services.py
@@ -6,8 +6,8 @@ from typing import List, Optional, Tuple
from aiogram.types import FSInputFile
-from voice_bot.handlers.exceptions import VoiceMessageError, AudioProcessingError, DatabaseError, FileOperationError
-from voice_bot.handlers.constants import (
+from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError, DatabaseError, FileOperationError
+from helper_bot.handlers.voice.constants import (
VOICE_USERS_DIR, STICK_DIR, STICK_PATTERN, STICKER_DELAY,
MESSAGE_DELAY_1, MESSAGE_DELAY_2, MESSAGE_DELAY_3, MESSAGE_DELAY_4
)
@@ -192,7 +192,7 @@ class VoiceBotService:
def _get_main_keyboard(self):
"""Получить основную клавиатуру"""
- from voice_bot.keyboards.keyboards import get_main_keyboard
+ from helper_bot.keyboards.keyboards import get_main_keyboard
return get_main_keyboard()
async def _send_error_to_logs(self, message: str) -> None:
@@ -239,10 +239,10 @@ class AudioFileService:
logger.error(f"Ошибка при генерации имени файла: {e}")
raise FileOperationError(f"Не удалось сгенерировать имя файла: {e}")
- def save_audio_file(self, file_name: str, user_id: int, file_id: int, date_added: datetime) -> None:
+ def save_audio_file(self, file_name: str, user_id: int, date_added: datetime) -> None:
"""Сохранить информацию об аудио файле в базу данных"""
try:
- self.bot_db.add_audio_record(file_name, user_id, date_added, 0, file_id)
+ self.bot_db.add_audio_record(file_name, user_id, date_added)
except Exception as e:
logger.error(f"Ошибка при сохранении аудио файла в БД: {e}")
raise DatabaseError(f"Не удалось сохранить аудио файл в БД: {e}")
diff --git a/voice_bot/handlers/utils.py b/helper_bot/handlers/voice/utils.py
similarity index 98%
rename from voice_bot/handlers/utils.py
rename to helper_bot/handlers/voice/utils.py
index 6b26c7b..280d9c8 100644
--- a/voice_bot/handlers/utils.py
+++ b/helper_bot/handlers/voice/utils.py
@@ -3,7 +3,7 @@ import html
from datetime import datetime
from typing import Optional
-from voice_bot.handlers.exceptions import DatabaseError
+from helper_bot.handlers.voice.exceptions import DatabaseError
from logs.custom_logger import logger
diff --git a/helper_bot/handlers/voice/voice_handler.py b/helper_bot/handlers/voice/voice_handler.py
new file mode 100644
index 0000000..fd4f101
--- /dev/null
+++ b/helper_bot/handlers/voice/voice_handler.py
@@ -0,0 +1,336 @@
+import asyncio
+from datetime import datetime
+from pathlib import Path
+
+from aiogram import Router, types, F
+from aiogram.filters import Command, StateFilter, MagicData
+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.middlewares.dependencies_middleware import DependenciesMiddleware
+
+from helper_bot.utils import messages
+from helper_bot.utils.helper_func import get_first_name, update_user_info, check_user_emoji, send_voice_message
+from logs.custom_logger import logger
+from helper_bot.handlers.voice.constants import *
+from helper_bot.handlers.voice.services import VoiceBotService
+from helper_bot.handlers.voice.utils import get_last_message_text, validate_voice_message, get_user_emoji_safe
+from helper_bot.keyboards.keyboards import get_main_keyboard, get_reply_keyboard_for_voice
+from helper_bot.handlers.private.constants import BUTTON_TEXTS
+
+
+class VoiceHandlers:
+ def __init__(self, db, settings):
+ self.db = db
+ self.settings = settings
+ self.router = Router()
+ self._setup_handlers()
+ self._setup_middleware()
+
+ def _setup_middleware(self):
+ self.router.message.middleware(DependenciesMiddleware())
+ self.router.message.middleware(BlacklistMiddleware())
+
+ def _setup_handlers(self):
+ # Обработчик кнопки "Голосовой бот"
+ self.router.message.register(
+ self.voice_bot_button_handler,
+ ChatTypeFilter(chat_type=["private"]),
+ F.text == BUTTON_TEXTS["VOICE_BOT"]
+ )
+
+ # Команды
+ self.router.message.register(
+ self.restart_function,
+ ChatTypeFilter(chat_type=["private"]),
+ Command(CMD_RESTART)
+ )
+
+ self.router.message.register(
+ self.handle_emoji_message,
+ ChatTypeFilter(chat_type=["private"]),
+ Command(CMD_EMOJI)
+ )
+
+ self.router.message.register(
+ self.help_function,
+ ChatTypeFilter(chat_type=["private"]),
+ Command(CMD_HELP)
+ )
+
+ self.router.message.register(
+ self.start,
+ ChatTypeFilter(chat_type=["private"]),
+ Command(CMD_START)
+ )
+
+ # Дополнительные команды
+ self.router.message.register(
+ self.refresh_listen_function,
+ ChatTypeFilter(chat_type=["private"]),
+ Command(CMD_REFRESH)
+ )
+
+ # Обработчики состояний и кнопок
+ self.router.message.register(
+ self.standup_write,
+ StateFilter(STATE_START),
+ ChatTypeFilter(chat_type=["private"]),
+ F.text == BTN_SPEAK
+ )
+
+ self.router.message.register(
+ self.suggest_voice,
+ StateFilter(STATE_STANDUP_WRITE),
+ ChatTypeFilter(chat_type=["private"]),
+ )
+
+ self.router.message.register(
+ self.standup_listen_audio,
+ StateFilter(STATE_START),
+ ChatTypeFilter(chat_type=["private"]),
+ F.text == BTN_LISTEN
+ )
+
+ async def voice_bot_button_handler(self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings")):
+ """Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
+ await self.start(message, state, bot_db, settings)
+
+ async def restart_function(
+ self,
+ message: types.Message,
+ state: FSMContext,
+ bot_db: MagicData("bot_db")
+ ):
+ await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
+ await update_user_info(VOICE_BOT_NAME, message)
+ check_user_emoji(message)
+ markup = get_main_keyboard()
+ await message.answer(text='Я перезапущен!', reply_markup=markup)
+ await state.set_state(STATE_START)
+
+ async def handle_emoji_message(
+ self,
+ message: types.Message,
+ state: FSMContext,
+ bot_db: MagicData("bot_db")
+ ):
+ await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
+ user_emoji = check_user_emoji(message)
+ await state.set_state(STATE_START)
+ if user_emoji is not None:
+ await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
+
+ async def help_function(
+ self,
+ message: types.Message,
+ state: FSMContext,
+ bot_db: MagicData("bot_db"),
+ settings: MagicData("settings")
+ ):
+ await message.forward(chat_id=settings['Telegram']['group_for_logs'])
+ await update_user_info(VOICE_BOT_NAME, message)
+ help_message = messages.get_message(get_first_name(message), 'HELP_MESSAGE')
+ await message.answer(
+ text=help_message,
+ disable_web_page_preview=not bot_db.settings['Telegram']['preview_link']
+ )
+ await state.set_state(STATE_START)
+
+ async def start(
+ self,
+ message: types.Message,
+ state: FSMContext,
+ bot_db: MagicData("bot_db"),
+ settings: MagicData("settings")
+ ):
+ await state.set_state(STATE_START)
+ await message.forward(chat_id=settings['Telegram']['group_for_logs'])
+ await update_user_info(VOICE_BOT_NAME, message)
+ user_emoji = get_user_emoji_safe(bot_db, message.from_user.id)
+
+ # Создаем сервис и отправляем приветственные сообщения
+ voice_service = VoiceBotService(bot_db, settings)
+ await voice_service.send_welcome_messages(message, user_emoji)
+
+
+ async def refresh_listen_function(
+ self,
+ message: types.Message,
+ state: FSMContext,
+ bot_db: MagicData("bot_db"),
+ settings: MagicData("settings")
+ ):
+ await message.forward(chat_id=settings['Telegram']['group_for_logs'])
+ await update_user_info(VOICE_BOT_NAME, message)
+ markup = get_main_keyboard()
+
+ # Очищаем прослушивания через сервис
+ voice_service = VoiceBotService(bot_db, settings)
+ voice_service.clear_user_listenings(message.from_user.id)
+
+ listenings_cleared_message = messages.get_message(get_first_name(message), 'LISTENINGS_CLEARED_MESSAGE')
+ await message.answer(
+ text=listenings_cleared_message,
+ disable_web_page_preview=not settings['Telegram']['preview_link'],
+ reply_markup=markup
+ )
+ await state.set_state(STATE_START)
+
+
+ async def standup_write(
+ self,
+ message: types.Message,
+ state: FSMContext,
+ bot_db: MagicData("bot_db"),
+ settings: MagicData("settings")
+ ):
+ await message.forward(chat_id=settings['Telegram']['group_for_logs'])
+ markup = types.ReplyKeyboardRemove()
+ record_voice_message = messages.get_message(get_first_name(message), 'RECORD_VOICE_MESSAGE')
+ await message.answer(text=record_voice_message, reply_markup=markup)
+
+ try:
+ message_with_date = get_last_message_text(bot_db)
+ if message_with_date:
+ await message.answer(text=message_with_date, parse_mode="html")
+ except Exception as e:
+ logger.error(f'Не удалось получить дату последнего сообщения - {e}')
+
+ await state.set_state(STATE_STANDUP_WRITE)
+
+
+ async def suggest_voice(
+ self,
+ message: types.Message,
+ state: FSMContext,
+ bot_db: MagicData("bot_db"),
+ settings: MagicData("settings")
+ ):
+ logger.info(
+ f"Вызов функции suggest_voice. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}"
+ )
+ await message.forward(chat_id=settings['Telegram']['group_for_logs'])
+ markup = get_main_keyboard()
+
+ if validate_voice_message(message):
+ markup_for_voice = get_reply_keyboard_for_voice()
+
+ # Отправляем аудио в приватный канал
+ sent_message = await send_voice_message(
+ settings['Telegram']['group_for_posts'],
+ message,
+ message.voice.file_id,
+ markup_for_voice
+ )
+
+ # Сохраняем в базу инфо о посте
+ bot_db.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id)
+
+ # Отправляем юзеру ответ и возвращаем его в меню
+ voice_saved_message = messages.get_message(get_first_name(message), 'VOICE_SAVED_MESSAGE')
+ await message.answer(text=voice_saved_message, reply_markup=markup)
+ await state.set_state(STATE_START)
+ else:
+ unknown_content_message = messages.get_message(get_first_name(message), 'UNKNOWN_CONTENT_MESSAGE')
+ await message.forward(chat_id=settings['Telegram']['group_for_logs'])
+ await message.answer(text=unknown_content_message, reply_markup=markup)
+ await state.set_state(STATE_STANDUP_WRITE)
+
+
+ async def standup_listen_audio(
+ self,
+ message: types.Message,
+ bot_db: MagicData("bot_db"),
+ settings: MagicData("settings")
+ ):
+ markup = get_main_keyboard()
+
+ # Создаем сервис для работы с аудио
+ voice_service = VoiceBotService(bot_db, settings)
+
+ try:
+ # Получаем случайное аудио
+ audio_data = voice_service.get_random_audio(message.from_user.id)
+
+ if not audio_data:
+ no_audio_message = messages.get_message(get_first_name(message), 'NO_AUDIO_MESSAGE')
+ await message.answer(text=no_audio_message, reply_markup=markup)
+ try:
+ message_with_date = get_last_message_text(bot_db)
+ if message_with_date:
+ await message.answer(text=message_with_date, parse_mode="html")
+ except Exception as e:
+ logger.error(f'Не удалось получить последнюю дату {e}')
+ return
+
+ audio_for_user, date_added, user_emoji = audio_data
+
+ # Получаем путь к файлу
+ path = Path(f'{VOICE_USERS_DIR}/{audio_for_user}.ogg')
+
+ # Проверяем существование файла
+ if not path.exists():
+ logger.error(f"Файл не найден: {path}")
+ await message.answer(
+ text="Файл аудио не найден. Обратитесь к администратору.",
+ reply_markup=markup
+ )
+ return
+
+ # Проверяем размер файла
+ if path.stat().st_size == 0:
+ logger.error(f"Файл пустой: {path}")
+ await message.answer(
+ text="Файл аудио поврежден. Обратитесь к администратору.",
+ reply_markup=markup
+ )
+ return
+
+ voice = FSInputFile(path)
+
+ # Формируем подпись
+ if user_emoji:
+ caption = f'{user_emoji}\nДата записи: {date_added}'
+ else:
+ caption = f'Дата записи: {date_added}'
+
+ try:
+ await message.bot.send_voice(
+ chat_id=message.chat.id,
+ voice=voice,
+ caption=caption,
+ reply_markup=markup
+ )
+
+ # Маркируем сообщение как прослушанное только после успешной отправки
+ voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
+
+ # Получаем количество оставшихся аудио только после успешной отправки
+ remaining_count = voice_service.get_remaining_audio_count(message.from_user.id) - 1
+ await message.answer(
+ text=f'Осталось непрослушанных: {remaining_count}',
+ reply_markup=markup
+ )
+
+ except Exception as voice_error:
+ if "VOICE_MESSAGES_FORBIDDEN" in str(voice_error):
+ # Если голосовые сообщения запрещены, отправляем информативное сообщение
+ logger.info(f"Пользователь {message.from_user.id} запретил получение голосовых сообщений")
+
+ privacy_message = "🔇 К сожалению, у тебя закрыт доступ к получению голосовых сообщений.\n\nДля продолжения взаимодействия с ботом необходимо дать возможность мне присылать войсы в настройках приватности Telegram.\n\n💡 Как это сделать:\n1. Открой настройки Telegram\n2. Перейди в 'Конфиденциальность и безопасность'\n3. Выбери 'Голосовые сообщения'\n4. Разреши получение от 'Всех' или добавь меня в исключения"
+
+ await message.answer(text=privacy_message, reply_markup=markup)
+ return # Выходим без записи о прослушивании
+
+ else:
+ raise voice_error
+
+ except Exception as e:
+ logger.error(f"Ошибка при прослушивании аудио: {e}")
+ await message.answer(
+ text="Произошла ошибка при получении аудио. Попробуйте позже.",
+ reply_markup=markup
+ )
diff --git a/helper_bot/keyboards/keyboards.py b/helper_bot/keyboards/keyboards.py
index 714ffcb..00dd310 100644
--- a/helper_bot/keyboards/keyboards.py
+++ b/helper_bot/keyboards/keyboards.py
@@ -29,6 +29,7 @@ def get_reply_keyboard(BotDB, user_id):
builder = ReplyKeyboardBuilder()
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
builder.row(types.KeyboardButton(text="📩Связаться с админами"))
+ builder.row(types.KeyboardButton(text=" 🎤Голосовой бот"))
builder.row(types.KeyboardButton(text="👋🏼Сказать пока!"))
if not BotDB.get_info_about_stickers(user_id=user_id):
builder.row(types.KeyboardButton(text="🤪Хочу стикеры"))
@@ -170,3 +171,23 @@ def create_keyboard_for_approve_ban():
builder.add(types.KeyboardButton(text="Отменить"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup
+
+
+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
diff --git a/helper_bot/main.py b/helper_bot/main.py
index 3b109ea..525b1b9 100644
--- a/helper_bot/main.py
+++ b/helper_bot/main.py
@@ -2,13 +2,13 @@ from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.strategy import FSMStrategy
-import asyncio
import logging
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
+from helper_bot.handlers.voice import VoiceHandlers
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
@@ -28,13 +28,17 @@ async def start_bot(bdf):
dp.update.outer_middleware(MetricsMiddleware())
dp.update.outer_middleware(BlacklistMiddleware())
+ # Создаем экземпляр VoiceHandlers
+ voice_handlers = VoiceHandlers(bdf, bdf.settings)
+ voice_router = voice_handlers.router
+
# Добавляем middleware напрямую к роутерам для тестирования
admin_router.message.middleware(MetricsMiddleware())
private_router.message.middleware(MetricsMiddleware())
callback_router.callback_query.middleware(MetricsMiddleware())
group_router.message.middleware(MetricsMiddleware())
-
- dp.include_routers(admin_router, private_router, callback_router, group_router)
+ voice_router.message.middleware(MetricsMiddleware())
+ dp.include_routers(admin_router, private_router, callback_router, group_router, voice_router)
await bot.delete_webhook(drop_pending_updates=True)
# Запускаем HTTP сервер для метрик параллельно с ботом
diff --git a/helper_bot/middlewares/metrics_middleware.py b/helper_bot/middlewares/metrics_middleware.py
index e8f113b..3f962e0 100644
--- a/helper_bot/middlewares/metrics_middleware.py
+++ b/helper_bot/middlewares/metrics_middleware.py
@@ -16,12 +16,20 @@ try:
from ..handlers.private.constants import BUTTON_COMMAND_MAPPING
from ..handlers.callback.constants import CALLBACK_COMMAND_MAPPING
from ..handlers.admin.constants import ADMIN_BUTTON_COMMAND_MAPPING, ADMIN_COMMANDS
+ from ..handlers.voice.constants import (
+ BUTTON_COMMAND_MAPPING as VOICE_BUTTON_COMMAND_MAPPING,
+ COMMAND_MAPPING as VOICE_COMMAND_MAPPING,
+ CALLBACK_COMMAND_MAPPING as VOICE_CALLBACK_COMMAND_MAPPING
+ )
except ImportError:
# Fallback if constants not available
BUTTON_COMMAND_MAPPING = {}
CALLBACK_COMMAND_MAPPING = {}
ADMIN_BUTTON_COMMAND_MAPPING = {}
ADMIN_COMMANDS = {}
+ VOICE_BUTTON_COMMAND_MAPPING = {}
+ VOICE_COMMAND_MAPPING = {}
+ VOICE_CALLBACK_COMMAND_MAPPING = {}
class MetricsMiddleware(BaseMiddleware):
@@ -183,6 +191,13 @@ class MetricsMiddleware(BaseMiddleware):
'user_type': "admin" if message.from_user else "unknown",
'handler_type': "admin_handler"
}
+ # Check if it's a voice bot command
+ elif command_name in VOICE_COMMAND_MAPPING:
+ return {
+ 'command': VOICE_COMMAND_MAPPING[command_name],
+ 'user_type': "user" if message.from_user else "unknown",
+ 'handler_type': "voice_command_handler"
+ }
else:
return {
'command': command_name,
@@ -206,6 +221,14 @@ class MetricsMiddleware(BaseMiddleware):
'handler_type': "button_handler"
}
+ # Check if it's a voice bot button click
+ if message.text in VOICE_BUTTON_COMMAND_MAPPING:
+ return {
+ 'command': VOICE_BUTTON_COMMAND_MAPPING[message.text],
+ 'user_type': "user" if message.from_user else "unknown",
+ 'handler_type': "voice_button_handler"
+ }
+
return None
def _extract_callback_command_info(self, callback: CallbackQuery) -> Optional[Dict[str, str]]:
@@ -222,6 +245,14 @@ class MetricsMiddleware(BaseMiddleware):
'handler_type': "callback_handler"
}
+ # Check if it's a voice bot callback
+ if parts and parts[0] in VOICE_CALLBACK_COMMAND_MAPPING:
+ return {
+ 'command': VOICE_CALLBACK_COMMAND_MAPPING[parts[0]],
+ 'user_type': "user" if callback.from_user else "unknown",
+ 'handler_type': "voice_callback_handler"
+ }
+
return None
diff --git a/helper_bot/utils/messages.py b/helper_bot/utils/messages.py
index d422680..24c0d8c 100644
--- a/helper_bot/utils/messages.py
+++ b/helper_bot/utils/messages.py
@@ -35,12 +35,28 @@ constants = {
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
"QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉",
"SUCCESS_SEND_MESSAGE": "Пост успешно отправлен❤️ Ожидай одобрения😊",
+ # Voice handler messages
"MESSAGE_FOR_STANDUP": "Отлично, ты вошел в режим стендапа 📣"
"&Это свободное пространство, в котором может высказаться каждый житель нашего города, и он будет услышан🙌🏼"
"&Для того чтобы высказаться, нажми кнопку: 'Высказаться' и запиши голосовое сообщение, оно выпадет анонимно кому-то другому🗣"
"&Для того чтобы послушать о чем говорит наш город, нажми кнопку: 'Послушать'👂"
"&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤"
- "&❗️Но пожалуйста не оскорбляй никого, и будь вежлив."
+ "&❗️Но пожалуйста не оскорбляй никого, и будь вежлив.",
+ 'WELCOME_MESSAGE': "Привет.",
+ 'DESCRIPTION_MESSAGE': "Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска",
+ 'ANALOGY_MESSAGE': "Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
+ 'RULES_MESSAGE': "Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, хотя бы на 5-10 секунд",
+ 'ANONYMITY_MESSAGE': "Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)",
+ 'SUGGESTION_MESSAGE': "Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
+ 'EMOJI_INFO_MESSAGE': "Любые войсы будут помечены эмоджи. Твой эмоджи - {emoji}Таким эмоджи будут помечены твои сообщения для других Но другие люди не узнают кто за каким эмоджи скрывается:)",
+ 'HELP_INFO_MESSAGE': "Так же можешь ознакомиться с инструкцией к боту по команде /help",
+ 'FINAL_MESSAGE': "Ну всё, достаточно инструкций. записывайся! Микрофон твой - 🎤",
+ 'HELP_MESSAGE': "Скорее всего ответы на твои вопросы есть здесь, ознакомься: https://telegra.ph/Instrukciya-k-botu-Golosa-Bijsk-10-11-2\nЕсли это не поможет, пиши в личку: @Kerrad1",
+ 'VOICE_SAVED_MESSAGE': "Окей, сохранил!👌",
+ 'LISTENINGS_CLEARED_MESSAGE': "Прослушивания очищены. Можешь начать слушать заново🤗",
+ 'NO_AUDIO_MESSAGE': "Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится",
+ 'UNKNOWN_CONTENT_MESSAGE': "Я тебя не понимаю🤷♀️ запиши голосовое",
+ 'RECORD_VOICE_MESSAGE': "Хорошо, теперь пришли мне свое голосовое сообщение"
}
diff --git a/tests/test_async_db.py b/tests/test_async_db.py
index 8ade705..c494813 100644
--- a/tests/test_async_db.py
+++ b/tests/test_async_db.py
@@ -136,11 +136,7 @@ async def test_audio_operations(temp_db):
# Добавляем аудио запись
with pytest.raises(sqlite3.IntegrityError):
- await temp_db.add_audio_record(file_name, user_id, file_id)
-
- # # Получаем file_id
- # retrieved_file_id = await temp_db.get_audio_file_id(user_id)
- # assert retrieved_file_id == file_id
+ await temp_db.add_audio_record(file_name, user_id)
# # Получаем имя файла
# retrieved_file_name = await temp_db.get_audio_file_name(user_id)
diff --git a/tests/test_voice_bot_architecture.py b/tests/test_voice_bot_architecture.py
new file mode 100644
index 0000000..a82214d
--- /dev/null
+++ b/tests/test_voice_bot_architecture.py
@@ -0,0 +1,195 @@
+import pytest
+from unittest.mock import Mock, AsyncMock, patch
+from datetime import datetime
+
+from helper_bot.handlers.voice.services import VoiceBotService
+from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError
+from helper_bot.handlers.voice.utils import get_last_message_text, validate_voice_message, get_user_emoji_safe
+
+
+class TestVoiceBotService:
+ """Тесты для VoiceBotService"""
+
+ @pytest.fixture
+ def mock_bot_db(self):
+ """Мок для базы данных"""
+ mock_db = Mock()
+ mock_db.settings = {
+ 'Settings': {'logs': True},
+ 'Telegram': {'important_logs': 'test_chat_id'}
+ }
+ return mock_db
+
+ @pytest.fixture
+ def mock_settings(self):
+ """Мок для настроек"""
+ return {
+ 'Settings': {'logs': True},
+ 'Telegram': {'preview_link': True}
+ }
+
+ @pytest.fixture
+ def voice_service(self, mock_bot_db, mock_settings):
+ """Экземпляр VoiceBotService для тестов"""
+ return VoiceBotService(mock_bot_db, mock_settings)
+
+ @pytest.mark.asyncio
+ async def test_get_welcome_sticker_success(self, voice_service, mock_settings):
+ """Тест успешного получения стикера"""
+ with patch('pathlib.Path.rglob') as mock_rglob:
+ mock_rglob.return_value = ['/path/to/sticker1.tgs', '/path/to/sticker2.tgs']
+
+ sticker = await voice_service.get_welcome_sticker()
+
+ assert sticker is not None
+ mock_rglob.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_get_welcome_sticker_no_stickers(self, voice_service, mock_settings):
+ """Тест получения стикера когда их нет"""
+ with patch('pathlib.Path.rglob') as mock_rglob:
+ mock_rglob.return_value = []
+
+ sticker = await voice_service.get_welcome_sticker()
+
+ assert sticker is None
+
+ def test_get_random_audio_success(self, voice_service, mock_bot_db):
+ """Тест успешного получения случайного аудио"""
+ mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2']
+ mock_bot_db.get_user_id_by_file_name.return_value = 123
+ mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00'
+ mock_bot_db.check_emoji_for_user.return_value = '😊'
+
+ result = voice_service.get_random_audio(456)
+
+ assert result is not None
+ assert len(result) == 3
+ assert result[0] in ['audio1', 'audio2']
+ assert result[1] == '2025-01-01 12:00:00'
+ assert result[2] == '😊'
+
+ def test_get_random_audio_no_audio(self, voice_service, mock_bot_db):
+ """Тест получения аудио когда их нет"""
+ mock_bot_db.check_listen_audio.return_value = []
+
+ result = voice_service.get_random_audio(456)
+
+ assert result is None
+
+ def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db):
+ """Тест успешной пометки аудио как прослушанного"""
+ voice_service.mark_audio_as_listened('test_audio', 123)
+
+ mock_bot_db.mark_listened_audio.assert_called_once_with('test_audio', user_id=123)
+
+ def test_clear_user_listenings_success(self, voice_service, mock_bot_db):
+ """Тест успешной очистки прослушиваний"""
+ voice_service.clear_user_listenings(123)
+
+ mock_bot_db.delete_listen_count_for_user.assert_called_once_with(123)
+
+
+class TestVoiceHandlers:
+ """Тесты для VoiceHandlers"""
+
+ @pytest.fixture
+ def mock_db(self):
+ """Мок для базы данных"""
+ return Mock()
+
+ @pytest.fixture
+ def mock_settings(self):
+ """Мок для настроек"""
+ return {
+ 'Telegram': {
+ 'group_for_logs': 'test_logs_chat',
+ 'group_for_posts': 'test_posts_chat',
+ 'preview_link': True
+ }
+ }
+
+ @pytest.fixture
+ def voice_handlers(self, mock_db, mock_settings):
+ """Экземпляр VoiceHandlers для тестов"""
+ from helper_bot.handlers.voice.voice_handler import VoiceHandlers
+ return VoiceHandlers(mock_db, mock_settings)
+
+ def test_voice_handlers_initialization(self, voice_handlers):
+ """Тест инициализации VoiceHandlers"""
+ assert voice_handlers.db is not None
+ assert voice_handlers.settings is not None
+ assert voice_handlers.router is not None
+
+ def test_setup_handlers(self, voice_handlers):
+ """Тест настройки обработчиков"""
+ # Проверяем, что роутер содержит обработчики
+ assert len(voice_handlers.router.message.handlers) > 0
+
+
+class TestUtils:
+ """Тесты для утилит"""
+
+ @pytest.fixture
+ def mock_bot_db(self):
+ """Мок для базы данных"""
+ return Mock()
+
+ def test_get_last_message_text(self, mock_bot_db):
+ """Тест получения последнего сообщения"""
+ mock_bot_db.last_date_audio.return_value = "2025-01-01 12:00:00"
+
+ result = get_last_message_text(mock_bot_db)
+
+ assert result is not None
+ assert "минут" in result or "часа" in result or "дня" in result
+ mock_bot_db.last_date_audio.assert_called_once()
+
+ def test_validate_voice_message_valid(self):
+ """Тест валидации голосового сообщения"""
+ mock_message = Mock()
+ mock_message.content_type = 'voice'
+
+ result = validate_voice_message(mock_message)
+
+ assert result is True
+
+ def test_validate_voice_message_invalid(self):
+ """Тест валидации невалидного сообщения"""
+ mock_message = Mock()
+ mock_message.voice = None
+
+ result = validate_voice_message(mock_message)
+
+ assert result is False
+
+ def test_get_user_emoji_safe(self, mock_bot_db):
+ """Тест безопасного получения эмодзи пользователя"""
+ mock_bot_db.check_emoji_for_user.return_value = "😊"
+
+ result = get_user_emoji_safe(mock_bot_db, 123)
+
+ assert result == "😊"
+ mock_bot_db.check_emoji_for_user.assert_called_once_with(123)
+
+
+class TestExceptions:
+ """Тесты для исключений"""
+
+ def test_voice_message_error(self):
+ """Тест VoiceMessageError"""
+ try:
+ raise VoiceMessageError("Тестовая ошибка")
+ except VoiceMessageError as e:
+ assert str(e) == "Тестовая ошибка"
+
+ def test_audio_processing_error(self):
+ """Тест AudioProcessingError"""
+ try:
+ raise AudioProcessingError("Ошибка обработки")
+ except AudioProcessingError as e:
+ assert str(e) == "Ошибка обработки"
+
+
+if __name__ == '__main__':
+ pytest.main([__file__])
diff --git a/voice_bot/handlers/__init__.py b/voice_bot/handlers/__init__.py
deleted file mode 100644
index cdd54de..0000000
--- a/voice_bot/handlers/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from .voice_handler import voice_router
-from .callback_handler import callback_router
-from .dependencies import VoiceBotMiddleware, BotDB, Settings
-from .exceptions import VoiceBotError, VoiceMessageError, AudioProcessingError, DatabaseError, FileOperationError
-from .services import VoiceBotService, AudioFileService, VoiceMessage
-from .utils import (
- format_time_ago, plural_time, get_last_message_text,
- validate_voice_message, get_user_emoji_safe
-)
-
-__all__ = [
- 'voice_router',
- 'callback_router',
- 'VoiceBotMiddleware',
- 'BotDB',
- 'Settings',
- 'VoiceBotError',
- 'VoiceMessageError',
- 'AudioProcessingError',
- 'DatabaseError',
- 'FileOperationError',
- 'VoiceBotService',
- 'AudioFileService',
- 'VoiceMessage',
- 'format_time_ago',
- 'plural_time',
- 'get_last_message_text',
- 'validate_voice_message',
- 'get_user_emoji_safe'
-]
diff --git a/voice_bot/handlers/callback_handler.py b/voice_bot/handlers/callback_handler.py
deleted file mode 100644
index b6784de..0000000
--- a/voice_bot/handlers/callback_handler.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import time
-from datetime import datetime
-
-from aiogram import Router, F
-from aiogram.types import CallbackQuery
-
-from voice_bot.handlers.constants import CALLBACK_SAVE, CALLBACK_DELETE, VOICE_USERS_DIR
-from voice_bot.handlers.dependencies import VoiceBotMiddleware, BotDB
-from voice_bot.handlers.services import AudioFileService
-from logs.custom_logger import logger
-
-callback_router = Router()
-
-# Middleware
-callback_router.callback_query.middleware(VoiceBotMiddleware())
-
-
-@callback_router.callback_query(F.data == CALLBACK_SAVE)
-async def save_voice_message(call: CallbackQuery, bot_db: BotDB):
- try:
- # Создаем сервис для работы с аудио файлами
- audio_service = AudioFileService(bot_db)
-
- # Получаем ID пользователя из базы
- user_id = bot_db.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
-
- # Генерируем имя файла
- file_name = audio_service.generate_file_name(user_id)
-
- # Собираем инфо о сообщении
- time_UTC = int(time.time())
- date_added = datetime.fromtimestamp(time_UTC)
-
- # Определяем file_id
- file_id = 1
- if bot_db.get_last_user_audio_record(user_id=user_id):
- file_id = bot_db.get_id_for_audio_record(user_id) + 1
-
- # Сохраняем в базу данных
- audio_service.save_audio_file(file_name, user_id, file_id, date_added)
-
- # Скачиваем и сохраняем файл
- await audio_service.download_and_save_audio(call.bot, call.message.message_id, file_name)
-
- # Удаляем сообщение из предложки
- await call.bot.delete_message(
- chat_id=bot_db.settings['Telegram']['group_for_posts'],
- message_id=call.message.message_id
- )
-
- await call.answer(text='Сохранено!', cache_time=3)
-
- except Exception as e:
- logger.error(f"Ошибка при сохранении голосового сообщения: {e}")
- await call.answer(text='Ошибка при сохранении!', cache_time=3)
-
-
-@callback_router.callback_query(F.data == CALLBACK_DELETE)
-async def delete_voice_message(call: CallbackQuery, bot_db: BotDB):
- try:
- # Удаляем сообщение из предложки
- await call.bot.delete_message(
- chat_id=bot_db.settings['Telegram']['group_for_posts'],
- message_id=call.message.message_id
- )
-
- await call.answer(text='Удалено!', cache_time=3)
-
- except Exception as e:
- logger.error(f"Ошибка при удалении голосового сообщения: {e}")
- await call.answer(text='Ошибка при удалении!', cache_time=3)
diff --git a/voice_bot/handlers/constants.py b/voice_bot/handlers/constants.py
deleted file mode 100644
index 716131b..0000000
--- a/voice_bot/handlers/constants.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Voice bot constants
-VOICE_BOT_NAME = "voice"
-
-# States
-STATE_START = "START"
-STATE_STANDUP_WRITE = "STANDUP_WRITE"
-
-# Commands
-CMD_START = "start"
-CMD_HELP = "help"
-CMD_RESTART = "restart"
-CMD_EMOJI = "emoji"
-CMD_REFRESH = "refresh"
-
-# Button texts
-BTN_SPEAK = "🎤Высказаться"
-BTN_LISTEN = "🎧Послушать"
-
-# Callback data
-CALLBACK_SAVE = "save"
-CALLBACK_DELETE = "delete"
-
-# File paths
-VOICE_USERS_DIR = "voice_users"
-STICK_DIR = "Stick"
-STICK_PATTERN = "Hello_*"
-
-# Messages
-WELCOME_MESSAGE = "Привет."
-DESCRIPTION_MESSAGE = "Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска"
-ANALOGY_MESSAGE = "Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится.."
-RULES_MESSAGE = "Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, хотя бы на 5-10 секунд"
-ANONYMITY_MESSAGE = "Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)"
-SUGGESTION_MESSAGE = "Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)"
-EMOJI_INFO_MESSAGE = "Любые войсы будут помечены эмоджи. Твой эмоджи - {emoji}Таким эмоджи будут помечены твои сообщения для других Но другие люди не узнают кто за каким эмоджи скрывается:)"
-HELP_INFO_MESSAGE = "Так же можешь ознакомиться с инструкцией к боту по команде /help"
-FINAL_MESSAGE = "Ну всё, достаточно инструкций. записывайся! Микрофон твой - 🎤"
-
-# Help message
-HELP_MESSAGE = "Скорее всего ответы на твои вопросы есть здесь, ознакомься: https://telegra.ph/Instrukciya-k-botu-Golosa-Bijsk-10-11-2\nЕсли это не поможет, пиши в личку: @Kerrad1"
-
-# Success messages
-VOICE_SAVED_MESSAGE = "Окей, сохранил!👌"
-LISTENINGS_CLEARED_MESSAGE = "Прослушивания очищены. Можешь начать слушать заново🤗"
-NO_AUDIO_MESSAGE = "Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится"
-
-# Error messages
-UNKNOWN_CONTENT_MESSAGE = "Я тебя не понимаю🤷♀️ запиши голосовое"
-RECORD_VOICE_MESSAGE = "Хорошо, теперь пришли мне свое голосовое сообщение"
-
-# Time delays
-STICKER_DELAY = 0.3
-MESSAGE_DELAY_1 = 1.0
-MESSAGE_DELAY_2 = 1.5
-MESSAGE_DELAY_3 = 1.3
-MESSAGE_DELAY_4 = 0.8
diff --git a/voice_bot/handlers/dependencies.py b/voice_bot/handlers/dependencies.py
deleted file mode 100644
index e733b06..0000000
--- a/voice_bot/handlers/dependencies.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from typing import Dict, Any
-try:
- from typing import Annotated
-except ImportError:
- from typing_extensions import Annotated
-from aiogram import BaseMiddleware
-from aiogram.types import TelegramObject
-
-from helper_bot.utils.base_dependency_factory import get_global_instance
-from logs.custom_logger import logger
-
-
-class VoiceBotMiddleware(BaseMiddleware):
- """Middleware для voice_bot с dependency injection"""
-
- async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
- try:
- # Вызываем хендлер с data
- return await handler(event, data)
- except TypeError as e:
- if "missing 1 required positional argument: 'data'" in str(e):
- logger.error(f"Ошибка в VoiceBotMiddleware: {e}. Хендлер не принимает параметр 'data'")
- # Пытаемся вызвать хендлер без data (для совместимости с MagicData)
- return await handler(event)
- else:
- logger.error(f"TypeError в VoiceBotMiddleware: {e}")
- raise
- except Exception as e:
- logger.error(f"Неожиданная ошибка в VoiceBotMiddleware: {e}")
- raise
-
-
-# Dependency providers
-def get_bot_db():
- """Провайдер для получения экземпляра БД"""
- bdf = get_global_instance()
- return bdf.get_db()
-
-
-def get_settings():
- """Провайдер для получения настроек"""
- bdf = get_global_instance()
- return bdf.settings
-
-
-# Type aliases for dependency injection
-BotDB = Annotated[object, get_bot_db()]
-Settings = Annotated[dict, get_settings()]
diff --git a/voice_bot/handlers/voice_handler.py b/voice_bot/handlers/voice_handler.py
deleted file mode 100644
index 0d1fcfb..0000000
--- a/voice_bot/handlers/voice_handler.py
+++ /dev/null
@@ -1,215 +0,0 @@
-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.helper_func import update_user_info, check_user_emoji, send_voice_message
-from logs.custom_logger import logger
-from voice_bot.handlers.constants import *
-from voice_bot.handlers.dependencies import VoiceBotMiddleware, BotDB, Settings
-from voice_bot.handlers.services import VoiceBotService
-from voice_bot.handlers.utils import get_last_message_text, validate_voice_message, get_user_emoji_safe
-from voice_bot.keyboards.keyboards import get_main_keyboard, get_reply_keyboard_for_voice
-
-voice_router = Router()
-
-# Middleware
-voice_router.message.middleware(VoiceBotMiddleware())
-voice_router.message.middleware(BlacklistMiddleware())
-
-
-@voice_router.message(
- ChatTypeFilter(chat_type=["private"]),
- Command(CMD_RESTART)
-)
-async def restart_function(message: types.Message, state: FSMContext, bot_db: BotDB):
- await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
- await update_user_info(VOICE_BOT_NAME, message)
- check_user_emoji(message)
- markup = get_main_keyboard()
- await message.answer(text='Я перезапущен!', reply_markup=markup)
- await state.set_state(STATE_START)
-
-
-@voice_router.message(
- ChatTypeFilter(chat_type=["private"]),
- Command(CMD_EMOJI)
-)
-async def handle_emoji_message(message: types.Message, state: FSMContext, bot_db: BotDB):
- await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
- user_emoji = check_user_emoji(message)
- await state.set_state(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(CMD_HELP)
-)
-async def help_function(message: types.Message, state: FSMContext, bot_db: BotDB):
- await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
- await update_user_info(VOICE_BOT_NAME, message)
- await message.answer(
- text=HELP_MESSAGE,
- disable_web_page_preview=not bot_db.settings['Telegram']['preview_link']
- )
- await state.set_state(STATE_START)
-
-
-@voice_router.message(
- ChatTypeFilter(chat_type=["private"]),
- Command(CMD_START)
-)
-async def start(message: types.Message, state: FSMContext, bot_db: BotDB, settings: Settings):
- await state.set_state(STATE_START)
- await message.forward(chat_id=settings['Telegram']['group_for_logs'])
- await update_user_info(VOICE_BOT_NAME, message)
- user_emoji = get_user_emoji_safe(bot_db, message.from_user.id)
-
- # Создаем сервис и отправляем приветственные сообщения
- voice_service = VoiceBotService(bot_db, settings)
- await voice_service.send_welcome_messages(message, user_emoji)
-
-
-@voice_router.message(
- ChatTypeFilter(chat_type=["private"]),
- Command(CMD_REFRESH)
-)
-async def refresh_listen_function(message: types.Message, state: FSMContext, bot_db: BotDB):
- await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
- await update_user_info(VOICE_BOT_NAME, message)
- markup = get_main_keyboard()
-
- # Очищаем прослушивания через сервис
- voice_service = VoiceBotService(bot_db, bot_db.settings)
- voice_service.clear_user_listenings(message.from_user.id)
-
- await message.answer(
- text=LISTENINGS_CLEARED_MESSAGE,
- disable_web_page_preview=not bot_db.settings['Telegram']['preview_link'],
- reply_markup=markup
- )
- await state.set_state(STATE_START)
-
-
-@voice_router.message(
- StateFilter(STATE_START),
- ChatTypeFilter(chat_type=["private"]),
- F.text == BTN_SPEAK
-)
-async def standup_write(message: types.Message, state: FSMContext, bot_db: BotDB):
- await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
- markup = types.ReplyKeyboardRemove()
- await message.answer(text=RECORD_VOICE_MESSAGE, reply_markup=markup)
-
- try:
- message_with_date = get_last_message_text(bot_db)
- if message_with_date:
- await message.answer(text=message_with_date, parse_mode="html")
- except Exception as e:
- logger.error(f'Не удалось получить дату последнего сообщения - {e}')
-
- await state.set_state(STATE_STANDUP_WRITE)
-
-
-@voice_router.message(
- StateFilter(STATE_STANDUP_WRITE),
- ChatTypeFilter(chat_type=["private"]),
-)
-async def suggest_voice(message: types.Message, state: FSMContext, bot_db: BotDB):
- logger.info(
- f"Вызов функции suggest_voice. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}"
- )
- await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
- markup = get_main_keyboard()
-
- if validate_voice_message(message):
- markup_for_voice = get_reply_keyboard_for_voice()
-
- # Отправляем аудио в приватный канал
- sent_message = await send_voice_message(
- bot_db.settings['Telegram']['group_for_posts'],
- message,
- message.voice.file_id,
- markup_for_voice
- )
-
- # Сохраняем в базу инфо о посте
- bot_db.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id)
-
- # Отправляем юзеру ответ и возвращаем его в меню
- await message.answer(text=VOICE_SAVED_MESSAGE, reply_markup=markup)
- await state.set_state(STATE_START)
- else:
- await message.forward(chat_id=bot_db.settings['Telegram']['group_for_logs'])
- await message.answer(text=UNKNOWN_CONTENT_MESSAGE, reply_markup=markup)
- await state.set_state(STATE_STANDUP_WRITE)
-
-
-@voice_router.message(
- StateFilter(STATE_START),
- ChatTypeFilter(chat_type=["private"]),
- F.text == BTN_LISTEN
-)
-async def standup_listen_audio(message: types.Message, bot_db: BotDB):
- markup = get_main_keyboard()
-
- # Создаем сервис для работы с аудио
- voice_service = VoiceBotService(bot_db, bot_db.settings)
-
- try:
- # Получаем случайное аудио
- audio_data = voice_service.get_random_audio(message.from_user.id)
-
- if not audio_data:
- await message.answer(text=NO_AUDIO_MESSAGE, reply_markup=markup)
- try:
- message_with_date = get_last_message_text(bot_db)
- if message_with_date:
- await message.answer(text=message_with_date, parse_mode="html")
- except Exception as e:
- logger.error(f'Не удалось получить последнюю дату {e}')
- return
-
- audio_for_user, date_added, user_emoji = audio_data
-
- # Получаем путь к файлу
- path = Path(f'{VOICE_USERS_DIR}/{audio_for_user}.ogg')
- voice = FSInputFile(path)
-
- # Маркируем сообщение как прослушанное
- voice_service.mark_audio_as_listened(audio_for_user, 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
- )
-
- # Получаем количество оставшихся аудио
- remaining_count = voice_service.get_remaining_audio_count(message.from_user.id) - 1
- await message.answer(
- text=f'Осталось непрослушанных: {remaining_count}',
- reply_markup=markup
- )
-
- except Exception as e:
- logger.error(f"Ошибка при прослушивании аудио: {e}")
- await message.answer(
- text="Произошла ошибка при получении аудио. Попробуйте позже.",
- reply_markup=markup
- )
diff --git a/voice_bot/keyboards/__init__.py b/voice_bot/keyboards/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/voice_bot/keyboards/keyboards.py b/voice_bot/keyboards/keyboards.py
deleted file mode 100644
index 592d3dc..0000000
--- a/voice_bot/keyboards/keyboards.py
+++ /dev/null
@@ -1,22 +0,0 @@
-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
diff --git a/voice_bot/tests/test_voice_bot_architecture.py b/voice_bot/tests/test_voice_bot_architecture.py
deleted file mode 100644
index 6a577bd..0000000
--- a/voice_bot/tests/test_voice_bot_architecture.py
+++ /dev/null
@@ -1,232 +0,0 @@
-import pytest
-from unittest.mock import Mock, AsyncMock, patch
-from datetime import datetime
-
-from voice_bot.handlers.services import VoiceBotService, AudioFileService
-from voice_bot.handlers.exceptions import VoiceMessageError, AudioProcessingError
-from voice_bot.handlers.utils import format_time_ago, plural_time
-
-
-class TestVoiceBotService:
- """Тесты для VoiceBotService"""
-
- @pytest.fixture
- def mock_bot_db(self):
- """Мок для базы данных"""
- mock_db = Mock()
- mock_db.settings = {
- 'Settings': {'logs': True},
- 'Telegram': {'important_logs': 'test_chat_id'}
- }
- return mock_db
-
- @pytest.fixture
- def mock_settings(self):
- """Мок для настроек"""
- return {
- 'Settings': {'logs': True},
- 'Telegram': {'preview_link': True}
- }
-
- @pytest.fixture
- def voice_service(self, mock_bot_db, mock_settings):
- """Экземпляр VoiceBotService для тестов"""
- return VoiceBotService(mock_bot_db, mock_settings)
-
- @pytest.mark.asyncio
- async def test_get_welcome_sticker_success(self, voice_service, mock_settings):
- """Тест успешного получения стикера"""
- with patch('pathlib.Path.rglob') as mock_rglob:
- mock_rglob.return_value = ['/path/to/sticker1.tgs', '/path/to/sticker2.tgs']
-
- sticker = await voice_service.get_welcome_sticker()
-
- assert sticker is not None
- mock_rglob.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_get_welcome_sticker_no_stickers(self, voice_service, mock_settings):
- """Тест получения стикера когда их нет"""
- with patch('pathlib.Path.rglob') as mock_rglob:
- mock_rglob.return_value = []
-
- sticker = await voice_service.get_welcome_sticker()
-
- assert sticker is None
-
- def test_get_random_audio_success(self, voice_service, mock_bot_db):
- """Тест успешного получения случайного аудио"""
- mock_bot_db.check_listen_audio.return_value = ['audio1', 'audio2']
- mock_bot_db.get_user_id_by_file_name.return_value = 123
- mock_bot_db.get_date_by_file_name.return_value = '2025-01-01 12:00:00'
- mock_bot_db.check_emoji_for_user.return_value = '😊'
-
- result = voice_service.get_random_audio(456)
-
- assert result is not None
- assert len(result) == 3
- assert result[0] in ['audio1', 'audio2']
- assert result[1] == '2025-01-01 12:00:00'
- assert result[2] == '😊'
-
- def test_get_random_audio_no_audio(self, voice_service, mock_bot_db):
- """Тест получения аудио когда их нет"""
- mock_bot_db.check_listen_audio.return_value = []
-
- result = voice_service.get_random_audio(456)
-
- assert result is None
-
- def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db):
- """Тест успешной пометки аудио как прослушанного"""
- voice_service.mark_audio_as_listened('test_audio', 123)
-
- mock_bot_db.mark_listened_audio.assert_called_once_with('test_audio', user_id=123)
-
- def test_clear_user_listenings_success(self, voice_service, mock_bot_db):
- """Тест успешной очистки прослушиваний"""
- voice_service.clear_user_listenings(123)
-
- mock_bot_db.delete_listen_count_for_user.assert_called_once_with(123)
-
-
-class TestAudioFileService:
- """Тесты для AudioFileService"""
-
- @pytest.fixture
- def mock_bot_db(self):
- """Мок для базы данных"""
- return Mock()
-
- @pytest.fixture
- def audio_service(self, mock_bot_db):
- """Экземпляр AudioFileService для тестов"""
- return AudioFileService(mock_bot_db)
-
- def test_generate_file_name_first_audio(self, audio_service, mock_bot_db):
- """Тест генерации имени для первого аудио пользователя"""
- mock_bot_db.get_last_user_audio_record.return_value = False
-
- file_name = audio_service.generate_file_name(123)
-
- assert file_name == 'message_from_123_number_1'
-
- def test_generate_file_name_subsequent_audio(self, audio_service, mock_bot_db):
- """Тест генерации имени для последующих аудио пользователя"""
- mock_bot_db.get_last_user_audio_record.return_value = True
- mock_bot_db.get_path_for_audio_record.return_value = 'existing_file'
- mock_bot_db.get_id_for_audio_record.return_value = 5
-
- with patch('pathlib.Path.exists') as mock_exists:
- mock_exists.return_value = True
-
- file_name = audio_service.generate_file_name(123)
-
- assert file_name == 'message_from_123_number_6'
-
- def test_save_audio_file_success(self, audio_service, mock_bot_db):
- """Тест успешного сохранения аудио файла в БД"""
- file_name = 'test_file'
- user_id = 123
- file_id = 1
- date_added = datetime.now()
-
- audio_service.save_audio_file(file_name, user_id, file_id, date_added)
-
- mock_bot_db.add_audio_record.assert_called_once_with(
- file_name, user_id, date_added, 0, file_id
- )
-
-
-class TestUtils:
- """Тесты для утилит"""
-
- def test_plural_time_minutes(self):
- """Тест множественного числа для минут"""
- assert plural_time(1, 1) == '1 минуту'
- assert plural_time(1, 2) == '2 минуты'
- assert plural_time(1, 5) == '5 минут'
- assert plural_time(1, 11) == '11 минут'
- assert plural_time(1, 21) == '21 минуту'
-
- def test_plural_time_hours(self):
- """Тест множественного числа для часов"""
- assert plural_time(2, 1) == '1 час'
- assert plural_time(2, 2) == '2 часа'
- assert plural_time(2, 5) == '5 часов'
- assert plural_time(2, 11) == '11 часов'
- assert plural_time(2, 21) == '21 час'
-
- def test_plural_time_days(self):
- """Тест множественного числа для дней"""
- assert plural_time(3, 1) == '1 день'
- assert plural_time(3, 2) == '2 дня'
- assert plural_time(3, 5) == '5 дней'
- assert plural_time(3, 11) == '11 дней'
- assert plural_time(3, 21) == '21 день'
-
- def test_format_time_ago_minutes(self):
- """Тест форматирования времени для минут"""
- from datetime import datetime, timedelta
-
- # Создаем время 30 минут назад
- past_time = datetime.now() - timedelta(minutes=30)
- time_str = past_time.strftime("%Y-%m-%d %H:%M:%S")
-
- result = format_time_ago(time_str)
-
- assert '30 минут' in result
- assert 'назад' in result
-
- def test_format_time_ago_hours(self):
- """Тест форматирования времени для часов"""
- from datetime import datetime, timedelta
-
- # Создаем время 2 часа назад
- past_time = datetime.now() - timedelta(hours=2)
- time_str = past_time.strftime("%Y-%m-%d %H:%M:%S")
-
- result = format_time_ago(time_str)
-
- assert '2 часа' in result
- assert 'назад' in result
-
- def test_format_time_ago_days(self):
- """Тест форматирования времени для дней"""
- from datetime import datetime, timedelta
-
- # Создаем время 3 дня назад
- past_time = datetime.now() - timedelta(days=3)
- time_str = past_time.strftime("%Y-%m-%d %H:%M:%S")
-
- result = format_time_ago(time_str)
-
- assert '3 дня' in result
- assert 'назад' in result
-
-
-class TestExceptions:
- """Тесты для исключений"""
-
- def test_voice_bot_error_inheritance(self):
- """Тест наследования исключений"""
- assert issubclass(VoiceMessageError, VoiceBotError)
- assert issubclass(AudioProcessingError, VoiceBotError)
- assert issubclass(DatabaseError, VoiceBotError)
- assert issubclass(FileOperationError, VoiceBotError)
-
- def test_exception_messages(self):
- """Тест сообщений исключений"""
- try:
- raise VoiceMessageError("Тестовая ошибка")
- except VoiceMessageError as e:
- assert str(e) == "Тестовая ошибка"
-
- try:
- raise AudioProcessingError("Ошибка обработки")
- except AudioProcessingError as e:
- assert str(e) == "Ошибка обработки"
-
-
-if __name__ == '__main__':
- pytest.main([__file__])
diff --git a/voice_bot/utils/__init__.py b/voice_bot/utils/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/voice_bot/utils/messages.py b/voice_bot/utils/messages.py
deleted file mode 100644
index 3cb6df3..0000000
--- a/voice_bot/utils/messages.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def get_message(username: str, type_message: str):
- constants = {
-
- }
- message = constants[type_message]
- return message.replace('username', username).replace('&', '\n')