pybot/farmer.py
2026-09-03 23:15:10 +03:00

125 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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())