""" controller.py — бот через Bot API, которым вы полностью управляете фармом: регистрируете аккаунты (вводите api_id/api_hash/номер/код/пароль 2FA прямо в чате), настраиваете цель/команду/интервал, ставите на паузу и т.д. Никакого ручного редактирования JSON с аккаунтами — всё через /addaccount и инлайн-меню. """ import json import os import time from pyrogram import Client, filters from pyrogram.types import ( Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, ) from pyrogram.errors import ( SessionPasswordNeeded, PhoneCodeInvalid, PhoneCodeExpired, PhoneNumberInvalid, FloodWait, ) import db CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json") with open(CONFIG_PATH, "r", encoding="utf-8") as f: config = json.load(f) ADMIN_IDS = set(config["admin_ids"]) app = Client( "controller_bot", bot_token=config["bot_token"], api_id=config["controller_api_id"], api_hash=config["controller_api_hash"], workdir=os.path.dirname(__file__), ) admin_filter = filters.user(list(ADMIN_IDS)) & filters.private # Состояние мастера регистрации: admin_id -> {"step": ..., "data": {...}, "client": Client, "phone_code_hash": ...} pending = {} # ---------- вспомогательное ---------- def fmt_eta(state): if not state["target_chat"] or not state["farm_command"]: return "не настроена цель/команда" interval = state["interval_hours"] * 3600 elapsed = time.time() - state["last_farm_ts"] remaining = interval - elapsed if remaining <= 0: return "готово к фарму" h, rem = divmod(int(remaining), 3600) m = rem // 60 return f"через {h}ч {m}м" def main_menu(): return InlineKeyboardMarkup([ [InlineKeyboardButton("➕ Добавить аккаунт", callback_data="menu_add")], [InlineKeyboardButton("📋 Мои аккаунты", callback_data="menu_accounts")], [InlineKeyboardButton("📊 Статус", callback_data="menu_status")], ]) async def cancel_flow(admin_id): state = pending.pop(admin_id, None) if state and state.get("client"): try: await state["client"].disconnect() except Exception: pass # ---------- базовые команды ---------- @app.on_message(filters.command("start") & admin_filter) async def start_cmd(client, message: Message): await message.reply( "Панель управления фарм-ботами.\n\n" "Команды:\n" "/addaccount — зарегистрировать новый аккаунт\n" "/accounts — список аккаунтов\n" "/status — статус всех аккаунтов\n" "/pause \n" "/resume \n" "/setinterval <часы>\n" "/settarget <@бот>\n" "/setcommand <команда>\n" "/farmnow \n" "/removeaccount \n" "/cancel — отменить текущий мастер", reply_markup=main_menu(), ) @app.on_message(filters.command("myid")) async def myid_cmd(client, message: Message): await message.reply(f"Ваш user_id: `{message.from_user.id}`\nДобавьте его в admin_ids в config.json.") @app.on_message(filters.command("cancel") & admin_filter) async def cancel_cmd(client, message: Message): await cancel_flow(message.from_user.id) await message.reply("Отменено.") @app.on_callback_query(filters.user(list(ADMIN_IDS))) async def callback_router(client, cq: CallbackQuery): if cq.data == "menu_add": await begin_addaccount(cq.from_user.id, cq.message) elif cq.data == "menu_accounts": await send_accounts_list(cq.message) elif cq.data == "menu_status": await send_status(cq.message) await cq.answer() # ---------- список / статус ---------- async def send_accounts_list(message: Message): states = db.get_all_states() if not states: await message.reply("Аккаунтов пока нет. Нажмите «Добавить аккаунт» или /addaccount.") return lines = [] for s in states: ready = "✅" if s["session_ready"] else "⏳ регистрация не завершена" lines.append(f"- *{s['name']}* {ready}") await message.reply("\n".join(lines)) async def send_status(message: Message): states = db.get_all_states() if not states: await message.reply("Нет аккаунтов в базе. Добавьте через /addaccount.") return lines = [] for s in states: if not s["session_ready"]: lines.append(f"*{s['name']}* — ⏳ регистрация не завершена") continue status = "⏸ пауза" if s["paused"] else "▶️ активен" lines.append( f"*{s['name']}* — {status}\n" f" интервал: {s['interval_hours']}ч, {fmt_eta(s)}\n" f" цель: {s['target_chat'] or '—'}, команда: `{s['farm_command'] or '—'}`\n" f" последний ответ: {s['last_response'] or '—'}" ) await message.reply("\n\n".join(lines)) @app.on_message(filters.command("accounts") & admin_filter) async def accounts_cmd(client, message: Message): await send_accounts_list(message) @app.on_message(filters.command("status") & admin_filter) async def status_cmd(client, message: Message): await send_status(message) # ---------- управление существующими аккаунтами ---------- @app.on_message(filters.command("pause") & admin_filter) async def pause_cmd(client, message: Message): args = message.text.split(maxsplit=1) if len(args) < 2: await message.reply("Используйте: /pause ") return name = args[1].strip() if name != "all" and not db.account_exists(name): await message.reply(f"Неизвестный аккаунт: {name}") return db.set_paused(name, True) await message.reply(f"{name}: поставлен на паузу.") @app.on_message(filters.command("resume") & admin_filter) async def resume_cmd(client, message: Message): args = message.text.split(maxsplit=1) if len(args) < 2: await message.reply("Используйте: /resume ") return name = args[1].strip() if name != "all" and not db.account_exists(name): await message.reply(f"Неизвестный аккаунт: {name}") return db.set_paused(name, False) await message.reply(f"{name}: снят с паузы.") @app.on_message(filters.command("setinterval") & admin_filter) async def setinterval_cmd(client, message: Message): args = message.text.split(maxsplit=2) if len(args) < 3: await message.reply("Используйте: /setinterval <часы>") return name, hours_str = args[1].strip(), args[2].strip() if not db.account_exists(name): await message.reply(f"Неизвестный аккаунт: {name}") return try: hours = float(hours_str) except ValueError: await message.reply("Часы должны быть числом, например 4 или 4.5") return db.set_interval(name, hours) await message.reply(f"{name}: интервал установлен на {hours}ч.") @app.on_message(filters.command("settarget") & admin_filter) async def settarget_cmd(client, message: Message): args = message.text.split(maxsplit=2) if len(args) < 3: await message.reply("Используйте: /settarget <@бот_или_id>") return name, target = args[1].strip(), args[2].strip() if not db.account_exists(name): await message.reply(f"Неизвестный аккаунт: {name}") return state = db.get_state(name) db.set_target(name, target, state["farm_command"]) await message.reply(f"{name}: цель установлена на {target}.") @app.on_message(filters.command("setcommand") & admin_filter) async def setcommand_cmd(client, message: Message): args = message.text.split(maxsplit=2) if len(args) < 3: await message.reply("Используйте: /setcommand <текст команды>") return name, command = args[1].strip(), args[2].strip() if not db.account_exists(name): await message.reply(f"Неизвестный аккаунт: {name}") return state = db.get_state(name) db.set_target(name, state["target_chat"], command) await message.reply(f"{name}: фарм-команда установлена на `{command}`.") @app.on_message(filters.command("farmnow") & admin_filter) async def farmnow_cmd(client, message: Message): args = message.text.split(maxsplit=1) if len(args) < 2: await message.reply("Используйте: /farmnow ") return name = args[1].strip() if not db.account_exists(name): await message.reply(f"Неизвестный аккаунт: {name}") return db.force_farm_now(name) await message.reply(f"{name}: фарм произойдёт в течение ~30–60 секунд.") @app.on_message(filters.command("removeaccount") & admin_filter) async def removeaccount_cmd(client, message: Message): args = message.text.split(maxsplit=1) if len(args) < 2: await message.reply("Используйте: /removeaccount ") return name = args[1].strip() if not db.account_exists(name): await message.reply(f"Неизвестный аккаунт: {name}") return db.remove_account(name) await message.reply( f"{name}: удалён из базы. Файл сессии `{name}_session.session` можно удалить вручную с диска." ) # ---------- мастер регистрации нового аккаунта ---------- async def begin_addaccount(admin_id, message: Message): if admin_id in pending: await message.reply("У вас уже есть незавершённый мастер добавления. /cancel чтобы прервать.") return pending[admin_id] = {"step": "name", "data": {}} await message.reply("Введите произвольное имя аккаунта (латиницей, например acc1):") @app.on_message(filters.command("addaccount") & admin_filter) async def addaccount_cmd(client, message: Message): await begin_addaccount(message.from_user.id, message) @app.on_message(filters.text & admin_filter & ~filters.regex(r"^/")) async def flow_router(client, message: Message): admin_id = message.from_user.id if admin_id not in pending: return # не в мастере — игнорируем, чтобы не мешать обычной переписке state = pending[admin_id] step = state["step"] text = message.text.strip() data = state["data"] try: if step == "name": if db.account_exists(text): await message.reply("Аккаунт с таким именем уже существует. Введите другое имя:") return data["name"] = text state["step"] = "api_id" await message.reply( "Введите *api_id* (число, получить на https://my.telegram.org → " "API development tools):" ) elif step == "api_id": if not text.isdigit(): await message.reply("api_id должен быть числом. Попробуйте ещё раз:") return data["api_id"] = int(text) state["step"] = "api_hash" await message.reply("Введите *api_hash*:") elif step == "api_hash": data["api_hash"] = text state["step"] = "phone" await message.reply("Введите номер телефона аккаунта, формат +79991234567:") elif step == "phone": data["phone"] = text data["session_name"] = f"{data['name']}_session" tg_client = Client( data["session_name"], api_id=data["api_id"], api_hash=data["api_hash"], workdir=os.path.dirname(__file__), ) await tg_client.connect() try: sent = await tg_client.send_code(text) except PhoneNumberInvalid: await message.reply("Неверный формат номера. Введите ещё раз, например +79991234567:") await tg_client.disconnect() return except FloodWait as e: await message.reply(f"Telegram просит подождать {e.value} сек перед повтором. /cancel и попробуйте позже.") await tg_client.disconnect() return except Exception as e: await message.reply(f"Не удалось отправить код: {e}\nПрервано, начните заново: /addaccount") await tg_client.disconnect() del pending[admin_id] return state["client"] = tg_client state["phone_code_hash"] = sent.phone_code_hash state["step"] = "code" await message.reply( "Код отправлен в Telegram на этот номер (или SMS). " "Введите код одной строкой, без пробелов:" ) elif step == "code": code = text.replace(" ", "") tg_client = state["client"] try: await tg_client.sign_in(data["phone"], state["phone_code_hash"], code) except SessionPasswordNeeded: state["step"] = "password" await message.reply("Включена двухфакторная защита (2FA). Введите пароль:") return except (PhoneCodeInvalid, PhoneCodeExpired): await message.reply("Неверный или просроченный код. Введите код ещё раз или /cancel:") return except Exception as e: await message.reply(f"Ошибка входа: {e}\nПрервано, начните заново: /addaccount") await tg_client.disconnect() del pending[admin_id] return await finish_login(admin_id, message) elif step == "password": tg_client = state["client"] try: await tg_client.check_password(text) except Exception as e: await message.reply(f"Неверный пароль: {e}. Попробуйте ещё раз или /cancel:") return await finish_login(admin_id, message) elif step == "target": data["target_chat"] = text state["step"] = "command" await message.reply("Введите фарм-команду, которую слать боту (например /farm):") elif step == "command": data["farm_command"] = text state["step"] = "interval" await message.reply("Введите интервал фарма в часах (например 4):") elif step == "interval": try: hours = float(text) except ValueError: await message.reply("Введите число, например 4 или 4.5:") return db.add_account( name=data["name"], api_id=data["api_id"], api_hash=data["api_hash"], session_name=data["session_name"], phone=data["phone"], ) db.set_target(data["name"], data["target_chat"], data["farm_command"]) db.set_interval(data["name"], hours) db.mark_session_ready(data["name"]) db.set_paused(data["name"], False) await message.reply( f"✅ Аккаунт *{data['name']}* добавлен и активен.\n" f"Если farmer.py уже запущен — подхватит новый аккаунт в течение минуты. " f"Если не запущен — запустите `python farmer.py`.", reply_markup=main_menu(), ) del pending[admin_id] except Exception: # страхуемся от любых неожиданных ошибок мастера, чтобы не зависнуть в pending навсегда await cancel_flow(admin_id) await message.reply("Что-то пошло не так, мастер прерван. Попробуйте /addaccount заново.") async def finish_login(admin_id, message: Message): state = pending[admin_id] tg_client = state["client"] me = await tg_client.get_me() await tg_client.disconnect() # сессия сохранена на диске, дальше её использует farmer.py state["step"] = "target" await message.reply( f"Успешный вход как {me.first_name} (@{me.username or 'без юзернейма'}).\n" f"Теперь укажите целевого РП-бота (юзернейм, например @some_rp_bot):" ) if __name__ == "__main__": db.init_db() print("Controller bot запущен.") app.run()