commit 2026da4bd01bcaac0dd9c917bc9253a094576603 Author: xzorisme Date: Thu Sep 3 23:15:10 2026 +0300 init comit diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0c83fa --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# RP Farm Bots (Pyrogram) + +Два процесса: + +- **`controller.py`** — обычный бот через Bot API. Через него вы **регистрируете + аккаунты, вводите данные и управляете фармом** — никакого ручного + редактирования JSON с аккаунтами. +- **`farmer.py`** — фоновый процесс, который читает базу и от лица + зарегистрированных аккаунтов шлёт фарм-команду по расписанию. Раз в минуту + сам подхватывает новые аккаунты, добавленные через контроллер — рестарт не нужен. + +Всё динамическое состояние — в `farm_state.db` (SQLite), создаётся автоматически. + +## Установка + +```bash +pip install -r requirements.txt +cp config.example.json config.json +``` + +В `config.json` нужны только секреты самого бота-контроллера (не аккаунтов +фарма!): + +1. **bot_token** — создайте бота через [@BotFather](https://t.me/BotFather). +2. **controller_api_id / controller_api_hash** — любая пара с + https://my.telegram.org → "API development tools" (можно использовать + свою личную, она не связана с фарм-аккаунтами). +3. **admin_ids** — ваш Telegram user_id. Запустите контроллер и напишите ему + `/myid`, чтобы узнать свой ID, затем впишите его в конфиг и перезапустите бота. + +## Запуск + +```bash +python controller.py +``` + +Напишите боту `/start` — увидите меню: + +- **➕ Добавить аккаунт** (`/addaccount`) — пошаговый мастер: + 1. Имя аккаунта (произвольное, например `acc1`) + 2. `api_id` и `api_hash` — для этого аккаунта, с my.telegram.org + (у каждого своего аккаунта должна быть отдельная пара) + 3. Номер телефона аккаунта + 4. Код из Telegram/SMS + 5. Пароль 2FA (если включён) + 6. Юзернейм/ID целевого РП-бота (`@some_rp_bot`) + 7. Текст фарм-команды (`/farm`, `/work`, `/daily`...) + 8. Интервал фарма в часах (например `4`) + + После этого аккаунт сразу активен. + +- **📋 Мои аккаунты** (`/accounts`) — список +- **📊 Статус** (`/status`) — пауза/интервал/сколько ждать/последний ответ бота + +Остальные команды: + +``` +/pause +/resume +/setinterval <часы> +/settarget <@бот> +/setcommand <текст> +/farmnow +/removeaccount +/cancel — прервать мастер добавления +``` + +Отдельным процессом (другой терминал / systemd-сервис): + +```bash +python farmer.py +``` + +## Важно + +- Код/пароль 2FA вводятся текстом прямо в чат с вашим ботом — это летит + через серверы Telegram (MTProto), обычная практика для ботов-помощников + входа, но `config.json` и файлы `*_session.session` всё равно держите + строго при себе, не выкладывайте в публичные репозитории. +- Юзерботы (автоматизация обычных аккаунтов через Pyrogram) формально не + разрешены правилами Telegram — интервалы и джиттер уже встроены, не + убирайте их и не гоняйте десятки аккаунтов с одинаковым паттерном. +- Проверьте правила самого РП-бота на предмет автофарма — бан там за + накрутку это отдельный от Telegram риск. diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..010214d --- /dev/null +++ b/config.example.json @@ -0,0 +1,6 @@ +{ + "bot_token": "СЮДА_ТОКЕН_ОТ_BOTFATHER", + "admin_ids": [123456789], + "controller_api_id": 1234567, + "controller_api_hash": "0123456789abcdef0123456789abcdef" +} diff --git a/controller.py b/controller.py new file mode 100644 index 0000000..3eb9ac3 --- /dev/null +++ b/controller.py @@ -0,0 +1,448 @@ +""" +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() diff --git a/db.py b/db.py new file mode 100644 index 0000000..0023dcc --- /dev/null +++ b/db.py @@ -0,0 +1,137 @@ +""" +Общая SQLite-база для farmer.py и controller.py. +Аккаунты (включая api_id/api_hash/session_name) регистрируются полностью +через меню в controller.py — никакого ручного редактирования JSON. +""" + +import sqlite3 +import time +import os + +DB_PATH = os.path.join(os.path.dirname(__file__), "farm_state.db") + + +def _connect(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def init_db(): + conn = _connect() + conn.execute(""" + CREATE TABLE IF NOT EXISTS accounts ( + name TEXT PRIMARY KEY, + api_id INTEGER NOT NULL, + api_hash TEXT NOT NULL, + session_name TEXT NOT NULL, + phone TEXT, + target_chat TEXT NOT NULL DEFAULT '', + farm_command TEXT NOT NULL DEFAULT '', + interval_hours REAL NOT NULL DEFAULT 4, + last_farm_ts REAL NOT NULL DEFAULT 0, + paused INTEGER NOT NULL DEFAULT 1, + last_response TEXT DEFAULT '', + session_ready INTEGER NOT NULL DEFAULT 0 + ) + """) + conn.commit() + conn.close() + + +def account_exists(name): + conn = _connect() + row = conn.execute("SELECT 1 FROM accounts WHERE name = ?", (name,)).fetchone() + conn.close() + return row is not None + + +def add_account(name, api_id, api_hash, session_name, phone): + conn = _connect() + conn.execute( + """INSERT INTO accounts (name, api_id, api_hash, session_name, phone, paused) + VALUES (?, ?, ?, ?, ?, 1)""", + (name, api_id, api_hash, session_name, phone), + ) + conn.commit() + conn.close() + + +def remove_account(name): + conn = _connect() + conn.execute("DELETE FROM accounts WHERE name = ?", (name,)) + conn.commit() + conn.close() + + +def mark_session_ready(name): + conn = _connect() + conn.execute("UPDATE accounts SET session_ready = 1 WHERE name = ?", (name,)) + conn.commit() + conn.close() + + +def set_target(name, target_chat, farm_command): + conn = _connect() + conn.execute( + "UPDATE accounts SET target_chat = ?, farm_command = ? WHERE name = ?", + (target_chat, farm_command, name), + ) + conn.commit() + conn.close() + + +def get_state(name): + conn = _connect() + row = conn.execute("SELECT * FROM accounts WHERE name = ?", (name,)).fetchone() + conn.close() + return dict(row) if row else None + + +def get_all_states(): + conn = _connect() + rows = conn.execute("SELECT * FROM accounts ORDER BY name").fetchall() + conn.close() + return [dict(r) for r in rows] + + +def update_last_farm(name, ts=None): + ts = ts if ts is not None else time.time() + conn = _connect() + conn.execute("UPDATE accounts SET last_farm_ts = ? WHERE name = ?", (ts, name)) + conn.commit() + conn.close() + + +def set_paused(name, paused: bool): + conn = _connect() + if name == "all": + conn.execute("UPDATE accounts SET paused = ?", (int(paused),)) + else: + conn.execute("UPDATE accounts SET paused = ? WHERE name = ?", (int(paused), name)) + conn.commit() + conn.close() + + +def set_interval(name, hours: float): + conn = _connect() + conn.execute("UPDATE accounts SET interval_hours = ? WHERE name = ?", (hours, name)) + conn.commit() + conn.close() + + +def force_farm_now(name): + conn = _connect() + conn.execute("UPDATE accounts SET last_farm_ts = 0 WHERE name = ?", (name,)) + conn.commit() + conn.close() + + +def save_response(name, text: str): + conn = _connect() + conn.execute( + "UPDATE accounts SET last_response = ? WHERE name = ?", + (text[:300], name), + ) + conn.commit() + conn.close() diff --git a/farmer.py b/farmer.py new file mode 100644 index 0000000..6e3d270 --- /dev/null +++ b/farmer.py @@ -0,0 +1,124 @@ +""" +farmer.py — запускает Pyrogram-клиенты для аккаунтов, зарегистрированных +через меню controller.py (/addaccount). Никакого ручного config.json — +все данные (api_id/api_hash/session_name/цель/команда/интервал) в БД. + +Раз в POLL_NEW_ACCOUNTS секунд проверяет базу на новые готовые аккаунты +(session_ready=1) и запускает для них клиент "на лету", без рестарта. +""" + +import asyncio +import logging +import os +import random +import time + +from pyrogram import Client, filters +from pyrogram.handlers import MessageHandler +from pyrogram.errors import FloodWait + +import db + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) +log = logging.getLogger("farmer") + +CHECK_INTERVAL = 30 # проверка паузы/таймера конкретного аккаунта, сек +POLL_NEW_ACCOUNTS = 60 # как часто искать новые аккаунты в базе, сек +MIN_JITTER = 5 # случайная задержка перед отправкой, сек +MAX_JITTER = 120 + + +def make_response_handler(name): + async def handler(client, message): + state = db.get_state(name) + if not state: + return + target = state["target_chat"] + chat_username = f"@{message.chat.username}" if message.chat.username else None + chat_id = str(message.chat.id) + if target and target in (chat_username, chat_id): + text = message.text or message.caption or "" + if text: + db.save_response(name, text) + return handler + + +async def account_loop(app: Client, name: str): + while True: + try: + state = db.get_state(name) + if state is None: + log.warning("Аккаунт %s исчез из базы, останавливаю цикл", name) + return + + if state["paused"] or not state["target_chat"] or not state["farm_command"]: + await asyncio.sleep(CHECK_INTERVAL) + continue + + interval_seconds = state["interval_hours"] * 3600 + elapsed = time.time() - state["last_farm_ts"] + + if elapsed >= interval_seconds: + jitter = random.uniform(MIN_JITTER, MAX_JITTER) + log.info("[%s] Пора фармить, жду %.0f сек (jitter)", name, jitter) + await asyncio.sleep(jitter) + + await app.send_message(state["target_chat"], state["farm_command"]) + db.update_last_farm(name, time.time()) + log.info("[%s] Отправлено '%s' в %s", name, state["farm_command"], state["target_chat"]) + + await asyncio.sleep(CHECK_INTERVAL) + + except FloodWait as e: + log.warning("[%s] FloodWait: жду %s сек", name, e.value) + await asyncio.sleep(e.value + 5) + except Exception: + log.exception("[%s] Ошибка в цикле фарма, повтор через 60 сек", name) + await asyncio.sleep(60) + + +async def start_account(name: str, state: dict) -> Client: + app = Client( + name=state["session_name"], + api_id=state["api_id"], + api_hash=state["api_hash"], + workdir=os.path.dirname(__file__), + ) + await app.start() + app.add_handler(MessageHandler(make_response_handler(name), filters.incoming)) + log.info("Аккаунт %s запущен", name) + return app + + +async def main(): + db.init_db() + running = {} # name -> asyncio.Task + + log.info("Farmer запущен, ищу зарегистрированные аккаунты...") + + while True: + try: + states = db.get_all_states() + for state in states: + name = state["name"] + if not state["session_ready"]: + continue # регистрация ещё не завершена (нет кода/2FA) + if name in running and not running[name].done(): + continue + try: + app = await start_account(name, state) + except Exception: + log.exception("Не удалось запустить аккаунт %s, попробую позже", name) + continue + running[name] = asyncio.create_task(account_loop(app, name)) + except Exception: + log.exception("Ошибка при опросе новых аккаунтов") + + await asyncio.sleep(POLL_NEW_ACCOUNTS) + + +if __name__ == "__main__": + asyncio.run(main())