diff --git a/.gitignore b/.gitignore index 29527f2..019b7d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ -/database/tg-bot-database +/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/ @@ -36,3 +41,4 @@ test.db .Trashes ehthumbs.db Thumbs.db +PERFORMANCE_IMPROVEMENTS.md diff --git a/database/db.py b/database/db.py index 498fbb0..26114f0 100644 --- a/database/db.py +++ b/database/db.py @@ -1,6 +1,8 @@ import os import sqlite3 +import asyncio from datetime import datetime +from concurrent.futures import ThreadPoolExecutor from logs.custom_logger import logger @@ -16,10 +18,15 @@ class BotDB: self.cursor = None self.logger = logger self.logger.info(f'Инициация базы данных: {self.db_file}') + # Создаем пул потоков для асинхронных операций + self.executor = ThreadPoolExecutor(max_workers=4) def connect(self): """Создание соединения и курсора.""" - self.conn = sqlite3.connect(self.db_file) + # Добавляем таймаут для предотвращения зависаний + self.conn = sqlite3.connect(self.db_file, timeout=10.0) + # Включаем WAL режим для лучшей производительности + self.conn.execute("PRAGMA journal_mode=WAL") self.cursor = self.conn.cursor() def create_table(self, sql_script): @@ -1205,3 +1212,17 @@ class BotDB: self.cursor.close() if self.conn: self.conn.close() + + async def check_user_in_blacklist_async(self, user_id: int): + """ + Асинхронная версия проверки пользователя в черном списке. + """ + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, self.check_user_in_blacklist, user_id) + + async def get_blacklist_users_by_id_async(self, user_id: int): + """ + Асинхронная версия получения информации о пользователе из черного списка. + """ + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, self.get_blacklist_users_by_id, user_id) diff --git a/helper_bot/handlers/admin/admin_handlers.py b/helper_bot/handlers/admin/admin_handlers.py index abf0bae..020c3af 100644 --- a/helper_bot/handlers/admin/admin_handlers.py +++ b/helper_bot/handlers/admin/admin_handlers.py @@ -1,4 +1,5 @@ import traceback +import html from aiogram import Router, types, F from aiogram.filters import Command, StateFilter @@ -116,9 +117,12 @@ async def ban_by_nickname_step_2(message: types.Message, state: FSMContext): 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"Выбран пользователь:\nid: {user_id}\nusername: {user_name}\n" - f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат", + text=f"Выбран пользователь:\nid: {user_id}\nusername: {user_name_escaped}\n" + f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат", reply_markup=markup) await state.set_state('BAN_2') @@ -148,9 +152,12 @@ async def ban_by_id_step_2(message: types.Message, state: FSMContext): 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"Выбран пользователь:\nid: {user_id}\nusername: {user_name}\n" - f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат", + text=f"Выбран пользователь:\nid: {user_id}\nusername: {user_name_escaped}\n" + f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат", reply_markup=markup) await state.set_state('BAN_2') @@ -201,9 +208,12 @@ async def ban_by_forward_step_2(message: types.Message, state: FSMContext): 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"Выбран пользователь из пересланного сообщения:\nid: {user_id}\nusername: {user_name}\n" - f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат", + text=f"Выбран пользователь из пересланного сообщения:\nid: {user_id}\nusername: {user_name_escaped}\n" + f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат", reply_markup=markup) await state.set_state('BAN_2') @@ -253,7 +263,9 @@ async def ban_user_step_2(message: types.Message, state: FSMContext): logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})") await state.update_data(message_for_user=message.text) markup = create_keyboard_for_ban_days() - await message.answer(f"Выбрана причина: {message.text}. Выбери срок бана в днях или напиши " + # Экранируем 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") @@ -273,8 +285,11 @@ async def ban_user_step_3(message: types.Message, state: FSMContext): 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Причина бана:{user_data['message_for_user']}\nСрок бана:{user_data['date_to_unban']}", + 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") @@ -297,7 +312,9 @@ async def approve_ban(message: types.Message, state: FSMContext): user_data['user_name'], user_data['message_for_user'], user_data['date_to_unban']) - await message.reply(f"Пользователь {user_data['user_name']} успешно заблокирован.") + # Экранируем 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() diff --git a/helper_bot/handlers/callback/callback_handlers.py b/helper_bot/handlers/callback/callback_handlers.py index 8075d5d..cbb5b90 100644 --- a/helper_bot/handlers/callback/callback_handlers.py +++ b/helper_bot/handlers/callback/callback_handlers.py @@ -221,8 +221,11 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext): 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"Выбран пользователь:\nid: {user_id}\nusername: {user_name}\nИмя:{call.message.from_user.full_name}\nВыбери причину бана из списка или напиши ее в чат", + text=f"Выбран пользователь:\nid: {user_id}\nusername: {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат", reply_markup=markup) await state.set_state('BAN_2') else: diff --git a/helper_bot/handlers/private/private_handlers.py b/helper_bot/handlers/private/private_handlers.py index ea95aae..83c131b 100644 --- a/helper_bot/handlers/private/private_handlers.py +++ b/helper_bot/handlers/private/private_handlers.py @@ -1,8 +1,9 @@ import random import traceback +import asyncio +import html from datetime import datetime from pathlib import Path -from time import sleep from aiogram import types, Router, F from aiogram.filters import Command, StateFilter @@ -38,6 +39,9 @@ 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"]), @@ -59,9 +63,11 @@ async def handle_start_message(message: types.Message, state: FSMContext): # Проверяем наличие 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} ({full_name}) обратился к боту без username') - logger.warning(f"Пользователь {user_id} ({full_name}) обратился к боту без username") + text=f'Пользователь {user_id} ({safe_full_name}) обратился к боту без username') + logger.warning(f"Пользователь {user_id} ({safe_full_name}) обратился к боту без username") # Устанавливаем значение по умолчанию для username username = "private_username" @@ -74,11 +80,15 @@ async def handle_start_message(message: types.Message, state: FSMContext): 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"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name} и ник @{username}") + f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}") await message.bot.send_message(chat_id=GROUP_FOR_LOGS, - text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}') - sleep(1) + 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( @@ -89,7 +99,7 @@ async def handle_start_message(message: types.Message, state: FSMContext): random_stick_hello = FSInputFile(path=random_stick_hello) logger.info(f"Стикер успешно получен из БД") await message.answer_sticker(random_stick_hello) - sleep(0.3) + 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, @@ -117,9 +127,11 @@ async def restart_function(message: types.Message, state: FSMContext): # Проверяем наличие 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} ({full_name}) обратился к боту без username') - logger.warning(f"Пользователь {user_id} ({full_name}) обратился к боту без username") + text=f'Пользователь {user_id} ({safe_full_name}) обратился к боту без username') + logger.warning(f"Пользователь {user_id} ({safe_full_name}) обратился к боту без username") # Устанавливаем значение по умолчанию для username username = "private_username" @@ -143,12 +155,14 @@ async def suggest_post(message: types.Message, state: FSMContext): 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} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id}. State - {current_state}") + 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) - sleep(0.3) + 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: @@ -171,15 +185,19 @@ async def end_message(message: types.Message, state: FSMContext): 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} Имя автора сообщения: {message.from_user.full_name}") + 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} Имя автора сообщения: {message.from_user.full_name}") + 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: @@ -188,8 +206,10 @@ async def end_message(message: types.Message, state: FSMContext): 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} Имя автора сообщения: {message.from_user.full_name}") + 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()}") @@ -199,14 +219,18 @@ async def end_message(message: types.Message, state: FSMContext): 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} Имя автора сообщения: {message.from_user.full_name}") + 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}, ник - {message.from_user.username}') + f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {safe_username}') else: await message.forward(chat_id=GROUP_FOR_LOGS) if message.content_type == 'text': @@ -347,7 +371,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list # Отправляем медиагруппу в секретный чат media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message, media_group, BotDB) - sleep(0.2) + await asyncio.sleep(0.2) # Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками markup = get_reply_keyboard_for_post() @@ -376,8 +400,10 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list 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} Имя автора сообщения: {message.from_user.full_name}") + 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) @@ -388,8 +414,10 @@ async def stickers(message: types.Message, state: FSMContext): 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} Имя автора сообщения: {message.from_user.full_name}") + f"Ошибка функции stickers. Ошибка: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}") @private_router.message( @@ -398,8 +426,10 @@ async def stickers(message: types.Message, state: FSMContext): 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} Имя автора сообщения: {message.from_user.full_name}") + 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") @@ -423,8 +453,10 @@ async def resend_message_in_group_for_message(message: types.Message, state: FSM 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} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id})") + 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") diff --git a/helper_bot/main.py b/helper_bot/main.py index 4451d4a..d9d3763 100644 --- a/helper_bot/main.py +++ b/helper_bot/main.py @@ -14,7 +14,7 @@ async def start_bot(bdf): 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) diff --git a/helper_bot/middlewares/album_middleware.py b/helper_bot/middlewares/album_middleware.py index 627d2bd..9110267 100644 --- a/helper_bot/middlewares/album_middleware.py +++ b/helper_bot/middlewares/album_middleware.py @@ -6,7 +6,7 @@ from aiogram.types import Message class AlbumMiddleware(BaseMiddleware): - def __init__(self, latency: Union[int, float] = 0.1): + 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 = {} diff --git a/helper_bot/middlewares/blacklist_middleware.py b/helper_bot/middlewares/blacklist_middleware.py index 866bcf5..e8e8530 100644 --- a/helper_bot/middlewares/blacklist_middleware.py +++ b/helper_bot/middlewares/blacklist_middleware.py @@ -1,4 +1,5 @@ from typing import Dict, Any +import html from aiogram import BaseMiddleware, types from helper_bot.utils.base_dependency_factory import get_global_instance @@ -11,11 +12,15 @@ 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 BotDB.check_user_in_blacklist(user_id=event.from_user.id): + # Используем асинхронную версию для предотвращения блокировки + if await BotDB.check_user_in_blacklist_async(user_id=event.from_user.id): logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} заблокирован!') - user_info = BotDB.get_blacklist_users_by_id(event.from_user.id) + 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"Ты заблокирован.\nПричина блокировки: {user_info[2]}\nДата разбана: {user_info[3]}") + f"Ты заблокирован.\nПричина блокировки: {reason}\nДата разбана: {date_unban}") return False logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} доступ разрешен') return await handler(event, data) diff --git a/helper_bot/middlewares/text_middleware.py b/helper_bot/middlewares/text_middleware.py index e41da8d..b18ed6f 100644 --- a/helper_bot/middlewares/text_middleware.py +++ b/helper_bot/middlewares/text_middleware.py @@ -7,7 +7,7 @@ from aiogram.types import Message class BulkTextMiddleware(BaseMiddleware): - def __init__(self, latency: Union[int, float] = 0.1): + 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) diff --git a/helper_bot/utils/helper_func.py b/helper_bot/utils/helper_func.py index d749d54..3aabc20 100644 --- a/helper_bot/utils/helper_func.py +++ b/helper_bot/utils/helper_func.py @@ -9,9 +9,43 @@ from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from logs.custom_logger import logger +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: - first_name = html.escape(message.from_user.first_name) - return first_name + """ + Безопасно получает и экранирует имя пользователя для использования в 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): @@ -26,18 +60,24 @@ def get_text_message(post_text: str, first_name: str, username: str = 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 username: - author_info = f"{first_name} @{username}" + 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{post_text}\n\nАвтор поста: {author_info}' + return f'Пост из ТГ:\n{safe_post_text}\n\nАвтор поста: {author_info}' elif "анон" in post_text: - return f'Пост из ТГ:\n{post_text}\n\nПост опубликован анонимно' + return f'Пост из ТГ:\n{safe_post_text}\n\nПост опубликован анонимно' else: - return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {author_info}' + return f'Пост из ТГ:\n{safe_post_text}\n\nАвтор поста: {author_info}' async def download_file(message: types.Message, file_id: str): @@ -79,6 +119,9 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''): 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): @@ -98,11 +141,11 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''): # Формируем объект MediaGroup с учетом типа медиа if i == len(album) - 1: if media_type == 'photo': - media_group.append(InputMediaPhoto(media=file_id, caption=post_caption)) + media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption)) elif media_type == 'video': - media_group.append(InputMediaVideo(media=file_id, caption=post_caption)) + media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption)) elif media_type == 'audio': - media_group.append(InputMediaAudio(media=file_id, caption=post_caption)) + media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption)) else: if media_type == 'photo': media_group.append(InputMediaPhoto(media=file_id)) @@ -208,23 +251,28 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: list[tupl # Добавляем подпись к последнему файлу if media: - media[-1].caption = post_text + # Экранируем 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=post_text + 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=post_text, + text=safe_post_text, reply_markup=markup ) message_id = sent_message.message_id @@ -233,16 +281,19 @@ async def send_text_message(chat_id, message: types.Message, post_text: str, mar 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=post_text, + caption=safe_post_text, photo=photo ) else: sent_message = await message.bot.send_photo( chat_id=chat_id, - caption=post_text, + caption=safe_post_text, photo=photo, reply_markup=markup ) @@ -251,16 +302,19 @@ async def send_photo_message(chat_id, message: types.Message, photo: str, post_t 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=post_text, + caption=safe_post_text, video=video ) else: sent_message = await message.bot.send_video( chat_id=chat_id, - caption=post_text, + caption=safe_post_text, video=video, reply_markup=markup ) @@ -285,16 +339,19 @@ async def send_video_note_message(chat_id, message: types.Message, video_note: s 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=post_text, + caption=safe_post_text, audio=audio ) else: sent_message = await message.bot.send_audio( chat_id=chat_id, - caption=post_text, + caption=safe_post_text, audio=audio, reply_markup=markup ) @@ -346,9 +403,14 @@ def get_banned_users_list(offset: int, bot_db): message = "Список заблокированных пользователей:\n" for user in users: - message += f"Пользователь: {user[0]}\n" - message += f"Причина бана: {user[2]}\n" - message += f"Дата разбана: {user[3]}\n\n" + # Экранируем пользовательские данные для безопасного использования + 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 @@ -367,7 +429,9 @@ def get_banned_users_buttons(bot_db): user_ids = [] for user in users: - user_ids.append((user[0], user[1])) + # Экранируем 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 @@ -388,7 +452,9 @@ def unban_notifier(self): unblocked_users = self.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" + # Экранируем 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) diff --git a/helper_bot/utils/messages.py b/helper_bot/utils/messages.py index 883ae1d..6551423 100644 --- a/helper_bot/utils/messages.py +++ b/helper_bot/utils/messages.py @@ -1,3 +1,4 @@ +import html def get_message(username: str, type_message: str): @@ -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 diff --git a/migrations/000_migrations_init.py b/migrations/000_migrations_init.py index 765de44..8c91f0e 100644 --- a/migrations/000_migrations_init.py +++ b/migrations/000_migrations_init.py @@ -1,4 +1,8 @@ import os +import sys + +# Добавляем путь к корневой директории проекта +sys.path.append(os.path.dirname(os.path.dirname(__file__))) from database.db import BotDB diff --git a/migrations/001_create_new_tables.py b/migrations/001_create_new_tables.py index 8be6907..4d4ab41 100644 --- a/migrations/001_create_new_tables.py +++ b/migrations/001_create_new_tables.py @@ -1,4 +1,8 @@ import os +import sys + +# Добавляем путь к корневой директории проекта +sys.path.append(os.path.dirname(os.path.dirname(__file__))) from database.db import BotDB diff --git a/migrations/002_create_tables_media_group.py b/migrations/002_create_tables_media_group.py index baa5713..890886d 100644 --- a/migrations/002_create_tables_media_group.py +++ b/migrations/002_create_tables_media_group.py @@ -1,4 +1,8 @@ import os +import sys + +# Добавляем путь к корневой директории проекта +sys.path.append(os.path.dirname(os.path.dirname(__file__))) from database.db import BotDB diff --git a/migrations/003_create_our_users_table.py b/migrations/003_create_our_users_table.py new file mode 100644 index 0000000..ad2cfd2 --- /dev/null +++ b/migrations/003_create_our_users_table.py @@ -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() + diff --git a/voice_bot/utils/helper_func.py b/voice_bot/utils/helper_func.py index 17d9d26..1ec0fb8 100644 --- a/voice_bot/utils/helper_func.py +++ b/voice_bot/utils/helper_func.py @@ -1,4 +1,5 @@ import time +import html from datetime import datetime from helper_bot.utils.base_dependency_factory import get_global_instance @@ -22,13 +23,19 @@ def last_message(): message_with_date = '' if much_minutes_ago <= 60: word_minute = plural_time(1, much_minutes_ago) - message_with_date = f'Последнее сообщение было записано {word_minute} назад' + # Экранируем потенциально проблемные символы + word_minute_escaped = html.escape(word_minute) + message_with_date = f'Последнее сообщение было записано {word_minute_escaped} назад' elif much_minutes_ago > 60 and much_hour_ago <= 24: word_hour = plural_time(2, much_hour_ago) - message_with_date = f'Последнее сообщение было записано {word_hour} назад' + # Экранируем потенциально проблемные символы + word_hour_escaped = html.escape(word_hour) + message_with_date = f'Последнее сообщение было записано {word_hour_escaped} назад' elif much_hour_ago > 24: word_day = plural_time(3, much_days_ago) - message_with_date = f'Последнее сообщение было записано {word_day} назад' + # Экранируем потенциально проблемные символы + word_day_escaped = html.escape(word_day) + message_with_date = f'Последнее сообщение было записано {word_day_escaped} назад' return message_with_date diff --git a/voice_bot/voice_handler/voice_handler.py b/voice_bot/voice_handler/voice_handler.py index ef3bcf4..9835e31 100644 --- a/voice_bot/voice_handler/voice_handler.py +++ b/voice_bot/voice_handler/voice_handler.py @@ -1,5 +1,5 @@ import random -import time +import asyncio from datetime import datetime from pathlib import Path @@ -64,44 +64,44 @@ async def start(message: types.Message, state: FSMContext): random_stick_hello = FSInputFile(path=random_stick_hello) logger.info(f"Стикер успешно получен из БД. Наименование стикера: {name_stick_hello}") await message.answer_sticker(random_stick_hello) - time.sleep(0.3) + 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="Привет.", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) - time.sleep(0.3) + await asyncio.sleep(0.3) await message.answer(text="Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из " "Бийска", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) - time.sleep(1) + await asyncio.sleep(1) await message.answer(text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не " "узнаешь, послушал его кто-то или нет и ответить тоже не получится..", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) - time.sleep(0.8) + await asyncio.sleep(0.8) await message.answer(text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, хотя " "бы на 5-10 секунд", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) - time.sleep(1.5) + await asyncio.sleep(1.5) await message.answer(text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, " "ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы " "выкладывать в собственные соцсети)", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) - time.sleep(1.3) + await asyncio.sleep(1.3) await message.answer(text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из " "недавно полученных или отправленных (или спеть, рассказать стихотворенье)", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) - time.sleep(0.8) + await asyncio.sleep(0.8) await message.answer(text="Так же можешь ознакомиться с инструкцией к боту по команде /help", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) - time.sleep(0.8) + await asyncio.sleep(0.8) await message.answer(text="ну всё, достаточно инструкций. записывайся! Микрофон твой - 🎤", parse_mode='html', reply_markup=markup, disable_web_page_preview=not PREVIEW_LINK) @@ -147,7 +147,7 @@ async def save_voice_message(message: types.Message, state: FSMContext): pass # Собираем инфо о сообщении author_id = message.from_user.id - time_UTC = int(time.time()) + time_UTC = int(datetime.now().timestamp()) date_added = datetime.fromtimestamp(time_UTC) # Сохраняем в базку