diff --git a/.env.example b/.env.example index f857d3a..97353bb 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,9 @@ #Токен бота Телеграм -API_TOKEN = "" +BOT_TOKEN= # Ваш Тг Ид -ADMIN_ID = 0 +ADMIN_ID= # конфиг ASF -ASF_URL = "http://127.0.0.1:1242/Api/ASF" -ASF_PASSWORD = "0000" +ASF_URL="http://127.0.0.1:1242/Api/ASF" +ASF_PASSWORD="0000" diff --git a/.gitignore b/.gitignore index d7f14ff..7c7d8de 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ cover/ # Django stuff: *.log +bot.log local_settings.py db.sqlite3 db.sqlite3-journal diff --git a/bot/api/asf.py b/bot/api/asf.py index ba38e50..deefc1c 100644 --- a/bot/api/asf.py +++ b/bot/api/asf.py @@ -2,8 +2,10 @@ import aiohttp import requests from bot.config import ASF_PASSWORD, ASF_URL +from bot.logging_utils import get_logger IPC_BASE_URL = "http://127.0.0.1:1242" +logger = get_logger(__name__) def _headers() -> dict[str, str | None]: @@ -11,15 +13,36 @@ def _headers() -> dict[str, str | None]: def asf_get(path: str) -> requests.Response: - return requests.get(f"{IPC_BASE_URL}{path}", headers=_headers()) + logger.info("Отправка GET-запроса в ASF: path=%s", path) + response = requests.get(f"{IPC_BASE_URL}{path}", headers=_headers()) + logger.info( + "Получен ответ ASF на GET-запрос: path=%s status=%s", + path, + response.status_code, + ) + return response def asf_post(path: str, payload: dict | None = None) -> requests.Response: - return requests.post(f"{IPC_BASE_URL}{path}", headers=_headers(), json=payload) + logger.info( + "Отправка POST-запроса в ASF: path=%s payload_keys=%s", + path, + sorted((payload or {}).keys()), + ) + response = requests.post(f"{IPC_BASE_URL}{path}", headers=_headers(), json=payload) + logger.info( + "Получен ответ ASF на POST-запрос: path=%s status=%s", + path, + response.status_code, + ) + return response def get_asf_status() -> requests.Response: - return requests.get(ASF_URL, headers=_headers()) # type: ignore[arg-type] + logger.info("Запрос статуса ASF") + response = requests.get(ASF_URL, headers=_headers()) # type: ignore[arg-type] + logger.info("Получен статус ASF: status=%s", response.status_code) + return response def get_all_bots() -> requests.Response: @@ -43,67 +66,129 @@ def restart_asf() -> requests.Response: async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict | None: + logger.info( + "Запрос инвентаря бота: bot=%s appid=%s contextid=%s", + bot_name, + appid, + contextid, + ) async with aiohttp.ClientSession() as session: async with session.get( f"{IPC_BASE_URL}/Api/Bot/{bot_name}/Inventory/{appid}/{contextid}", headers=_headers(), ) as response: if response.status != 200: + logger.warning( + "Не удалось получить инвентарь бота: bot=%s appid=%s contextid=%s status=%s", + bot_name, + appid, + contextid, + response.status, + ) return None data = await response.json() if not data.get("Success"): + logger.warning( + "ASF вернул ошибку при получении инвентаря: bot=%s appid=%s contextid=%s", + bot_name, + appid, + contextid, + ) return None + logger.info( + "Инвентарь бота получен: bot=%s appid=%s contextid=%s", + bot_name, + appid, + contextid, + ) return data.get("Result") async def get_2fa_token(bot_name: str) -> str: + logger.info("Запрос 2FA-кода: bot=%s", bot_name) async with aiohttp.ClientSession() as session: async with session.get( f"{IPC_BASE_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Token", headers=_headers(), ) as response: if response.status != 200: + logger.warning( + "Не удалось получить 2FA-код: bot=%s status=%s", + bot_name, + response.status, + ) return f"Ошибка HTTP {response.status}" data = await response.json() if not data.get("Success"): + logger.warning("ASF вернул ошибку при запросе 2FA-кода: bot=%s", bot_name) return "Ошибка ASF" try: + logger.info("2FA-код успешно получен: bot=%s", bot_name) return data["Result"][bot_name]["Result"] except Exception: + logger.exception("Не удалось извлечь 2FA-код из ответа ASF: bot=%s", bot_name) return "Не удалось получить код" async def get_confirmations(bot_name: str) -> list: + logger.info("Запрос подтверждений 2FA: bot=%s", bot_name) async with aiohttp.ClientSession() as session: async with session.get( f"{IPC_BASE_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations", headers=_headers(), ) as response: if response.status != 200: + logger.warning( + "Не удалось получить подтверждения 2FA: bot=%s status=%s", + bot_name, + response.status, + ) return [] data = await response.json() if not data.get("Success"): + logger.warning( + "ASF вернул ошибку при запросе подтверждений 2FA: bot=%s", + bot_name, + ) return [] try: - return data["Result"][bot_name]["Result"] + result = data["Result"][bot_name]["Result"] + logger.info( + "Подтверждения 2FA получены: bot=%s count=%s", + bot_name, + len(result), + ) + return result except Exception: + logger.exception( + "Не удалось извлечь подтверждения 2FA из ответа ASF: bot=%s", + bot_name, + ) return [] async def accept_confirmations(bot_name: str) -> int: + logger.info("Отправка запроса на подтверждение всех 2FA-операций: bot=%s", bot_name) async with aiohttp.ClientSession() as session: async with session.post( f"{IPC_BASE_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations/Accept", headers=_headers(), ) as response: + logger.info( + "Получен ответ на подтверждение 2FA-операций: bot=%s status=%s", + bot_name, + response.status, + ) return response.status def save_bot_config(bot_name: str, config: dict) -> requests.Response: + logger.info("Сохранение конфигурации бота: bot=%s", bot_name) return asf_post(f"/Api/Bot/{bot_name}", {"BotConfig": config}) async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]: + logger.info("Запрос активации ключа: bot=%s", bot_name) async with aiohttp.ClientSession() as session: async with session.post( f"{IPC_BASE_URL}/Api/Command", @@ -111,8 +196,15 @@ async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]: json={"Command": f"!redeem {bot_name} {key}"}, ) as response: if response.status != 200: + logger.warning( + "Не удалось активировать ключ: bot=%s status=%s", + bot_name, + response.status, + ) return False, f"HTTP_ERROR_{response.status}" data = await response.json() if not data.get("Success"): + logger.warning("ASF вернул ошибку при активации ключа: bot=%s", bot_name) return False, "ASF_ERROR" + logger.info("ASF вернул результат активации ключа: bot=%s", bot_name) return True, str(data.get("Result", "")) diff --git a/bot/config.py b/bot/config.py index 6483519..eabab43 100644 --- a/bot/config.py +++ b/bot/config.py @@ -4,7 +4,8 @@ from dotenv import load_dotenv load_dotenv() -API_TOKEN = os.getenv("API_TOKEN") +BOT_TOKEN = os.getenv("BOT_TOKEN") ADMIN_ID = int(os.getenv("ADMIN_ID")) # type: ignore[arg-type] + ASF_URL = os.getenv("ASF_URL") ASF_PASSWORD = os.getenv("ASF_PASSWORD") diff --git a/bot/filters.py b/bot/filters.py index 107ad07..15bf796 100644 --- a/bot/filters.py +++ b/bot/filters.py @@ -2,13 +2,29 @@ from aiogram.filters import BaseFilter from aiogram.types import CallbackQuery, Message from bot.config import ADMIN_ID +from bot.logging_utils import get_logger + +logger = get_logger(__name__) class AdminFilter(BaseFilter): async def __call__(self, message: Message) -> bool: - return message.from_user.id == ADMIN_ID # type: ignore[union-attr] + allowed = message.from_user.id == ADMIN_ID # type: ignore[union-attr] + if not allowed: + logger.warning( + "Отклонено сообщение от неавторизованного пользователя: user_id=%s", + message.from_user.id if message.from_user else None, + ) + return allowed class AdminCallbackFilter(BaseFilter): async def __call__(self, callback: CallbackQuery) -> bool: - return callback.from_user.id == ADMIN_ID + allowed = callback.from_user.id == ADMIN_ID + if not allowed: + logger.warning( + "Отклонен callback от неавторизованного пользователя: user_id=%s data=%s", + callback.from_user.id, + callback.data, + ) + return allowed diff --git a/bot/handlers/bots.py b/bot/handlers/bots.py index b4b6aed..63b0a55 100644 --- a/bot/handlers/bots.py +++ b/bot/handlers/bots.py @@ -3,39 +3,67 @@ from aiogram.types import CallbackQuery from bot.api.asf import get_all_bots from bot.filters import AdminCallbackFilter +from bot.logging_utils import describe_user, get_logger from bot.services.bots import is_idle_enabled, update_idle_game from bot.services.twofa import stop_twofa_task from bot.ui.formatters import format_bot_ui, get_farm_summary from bot.ui.keyboards import bot_details_keyboard, bots_keyboard, games_keyboard router = Router() +logger = get_logger(__name__) @router.callback_query(lambda c: c.data == "bot_loading", AdminCallbackFilter()) async def bot_loading_handler(callback: CallbackQuery) -> None: + logger.info( + "Пользователь %s нажал на еще не загруженного бота", + describe_user(callback.from_user), + ) await callback.answer( - "Аккаунт ещё загружается. Попробуйте через несколько секунд.", + "Аккаунт еще загружается. Попробуйте через несколько секунд.", show_alert=True, ) @router.callback_query(lambda c: c.data == "bots", AdminCallbackFilter()) async def bots_handler(callback: CallbackQuery) -> None: + logger.info( + "Пользователь %s открыл список ботов", + describe_user(callback.from_user), + ) await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await callback.answer() try: response = get_all_bots() if response.status_code != 200: + logger.warning( + "Не удалось загрузить список ботов для %s: HTTP %s", + describe_user(callback.from_user), + response.status_code, + ) await callback.message.edit_text("Ошибка API") # type: ignore[union-attr] return data = response.json() if not data.get("Success"): + logger.warning( + "ASF вернул неуспешный ответ при загрузке списка ботов для %s", + describe_user(callback.from_user), + ) await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr] return bots = data.get("Result", {}) + logger.info( + "Список ботов для %s загружен: %s бот(ов)", + describe_user(callback.from_user), + len(bots), + ) text = f"🤖 Ботов всего: {len(bots)}\n\n{get_farm_summary(bots)}" await callback.message.edit_text(text, reply_markup=bots_keyboard(bots)) # type: ignore[union-attr] except Exception as error: + logger.exception( + "Ошибка при открытии списка ботов для %s", + describe_user(callback.from_user), + ) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] @@ -46,18 +74,37 @@ async def bot_selected(callback: CallbackQuery) -> None: await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await callback.answer() if not callback.data: + logger.debug( + "Колбэк выбора бота пришел без data от %s", + describe_user(callback.from_user), + ) return bot_name = callback.data.replace("bot_", "") + logger.info( + "Пользователь %s выбрал бота %s", + describe_user(callback.from_user), + bot_name, + ) try: data = get_all_bots().json() bots = data.get("Result", {}) bot = bots.get(bot_name) if not bot or not bot.get("BotName") or not bot.get("SteamID"): + logger.info( + "Бот %s еще не готов к отображению для %s", + bot_name, + describe_user(callback.from_user), + ) await callback.answer( - "Аккаунт ещё загружается. Попробуйте через несколько секунд.", + "Аккаунт еще загружается. Попробуйте через несколько секунд.", show_alert=True, ) return + logger.info( + "Открыта карточка бота %s для %s", + bot_name, + describe_user(callback.from_user), + ) await callback.message.edit_text( # type: ignore[union-attr] format_bot_ui(bot), disable_web_page_preview=True, @@ -66,6 +113,11 @@ async def bot_selected(callback: CallbackQuery) -> None: ), ) except Exception as error: + logger.exception( + "Ошибка при открытии карточки бота %s для %s", + bot_name, + describe_user(callback.from_user), + ) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] @@ -75,9 +127,19 @@ async def bot_selected(callback: CallbackQuery) -> None: async def games(callback: CallbackQuery) -> None: await callback.answer() if not callback.data: + logger.debug( + "Колбэк выбора игры пришел без data от %s", + describe_user(callback.from_user), + ) return bot_name = callback.data.replace("games_", "") idle_enabled = is_idle_enabled(bot_name, 730) + logger.info( + "Пользователь %s открыл меню накрутки часов для бота %s. CS2=%s", + describe_user(callback.from_user), + bot_name, + idle_enabled, + ) await callback.message.edit_text( # type: ignore[union-attr] "⌚ Выберите игру для накрутки часов", reply_markup=games_keyboard(bot_name, idle_enabled), @@ -90,16 +152,42 @@ async def games(callback: CallbackQuery) -> None: async def farm_game(callback: CallbackQuery) -> None: await callback.answer() if not callback.data: + logger.debug( + "Колбэк изменения фарма пришел без data от %s", + describe_user(callback.from_user), + ) return _, bot_name, app_id_value = callback.data.split("|") + logger.info( + "Пользователь %s меняет игру для накрутки на боте %s: app_id=%s", + describe_user(callback.from_user), + bot_name, + app_id_value, + ) success, text, enabled = await update_idle_game(bot_name, int(app_id_value)) if not success: + logger.warning( + "Не удалось изменить игру для накрутки на боте %s для %s: %s", + bot_name, + describe_user(callback.from_user), + text, + ) await callback.answer(text, show_alert=True) return + logger.info( + "Игра для накрутки на боте %s обновлена для %s. CS2=%s", + bot_name, + describe_user(callback.from_user), + enabled, + ) await callback.answer(text, show_alert=True) try: await callback.message.edit_reply_markup( # type: ignore[union-attr] reply_markup=games_keyboard(bot_name, enabled) ) except Exception: - pass + logger.debug( + "Не удалось обновить клавиатуру игр для бота %s", + bot_name, + exc_info=True, + ) diff --git a/bot/handlers/console.py b/bot/handlers/console.py index 70d0ec6..b897ebf 100644 --- a/bot/handlers/console.py +++ b/bot/handlers/console.py @@ -2,15 +2,19 @@ from aiogram import Router from aiogram.types import CallbackQuery from bot.filters import AdminCallbackFilter +from bot.logging_utils import describe_user, get_logger from bot.state import console_users from bot.ui.formatters import get_asf_status_text from bot.ui.keyboards import console_keyboard, main_keyboard router = Router() +logger = get_logger(__name__) @router.callback_query(lambda c: c.data == "console", AdminCallbackFilter()) async def console_enter(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Пользователь вошел в режим консоли ASF: %s", user) await callback.answer() console_users.add(callback.from_user.id) await callback.message.edit_text( # type: ignore[union-attr] @@ -21,6 +25,8 @@ async def console_enter(callback: CallbackQuery) -> None: @router.callback_query(lambda c: c.data == "console_exit", AdminCallbackFilter()) async def console_exit(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Пользователь вышел из режима консоли ASF: %s", user) await callback.answer() console_users.discard(callback.from_user.id) await callback.message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) # type: ignore[union-attr] diff --git a/bot/handlers/inventory.py b/bot/handlers/inventory.py index fd743e5..09ab9a4 100644 --- a/bot/handlers/inventory.py +++ b/bot/handlers/inventory.py @@ -2,21 +2,26 @@ from aiogram import Router from aiogram.types import CallbackQuery from bot.filters import AdminCallbackFilter +from bot.logging_utils import describe_user, get_logger from bot.services.inventory import build_inventory_menu, render_inventory_page from bot.ui.keyboards import back_keyboard router = Router() +logger = get_logger(__name__) @router.callback_query( lambda c: c.data and c.data.startswith("inventory_"), AdminCallbackFilter() ) async def inventory_menu(callback: CallbackQuery) -> None: - await callback.answer() + user = describe_user(callback.from_user.id) bot_name = callback.data.replace("inventory_", "") # type: ignore[union-attr] + logger.info("Открыто меню инвентаря: %s bot=%s", user, bot_name) + await callback.answer() await callback.message.edit_text("Проверка inventory...") # type: ignore[union-attr] text, keyboard = await build_inventory_menu(bot_name) - if "пуст" in text: + if "пуст" in text.lower(): + logger.info("Показано пустое меню инвентаря: %s bot=%s", user, bot_name) keyboard = back_keyboard(f"bot_{bot_name}") await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] @@ -25,11 +30,14 @@ async def inventory_menu(callback: CallbackQuery) -> None: lambda c: c.data and c.data.startswith("inv_cs2_"), AdminCallbackFilter() ) async def cs2_inventory(callback: CallbackQuery) -> None: - await callback.answer() + user = describe_user(callback.from_user.id) bot_name = callback.data.replace("inv_cs2_", "") # type: ignore[union-attr] + logger.info("Открыт CS2-инвентарь: %s bot=%s", user, bot_name) + await callback.answer() await callback.message.edit_text("Загрузка inventory...") # type: ignore[union-attr] text, keyboard = await render_inventory_page(bot_name, "cs2", 730, 2, 0) if text is None: + logger.warning("Не удалось загрузить CS2-инвентарь: %s bot=%s", user, bot_name) await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] return await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] @@ -39,11 +47,14 @@ async def cs2_inventory(callback: CallbackQuery) -> None: lambda c: c.data and c.data.startswith("inv_steam_"), AdminCallbackFilter() ) async def steam_inventory(callback: CallbackQuery) -> None: - await callback.answer() + user = describe_user(callback.from_user.id) bot_name = callback.data.replace("inv_steam_", "") # type: ignore[union-attr] + logger.info("Открыт Steam-инвентарь: %s bot=%s", user, bot_name) + await callback.answer() await callback.message.edit_text("Загрузка inventory...") # type: ignore[union-attr] text, keyboard = await render_inventory_page(bot_name, "steam", 753, 6, 0) if text is None: + logger.warning("Не удалось загрузить Steam-инвентарь: %s bot=%s", user, bot_name) await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] return await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] @@ -53,13 +64,28 @@ async def steam_inventory(callback: CallbackQuery) -> None: lambda c: c.data and c.data.startswith("invpage|"), AdminCallbackFilter() ) async def inventory_page(callback: CallbackQuery) -> None: - await callback.answer() + user = describe_user(callback.from_user.id) _, inv_type, bot_name, page_value = callback.data.split("|") # type: ignore[union-attr] + logger.info( + "Открыта страница инвентаря: %s bot=%s type=%s page=%s", + user, + bot_name, + inv_type, + page_value, + ) + await callback.answer() app_id, context_id = (730, 2) if inv_type == "cs2" else (753, 6) text, keyboard = await render_inventory_page( bot_name, inv_type, app_id, context_id, int(page_value) ) if text is None: + logger.warning( + "Не удалось загрузить страницу инвентаря: %s bot=%s type=%s page=%s", + user, + bot_name, + inv_type, + page_value, + ) await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] return await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] diff --git a/bot/handlers/messages.py b/bot/handlers/messages.py index 9f743fd..e0b7b5a 100644 --- a/bot/handlers/messages.py +++ b/bot/handlers/messages.py @@ -5,6 +5,7 @@ from aiogram.types import Message from bot.api.asf import get_all_bots, send_command from bot.filters import AdminFilter +from bot.logging_utils import describe_user, get_logger from bot.services.redeem import ( extract_keys, redeem_all_bots_keys, @@ -25,29 +26,60 @@ from bot.ui.keyboards import ( ) router = Router() +logger = get_logger(__name__) async def handle_console_message(message: Message) -> bool: if message.from_user.id not in console_users: # type: ignore[union-attr] return False command = message.text.strip() # type: ignore[union-attr] + logger.info( + "Пользователь %s отправил консольную команду: %s", + describe_user(message.from_user), + command, + ) try: await message.delete() except Exception: - pass + logger.debug( + "Не удалось удалить сообщение с консольной командой от %s", + describe_user(message.from_user), + exc_info=True, + ) try: response = send_command(f"!{command}") if response.status_code != 200: + logger.warning( + "ASF вернул HTTP %s на консольную команду '%s' от %s", + response.status_code, + command, + describe_user(message.from_user), + ) text = f"Ошибка HTTP {response.status_code}" else: data = response.json() if not data.get("Success"): + logger.warning( + "ASF вернул неуспешный ответ на консольную команду '%s' от %s", + command, + describe_user(message.from_user), + ) text = "Ошибка ASF" else: + logger.info( + "Консольная команда '%s' от %s выполнена успешно", + command, + describe_user(message.from_user), + ) result = html.escape(str(data.get("Result", "Нет ответа"))) text = f"Команда: {command}\n\nОтвет:\n{result}" await message.answer(text, reply_markup=delete_message_keyboard()) except Exception as error: + logger.exception( + "Ошибка при выполнении консольной команды '%s' от %s", + command, + describe_user(message.from_user), + ) await message.answer(f"Ошибка: {error}") return True @@ -57,6 +89,11 @@ async def handle_single_bot_redeem(message: Message) -> bool: return False bot_name = bot_redeem_users.pop(message.from_user.id) # type: ignore[union-attr] menu_message = bot_redeem_messages.pop(message.from_user.id, None) # type: ignore[union-attr] + logger.info( + "Пользователь %s отправил ключи для бота %s", + describe_user(message.from_user), + bot_name, + ) if menu_message: try: response = get_all_bots() @@ -70,17 +107,38 @@ async def handle_single_bot_redeem(message: Message) -> bool: ), ) except Exception: - pass + logger.debug( + "Не удалось восстановить меню бота %s после ввода ключей от %s", + bot_name, + describe_user(message.from_user), + exc_info=True, + ) try: await message.bot.delete_message( chat_id=message.chat.id, message_id=message.message_id ) except Exception: - pass + logger.debug( + "Не удалось удалить сообщение с ключами для бота %s от %s", + bot_name, + describe_user(message.from_user), + exc_info=True, + ) keys = extract_keys(message.text or "") if not keys: + logger.info( + "В сообщении от %s для бота %s не найдено ключей", + describe_user(message.from_user), + bot_name, + ) await message.answer("Ключи не найдены") return True + logger.info( + "Для бота %s получено %s ключ(ей) от %s", + bot_name, + len(keys), + describe_user(message.from_user), + ) checking = await message.answer( f"Аккаунт: {bot_name}\nНайдено ключей: {len(keys)}\nНачинаю активацию..." ) @@ -93,35 +151,64 @@ async def handle_all_bots_redeem(message: Message) -> bool: return False redeem_users.discard(message.from_user.id) # type: ignore[union-attr] menu_message = redeem_messages.pop(message.from_user.id, None) # type: ignore[union-attr] + logger.info( + "Пользователь %s отправил ключи для активации на всех ботах", + describe_user(message.from_user), + ) if menu_message: try: await menu_message.edit_text( get_asf_status_text(), reply_markup=main_keyboard() ) except Exception: - pass + logger.debug( + "Не удалось восстановить главное меню после массовой активации для %s", + describe_user(message.from_user), + exc_info=True, + ) try: await message.bot.delete_message( chat_id=message.chat.id, message_id=message.message_id ) except Exception: - pass + logger.debug( + "Не удалось удалить сообщение с массовой активацией от %s", + describe_user(message.from_user), + exc_info=True, + ) keys = extract_keys(message.text or "") if not keys: + logger.info( + "В сообщении от %s для массовой активации не найдено ключей", + describe_user(message.from_user), + ) await message.answer("Ключи не найдены") return True + logger.info( + "Для массовой активации получено %s ключ(ей) от %s", + len(keys), + describe_user(message.from_user), + ) checking = await message.answer( f"Найдено ключей: {len(keys)}\nНачинаю активацию..." ) try: await checking.edit_text(await redeem_all_bots_keys(keys)) except Exception as error: + logger.exception( + "Ошибка при массовой активации ключей от %s", + describe_user(message.from_user), + ) await checking.edit_text(f"Ошибка: {error}") return True @router.message(AdminFilter()) async def text_router(message: Message) -> None: + logger.debug( + "Получено текстовое сообщение от %s для маршрутизации", + describe_user(message.from_user), + ) if await handle_console_message(message): return if await handle_single_bot_redeem(message): diff --git a/bot/handlers/navigation.py b/bot/handlers/navigation.py index 7960eee..4974af5 100644 --- a/bot/handlers/navigation.py +++ b/bot/handlers/navigation.py @@ -5,6 +5,7 @@ from aiogram.types import CallbackQuery from bot.api.asf import get_plugins, restart_asf as restart_asf_request from bot.filters import AdminCallbackFilter +from bot.logging_utils import describe_user, get_logger from bot.services.twofa import stop_twofa_task from bot.state import ( bot_redeem_messages, @@ -17,6 +18,7 @@ from bot.ui.formatters import get_asf_status_text from bot.ui.keyboards import back_keyboard, confirm_action_keyboard, main_keyboard router = Router() +logger = get_logger(__name__) def clear_user_state(user_id: int) -> None: @@ -25,39 +27,53 @@ def clear_user_state(user_id: int) -> None: console_users.discard(user_id) bot_redeem_users.pop(user_id, None) bot_redeem_messages.pop(user_id, None) + logger.info("Состояние пользователя очищено: user_id=%s", user_id) @router.callback_query(lambda c: c.data == "back", AdminCallbackFilter()) async def back_button(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Нажата кнопка возврата: %s", user) clear_user_state(callback.from_user.id) await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await callback.answer() try: await callback.message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) # type: ignore[union-attr] + logger.info("Пользователь возвращен в главное меню: %s", user) except Exception as error: + logger.exception("Не удалось вернуть пользователя в главное меню: %s", user) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] @router.callback_query(lambda c: c.data == "refresh", AdminCallbackFilter()) async def refresh_handler(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Запрошено обновление главного экрана: %s", user) await callback.answer() try: await callback.message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) # type: ignore[union-attr] + logger.info("Главный экран обновлен: %s", user) except Exception as error: + logger.exception("Не удалось обновить главный экран: %s", user) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] @router.callback_query(lambda c: c.data == "delete_msg", AdminCallbackFilter()) async def delete_message(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Запрошено удаление сообщения: %s", user) await callback.answer() try: await callback.message.delete() # type: ignore[union-attr] + logger.info("Сообщение удалено: %s", user) except Exception: - pass + logger.exception("Не удалось удалить сообщение: %s", user) @router.callback_query(lambda c: c.data == "restart_asf_confirm", AdminCallbackFilter()) async def restart_asf_confirm(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Открыто подтверждение перезапуска ASF: %s", user) await callback.answer() await callback.message.edit_text( # type: ignore[union-attr] "♻ Точно перезапустить ASF?\n\nВо время перезапуска IPC будет недоступен несколько секунд.", @@ -67,11 +83,18 @@ async def restart_asf_confirm(callback: CallbackQuery) -> None: @router.callback_query(lambda c: c.data == "restart_asf", AdminCallbackFilter()) async def restart_asf(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Подтвержден перезапуск ASF: %s", user) await callback.answer() try: await callback.message.edit_text("♻ ASF перезапускается...") # type: ignore[union-attr] response = restart_asf_request() if response.status_code != 200: + logger.warning( + "Перезапуск ASF завершился ошибкой HTTP: %s status=%s", + user, + response.status_code, + ) await callback.message.edit_text( # type: ignore[union-attr] f"Ошибка HTTP {response.status_code}", reply_markup=main_keyboard(), @@ -82,7 +105,9 @@ async def restart_asf(callback: CallbackQuery) -> None: f"ASF успешно перезапущен\n\n{get_asf_status_text()}", reply_markup=main_keyboard(), ) + logger.info("ASF успешно перезапущен: %s", user) except Exception as error: + logger.exception("Не удалось перезапустить ASF: %s", user) await callback.message.edit_text( # type: ignore[union-attr] f"Ошибка: {error}", reply_markup=main_keyboard(), @@ -91,21 +116,27 @@ async def restart_asf(callback: CallbackQuery) -> None: @router.callback_query(lambda c: c.data == "plugins", AdminCallbackFilter()) async def plugins_handler(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Открыт список плагинов ASF: %s", user) await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await callback.answer() try: response = get_plugins() if response.status_code != 200: + logger.warning("Не удалось получить список плагинов: %s status=%s", user, response.status_code) await callback.message.edit_text("Ошибка API") # type: ignore[union-attr] return data = response.json() if not data.get("Success"): + logger.warning("ASF вернул ошибку при получении плагинов: %s", user) await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr] return plugins = data.get("Result", []) if not plugins: + logger.info("Плагины ASF не установлены: %s", user) text = "Плагины не установлены" else: + logger.info("Получен список плагинов ASF: %s count=%s", user, len(plugins)) text = "📁 Плагины ASF:\n\n" for plugin in plugins: text += ( @@ -113,4 +144,5 @@ async def plugins_handler(callback: CallbackQuery) -> None: ) await callback.message.edit_text(text.strip(), reply_markup=back_keyboard()) # type: ignore[union-attr] except Exception as error: + logger.exception("Не удалось отобразить список плагинов: %s", user) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] diff --git a/bot/handlers/redeem.py b/bot/handlers/redeem.py index e75e73c..4592c81 100644 --- a/bot/handlers/redeem.py +++ b/bot/handlers/redeem.py @@ -2,6 +2,7 @@ from aiogram import Router from aiogram.types import CallbackQuery from bot.filters import AdminCallbackFilter +from bot.logging_utils import describe_user, get_logger from bot.state import ( bot_redeem_messages, bot_redeem_users, @@ -11,10 +12,13 @@ from bot.state import ( from bot.ui.keyboards import back_keyboard router = Router() +logger = get_logger(__name__) @router.callback_query(lambda c: c.data == "redeem_keys", AdminCallbackFilter()) async def redeem_keys_menu(callback: CallbackQuery) -> None: + user = describe_user(callback.from_user.id) + logger.info("Открыт режим активации ключей по всем ботам: %s", user) await callback.answer() redeem_users.add(callback.from_user.id) redeem_messages[callback.from_user.id] = callback.message @@ -31,8 +35,10 @@ async def redeem_keys_menu(callback: CallbackQuery) -> None: lambda c: c.data and c.data.startswith("redeem_bot_"), AdminCallbackFilter() ) async def redeem_bot_menu(callback: CallbackQuery) -> None: - await callback.answer() + user = describe_user(callback.from_user.id) bot_name = callback.data.replace("redeem_bot_", "") # type: ignore[union-attr] + logger.info("Открыт режим активации ключей для одного бота: %s bot=%s", user, bot_name) + await callback.answer() bot_redeem_users[callback.from_user.id] = bot_name bot_redeem_messages[callback.from_user.id] = callback.message await callback.message.edit_text( # type: ignore[union-attr] diff --git a/bot/handlers/start.py b/bot/handlers/start.py index bf0cb9a..e3bb509 100644 --- a/bot/handlers/start.py +++ b/bot/handlers/start.py @@ -3,19 +3,26 @@ from aiogram.filters import Command from aiogram.types import Message from bot.filters import AdminFilter +from bot.logging_utils import describe_user, get_logger from bot.ui.formatters import get_asf_status_text from bot.ui.keyboards import main_keyboard router = Router() +logger = get_logger(__name__) @router.message(Command("start"), AdminFilter()) async def start_handler(message: Message) -> None: + user = describe_user(message.from_user.id if message.from_user else None) + logger.info("Получена команда /start: %s", user) try: await message.delete() + logger.info("Сообщение /start удалено: %s", user) except Exception: - pass + logger.exception("Не удалось удалить сообщение /start: %s", user) try: await message.answer(get_asf_status_text(), reply_markup=main_keyboard()) + logger.info("Главное меню отправлено: %s", user) except Exception as error: + logger.exception("Не удалось отправить главное меню: %s", user) await message.answer(f"Ошибка: {error}") diff --git a/bot/handlers/twofa.py b/bot/handlers/twofa.py index c8361ad..ce2f1b5 100644 --- a/bot/handlers/twofa.py +++ b/bot/handlers/twofa.py @@ -5,11 +5,13 @@ from aiogram.types import CallbackQuery from bot.api.asf import accept_confirmations, get_confirmations from bot.filters import AdminCallbackFilter +from bot.logging_utils import describe_user, get_logger from bot.services.twofa import auto_update_2fa, stop_twofa_task from bot.state import twofa_tasks from bot.ui.keyboards import back_keyboard, confirmations_keyboard router = Router() +logger = get_logger(__name__) @router.callback_query( @@ -21,9 +23,18 @@ router = Router() async def twofa_handler(callback: CallbackQuery) -> None: await callback.answer() if not callback.data: + logger.debug( + "Колбэк открытия 2FA пришел без data от %s", + describe_user(callback.from_user), + ) return bot_name = callback.data.replace("2fa_", "") message_id = callback.message.message_id # type: ignore[union-attr] + logger.info( + "Пользователь %s открыл 2FA для бота %s", + describe_user(callback.from_user), + bot_name, + ) await stop_twofa_task(message_id) await callback.message.edit_text( # type: ignore[union-attr] f"🔐 {bot_name}\n\nЗагрузка 2FA...", @@ -31,6 +42,11 @@ async def twofa_handler(callback: CallbackQuery) -> None: ) task = asyncio.create_task(auto_update_2fa(callback.message, bot_name)) twofa_tasks[message_id] = task + logger.info( + "Запущено фоновое обновление 2FA для бота %s в сообщении %s", + bot_name, + message_id, + ) @router.callback_query( @@ -43,14 +59,29 @@ async def twofa_handler(callback: CallbackQuery) -> None: async def confirm_trades(callback: CallbackQuery) -> None: await callback.answer() bot_name = callback.data.replace("confirm_", "") # type: ignore[union-attr] + logger.info( + "Пользователь %s подтвердил сделки для бота %s", + describe_user(callback.from_user), + bot_name, + ) try: status = await accept_confirmations(bot_name) if status != 200: + logger.warning( + "Не удалось подтвердить сделки для бота %s: HTTP %s", + bot_name, + status, + ) await callback.answer("Ошибка HTTP", show_alert=True) return + logger.info("Сделки для бота %s подтверждены", bot_name) await callback.answer("Подтверждено", show_alert=True) await twofa_handler(callback) except Exception as error: + logger.exception( + "Ошибка при подтверждении сделок для бота %s", + bot_name, + ) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] @@ -61,18 +92,37 @@ async def confirm_list(callback: CallbackQuery) -> None: await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await callback.answer() if not callback.data: + logger.debug( + "Колбэк списка подтверждений пришел без data от %s", + describe_user(callback.from_user), + ) return bot_name = callback.data.replace("confirm_list_", "") + logger.info( + "Пользователь %s открыл список подтверждений для бота %s", + describe_user(callback.from_user), + bot_name, + ) try: confirmations = await get_confirmations(bot_name) if not confirmations: + logger.info("Для бота %s подтверждений не найдено", bot_name) text = "Подтверждений нет" else: + logger.info( + "Для бота %s найдено %s подтверждени(й)", + bot_name, + len(confirmations), + ) text = f"🔐 {bot_name}\n\nПодтверждения:\n\n" for conf in confirmations: text += f"- {conf['type_name']}\nID: {conf['id']}\n\n" await callback.message.edit_text(text, reply_markup=confirmations_keyboard(bot_name)) # type: ignore[union-attr] except Exception as error: + logger.exception( + "Ошибка при загрузке списка подтверждений для бота %s", + bot_name, + ) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] @@ -83,21 +133,45 @@ async def confirm_all(callback: CallbackQuery) -> None: await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await callback.answer() if not callback.data: + logger.debug( + "Колбэк подтверждения всех сделок пришел без data от %s", + describe_user(callback.from_user), + ) return bot_name = callback.data.replace("confirm_all_", "") + logger.info( + "Пользователь %s запустил подтверждение всех сделок для бота %s", + describe_user(callback.from_user), + bot_name, + ) try: status = await accept_confirmations(bot_name) if status != 200: + logger.warning( + "Не удалось подтвердить все сделки для бота %s: HTTP %s", + bot_name, + status, + ) await callback.answer("Ошибка HTTP", show_alert=True) return confirmations = await get_confirmations(bot_name) if not confirmations: + logger.info("После подтверждения у бота %s не осталось сделок", bot_name) text = f"🔐 {bot_name}\n\nПодтверждений больше нет" else: + logger.info( + "После подтверждения у бота %s осталось %s подтверждени(й)", + bot_name, + len(confirmations), + ) text = f"🔐 {bot_name}\n\nПодтверждения:\n\n" for conf in confirmations: text += f"- {conf['type_name']}\nID: {conf['id']}\n\n" await callback.message.edit_text(text, reply_markup=confirmations_keyboard(bot_name)) # type: ignore[union-attr] await callback.answer("Все подтверждено", show_alert=True) except Exception as error: + logger.exception( + "Ошибка при подтверждении всех сделок для бота %s", + bot_name, + ) await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] diff --git a/bot/logging_utils.py b/bot/logging_utils.py new file mode 100644 index 0000000..0edc394 --- /dev/null +++ b/bot/logging_utils.py @@ -0,0 +1,20 @@ +import logging +from typing import Any + + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) + + +def describe_user(user: Any) -> str: + if user is None: + return "пользователь=" + + user_id = getattr(user, "id", user) + return f"пользователь={user_id}" + + +def mask_key(key: str) -> str: + if len(key) < 9: + return "***" + return f"{key[:5]}-*****-*****" diff --git a/bot/services/bots.py b/bot/services/bots.py index d3ea260..a174522 100644 --- a/bot/services/bots.py +++ b/bot/services/bots.py @@ -1,27 +1,53 @@ import asyncio from bot.api.asf import get_all_bots, get_bot, save_bot_config, send_command +from bot.logging_utils import get_logger + +logger = get_logger(__name__) def is_bot_playing(bot_name: str) -> bool: + logger.info("Проверка, играет ли бот: bot=%s", bot_name) response = get_all_bots() if response.status_code != 200: + logger.warning( + "Не удалось проверить состояние игры бота: bot=%s status=%s", + bot_name, + response.status_code, + ) return False + data = response.json() bot = data.get("Result", {}).get(bot_name, {}) - return bool(bot.get("PlayingBlocked", False)) or bool( + playing = bool(bot.get("PlayingBlocked", False)) or bool( bot.get("CurrentGamesPlayed", []) ) + logger.info("Состояние игры бота определено: bot=%s playing=%s", bot_name, playing) + return playing def is_idle_enabled(bot_name: str, app_id: int) -> bool: + logger.info("Проверка статуса idle-игры: bot=%s app_id=%s", bot_name, app_id) response = get_bot(bot_name) if response.status_code != 200: + logger.warning( + "Не удалось получить конфиг бота для проверки idle: bot=%s status=%s", + bot_name, + response.status_code, + ) return False + data = response.json() bot = data.get("Result", {}).get(bot_name, {}) config = bot.get("BotConfig", {}) - return app_id in config.get("GamesPlayedWhileIdle", []) + enabled = app_id in config.get("GamesPlayedWhileIdle", []) + logger.info( + "Статус idle-игры определен: bot=%s app_id=%s enabled=%s", + bot_name, + app_id, + enabled, + ) + return enabled def toggle_idle_game(config: dict, app_id: int) -> tuple[dict, bool]: @@ -38,22 +64,45 @@ def toggle_idle_game(config: dict, app_id: int) -> tuple[dict, bool]: async def reload_bot(bot_name: str) -> None: try: + logger.info("Отправка команды перезагрузки бота: bot=%s", bot_name) send_command(f"!reload {bot_name}") except Exception: - pass + logger.exception( + "Не удалось отправить команду перезагрузки бота: bot=%s", bot_name + ) async def update_idle_game(bot_name: str, app_id: int) -> tuple[bool, str, bool]: + logger.info("Изменение настройки idle-игры: bot=%s app_id=%s", bot_name, app_id) response = get_bot(bot_name) if response.status_code != 200: + logger.warning( + "Не удалось загрузить конфиг бота для idle: bot=%s status=%s", + bot_name, + response.status_code, + ) return False, "Ошибка API", False + data = response.json() bot_data = data.get("Result", {}).get(bot_name, {}) config = bot_data.get("BotConfig", {}) config, enabled = toggle_idle_game(config, app_id) save = save_bot_config(bot_name, config) if save.status_code != 200: + logger.warning( + "Не удалось сохранить конфиг idle-игры: bot=%s app_id=%s status=%s", + bot_name, + app_id, + save.status_code, + ) return False, "Ошибка сохранения", enabled + asyncio.create_task(reload_bot(bot_name)) action = "Idle включен" if enabled else "Idle выключен" + logger.info( + "Настройка idle-игры изменена: bot=%s app_id=%s enabled=%s", + bot_name, + app_id, + enabled, + ) return True, action, enabled diff --git a/bot/services/inventory.py b/bot/services/inventory.py index 2f1bee8..bb81057 100644 --- a/bot/services/inventory.py +++ b/bot/services/inventory.py @@ -1,10 +1,14 @@ from bot.api.asf import get_bot_inventory from bot.constants import PAGE_SIZE +from bot.logging_utils import get_logger from bot.ui.formatters import get_inventory_icon from bot.ui.keyboards import inventory_menu_keyboard, inventory_page_keyboard +logger = get_logger(__name__) + async def build_inventory_menu(bot_name: str) -> tuple[str, object]: + logger.info("Формирование меню инвентаря: bot=%s", bot_name) cs2_inventory = await get_bot_inventory(bot_name, 730, 2) steam_inventory = await get_bot_inventory(bot_name, 753, 6) cs2_assets = [] @@ -12,13 +16,23 @@ async def build_inventory_menu(bot_name: str) -> tuple[str, object]: try: cs2_assets = cs2_inventory.get(bot_name, {}).get("Assets", []) # type: ignore[union-attr] except Exception: - pass + logger.exception("Ошибка чтения CS2-инвентаря: bot=%s", bot_name) try: steam_assets = steam_inventory.get(bot_name, {}).get("Assets", []) # type: ignore[union-attr] except Exception: - pass + logger.exception("Ошибка чтения Steam-инвентаря: bot=%s", bot_name) if not cs2_assets and not steam_assets: - return f"📦 Инвентарь {bot_name} пуст", inventory_menu_keyboard(bot_name, 0, 0) + logger.info("Инвентарь пуст: bot=%s", bot_name) + return ( + f"📦 Инвентарь {bot_name} пуст", + inventory_menu_keyboard(bot_name, 0, 0), + ) + logger.info( + "Меню инвентаря сформировано: bot=%s cs2_items=%s steam_items=%s", + bot_name, + len(cs2_assets), + len(steam_assets), + ) text = ( f"📦 Инвентарь {bot_name}\n\n" "🔄 — можно трейдить\n" @@ -32,13 +46,28 @@ async def build_inventory_menu(bot_name: str) -> tuple[str, object]: async def render_inventory_page( bot_name: str, inventory_type: str, appid: int, contextid: int, page: int ) -> tuple[str, object] | tuple[None, None]: + logger.info( + "Формирование страницы инвентаря: bot=%s type=%s appid=%s contextid=%s page=%s", + bot_name, + inventory_type, + appid, + contextid, + page, + ) inventory = await get_bot_inventory(bot_name, appid, contextid) if not inventory: + logger.warning( + "Не удалось получить данные для страницы инвентаря: bot=%s type=%s page=%s", + bot_name, + inventory_type, + page, + ) return None, None bot_inventory = inventory.get(bot_name, {}) assets = bot_inventory.get("Assets", []) descriptions = bot_inventory.get("Descriptions", []) if not assets: + logger.info("Инвентарь выбранного типа пуст: bot=%s type=%s", bot_name, inventory_type) return f"Инвентарь {bot_name} пуст", inventory_menu_keyboard(bot_name, 0, 0) desc_map = {} for desc in descriptions: @@ -67,7 +96,7 @@ async def render_inventory_page( marketable_count += 1 if desc.get("tradable"): tradable_count += 1 - if icon == "🃏": + if icon == "🎏": cards_count += amount elif icon == "😀": emotes_count += amount @@ -78,22 +107,30 @@ async def render_inventory_page( lines.append(f"{icon} {tradable}{marketable} {name} x{amount}") inv_name = "CS2" if appid == 730 else "Steam" stats = ( - f"📦 Items: {len(assets)}\n" - f"💰 Marketable: {marketable_count}\n" - f"🔄 Tradable: {tradable_count}" + f"📦 Предметов: {len(assets)}\n" + f"💰 Можно продать: {marketable_count}\n" + f"🔄 Можно трейдить: {tradable_count}" ) if appid == 753: stats += ( - f"\n🃏 Cards: {cards_count}" - f"\n😀 Emotes: {emotes_count}" - f"\n🖼 Backgrounds: {backgrounds_count}" - f"\n💎 Gems: {gems_count}" + f"\n🎏 Карточек: {cards_count}" + f"\n😀 Смайлов: {emotes_count}" + f"\n🖼 Фонов: {backgrounds_count}" + f"\n💎 Самоцветов: {gems_count}" ) text = ( f"📦 {inv_name} Inventory {bot_name}\n" f"📄 Страница {page + 1}/{total_pages}\n\n" f"{stats}\n\n" + "\n".join(lines) ) + logger.info( + "Страница инвентаря сформирована: bot=%s type=%s page=%s total_pages=%s items_on_page=%s", + bot_name, + inventory_type, + page, + total_pages, + len(page_assets), + ) return text[:4000], inventory_page_keyboard( inventory_type, bot_name, page, total_pages ) diff --git a/bot/services/redeem.py b/bot/services/redeem.py index eae94f4..08cac8b 100644 --- a/bot/services/redeem.py +++ b/bot/services/redeem.py @@ -2,13 +2,17 @@ import asyncio import re from bot.api.asf import get_all_bots, redeem_key +from bot.logging_utils import get_logger, mask_key KEY_PATTERN = r"[A-Z0-9]{5}(?:-[A-Z0-9]{5}){2}" +logger = get_logger(__name__) def extract_keys(text: str) -> list[str]: keys = re.findall(KEY_PATTERN, text.upper()) - return list(dict.fromkeys(keys)) + unique_keys = list(dict.fromkeys(keys)) + logger.info("Из текста извлечены ключи: count=%s", len(unique_keys)) + return unique_keys def parse_redeem_result(result: str, bot_name: str | None = None) -> str: @@ -18,6 +22,7 @@ def parse_redeem_result(result: str, bot_name: str | None = None) -> str: if f"<{bot_name.lower()}>" in line.lower(): result_lower = line.lower() break + if "ok/nodetail" in result_lower or "ok/" in result_lower: return "success" if "ratelimited" in result_lower: @@ -34,17 +39,42 @@ def parse_redeem_result(result: str, bot_name: str | None = None) -> str: async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str: + logger.info( + "Начата активация ключей для одного бота: bot=%s count=%s", + bot_name, + len(keys), + ) success_count = 0 failed_count = 0 results = [] + for key in keys: + logger.info( + "Попытка активации ключа для одного бота: bot=%s key=%s", + bot_name, + mask_key(key), + ) success, result = await redeem_key(bot_name, key) await asyncio.sleep(2) + if not success or not result: failed_count += 1 + logger.warning( + "Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s", + bot_name, + mask_key(key), + ) results.append(f"❌ {key}: ASF ERROR") continue + status = parse_redeem_result(result) + logger.info( + "Получен результат активации ключа: bot=%s key=%s status=%s", + bot_name, + mask_key(key), + status, + ) + if status == "success": success_count += 1 results.append(f"✅ {key}: активировано") @@ -66,31 +96,62 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str: else: failed_count += 1 results.append(f"❔ {key}: неизвестный ответ") + text = ( f"Результат активации для {bot_name}\n\n" f"✅ Успешно: {success_count}\n" f"❌ Ошибок: {failed_count}\n\n" + "\n".join(results[:50]) ) + logger.info( + "Завершена активация ключей для одного бота: bot=%s success=%s failed=%s", + bot_name, + success_count, + failed_count, + ) return text[:4000] + ("\n\n...обрезано" if len(text) > 4000 else "") async def redeem_all_bots_keys(keys: list[str]) -> str: + logger.info("Начата активация ключей по всем ботам: count=%s", len(keys)) response = get_all_bots() data = response.json() bots = list(data.get("Result", {}).keys()) + logger.info("Для массовой активации найдено ботов: count=%s", len(bots)) success_count = 0 failed_count = 0 results = [] + for key in keys: activated = False key_report = [] + logger.info("Начата обработка ключа по всем ботам: key=%s", mask_key(key)) + for bot_name in bots: + logger.info( + "Попытка активации ключа на боте: bot=%s key=%s", + bot_name, + mask_key(key), + ) success, result = await redeem_key(bot_name, key) await asyncio.sleep(2) + if not success or not result: + logger.warning( + "Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s", + bot_name, + mask_key(key), + ) key_report.append(f"❌ {bot_name}: ASF ERROR") continue + status = parse_redeem_result(result, bot_name) + logger.info( + "Получен результат активации ключа: bot=%s key=%s status=%s", + bot_name, + mask_key(key), + status, + ) + if status == "success": key_report.append(f"✅ {bot_name}: активировано") success_count += 1 @@ -112,12 +173,24 @@ async def redeem_all_bots_keys(keys: list[str]) -> str: key_report.append(f"❌ {bot_name}: неверный ключ") break key_report.append(f"❔ {bot_name}: неизвестный ответ") + if not activated: failed_count += 1 + logger.warning( + "Ключ не удалось активировать ни на одном боте: key=%s", + mask_key(key), + ) + results.append(f"\n🔑 {key}\n" + "\n".join(key_report)) + text = ( "Результат активации\n\n" f"✅ Успешно: {success_count}\n" f"❌ Ошибок: {failed_count}\n\n" + "\n".join(results[:50]) ) + logger.info( + "Завершена массовая активация ключей: success=%s failed=%s", + success_count, + failed_count, + ) return text[:4000] + ("\n\n...обрезано" if len(text) > 4000 else "") diff --git a/bot/services/twofa.py b/bot/services/twofa.py index b98d08f..f35efe2 100644 --- a/bot/services/twofa.py +++ b/bot/services/twofa.py @@ -1,23 +1,37 @@ import asyncio from bot.api.asf import get_2fa_token, get_confirmations +from bot.logging_utils import get_logger from bot.state import twofa_tasks from bot.ui.keyboards import twofa_keyboard +logger = get_logger(__name__) + async def stop_twofa_task(message_id: int) -> None: task = twofa_tasks.pop(message_id, None) if task: + logger.info( + "Остановка фоновой задачи обновления 2FA: message_id=%s", message_id + ) task.cancel() try: await task except Exception: - pass + logger.exception( + "Ошибка при завершении фоновой задачи обновления 2FA: message_id=%s", + message_id, + ) async def auto_update_2fa(message, bot_name: str) -> None: last_code = None message_id = message.message_id + logger.info( + "Запуск фонового обновления 2FA: bot=%s message_id=%s", + bot_name, + message_id, + ) try: while True: code = await get_2fa_token(bot_name) @@ -30,12 +44,37 @@ async def auto_update_2fa(message, bot_name: str) -> None: reply_markup=twofa_keyboard(bot_name, bool(confirmations)), ) last_code = code + logger.info( + "Сообщение 2FA обновлено: bot=%s message_id=%s confirmations=%s", + bot_name, + message_id, + len(confirmations), + ) except Exception: + logger.exception( + "Не удалось обновить сообщение 2FA: bot=%s message_id=%s", + bot_name, + message_id, + ) break await asyncio.sleep(15) except asyncio.CancelledError: - pass + logger.info( + "Фоновое обновление 2FA отменено: bot=%s message_id=%s", + bot_name, + message_id, + ) except Exception as error: - print(f"2FA updater error: {error}") + logger.exception( + "Фоновое обновление 2FA завершилось ошибкой: bot=%s message_id=%s error=%s", + bot_name, + message_id, + error, + ) finally: twofa_tasks.pop(message_id, None) + logger.info( + "Фоновая задача обновления 2FA завершена: bot=%s message_id=%s", + bot_name, + message_id, + ) diff --git a/bot/ui/formatters.py b/bot/ui/formatters.py index 434a9b9..0dfe095 100644 --- a/bot/ui/formatters.py +++ b/bot/ui/formatters.py @@ -29,7 +29,7 @@ def get_inventory_icon(desc: dict) -> str: name = tag.get("localized_tag_name", "") if category == "item_class": if "Trading Card" in name: - return "🃏" + return "🎏" if "Emoticon" in name: return "😀" if "Profile Background" in name: @@ -40,6 +40,7 @@ def get_inventory_icon(desc: dict) -> str: return "💎" if "Gift" in name: return "🎁" + item_type = desc.get("type", "").lower() if "knife" in item_type: return "🔪" @@ -98,7 +99,11 @@ def get_bot_status_icon(bot: dict) -> str: def format_bot_ui(bot: dict) -> str: online = get_bot_status_icon(bot) now_farming = bot.get("CardsFarmer", {}).get("NowFarming") - status_line = "🃏 Идет фарм карточек" if now_farming else "🃏 Карточки не фармится" + status_line = ( + "🎴 Идет фарм карточек" + if now_farming + else "🎴 Карточки не фармятся" + ) time = bot.get("CardsFarmer", {}).get("TimeRemaining", "00:00:00") games_to_farm = len(bot.get("CardsFarmer", {}).get("GamesToFarm", [])) twofa = "есть" if bot.get("HasMobileAuthenticator") else "нет" @@ -110,9 +115,10 @@ def format_bot_ui(bot: dict) -> str: profile_url = f"https://steamcommunity.com/profiles/{steam_id}" text = ( f"{online} {bot.get('BotName') or 'Загрузка...'}" - f'({nickname})\n' + f' ({nickname})\n' f"{status_line}\n" ) + idle_games = bot.get("BotConfig", {}).get("GamesPlayedWhileIdle", []) if idle_games: names = [GAMES.get(game, str(game)) for game in idle_games] @@ -126,8 +132,9 @@ def format_bot_ui(bot: dict) -> str: text += f"{idle_text}\n" else: text += "⌚ Накрутка часов: выключена\n" + text += ( - f"🆔 : {steam_id}\n" + f"🆔: {steam_id}\n" f"🔐 2FA: {twofa}\n\n" f"💰 Баланс: {balance:.2f} {currency}\n" ) @@ -161,8 +168,10 @@ def get_farm_summary(bots: dict) -> str: time_str = farmer.get("TimeRemaining", "00:00:00") hours, minutes, seconds = map(int, time_str.split(":")) total_time_seconds += hours * 3600 + minutes * 60 + seconds + if total_games == 0: - return "На аккаунтах нечего не фармиться" + return "На аккаунтах нечего фармить" + hours = total_time_seconds // 3600 minutes = (total_time_seconds % 3600) // 60 return ( diff --git a/bot/ui/keyboards.py b/bot/ui/keyboards.py index 3a3d59f..58325eb 100644 --- a/bot/ui/keyboards.py +++ b/bot/ui/keyboards.py @@ -35,7 +35,11 @@ def back_keyboard(callback_data: str = "back") -> InlineKeyboardMarkup: def games_keyboard(bot_name: str, is_enabled: bool) -> InlineKeyboardMarkup: - text = "Остановить накрутку CS2" if is_enabled else "⌚ Накрутить часы CS2" + text = ( + "Остановить накрутку CS2" + if is_enabled + else "⌚ Накрутить часы CS2" + ) return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text=text, callback_data=f"farm|{bot_name}|730")], @@ -179,6 +183,7 @@ def inventory_page_keyboard( callback_data=f"invpage|{inventory_type}|{bot_name}|{page + 1}", ) ) + keyboard = [] if nav_buttons: keyboard.append(nav_buttons) diff --git a/main.py b/main.py index 228265a..c11adc6 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,11 @@ import asyncio +import logging from aiogram import Bot, Dispatcher from aiogram.client.default import DefaultBotProperties -from bot.config import API_TOKEN +from bot.config import BOT_TOKEN + from bot.handlers.bots import router as bots_router from bot.handlers.console import router as console_router from bot.handlers.inventory import router as inventory_router @@ -13,7 +15,9 @@ from bot.handlers.redeem import router as redeem_router from bot.handlers.start import router as start_router from bot.handlers.twofa import router as twofa_router -bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode="HTML")) # type: ignore[arg-type] +logger = logging.getLogger(__name__) + +bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="HTML")) # type: ignore[arg-type] dp = Dispatcher() dp.include_router(start_router) @@ -27,8 +31,23 @@ dp.include_router(messages_router) async def main() -> None: + logger.info("Запуск Telegram-бота ASF Control Bot") await dp.start_polling(bot) if __name__ == "__main__": - asyncio.run(main()) + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", + handlers=[ + logging.FileHandler("bot.log", encoding="utf-8"), + logging.StreamHandler(), + ], + ) + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Получен сигнал остановки, бот завершает работу") + except Exception: + logger.exception("Бот завершился из-за необработанной ошибки") + raise