diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bot/__init__.py @@ -0,0 +1 @@ + diff --git a/bot/api/__init__.py b/bot/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bot/api/__init__.py @@ -0,0 +1 @@ + diff --git a/bot/api/asf.py b/bot/api/asf.py new file mode 100644 index 0000000..ba38e50 --- /dev/null +++ b/bot/api/asf.py @@ -0,0 +1,118 @@ +import aiohttp +import requests + +from bot.config import ASF_PASSWORD, ASF_URL + +IPC_BASE_URL = "http://127.0.0.1:1242" + + +def _headers() -> dict[str, str | None]: + return {"Authentication": ASF_PASSWORD} + + +def asf_get(path: str) -> requests.Response: + return requests.get(f"{IPC_BASE_URL}{path}", headers=_headers()) + + +def asf_post(path: str, payload: dict | None = None) -> requests.Response: + return requests.post(f"{IPC_BASE_URL}{path}", headers=_headers(), json=payload) + + +def get_asf_status() -> requests.Response: + return requests.get(ASF_URL, headers=_headers()) # type: ignore[arg-type] + + +def get_all_bots() -> requests.Response: + return asf_get("/Api/Bot/ASF") + + +def get_bot(bot_name: str) -> requests.Response: + return asf_get(f"/Api/Bot/{bot_name}") + + +def get_plugins() -> requests.Response: + return asf_get("/Api/Plugins") + + +def send_command(command: str) -> requests.Response: + return asf_post("/Api/Command", {"Command": command}) + + +def restart_asf() -> requests.Response: + return send_command("!restart") + + +async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict | None: + 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: + return None + data = await response.json() + if not data.get("Success"): + return None + return data.get("Result") + + +async def get_2fa_token(bot_name: str) -> str: + 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: + return f"Ошибка HTTP {response.status}" + data = await response.json() + if not data.get("Success"): + return "Ошибка ASF" + try: + return data["Result"][bot_name]["Result"] + except Exception: + return "Не удалось получить код" + + +async def get_confirmations(bot_name: str) -> list: + 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: + return [] + data = await response.json() + if not data.get("Success"): + return [] + try: + return data["Result"][bot_name]["Result"] + except Exception: + return [] + + +async def accept_confirmations(bot_name: str) -> int: + 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: + return response.status + + +def save_bot_config(bot_name: str, config: dict) -> requests.Response: + return asf_post(f"/Api/Bot/{bot_name}", {"BotConfig": config}) + + +async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]: + async with aiohttp.ClientSession() as session: + async with session.post( + f"{IPC_BASE_URL}/Api/Command", + headers=_headers(), + json={"Command": f"!redeem {bot_name} {key}"}, + ) as response: + if response.status != 200: + return False, f"HTTP_ERROR_{response.status}" + data = await response.json() + if not data.get("Success"): + return False, "ASF_ERROR" + return True, str(data.get("Result", "")) diff --git a/bot/config.py b/bot/config.py new file mode 100644 index 0000000..6483519 --- /dev/null +++ b/bot/config.py @@ -0,0 +1,10 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + +API_TOKEN = os.getenv("API_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/constants.py b/bot/constants.py new file mode 100644 index 0000000..0c416ed --- /dev/null +++ b/bot/constants.py @@ -0,0 +1,5 @@ +PAGE_SIZE = 40 +GAMES = {730: "CS2", 440: "TF2", 570: "Dota 2"} + +appid = 753 +contextid = 6 diff --git a/bot/filters.py b/bot/filters.py new file mode 100644 index 0000000..107ad07 --- /dev/null +++ b/bot/filters.py @@ -0,0 +1,14 @@ +from aiogram.filters import BaseFilter +from aiogram.types import CallbackQuery, Message + +from bot.config import ADMIN_ID + + +class AdminFilter(BaseFilter): + async def __call__(self, message: Message) -> bool: + return message.from_user.id == ADMIN_ID # type: ignore[union-attr] + + +class AdminCallbackFilter(BaseFilter): + async def __call__(self, callback: CallbackQuery) -> bool: + return callback.from_user.id == ADMIN_ID diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bot/handlers/__init__.py @@ -0,0 +1 @@ + diff --git a/bot/handlers/bots.py b/bot/handlers/bots.py new file mode 100644 index 0000000..d324a64 --- /dev/null +++ b/bot/handlers/bots.py @@ -0,0 +1,97 @@ +from aiogram import Router +from aiogram.types import CallbackQuery + +from bot.api.asf import get_all_bots +from bot.filters import AdminCallbackFilter +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() + + +@router.callback_query(lambda c: c.data == "bot_loading", AdminCallbackFilter()) +async def bot_loading_handler(callback: CallbackQuery) -> None: + await callback.answer( + "Аккаунт ещё загружается. Попробуйте через несколько секунд.", + show_alert=True, + ) + + +@router.callback_query(lambda c: c.data == "bots", AdminCallbackFilter()) +async def bots_handler(callback: CallbackQuery) -> None: + 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: + await callback.message.edit_text("Ошибка API") # type: ignore[union-attr] + return + data = response.json() + if not data.get("Success"): + await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr] + return + bots = data.get("Result", {}) + 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: + await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] + + +@router.callback_query(lambda c: c.data and c.data.startswith("bot_"), AdminCallbackFilter()) +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: + return + bot_name = callback.data.replace("bot_", "") + 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"): + await callback.answer( + "Аккаунт ещё загружается. Попробуйте через несколько секунд.", + show_alert=True, + ) + return + await callback.message.edit_text( # type: ignore[union-attr] + format_bot_ui(bot), + disable_web_page_preview=True, + reply_markup=bot_details_keyboard(bot_name, bool(bot.get("HasMobileAuthenticator"))), + ) + except Exception as error: + await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] + + +@router.callback_query(lambda c: c.data and c.data.startswith("games_"), AdminCallbackFilter()) +async def games(callback: CallbackQuery) -> None: + await callback.answer() + if not callback.data: + return + bot_name = callback.data.replace("games_", "") + idle_enabled = is_idle_enabled(bot_name, 730) + await callback.message.edit_text( # type: ignore[union-attr] + "⌚ Выберите игру для накрутки часов", + reply_markup=games_keyboard(bot_name, idle_enabled), + ) + + +@router.callback_query(lambda c: c.data and c.data.startswith("farm|"), AdminCallbackFilter()) +async def farm_game(callback: CallbackQuery) -> None: + await callback.answer() + if not callback.data: + return + _, bot_name, app_id_value = callback.data.split("|") + success, text, enabled = await update_idle_game(bot_name, int(app_id_value)) + if not success: + await callback.answer(text, show_alert=True) + return + 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 diff --git a/bot/handlers/console.py b/bot/handlers/console.py new file mode 100644 index 0000000..70d0ec6 --- /dev/null +++ b/bot/handlers/console.py @@ -0,0 +1,26 @@ +from aiogram import Router +from aiogram.types import CallbackQuery + +from bot.filters import AdminCallbackFilter +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() + + +@router.callback_query(lambda c: c.data == "console", AdminCallbackFilter()) +async def console_enter(callback: CallbackQuery) -> None: + await callback.answer() + console_users.add(callback.from_user.id) + await callback.message.edit_text( # type: ignore[union-attr] + "💻 Консоль ASF\n\nОтправь команду (без !)", + reply_markup=console_keyboard(), + ) + + +@router.callback_query(lambda c: c.data == "console_exit", AdminCallbackFilter()) +async def console_exit(callback: CallbackQuery) -> None: + 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 new file mode 100644 index 0000000..9321a8c --- /dev/null +++ b/bot/handlers/inventory.py @@ -0,0 +1,55 @@ +from aiogram import Router +from aiogram.types import CallbackQuery + +from bot.filters import AdminCallbackFilter +from bot.services.inventory import build_inventory_menu, render_inventory_page +from bot.ui.keyboards import back_keyboard + +router = Router() + + +@router.callback_query(lambda c: c.data and c.data.startswith("inventory_"), AdminCallbackFilter()) +async def inventory_menu(callback: CallbackQuery) -> None: + await callback.answer() + bot_name = callback.data.replace("inventory_", "") # type: ignore[union-attr] + await callback.message.edit_text("Проверка inventory...") # type: ignore[union-attr] + text, keyboard = await build_inventory_menu(bot_name) + if "пуст" in text: + keyboard = back_keyboard(f"bot_{bot_name}") + await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] + + +@router.callback_query(lambda c: c.data and c.data.startswith("inv_cs2_"), AdminCallbackFilter()) +async def cs2_inventory(callback: CallbackQuery) -> None: + await callback.answer() + bot_name = callback.data.replace("inv_cs2_", "") # type: ignore[union-attr] + 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: + await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] + return + await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] + + +@router.callback_query(lambda c: c.data and c.data.startswith("inv_steam_"), AdminCallbackFilter()) +async def steam_inventory(callback: CallbackQuery) -> None: + await callback.answer() + bot_name = callback.data.replace("inv_steam_", "") # type: ignore[union-attr] + 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: + await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] + return + await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] + + +@router.callback_query(lambda c: c.data and c.data.startswith("invpage|"), AdminCallbackFilter()) +async def inventory_page(callback: CallbackQuery) -> None: + await callback.answer() + _, inv_type, bot_name, page_value = callback.data.split("|") # type: ignore[union-attr] + 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: + 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 new file mode 100644 index 0000000..2cd773a --- /dev/null +++ b/bot/handlers/messages.py @@ -0,0 +1,113 @@ +import html + +from aiogram import Router +from aiogram.types import Message + +from bot.api.asf import get_all_bots, send_command +from bot.filters import AdminFilter +from bot.services.redeem import extract_keys, redeem_all_bots_keys, redeem_single_bot_keys +from bot.state import ( + bot_redeem_messages, + bot_redeem_users, + console_users, + redeem_messages, + redeem_users, +) +from bot.ui.formatters import format_bot_ui, get_asf_status_text +from bot.ui.keyboards import bot_details_keyboard, delete_message_keyboard, main_keyboard + +router = Router() + + +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] + try: + await message.delete() + except Exception: + pass + try: + response = send_command(f"!{command}") + if response.status_code != 200: + text = f"Ошибка HTTP {response.status_code}" + else: + data = response.json() + if not data.get("Success"): + text = "Ошибка ASF" + else: + 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: + await message.answer(f"Ошибка: {error}") + return True + + +async def handle_single_bot_redeem(message: Message) -> bool: + if message.from_user.id not in bot_redeem_users: # type: ignore[union-attr] + 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] + if menu_message: + try: + response = get_all_bots() + bot_data = response.json().get("Result", {}).get(bot_name) + if bot_data: + await menu_message.edit_text( + format_bot_ui(bot_data), + disable_web_page_preview=True, + reply_markup=bot_details_keyboard( + bot_name, bool(bot_data.get("HasMobileAuthenticator")) + ), + ) + except Exception: + pass + try: + await message.bot.delete_message(chat_id=message.chat.id, message_id=message.message_id) + except Exception: + pass + keys = extract_keys(message.text or "") + if not keys: + await message.answer("Ключи не найдены") + return True + checking = await message.answer( + f"Аккаунт: {bot_name}\nНайдено ключей: {len(keys)}\nНачинаю активацию..." + ) + await checking.edit_text(await redeem_single_bot_keys(bot_name, keys)) + return True + + +async def handle_all_bots_redeem(message: Message) -> bool: + if message.from_user.id not in redeem_users: # type: ignore[union-attr] + 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] + if menu_message: + try: + await menu_message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) + except Exception: + pass + try: + await message.bot.delete_message(chat_id=message.chat.id, message_id=message.message_id) + except Exception: + pass + keys = extract_keys(message.text or "") + if not keys: + await message.answer("Ключи не найдены") + return True + checking = await message.answer(f"Найдено ключей: {len(keys)}\nНачинаю активацию...") + try: + await checking.edit_text(await redeem_all_bots_keys(keys)) + except Exception as error: + await checking.edit_text(f"Ошибка: {error}") + return True + + +@router.message(AdminFilter()) +async def text_router(message: Message) -> None: + if await handle_console_message(message): + return + if await handle_single_bot_redeem(message): + return + await handle_all_bots_redeem(message) diff --git a/bot/handlers/navigation.py b/bot/handlers/navigation.py new file mode 100644 index 0000000..b6aa1cb --- /dev/null +++ b/bot/handlers/navigation.py @@ -0,0 +1,114 @@ +import asyncio + +from aiogram import Router +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.services.twofa import stop_twofa_task +from bot.state import ( + bot_redeem_messages, + bot_redeem_users, + console_users, + redeem_messages, + redeem_users, +) +from bot.ui.formatters import get_asf_status_text +from bot.ui.keyboards import back_keyboard, confirm_action_keyboard, main_keyboard + +router = Router() + + +def clear_user_state(user_id: int) -> None: + redeem_users.discard(user_id) + redeem_messages.pop(user_id, None) + console_users.discard(user_id) + bot_redeem_users.pop(user_id, None) + bot_redeem_messages.pop(user_id, None) + + +@router.callback_query(lambda c: c.data == "back", AdminCallbackFilter()) +async def back_button(callback: CallbackQuery) -> None: + 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] + except Exception as error: + 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: + await callback.answer() + try: + await callback.message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) # type: ignore[union-attr] + except Exception as error: + 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: + await callback.answer() + try: + await callback.message.delete() # type: ignore[union-attr] + except Exception: + pass + + +@router.callback_query(lambda c: c.data == "restart_asf_confirm", AdminCallbackFilter()) +async def restart_asf_confirm(callback: CallbackQuery) -> None: + await callback.answer() + await callback.message.edit_text( # type: ignore[union-attr] + "♻ Точно перезапустить ASF?\n\nВо время перезапуска IPC будет недоступен несколько секунд.", + reply_markup=confirm_action_keyboard("restart_asf", "back"), + ) + + +@router.callback_query(lambda c: c.data == "restart_asf", AdminCallbackFilter()) +async def restart_asf(callback: CallbackQuery) -> None: + await callback.answer() + try: + await callback.message.edit_text("♻ ASF перезапускается...") # type: ignore[union-attr] + response = restart_asf_request() + if response.status_code != 200: + await callback.message.edit_text( # type: ignore[union-attr] + f"Ошибка HTTP {response.status_code}", + reply_markup=main_keyboard(), + ) + return + await asyncio.sleep(6) + await callback.message.edit_text( # type: ignore[union-attr] + f"ASF успешно перезапущен\n\n{get_asf_status_text()}", + reply_markup=main_keyboard(), + ) + except Exception as error: + await callback.message.edit_text( # type: ignore[union-attr] + f"Ошибка: {error}", + reply_markup=main_keyboard(), + ) + + +@router.callback_query(lambda c: c.data == "plugins", AdminCallbackFilter()) +async def plugins_handler(callback: CallbackQuery) -> None: + await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] + await callback.answer() + try: + response = get_plugins() + if response.status_code != 200: + await callback.message.edit_text("Ошибка API") # type: ignore[union-attr] + return + data = response.json() + if not data.get("Success"): + await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr] + return + plugins = data.get("Result", []) + if not plugins: + text = "Плагины не установлены" + else: + text = "📁 Плагины ASF:\n\n" + for plugin in plugins: + text += f"{plugin.get('Name', '???')} ({plugin.get('Version', '???')})\n" + await callback.message.edit_text(text.strip(), reply_markup=back_keyboard()) # type: ignore[union-attr] + except Exception as error: + await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] diff --git a/bot/handlers/redeem.py b/bot/handlers/redeem.py new file mode 100644 index 0000000..15e3a93 --- /dev/null +++ b/bot/handlers/redeem.py @@ -0,0 +1,40 @@ +from aiogram import Router +from aiogram.types import CallbackQuery + +from bot.filters import AdminCallbackFilter +from bot.state import ( + bot_redeem_messages, + bot_redeem_users, + redeem_messages, + redeem_users, +) +from bot.ui.keyboards import back_keyboard + +router = Router() + + +@router.callback_query(lambda c: c.data == "redeem_keys", AdminCallbackFilter()) +async def redeem_keys_menu(callback: CallbackQuery) -> None: + await callback.answer() + redeem_users.add(callback.from_user.id) + redeem_messages[callback.from_user.id] = callback.message + await callback.message.edit_text( # type: ignore[union-attr] + "Отправьте ключ Steam для активации\n\n" + "Можно отправить сразу несколько ключей.\n\n" + "Пример:\n" + "AAAAA-BBBBB-CCCCC\nDDDDD-EEEEE-FFFFF", + reply_markup=back_keyboard(), + ) + + +@router.callback_query(lambda c: c.data and c.data.startswith("redeem_bot_"), AdminCallbackFilter()) +async def redeem_bot_menu(callback: CallbackQuery) -> None: + await callback.answer() + bot_name = callback.data.replace("redeem_bot_", "") # type: ignore[union-attr] + 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] + f"Отправьте ключ Steam для активации на аккаунте:\n{bot_name}\n\n" + "Можно отправить сразу несколько ключей.", + reply_markup=back_keyboard(f"bot_{bot_name}"), + ) diff --git a/bot/handlers/start.py b/bot/handlers/start.py new file mode 100644 index 0000000..bf0cb9a --- /dev/null +++ b/bot/handlers/start.py @@ -0,0 +1,21 @@ +from aiogram import Router +from aiogram.filters import Command +from aiogram.types import Message + +from bot.filters import AdminFilter +from bot.ui.formatters import get_asf_status_text +from bot.ui.keyboards import main_keyboard + +router = Router() + + +@router.message(Command("start"), AdminFilter()) +async def start_handler(message: Message) -> None: + try: + await message.delete() + except Exception: + pass + try: + await message.answer(get_asf_status_text(), reply_markup=main_keyboard()) + except Exception as error: + await message.answer(f"Ошибка: {error}") diff --git a/bot/handlers/twofa.py b/bot/handlers/twofa.py new file mode 100644 index 0000000..eb21d6d --- /dev/null +++ b/bot/handlers/twofa.py @@ -0,0 +1,97 @@ +import asyncio + +from aiogram import Router +from aiogram.types import CallbackQuery + +from bot.api.asf import accept_confirmations, get_confirmations +from bot.filters import AdminCallbackFilter +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() + + +@router.callback_query( + lambda c: c.data and c.data.startswith("2fa_") and not c.data.startswith("2fa_refresh_"), + AdminCallbackFilter(), +) +async def twofa_handler(callback: CallbackQuery) -> None: + await callback.answer() + if not callback.data: + return + bot_name = callback.data.replace("2fa_", "") + message_id = callback.message.message_id # type: ignore[union-attr] + await stop_twofa_task(message_id) + await callback.message.edit_text( # type: ignore[union-attr] + f"🔐 {bot_name}\n\nЗагрузка 2FA...", + reply_markup=back_keyboard(f"bot_{bot_name}"), + ) + task = asyncio.create_task(auto_update_2fa(callback.message, bot_name)) + twofa_tasks[message_id] = task + + +@router.callback_query( + lambda c: c.data + and c.data.startswith("confirm_") + and not c.data.startswith("confirm_list_") + and not c.data.startswith("confirm_all_"), + AdminCallbackFilter(), +) +async def confirm_trades(callback: CallbackQuery) -> None: + await callback.answer() + bot_name = callback.data.replace("confirm_", "") # type: ignore[union-attr] + try: + status = await accept_confirmations(bot_name) + if status != 200: + await callback.answer("Ошибка HTTP", show_alert=True) + return + await callback.answer("Подтверждено", show_alert=True) + await twofa_handler(callback) + except Exception as error: + await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] + + +@router.callback_query(lambda c: c.data and c.data.startswith("confirm_list_"), AdminCallbackFilter()) +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: + return + bot_name = callback.data.replace("confirm_list_", "") + try: + confirmations = await get_confirmations(bot_name) + if not confirmations: + text = "Подтверждений нет" + else: + 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: + await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] + + +@router.callback_query(lambda c: c.data and c.data.startswith("confirm_all_"), AdminCallbackFilter()) +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: + return + bot_name = callback.data.replace("confirm_all_", "") + try: + status = await accept_confirmations(bot_name) + if status != 200: + await callback.answer("Ошибка HTTP", show_alert=True) + return + confirmations = await get_confirmations(bot_name) + if not confirmations: + text = f"🔐 {bot_name}\n\nПодтверждений больше нет" + else: + 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: + await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] diff --git a/bot/services/__init__.py b/bot/services/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bot/services/__init__.py @@ -0,0 +1 @@ + diff --git a/bot/services/bots.py b/bot/services/bots.py new file mode 100644 index 0000000..b731fc2 --- /dev/null +++ b/bot/services/bots.py @@ -0,0 +1,57 @@ +import asyncio + +from bot.api.asf import get_all_bots, get_bot, save_bot_config, send_command + + +def is_bot_playing(bot_name: str) -> bool: + response = get_all_bots() + if response.status_code != 200: + return False + data = response.json() + bot = data.get("Result", {}).get(bot_name, {}) + return bool(bot.get("PlayingBlocked", False)) or bool(bot.get("CurrentGamesPlayed", [])) + + +def is_idle_enabled(bot_name: str, app_id: int) -> bool: + response = get_bot(bot_name) + if response.status_code != 200: + return False + data = response.json() + bot = data.get("Result", {}).get(bot_name, {}) + config = bot.get("BotConfig", {}) + return app_id in config.get("GamesPlayedWhileIdle", []) + + +def toggle_idle_game(config: dict, app_id: int) -> tuple[dict, bool]: + idle_games = config.get("GamesPlayedWhileIdle", []) + if app_id in idle_games: + idle_games.remove(app_id) + enabled = False + else: + idle_games.append(app_id) + enabled = True + config["GamesPlayedWhileIdle"] = idle_games + return config, enabled + + +async def reload_bot(bot_name: str) -> None: + try: + send_command(f"!reload {bot_name}") + except Exception: + pass + + +async def update_idle_game(bot_name: str, app_id: int) -> tuple[bool, str, bool]: + response = get_bot(bot_name) + if response.status_code != 200: + 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: + return False, "Ошибка сохранения", enabled + asyncio.create_task(reload_bot(bot_name)) + action = "Idle включен" if enabled else "Idle выключен" + return True, action, enabled diff --git a/bot/services/inventory.py b/bot/services/inventory.py new file mode 100644 index 0000000..122e10a --- /dev/null +++ b/bot/services/inventory.py @@ -0,0 +1,98 @@ +from bot.api.asf import get_bot_inventory +from bot.constants import PAGE_SIZE +from bot.ui.formatters import get_inventory_icon +from bot.ui.keyboards import inventory_menu_keyboard, inventory_page_keyboard + + +async def build_inventory_menu(bot_name: str) -> tuple[str, object]: + cs2_inventory = await get_bot_inventory(bot_name, 730, 2) + steam_inventory = await get_bot_inventory(bot_name, 753, 6) + cs2_assets = [] + steam_assets = [] + try: + cs2_assets = cs2_inventory.get(bot_name, {}).get("Assets", []) # type: ignore[union-attr] + except Exception: + pass + try: + steam_assets = steam_inventory.get(bot_name, {}).get("Assets", []) # type: ignore[union-attr] + except Exception: + pass + if not cs2_assets and not steam_assets: + return f"📦 Инвентарь {bot_name} пуст", inventory_menu_keyboard(bot_name, 0, 0) + text = ( + f"📦 Инвентарь {bot_name}\n\n" + "🔄 — можно трейдить\n" + "💰 — можно продавать\n" + "🔒 — нельзя продавать\n" + "❌ — нельзя трейдить" + ) + return text, inventory_menu_keyboard(bot_name, len(cs2_assets), len(steam_assets)) + + +async def render_inventory_page( + bot_name: str, inventory_type: str, appid: int, contextid: int, page: int +) -> tuple[str, object] | tuple[None, None]: + inventory = await get_bot_inventory(bot_name, appid, contextid) + if not inventory: + return None, None + bot_inventory = inventory.get(bot_name, {}) + assets = bot_inventory.get("Assets", []) + descriptions = bot_inventory.get("Descriptions", []) + if not assets: + return f"Инвентарь {bot_name} пуст", inventory_menu_keyboard(bot_name, 0, 0) + desc_map = {} + for desc in descriptions: + key = (str(desc.get("classid")), str(desc.get("instanceid"))) + desc_map[key] = desc + total_pages = (len(assets) + PAGE_SIZE - 1) // PAGE_SIZE + start = page * PAGE_SIZE + end = start + PAGE_SIZE + page_assets = assets[start:end] + lines = [] + marketable_count = 0 + tradable_count = 0 + cards_count = 0 + emotes_count = 0 + backgrounds_count = 0 + gems_count = 0 + for asset in page_assets: + key = (str(asset.get("classid")), str(asset.get("instanceid"))) + desc = desc_map.get(key, {}) + name = desc.get("market_name", "Unknown") + amount = asset.get("amount", 1) + tradable = "🔄" if desc.get("tradable") else "❌" + marketable = "💰" if desc.get("marketable") else "🔒" + icon = get_inventory_icon(desc) + if desc.get("marketable"): + marketable_count += 1 + if desc.get("tradable"): + tradable_count += 1 + if icon == "🃏": + cards_count += amount + elif icon == "😀": + emotes_count += amount + elif icon == "🖼": + backgrounds_count += amount + elif icon == "💎": + gems_count += amount + 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}" + ) + if appid == 753: + stats += ( + f"\n🃏 Cards: {cards_count}" + f"\n😀 Emotes: {emotes_count}" + f"\n🖼 Backgrounds: {backgrounds_count}" + f"\n💎 Gems: {gems_count}" + ) + text = ( + f"📦 {inv_name} Inventory {bot_name}\n" + f"📄 Страница {page + 1}/{total_pages}\n\n" + f"{stats}\n\n" + + "\n".join(lines) + ) + 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 new file mode 100644 index 0000000..ab583ca --- /dev/null +++ b/bot/services/redeem.py @@ -0,0 +1,125 @@ +import asyncio +import re + +from bot.api.asf import get_all_bots, redeem_key + +KEY_PATTERN = r"[A-Z0-9]{5}(?:-[A-Z0-9]{5}){2}" + + +def extract_keys(text: str) -> list[str]: + keys = re.findall(KEY_PATTERN, text.upper()) + return list(dict.fromkeys(keys)) + + +def parse_redeem_result(result: str, bot_name: str | None = None) -> str: + result_lower = result.lower() + if bot_name: + for line in result.splitlines(): + 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: + return "rate_limited" + if "alreadypurchased" in result_lower or "already purchased" in result_lower: + return "already_owned" + if "duplicateactivationcode" in result_lower: + return "duplicate" + if "regionlocked" in result_lower: + return "region_locked" + if "invalidactivationcode" in result_lower or "invalid" in result_lower: + return "invalid" + return "unknown" + + +async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str: + success_count = 0 + failed_count = 0 + results = [] + for key in keys: + success, result = await redeem_key(bot_name, key) + await asyncio.sleep(2) + if not success or not result: + failed_count += 1 + results.append(f"❌ {key}: ASF ERROR") + continue + status = parse_redeem_result(result) + if status == "success": + success_count += 1 + results.append(f"✅ {key}: активировано") + elif status == "rate_limited": + failed_count += 1 + results.append(f"⏳ {key}: rate limit") + elif status == "already_owned": + failed_count += 1 + results.append(f"⚠️ {key}: игра уже есть") + elif status == "duplicate": + failed_count += 1 + results.append(f"❌ {key}: ключ уже использован") + elif status == "region_locked": + failed_count += 1 + results.append(f"🌍 {key}: регион лок") + elif status == "invalid": + failed_count += 1 + results.append(f"❌ {key}: неверный ключ") + 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]) + ) + return text[:4000] + ("\n\n...обрезано" if len(text) > 4000 else "") + + +async def redeem_all_bots_keys(keys: list[str]) -> str: + response = get_all_bots() + data = response.json() + bots = list(data.get("Result", {}).keys()) + success_count = 0 + failed_count = 0 + results = [] + for key in keys: + activated = False + key_report = [] + for bot_name in bots: + success, result = await redeem_key(bot_name, key) + await asyncio.sleep(2) + if not success or not result: + key_report.append(f"❌ {bot_name}: ASF ERROR") + continue + status = parse_redeem_result(result, bot_name) + if status == "success": + key_report.append(f"✅ {bot_name}: активировано") + success_count += 1 + activated = True + break + if status == "rate_limited": + key_report.append(f"⏳ {bot_name}: rate limit") + continue + if status == "already_owned": + key_report.append(f"⚠️ {bot_name}: игра уже есть") + continue + if status == "duplicate": + key_report.append(f"❌ {bot_name}: ключ уже использован") + break + if status == "region_locked": + key_report.append(f"🌍 {bot_name}: регион лок") + continue + if status == "invalid": + key_report.append(f"❌ {bot_name}: неверный ключ") + break + key_report.append(f"❔ {bot_name}: неизвестный ответ") + if not activated: + failed_count += 1 + 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]) + ) + return text[:4000] + ("\n\n...обрезано" if len(text) > 4000 else "") diff --git a/bot/services/twofa.py b/bot/services/twofa.py new file mode 100644 index 0000000..b98d08f --- /dev/null +++ b/bot/services/twofa.py @@ -0,0 +1,41 @@ +import asyncio + +from bot.api.asf import get_2fa_token, get_confirmations +from bot.state import twofa_tasks +from bot.ui.keyboards import twofa_keyboard + + +async def stop_twofa_task(message_id: int) -> None: + task = twofa_tasks.pop(message_id, None) + if task: + task.cancel() + try: + await task + except Exception: + pass + + +async def auto_update_2fa(message, bot_name: str) -> None: + last_code = None + message_id = message.message_id + try: + while True: + code = await get_2fa_token(bot_name) + if code != last_code: + confirmations = await get_confirmations(bot_name) + text = f"🔐 {bot_name}\n\n{code}" + try: + await message.edit_text( + text, + reply_markup=twofa_keyboard(bot_name, bool(confirmations)), + ) + last_code = code + except Exception: + break + await asyncio.sleep(15) + except asyncio.CancelledError: + pass + except Exception as error: + print(f"2FA updater error: {error}") + finally: + twofa_tasks.pop(message_id, None) diff --git a/bot/state.py b/bot/state.py new file mode 100644 index 0000000..1560a08 --- /dev/null +++ b/bot/state.py @@ -0,0 +1,7 @@ +console_users = set() +redeem_users = set() +twofa_tasks = {} +inventory_cache = {} +redeem_messages = {} +bot_redeem_users = {} +bot_redeem_messages = {} diff --git a/bot/ui/__init__.py b/bot/ui/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bot/ui/__init__.py @@ -0,0 +1 @@ + diff --git a/bot/ui/formatters.py b/bot/ui/formatters.py new file mode 100644 index 0000000..434a9b9 --- /dev/null +++ b/bot/ui/formatters.py @@ -0,0 +1,176 @@ +import html +import json +from datetime import datetime, timezone + +from bot.api.asf import get_asf_status +from bot.constants import GAMES + + +def get_currency_name(currency_id: int) -> str: + if currency_id == 1: + return "USD" + if currency_id == 3: + return "EUR" + if currency_id == 5: + return "RUB" + if currency_id == 18: + return "UAH" + if currency_id == 6: + return "PLN" + if currency_id == 0: + return "" + return "???" + + +def get_inventory_icon(desc: dict) -> str: + tags = desc.get("tags", []) + for tag in tags: + category = tag.get("category", "") + name = tag.get("localized_tag_name", "") + if category == "item_class": + if "Trading Card" in name: + return "🃏" + if "Emoticon" in name: + return "😀" + if "Profile Background" in name: + return "🖼" + if "Booster Pack" in name: + return "📦" + if "Steam Gems" in name: + return "💎" + if "Gift" in name: + return "🎁" + item_type = desc.get("type", "").lower() + if "knife" in item_type: + return "🔪" + if "pistol" in item_type: + return "🔫" + if "rifle" in item_type: + return "🎯" + if "graffiti" in item_type: + return "🎨" + if "music kit" in item_type: + return "🎵" + return "📦" + + +def get_uptime(start_time_str: str) -> str: + start_time_str = start_time_str[:26] + "Z" + start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00")) + now = datetime.now(timezone.utc) + delta = now - start_time + days = delta.days + hours, remainder = divmod(delta.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + return f"{days}д {hours}ч {minutes}м {seconds}с" + + +def format_asf(data: dict) -> str: + result = data.get("Result", {}) + current_version = result.get("Version") + latest_version = result.get("LatestVersion", current_version) + lines = [f"Версия ASF: {current_version}"] + if current_version != latest_version: + lines.append(f"Доступно обновление: {latest_version}") + memory_kb = result.get("MemoryUsage", 0) + memory_mb = memory_kb / 1024 + lines.append(f"Используется памяти: {memory_mb:.2f} MB") + lines.append(f"Работает: {get_uptime(result.get('ProcessStartTime'))}") + return "\n".join(lines) + + +def get_bot_status_icon(bot: dict) -> str: + online = bot.get("IsConnectedAndLoggedOn", False) + farming = bot.get("CardsFarmer", {}).get("NowFarming", False) + idle_games = bot.get("BotConfig", {}).get("GamesPlayedWhileIdle", []) + loaded = bool(bot.get("BotName")) and bool(bot.get("SteamID")) + if not loaded: + return "🔄" + if farming: + return "🎴" + if idle_games and online: + return "⌚" + if online: + return "🟢" + return "🔴" + + +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 "🃏 Карточки не фармится" + time = bot.get("CardsFarmer", {}).get("TimeRemaining", "00:00:00") + games_to_farm = len(bot.get("CardsFarmer", {}).get("GamesToFarm", [])) + twofa = "есть" if bot.get("HasMobileAuthenticator") else "нет" + balance = bot.get("WalletBalance", 0) / 100 + currency = get_currency_name(bot.get("WalletCurrency", 0)) + redeem = bot.get("GamesToRedeemInBackgroundCount", 0) + steam_id = bot.get("SteamID") + nickname = html.escape(str(bot.get("Nickname") or "")) + profile_url = f"https://steamcommunity.com/profiles/{steam_id}" + text = ( + f"{online} {bot.get('BotName') or 'Загрузка...'}" + 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] + if bot.get("IsConnectedAndLoggedOn"): + idle_text = f"⌚ Накрутка часов: включена ({', '.join(names)})" + else: + idle_text = ( + f"⌚ Накрутка часов: включена ({', '.join(names)}) " + "применится после запуска" + ) + text += f"{idle_text}\n" + else: + text += "⌚ Накрутка часов: выключена\n" + text += ( + f"🆔 : {steam_id}\n" + f"🔐 2FA: {twofa}\n\n" + f"💰 Баланс: {balance:.2f} {currency}\n" + ) + if redeem > 0: + text += f"\nКлючей в очереди: {redeem}\n" + if now_farming: + text += f"\nОсталось: {time}\n" + if games_to_farm > 0: + text += f"В очереди игр: {games_to_farm}\n" + return text + + +def get_asf_status_text() -> str: + response = get_asf_status() + if response.status_code != 200: + return f"Ошибка API: {response.status_code}" + data = response.json() + if not data.get("Success"): + return "ASF вернул ошибку" + return f"💻 Панель управления ASF\n\n{format_asf(data)}" + + +def get_farm_summary(bots: dict) -> str: + total_games = 0 + total_time_seconds = 0 + for bot in bots.values(): + farmer = bot.get("CardsFarmer", {}) + if farmer.get("NowFarming"): + games = farmer.get("CurrentGamesFarming", []) + total_games += len(games) + 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 "На аккаунтах нечего не фармиться" + hours = total_time_seconds // 3600 + minutes = (total_time_seconds % 3600) // 60 + return ( + f"Игр фармится: {total_games}\n" + f"Осталось времени: {hours}ч {minutes}м\n" + f"Карт осталось: ~{total_games * 3}" + ) + + +def format_bot(bot_data: dict) -> str: + return json.dumps(bot_data, indent=2, ensure_ascii=False) diff --git a/bot/ui/keyboards.py b/bot/ui/keyboards.py new file mode 100644 index 0000000..482018b --- /dev/null +++ b/bot/ui/keyboards.py @@ -0,0 +1,192 @@ +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup + +from bot.ui.formatters import get_bot_status_icon + + +def main_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="🆙 Обновить", callback_data="refresh")], + [InlineKeyboardButton(text="🤖 Боты", callback_data="bots")], + [ + InlineKeyboardButton( + text="🔑 Активация ключей на всех аккаунтах", + callback_data="redeem_keys", + ) + ], + [InlineKeyboardButton(text="📁 Плагины", callback_data="plugins")], + [InlineKeyboardButton(text="💻 Консоль", callback_data="console")], + [ + InlineKeyboardButton( + text="♻ Перезапустить ASF", + callback_data="restart_asf_confirm", + ) + ], + ] + ) + + +def back_keyboard(callback_data: str = "back") -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="🔙 Назад", callback_data=callback_data)] + ] + ) + + +def games_keyboard(bot_name: str, is_enabled: bool) -> InlineKeyboardMarkup: + text = "Остановить накрутку CS2" if is_enabled else "⌚ Накрутить часы CS2" + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text=text, callback_data=f"farm|{bot_name}|730")], + [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")], + ] + ) + + +def bots_keyboard(bots: dict) -> InlineKeyboardMarkup: + keyboard = [] + for name, bot in bots.items(): + status = get_bot_status_icon(bot) + loaded = bool(bot.get("BotName")) and bool(bot.get("SteamID")) + callback_data = f"bot_{name}" if loaded else "bot_loading" + keyboard.append( + [InlineKeyboardButton(text=f"{status} {name}", callback_data=callback_data)] + ) + keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back")]) + return InlineKeyboardMarkup(inline_keyboard=keyboard) + + +def bot_details_keyboard( + bot_name: str, has_mobile_authenticator: bool +) -> InlineKeyboardMarkup: + keyboard = [] + if has_mobile_authenticator: + keyboard.append( + [InlineKeyboardButton(text="🔐 2FA", callback_data=f"2fa_{bot_name}")] + ) + keyboard.append( + [InlineKeyboardButton(text="⌚ Накрутка часов", callback_data=f"games_{bot_name}")] + ) + keyboard.append( + [ + InlineKeyboardButton( + text="🔑 Активировать ключ", + callback_data=f"redeem_bot_{bot_name}", + ) + ] + ) + keyboard.append( + [InlineKeyboardButton(text="📦 Инвентарь", callback_data=f"inventory_{bot_name}")] + ) + keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data="bots")]) + return InlineKeyboardMarkup(inline_keyboard=keyboard) + + +def twofa_keyboard(bot_name: str, has_confirmations: bool) -> InlineKeyboardMarkup: + keyboard = [] + if has_confirmations: + keyboard.append( + [ + InlineKeyboardButton( + text="Подтверждения", + callback_data=f"confirm_list_{bot_name}", + ) + ] + ) + keyboard.append( + [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")] + ) + return InlineKeyboardMarkup(inline_keyboard=keyboard) + + +def confirmations_keyboard(bot_name: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Подтвердить всё", + callback_data=f"confirm_all_{bot_name}", + ) + ], + [InlineKeyboardButton(text="🔙 Назад", callback_data=f"2fa_{bot_name}")], + ] + ) + + +def confirm_action_keyboard(confirm_data: str, cancel_data: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Да", callback_data=confirm_data)], + [InlineKeyboardButton(text="Нет", callback_data=cancel_data)], + ] + ) + + +def inventory_menu_keyboard( + bot_name: str, cs2_count: int, steam_count: int +) -> InlineKeyboardMarkup: + keyboard = [] + if cs2_count: + keyboard.append( + [ + InlineKeyboardButton( + text=f"CS2 ({cs2_count})", + callback_data=f"inv_cs2_{bot_name}", + ) + ] + ) + if steam_count: + keyboard.append( + [ + InlineKeyboardButton( + text=f"Steam ({steam_count})", + callback_data=f"inv_steam_{bot_name}", + ) + ] + ) + keyboard.append( + [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")] + ) + return InlineKeyboardMarkup(inline_keyboard=keyboard) + + +def inventory_page_keyboard( + inventory_type: str, bot_name: str, page: int, total_pages: int +) -> InlineKeyboardMarkup: + nav_buttons = [] + if page > 0: + nav_buttons.append( + InlineKeyboardButton( + text="◀", + callback_data=f"invpage|{inventory_type}|{bot_name}|{page - 1}", + ) + ) + if page < total_pages - 1: + nav_buttons.append( + InlineKeyboardButton( + text="▶", + callback_data=f"invpage|{inventory_type}|{bot_name}|{page + 1}", + ) + ) + keyboard = [] + if nav_buttons: + keyboard.append(nav_buttons) + keyboard.append( + [InlineKeyboardButton(text="🔙 Назад", callback_data=f"inventory_{bot_name}")] + ) + return InlineKeyboardMarkup(inline_keyboard=keyboard) + + +def console_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="🔙 Назад", callback_data="console_exit")] + ] + ) + + +def delete_message_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[[InlineKeyboardButton(text="Удалить", callback_data="delete_msg")]] + ) diff --git a/main.py b/main.py index cacbb86..228265a 100644 --- a/main.py +++ b/main.py @@ -1,995 +1,34 @@ -import asyncio, aiohttp, json, html, requests, re, os -from datetime import datetime, timezone -from typing import cast +import asyncio + from aiogram import Bot, Dispatcher -from aiogram.filters import Command, BaseFilter -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery, Message from aiogram.client.default import DefaultBotProperties -from dotenv import load_dotenv -load_dotenv() -API_TOKEN = os.getenv("API_TOKEN") -ADMIN_ID = int(os.getenv("ADMIN_ID")) # type: ignore -ASF_URL = os.getenv("ASF_URL") -ASF_PASSWORD = os.getenv("ASF_PASSWORD") +from bot.config import API_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 +from bot.handlers.messages import router as messages_router +from bot.handlers.navigation import router as navigation_router +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 -PAGE_SIZE = 40 -GAMES = {730: "CS2", 440: "TF2", 570: "Dota 2"} -appid = 753 -contextid = 6 -console_users, redeem_users = set(), set() -twofa_tasks, inventory_cache, redeem_messages, bot_redeem_users, bot_redeem_messages = {}, {}, {}, {}, {} - -bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode="HTML")) # type: ignore +bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode="HTML")) # type: ignore[arg-type] dp = Dispatcher() -class AdminFilter(BaseFilter): - async def __call__(self, message: Message) -> bool: - return message.from_user.id == ADMIN_ID # type: ignore +dp.include_router(start_router) +dp.include_router(navigation_router) +dp.include_router(console_router) +dp.include_router(bots_router) +dp.include_router(twofa_router) +dp.include_router(inventory_router) +dp.include_router(redeem_router) +dp.include_router(messages_router) -class AdminCallbackFilter(BaseFilter): - async def __call__(self, callback: CallbackQuery) -> bool: - return callback.from_user.id == ADMIN_ID - -def asf_get(path: str): - return requests.get(f"http://127.0.0.1:1242{path}", headers={"Authentication": ASF_PASSWORD}) -def get_currency_name(currency_id: int) -> str: - if currency_id == 1: - return "USD" - elif currency_id == 3: - return "EUR" - elif currency_id == 5: - return "RUB" - elif currency_id == 18: - return "UAH" - elif currency_id == 6: - return "PLN" - elif currency_id == 0: - return "" - else: - return "???" - -def get_inventory_icon(desc: dict) -> str: - tags = desc.get("tags", []) - for tag in tags: - category = tag.get("category", "") - name = tag.get("localized_tag_name", "") - if category == "item_class": - if "Trading Card" in name: - return "🃏" - if "Emoticon" in name: - return "😀" - if "Profile Background" in name: - return "🖼" - if "Booster Pack" in name: - return "📦" - if "Steam Gems" in name: - return "💎" - if "Gift" in name: - return "🎁" - item_type = desc.get("type", "").lower() - if "knife" in item_type: - return "🔪" - if "pistol" in item_type: - return "🔫" - if "rifle" in item_type: - return "🎯" - if "graffiti" in item_type: - return "🎨" - if "music kit" in item_type: - return "🎵" - return "📦" - -def format_asf(data: dict) -> str: - result = data.get("Result", {}) - config = result.get("GlobalConfig", {}) - current_version = result.get("Version") - latest_version = result.get("LatestVersion", current_version) - lines = [f"Версия ASF: {current_version}"] - if current_version != latest_version: - lines.append(f"Доступно обновление: {latest_version}") - memory_kb = result.get("MemoryUsage", 0) - memory_mb = memory_kb / 1024 - lines.append(f"Используется памяти: {memory_mb:.2f} MB") - lines.append(f"Работает: {get_uptime(result.get('ProcessStartTime'))}") - return "\n".join(lines) - -def get_uptime(start_time_str: str) -> str: - start_time_str = start_time_str[:26] + "Z" - start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00")) - now = datetime.now(timezone.utc) - delta = now - start_time - days = delta.days - hours, remainder = divmod(delta.seconds, 3600) - minutes, seconds = divmod(remainder, 60) - return f"{days}д {hours}ч {minutes}м {seconds}с" - -def get_bot_status_icon(bot: dict) -> str: - online = bot.get("IsConnectedAndLoggedOn", False) - farming = bot.get("CardsFarmer", {}).get("NowFarming", False) - idle_games = bot.get("BotConfig", {}).get("GamesPlayedWhileIdle", []) - loaded = bool(bot.get("BotName")) and bool(bot.get("SteamID")) - if not loaded: - return "🔄" - if farming: - return "🎴" - if idle_games and online: - return "⌚" - if online: - return "🟢" - return "🔴" - -def format_bot_ui(bot: dict) -> str: - online = get_bot_status_icon(bot) - now_farming = bot.get("CardsFarmer", {}).get("NowFarming") - if now_farming: - status_line = "🃏 Идет фарм карточек" - else: - status_line = "🃏 Карточки не фармится" - time = bot.get("CardsFarmer", {}).get("TimeRemaining", "00:00:00") - games_to_farm = len(bot.get("CardsFarmer", {}).get("GamesToFarm", [])) - twofa = "есть" if bot.get("HasMobileAuthenticator") else "нет" - balance = bot.get("WalletBalance", 0) / 100 - currency = get_currency_name(bot.get("WalletCurrency", 0)) - redeem = bot.get("GamesToRedeemInBackgroundCount", 0) - steam_id = bot.get("SteamID") - nickname = html.escape(str(bot.get("Nickname") or "")) - profile_url = f"https://steamcommunity.com/profiles/{steam_id}" - text = ( - f"{online}" - f" {bot.get('BotName') or 'Загрузка...'}" - f"({nickname})\n" - f"{status_line}\n") - idle_games = bot.get("BotConfig", {}).get("GamesPlayedWhileIdle", []) - if idle_games: - names = [GAMES.get(g, str(g)) for g in idle_games] - if bot.get("IsConnectedAndLoggedOn"): - idle_text = f"⌚ Накрутка часов: включена ({', '.join(names)})" - else: - idle_text = f"⌚ Накрутка часов: включена ({', '.join(names)}) применится после запуска" - text += f"{idle_text}\n" - else: - text += "⌚ Накрутка часов: выключена\n" - text += ( - f"🆔 : {steam_id}\n" - f"🔐 2FA: {twofa}\n\n" - f"💰 Баланс: {balance:.2f} {currency}\n") - if redeem > 0: - text += f"\nКлючей в очереди: {redeem}\n" - if now_farming: - text += f"\nОсталось: {time}\n" - if games_to_farm > 0: - text += f"В очереди игр: {games_to_farm}\n" - return text - -def get_asf_status_text(): - response = requests.get(ASF_URL, headers={"Authentication": ASF_PASSWORD}) # type: ignore - if response.status_code != 200: - return f"Ошибка API: {response.status_code}" - data = response.json() - if not data.get("Success"): - return "ASF вернул ошибку" - status_text = format_asf(data) - return f"💻 Панель управления ASF\n\n{status_text}" - -def get_farm_summary(bots: dict) -> str: - total_games = 0 - total_time_seconds = 0 - for bot in bots.values(): - farmer = bot.get("CardsFarmer", {}) - if farmer.get("NowFarming"): - games = farmer.get("CurrentGamesFarming", []) - total_games += len(games) - time_str = farmer.get("TimeRemaining", "00:00:00") - h, m, s = map(int, time_str.split(":")) - total_time_seconds += h * 3600 + m * 60 + s - if total_games == 0: - return "На аккаунтах нечего не фармиться" - hours = total_time_seconds // 3600 - minutes = (total_time_seconds % 3600) // 60 - return ( - f"Игр фармится: {total_games}\n" - f"Осталось времени: {hours}ч {minutes}м\n" - f"Карт осталось: ~{total_games * 3}") - -def format_bot(bot_data: dict) -> str: - return json.dumps(bot_data, indent=2, ensure_ascii=False) - -def main_keyboard(): - return InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🆙 Обновить", callback_data="refresh")], - [InlineKeyboardButton(text="🤖 Боты", callback_data="bots")], - [InlineKeyboardButton(text="🔑 Активация ключей на всех аккаунтах", callback_data="redeem_keys")], - [InlineKeyboardButton(text="📁 Плагины", callback_data="plugins")], - [InlineKeyboardButton(text="💻 Консоль", callback_data="console")], - [InlineKeyboardButton(text="♻ Перезапустить ASF", callback_data="restart_asf_confirm")]]) - -def back_keyboard(): - return InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data="back")]]) - -def games_keyboard(bot_name, is_enabled): - if is_enabled: - text = "Остановить накрутку CS2" - else: - text = "⌚ Накрутить часы CS2" - return InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text=text, callback_data=f"farm|{bot_name}|730")], - [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]]) - -def bots_keyboard(bots: dict): - keyboard = [] - for name, bot in bots.items(): - status = get_bot_status_icon(bot) - loaded = (bool(bot.get("BotName")) and bool(bot.get("SteamID"))) - if not loaded: - callback_data = "bot_loading" - else: - callback_data = f"bot_{name}" - keyboard.append([InlineKeyboardButton(text=f"{status} {name}", callback_data=callback_data)]) - keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back")]) - return InlineKeyboardMarkup(inline_keyboard=keyboard) - -def is_bot_playing(bot_name: str) -> bool: - response = asf_get("/Api/Bot/ASF") - if response.status_code != 200: - return False - data = response.json() - bot = data.get("Result", {}).get(bot_name, {}) - return bool(bot.get("PlayingBlocked", False)) or bool(bot.get("CurrentGamesPlayed", [])) - -def is_idle_enabled(bot_name: str, app_id: int) -> bool: - response = requests.get(f"http://127.0.0.1:1242/Api/Bot/{bot_name}", headers={"Authentication": ASF_PASSWORD}) - if response.status_code != 200: - return False - data = response.json() - bot = data.get("Result", {}).get(bot_name, {}) - config = bot.get("BotConfig", {}) - return app_id in config.get("GamesPlayedWhileIdle", []) - -async def get_2fa_code(bot_name: str) -> str: - async with aiohttp.ClientSession() as session: - async with session.get(f"http://127.0.0.1:1242/Api/Bot/{bot_name}/TwoFactorAuthentication/Token", headers={"Authentication": ASF_PASSWORD}) as response: # type: ignore - if response.status != 200: - return f"Ошибка HTTP {response.status}" - data = await response.json() - if not data.get("Success"): - return "Ошибка ASF" - try: - return data["Result"][bot_name]["Result"] - except: - return "Не удалось получить код" - -async def get_confirmations(bot_name: str): - async with aiohttp.ClientSession() as session: - async with session.get(f"http://127.0.0.1:1242/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations", headers={"Authentication": ASF_PASSWORD}) as response: # type: ignore - if response.status != 200: - return [] - data = await response.json() - if not data.get("Success"): - return [] - try: - return data["Result"][bot_name]["Result"] - except: - return [] - -async def get_inventory(bot_name: str, appid: int, contextid: int = 2): - url = (f"http://127.0.0.1:1242/Api/Bot/{bot_name}/Inventory/{appid}/{contextid}") - async with aiohttp.ClientSession() as session: - async with session.get(url, headers={"Authentication": ASF_PASSWORD}) as response: # type: ignore - if response.status != 200: - return None - data = await response.json() - if not data.get("Success"): - return None - try: - return data["Result"] - except: - return None - -async def render_inventory_page(callback: CallbackQuery, bot_name: str, inventory_type: str, appid: int, contextid: int, page: int): - await callback.message.edit_text("Загрузка inventory...") # type: ignore - inventory = await get_inventory(bot_name, appid, contextid) - if not inventory: - await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore - return - bot_inventory = inventory.get(bot_name, {}) - assets = bot_inventory.get("Assets", []) - descriptions = bot_inventory.get("Descriptions", []) - if not assets: - await callback.message.edit_text("Инвентарь пуст", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data=f"inventory_{bot_name}")]])) # type: ignore - return - desc_map = {} - for desc in descriptions: - key = (str(desc.get("classid")), str(desc.get("instanceid"))) - desc_map[key] = desc - total_pages = (len(assets) + PAGE_SIZE - 1) // PAGE_SIZE - start = page * PAGE_SIZE - end = start + PAGE_SIZE - page_assets = assets[start:end] - lines = [] - marketable_count = 0 - tradable_count = 0 - cards_count = 0 - emotes_count = 0 - backgrounds_count = 0 - gems_count = 0 - for asset in page_assets: - key = (str(asset.get("classid")), str(asset.get("instanceid"))) - desc = desc_map.get(key, {}) - name = desc.get("market_name", "Unknown") - amount = asset.get("amount", 1) - tradable = ("🔄" if desc.get("tradable") else "❌") - marketable = ("💰" if desc.get("marketable") else "🔒") - icon = get_inventory_icon(desc) - if desc.get("marketable"): - marketable_count += 1 - if desc.get("tradable"): - tradable_count += 1 - if icon == "🃏": - cards_count += amount - elif icon == "😀": - emotes_count += amount - elif icon == "🖼": - backgrounds_count += amount - elif icon == "💎": - gems_count += amount - 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}") - if appid == 753: - stats += ( - f"\n🃏 Cards: {cards_count}" - f"\n😀 Emotes: {emotes_count}" - f"\n🖼 Backgrounds: {backgrounds_count}" - f"\n💎 Gems: {gems_count}") - text = ( - f"📦 {inv_name} Inventory {bot_name}\n" - f"📄 Страница {page + 1}/{total_pages}\n\n" - f"{stats}\n\n" - + "\n".join(lines)) - nav_buttons = [] - if page > 0: - nav_buttons.append(InlineKeyboardButton(text="◀", callback_data=f"invpage|{inventory_type}|{bot_name}|{page - 1}")) - if page < total_pages - 1: - nav_buttons.append(InlineKeyboardButton(text="▶", callback_data=f"invpage|{inventory_type}|{bot_name}|{page + 1}")) - keyboard = [] - if nav_buttons: - keyboard.append(nav_buttons) - keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data=f"inventory_{bot_name}")]) - await callback.message.edit_text(text[:4000], reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard)) # type: ignore - -async def reload_bot(bot_name: str): - try: - requests.post("http://127.0.0.1:1242/Api/Command", headers={"Authentication": ASF_PASSWORD}, json={"Command": f"!reload {bot_name}"}) - except: - pass - -async def stop_twofa_task(message_id: int): - task = twofa_tasks.pop(message_id, None) - if task: - task.cancel() - try: - await task - except: - pass - -async def redeem_key(bot_name: str, key: str): - async with aiohttp.ClientSession() as session: - async with session.post("http://127.0.0.1:1242/Api/Command", headers={"Authentication": ASF_PASSWORD}, json={"Command": f"!redeem {bot_name} {key}"}) as response: # type: ignore - if response.status != 200: - return False, f"HTTP_ERROR_{response.status}" - data = await response.json() - if not data.get("Success"): - return False, "ASF_ERROR" - return True, str(data.get("Result", "")) - -@dp.callback_query(lambda c: c.data == "bot_loading", AdminCallbackFilter()) -async def bot_loading_handler(callback: CallbackQuery): - await callback.answer("Аккаунт ещё загружается. Попробуйте через несколько секунд.", show_alert=True) - -async def auto_update_2fa(message, bot_name): - last_code = None - message_id = message.message_id - try: - while True: - code = await get_2fa_code(bot_name) - if code != last_code: - confirmations = await get_confirmations(bot_name) - keyboard = [] - if confirmations: - keyboard.append([InlineKeyboardButton(text="Подтверждения", callback_data=f"confirm_list_{bot_name}")]) - keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]) - text = ( - f"🔐 {bot_name}\n\n" - f"{code}") - try: - await message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard)) - last_code = code - except: - break - await asyncio.sleep(15) - except asyncio.CancelledError: - pass - except Exception as e: - print(f"2FA updater error: {e}") - finally: - twofa_tasks.pop(message_id, None) - -@dp.message(Command("start"), AdminFilter()) -async def start_handler(message: Message): - try: - await message.delete() - except: - pass - try: - text = get_asf_status_text() - await message.answer(text, reply_markup=main_keyboard()) - except Exception as e: - await message.answer(f"Ошибка: {e}") - -@dp.callback_query(lambda c: c.data == "console", AdminCallbackFilter()) -async def console_enter(callback: CallbackQuery): - await callback.answer() - console_users.add(callback.from_user.id) - await callback.message.edit_text("💻 Консоль ASF\n\nОтправь команду (без !)", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data="console_exit")]])) # type: ignore - -@dp.callback_query(lambda c: c.data == "console_exit", AdminCallbackFilter()) -async def console_exit(callback: CallbackQuery): - await callback.answer() - console_users.discard(callback.from_user.id) - text = get_asf_status_text() - await callback.message.edit_text(text, reply_markup=main_keyboard()) # type: ignore - -@dp.callback_query(lambda c: c.data == "delete_msg", AdminCallbackFilter()) -async def delete_message(callback: CallbackQuery): - await callback.answer() - try: - await callback.message.delete() # type: ignore - except: - pass - -@dp.callback_query(lambda c: c.data == "bots", AdminCallbackFilter()) -async def bots_handler(callback: CallbackQuery): - await stop_twofa_task(callback.message.message_id) # type: ignore - await callback.answer() - try: - response = requests.get("http://127.0.0.1:1242/Api/Bot/ASF", headers={"Authentication": ASF_PASSWORD}) - if response.status_code != 200: - await callback.message.edit_text("Ошибка API") # type: ignore - return - data = response.json() - if not data.get("Success"): - await callback.message.edit_text("ASF вернул ошибку") # type: ignore - return - bots = data.get("Result", {}) - count = len(bots) - summary = get_farm_summary(bots) - text = ( - f"🤖 Ботов всего: {count}\n\n" - f"{summary}") - await callback.message.edit_text(text, reply_markup=bots_keyboard(bots)) # type: ignore - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data and c.data.startswith("bot_"), AdminCallbackFilter()) -async def bot_selected(callback: CallbackQuery): - await stop_twofa_task(callback.message.message_id) # type: ignore - await callback.answer() - if not callback.data: - return - bot_name = callback.data.replace("bot_", "") - try: - response = asf_get("/Api/Bot/ASF") - data = response.json() - bots = data.get("Result", {}) - bot = bots.get(bot_name) - if not bot or not bot.get("BotName") or not bot.get("SteamID"): - await callback.answer("Аккаунт ещё загружается. Попробуйте через несколько секунд.", show_alert=True) - return - if not bot: - await callback.message.edit_text("Бот не найден") # type: ignore - return - text = format_bot_ui(bot) - keyboard = [] - if bot.get("HasMobileAuthenticator"): - keyboard.append([InlineKeyboardButton(text="🔐 2FA", callback_data=f"2fa_{bot_name}")]) - keyboard.append([InlineKeyboardButton(text="⌚ Накрутка часов", callback_data=f"games_{bot_name}")]) - keyboard.append([InlineKeyboardButton(text="🔑 Активировать ключ", callback_data=f"redeem_bot_{bot_name}")]) - keyboard.append([InlineKeyboardButton(text="📦 Инвентарь", callback_data=f"inventory_{bot_name}")]) - keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data="bots")]) - - await callback.message.edit_text(text, disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard)) # type: ignore - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data and c.data.startswith("games_"), AdminCallbackFilter()) -async def games(callback: CallbackQuery): - await callback.answer() - if not callback.data: - return - bot_name = callback.data.replace("games_", "") - idle_enabled = is_idle_enabled(bot_name, 730) - await callback.message.edit_text("⌚ Выберите игру для накрутки часов", reply_markup=games_keyboard(bot_name, idle_enabled)) # type: ignore - -@dp.callback_query(lambda c: c.data == "back", AdminCallbackFilter()) -async def back_button(callback: CallbackQuery): - redeem_users.discard(callback.from_user.id) - redeem_messages.pop(callback.from_user.id, None) - console_users.discard(callback.from_user.id) - bot_redeem_users.pop(callback.from_user.id, None) - bot_redeem_messages.pop(callback.from_user.id, None) - await stop_twofa_task(callback.message.message_id) # type: ignore - await callback.answer() - try: - text = get_asf_status_text() - await callback.message.edit_text(text, reply_markup=main_keyboard()) # type: ignore - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data == "refresh", AdminCallbackFilter()) -async def refresh_handler(callback: CallbackQuery): - await callback.answer() - try: - text = get_asf_status_text() - await callback.message.edit_text(text, reply_markup=main_keyboard()) # type: ignore - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data == "restart_asf_confirm", AdminCallbackFilter()) -async def restart_asf_confirm(callback: CallbackQuery): - await callback.answer() - await callback.message.edit_text( # type: ignore - "♻ Точно перезапустить ASF?\n\n" - "Во время перезапуска IPC будет недоступен несколько секунд.", - reply_markup=InlineKeyboardMarkup( - inline_keyboard=[ - [InlineKeyboardButton(text="Да", callback_data="restart_asf")], - [InlineKeyboardButton(text="Нет", callback_data="back")]])) - -@dp.callback_query(lambda c: c.data == "restart_asf", AdminCallbackFilter()) -async def restart_asf(callback: CallbackQuery): - await callback.answer() - try: - await callback.message.edit_text("♻ ASF перезапускается...") # type: ignore - response = requests.post("http://127.0.0.1:1242/Api/Command", headers={"Authentication": ASF_PASSWORD}, json={"Command": "!restart"}) - if response.status_code != 200: - await callback.message.edit_text(f"Ошибка HTTP {response.status_code}", reply_markup=main_keyboard()) # type: ignore - return - await asyncio.sleep(6) - text = get_asf_status_text() - await callback.message.edit_text(f"ASF успешно перезапущен\n\n{text}", reply_markup=main_keyboard()) # type: ignore - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}", reply_markup=main_keyboard()) # type: ignore - -@dp.callback_query(lambda c: c.data and c.data.startswith("2fa_") and not c.data.startswith("2fa_refresh_"), AdminCallbackFilter()) -async def twofa_handler(callback: CallbackQuery): - await callback.answer() - if not callback.data: - return - bot_name = callback.data.replace("2fa_", "") - message_id = callback.message.message_id # type: ignore - old_task = twofa_tasks.pop(message_id, None) - if old_task: - old_task.cancel() - try: - await old_task - except: - pass - await callback.message.edit_text( # type: ignore - f"🔐 {bot_name}\n\nЗагрузка 2FA...", - reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]])) - task = asyncio.create_task(auto_update_2fa(callback.message, bot_name)) - twofa_tasks[message_id] = task - -@dp.callback_query(lambda c: c.data and c.data.startswith("confirm_") and not c.data.startswith("confirm_list_") and not c.data.startswith("confirm_all_"), AdminCallbackFilter()) -async def confirm_trades(callback: CallbackQuery): - await callback.answer() - bot_name = callback.data.replace("confirm_", "") # type: ignore - try: - response = requests.post(f"http://127.0.0.1:1242/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations/Accept", headers={"Authentication": ASF_PASSWORD}) - if response.status_code != 200: - await callback.answer("Ошибка HTTP", show_alert=True) - return - await callback.answer("Подтверждено", show_alert=True) - await twofa_handler(callback) - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data == "plugins", AdminCallbackFilter()) -async def plugins_handler(callback: CallbackQuery): - await stop_twofa_task(callback.message.message_id) # type: ignore - await callback.answer() - try: - response = requests.get("http://127.0.0.1:1242/Api/Plugins", headers={"Authentication": ASF_PASSWORD}) - if response.status_code != 200: - await callback.message.edit_text("Ошибка API") # type: ignore - return - data = response.json() - if not data.get("Success"): - await callback.message.edit_text("ASF вернул ошибку") # type: ignore - return - plugins = data.get("Result", []) - if not plugins: - text = "Плагины не установлены" - else: - text = "📁 Плагины ASF:\n\n" - for plugin in plugins: - name = plugin.get("Name", "???") - version = plugin.get("Version", "???") - text += f"{name} ({version})\n" - await callback.message.edit_text(text.strip(), reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data="back")]])) # type: ignore - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data and c.data.startswith("confirm_list_"), AdminCallbackFilter()) -async def confirm_list(callback: CallbackQuery): - await stop_twofa_task(callback.message.message_id) # type: ignore - await callback.answer() - if not callback.data: - return - bot_name = callback.data.replace("confirm_list_", "") - try: - confirmations = await get_confirmations(bot_name) - if not confirmations: - text = "Подтверждений нет" - else: - text = f" {bot_name}\n\nПодтверждения:\n\n" - for conf in confirmations: - text += ( - f"- {conf['type_name']}\n" - f"ID: {conf['id']}\n\n") - await callback.message.edit_text(text, # type: ignore - reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Подтвердить всё", callback_data=f"confirm_all_{bot_name}")], - [InlineKeyboardButton(text="🔙 Назад", callback_data=f"2fa_{bot_name}")]])) - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data and c.data.startswith("confirm_all_"), AdminCallbackFilter()) -async def confirm_all(callback: CallbackQuery): - await stop_twofa_task(callback.message.message_id) # type: ignore - await callback.answer() - if not callback.data: - return - bot_name = callback.data.replace("confirm_all_", "") - try: - response = requests.post(f"http://127.0.0.1:1242/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations/Accept", headers={"Authentication": ASF_PASSWORD}) - if response.status_code != 200: - await callback.answer("Ошибка HTTP", show_alert=True) - return - # заново получаем список - confirmations = await get_confirmations(bot_name) - if not confirmations: - text = f" {bot_name}\n\nПодтверждений больше нет" - else: - text = f" {bot_name}\n\nПодтверждения:\n\n" - for conf in confirmations: - text += ( - f"- {conf['type_name']}\n" - f"ID: {conf['id']}\n\n") - await callback.message.edit_text( # type: ignore - text, - reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Подтвердить всё", callback_data=f"confirm_all_{bot_name}")], - [InlineKeyboardButton(text="Назад", callback_data=f"2fa_{bot_name}")]])) - await callback.answer(" Все подтверждено", show_alert=True) - except Exception as e: - await callback.message.edit_text(f"Ошибка: {e}") # type: ignore - -@dp.callback_query(lambda c: c.data and c.data.startswith("farm|"), AdminCallbackFilter()) -async def farm_game(callback: CallbackQuery): - await callback.answer() - if not callback.data: - return - _, bot_name, app_id = callback.data.split("|") - app_id = int(app_id) - try: - # получаем текущий конфиг - response = requests.get(f"http://127.0.0.1:1242/Api/Bot/{bot_name}", headers={"Authentication": ASF_PASSWORD}) - data = response.json() - bot_data = data.get("Result", {}).get(bot_name, {}) - config = bot_data.get("BotConfig", {}) - idle_games = config.get("GamesPlayedWhileIdle", []) - if app_id in idle_games: - idle_games.remove(app_id) - action = "Idle выключен" - else: - idle_games.append(app_id) - action = "Idle включен" - config["GamesPlayedWhileIdle"] = idle_games - save = requests.post( - f"http://127.0.0.1:1242/Api/Bot/{bot_name}", headers={"Authentication": ASF_PASSWORD}, json={"BotConfig": config}) - if save.status_code != 200: - await callback.answer("Ошибка сохранения", show_alert=True) - return - asyncio.create_task(reload_bot(bot_name)) - await callback.answer(action, show_alert=True) - try: - await callback.message.edit_reply_markup(reply_markup=games_keyboard(bot_name, app_id in idle_games)) # type: ignore - except: - pass - except Exception as e: - await callback.answer(f"Ошибка: {e}", show_alert=True) - -@dp.callback_query(lambda c: c.data and c.data.startswith("inventory_"), AdminCallbackFilter()) -async def inventory_menu(callback: CallbackQuery): - await callback.answer() - bot_name = callback.data.replace("inventory_", "") # type: ignore - await callback.message.edit_text("Проверка inventory...") # type: ignore - cs2_inventory = await get_inventory(bot_name, 730, 2) - steam_inventory = await get_inventory(bot_name, 753, 6) - cs2_assets = [] - steam_assets = [] - try: - cs2_assets = (cs2_inventory.get(bot_name, {}).get("Assets", [])) # type: ignore - except: - pass - try: - steam_assets = (steam_inventory.get(bot_name, {}).get("Assets", [])) # type: ignore - except: - pass - if not cs2_assets and not steam_assets: - await callback.message.edit_text(f"📦 Инвентарь {bot_name} пуст", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]])) # type: ignore - return - keyboard = [] - if cs2_assets: - keyboard.append([InlineKeyboardButton(text=f"CS2 ({len(cs2_assets)})", callback_data=f"inv_cs2_{bot_name}")]) - if steam_assets: - keyboard.append([InlineKeyboardButton(text=f"Steam ({len(steam_assets)})", callback_data=f"inv_steam_{bot_name}")]) - keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]) - await callback.message.edit_text( # type: ignore - ( - f"📦 Инвентарь {bot_name}\n\n" - "🔄 — можно трейдить\n" - "💰 — можно продавать\n" - "🔒 — нельзя продавать\n" - "❌ — нельзя трейдить" - ), - reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard) - ) - -@dp.callback_query(lambda c: c.data and c.data.startswith("inv_cs2_"), AdminCallbackFilter()) -async def cs2_inventory(callback: CallbackQuery): - await callback.answer() - bot_name = callback.data.replace("inv_cs2_", "") # type: ignore - await render_inventory_page(callback, bot_name, "cs2", 730, 2, 0) - -@dp.callback_query(lambda c: c.data and c.data.startswith("inv_steam_"), AdminCallbackFilter()) -async def steam_inventory(callback: CallbackQuery): - await callback.answer() - bot_name = callback.data.replace("inv_steam_", "") # type: ignore - await render_inventory_page(callback, bot_name, "steam", 753, 6, 0) - -@dp.callback_query(lambda c: c.data and c.data.startswith("invpage|"), AdminCallbackFilter()) -async def inventory_page(callback: CallbackQuery): - await callback.answer() - _, inv_type, bot_name, page = callback.data.split("|") # type: ignore - page = int(page) - if inv_type == "cs2": - await render_inventory_page(callback, bot_name, "cs2", 730, 2, page) - else: - await render_inventory_page(callback, bot_name, "steam", 753, 6, page) - -@dp.callback_query(lambda c: c.data == "redeem_keys", AdminCallbackFilter()) -async def redeem_keys_menu(callback: CallbackQuery): - await callback.answer() - redeem_users.add(callback.from_user.id) - redeem_messages[callback.from_user.id] = callback.message - await callback.message.edit_text( # type: ignore - "Отправьте ключ Steam для активации\n\n" - "Можно отправить сразу несколько ключей.\n\n" - "Пример:\n" - "AAAAA-BBBBB-CCCCC\nDDDDD-EEEEE-FFFFF", - reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data="back")]])) - -@dp.callback_query(lambda c: c.data and c.data.startswith("redeem_bot_"), AdminCallbackFilter()) -async def redeem_bot_menu(callback: CallbackQuery): - await callback.answer() - bot_name = callback.data.replace("redeem_bot_", "") # type: ignore - 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 - f"Отправьте ключ Steam для активации на аккаунте:\n" - f"{bot_name}\n\n" - f"Можно отправить сразу несколько ключей.", - reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]])) - -@dp.message(AdminFilter()) -async def text_router(message: Message): - # console mode - if message.from_user.id in console_users: # type: ignore - command = message.text.strip() # type: ignore - try: - await message.delete() - except: - pass - try: - response = requests.post("http://127.0.0.1:1242/Api/Command", headers={"Authentication": ASF_PASSWORD}, json={"Command": f"!{command}"}) - if response.status_code != 200: - text = f"Ошибка HTTP {response.status_code}" - else: - data = response.json() - if not data.get("Success"): - text = "Ошибка ASF" - else: - result = html.escape(str(data.get("Result", "Нет ответа"))) - text = ( - f"Команда: {command}\n\n" - f"Ответ:\n{result}" - ) - await message.answer(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Удалить", callback_data="delete_msg")]])) - except Exception as e: - await message.answer(f"Ошибка: {e}") - return - # single bot redeem mode - if message.from_user.id in bot_redeem_users: # type: ignore - bot_name = bot_redeem_users.pop(message.from_user.id) # type: ignore - menu_message = bot_redeem_messages.pop(message.from_user.id, None) # type: ignore - if menu_message: - try: - response = requests.get("http://127.0.0.1:1242/Api/Bot/ASF",headers={"Authentication": ASF_PASSWORD}) - data = response.json() - bot_data = data.get("Result", {}).get(bot_name) - if bot_data: - await menu_message.edit_text( - format_bot_ui(bot_data), - disable_web_page_preview=True, - reply_markup=InlineKeyboardMarkup( - inline_keyboard=[ - [InlineKeyboardButton(text="🔐 2FA", callback_data=f"2fa_{bot_name}")] - if bot_data.get("HasMobileAuthenticator") else [], - [InlineKeyboardButton(text="⌚ Накрутка часов", callback_data=f"games_{bot_name}")], - [InlineKeyboardButton(text="🔑 Активировать ключ", callback_data=f"redeem_bot_{bot_name}")], - [InlineKeyboardButton(text="📦 Инвентарь", callback_data=f"inventory_{bot_name}")], - [InlineKeyboardButton(text="🔙 Назад", callback_data="bots")]])) - except: - pass - try: - await bot.delete_message(chat_id=message.chat.id, message_id=message.message_id) - except: - pass - raw_text = message.text.upper() # type: ignore - keys = re.findall(r"[A-Z0-9]{5}(?:-[A-Z0-9]{5}){2}", raw_text) - keys = list(dict.fromkeys(keys)) - if not keys: - await message.answer("Ключи не найдены") - return - checking = await message.answer( - f"Аккаунт: {bot_name}\n" - f"Найдено ключей: {len(keys)}\n" - f"Начинаю активацию...") - success_count = 0 - failed_count = 0 - results = [] - for key in keys: - success, result = await redeem_key(bot_name, key) - await asyncio.sleep(2) - if not success or not result: - failed_count += 1 - results.append(f"❌ {key}: ASF ERROR") - continue - result_lower = result.lower() - if "ok/nodetail" in result_lower or "ok/" in result_lower: - success_count += 1 - results.append(f"✅ {key}: активировано") - elif "ratelimited" in result_lower: - failed_count += 1 - results.append(f"⏳ {key}: rate limit") - elif "alreadypurchased" in result_lower: - failed_count += 1 - results.append(f"⚠️ {key}: игра уже есть") - elif "duplicateactivationcode" in result_lower: - failed_count += 1 - results.append(f"❌ {key}: ключ уже использован") - elif "regionlocked" in result_lower: - failed_count += 1 - results.append(f"🌍 {key}: регион лок") - elif "invalidactivationcode" in result_lower or "invalid" in result_lower: - failed_count += 1 - results.append(f"❌ {key}: неверный ключ") - 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])) - if len(text) > 4000: - text = text[:4000] + "\n\n...обрезано" - await checking.edit_text(text) - return - if message.from_user.id in redeem_users: # type: ignore - redeem_users.discard(message.from_user.id) # type: ignore - menu_message = redeem_messages.pop(message.from_user.id, None) # type: ignore - if menu_message: - try: - await menu_message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) - except: - pass - try: - await bot.delete_message(chat_id=message.chat.id, message_id=message.message_id) - except: - pass - raw_text = message.text.upper() # type: ignore - keys = re.findall(r"[A-Z0-9]{5}(?:-[A-Z0-9]{5}){2}", raw_text) - keys = list(dict.fromkeys(keys)) - if not keys: - await message.answer("Ключи не найдены") - return - checking = await message.answer(f"Найдено ключей: {len(keys)}\nНачинаю активацию...") - try: - response = asf_get("/Api/Bot/ASF") - data = response.json() - bots = list(data.get("Result", {}).keys()) - success_count = 0 - failed_count = 0 - results = [] - for key in keys: - activated = False - key_report = [] - for bot_name in bots: - success, result = await redeem_key(bot_name, key) - await asyncio.sleep(2) - if not success or not result: - key_report.append(f"❌ {bot_name}: ASF ERROR") - continue - print(result) - result_lower = result.lower() - bot_line = "" - for line in result.splitlines(): - if f"<{bot_name.lower()}>" in line.lower(): - bot_line = line - break - if bot_line: - parsed = bot_line.lower() - else: - parsed = result_lower - if "ok/nodetail" in parsed or "ok/" in parsed: - key_report.append(f"✅ {bot_name}: активировано") - success_count += 1 - activated = True - break - elif "ratelimited" in parsed: - key_report.append(f"⏳ {bot_name}: rate limit") - continue - elif "alreadypurchased" in parsed or "already purchased" in parsed: - key_report.append(f"⚠️ {bot_name}: игра уже есть") - continue - elif "duplicateactivationcode" in parsed: - key_report.append(f"❌ {bot_name}: ключ уже использован") - break - elif "regionlocked" in parsed: - key_report.append(f"🌍 {bot_name}: регион лок") - continue - elif "invalidactivationcode" in parsed or "invalid" in parsed: - key_report.append(f"❌ {bot_name}: неверный ключ") - break - else: - key_report.append(f"❔ {bot_name}: неизвестный ответ") - continue - if not activated: - failed_count += 1 - results.append(f"\n🔑 {key}\n" + "\n".join(key_report)) - text = ( - f"Результат активации\n\n" - f"✅ Успешно: {success_count}\n" - f"❌ Ошибок: {failed_count}\n\n" - + "\n".join(results[:50])) - if len(text) > 4000: - text = text[:4000] + "\n\n...обрезано" - await checking.edit_text(text) - - except Exception as e: - await checking.edit_text(f"Ошибка: {e}") - return - -async def main(): +async def main() -> None: await dp.start_polling(bot) + if __name__ == "__main__": asyncio.run(main())