125 lines
4.7 KiB
Python
125 lines
4.7 KiB
Python
"""
|
||
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())
|