init comit
This commit is contained in:
commit
2026da4bd0
84
README.md
Normal file
84
README.md
Normal file
@ -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 <name|all>
|
||||
/resume <name|all>
|
||||
/setinterval <name> <часы>
|
||||
/settarget <name> <@бот>
|
||||
/setcommand <name> <текст>
|
||||
/farmnow <name>
|
||||
/removeaccount <name>
|
||||
/cancel — прервать мастер добавления
|
||||
```
|
||||
|
||||
Отдельным процессом (другой терминал / systemd-сервис):
|
||||
|
||||
```bash
|
||||
python farmer.py
|
||||
```
|
||||
|
||||
## Важно
|
||||
|
||||
- Код/пароль 2FA вводятся текстом прямо в чат с вашим ботом — это летит
|
||||
через серверы Telegram (MTProto), обычная практика для ботов-помощников
|
||||
входа, но `config.json` и файлы `*_session.session` всё равно держите
|
||||
строго при себе, не выкладывайте в публичные репозитории.
|
||||
- Юзерботы (автоматизация обычных аккаунтов через Pyrogram) формально не
|
||||
разрешены правилами Telegram — интервалы и джиттер уже встроены, не
|
||||
убирайте их и не гоняйте десятки аккаунтов с одинаковым паттерном.
|
||||
- Проверьте правила самого РП-бота на предмет автофарма — бан там за
|
||||
накрутку это отдельный от Telegram риск.
|
||||
6
config.example.json
Normal file
6
config.example.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"bot_token": "СЮДА_ТОКЕН_ОТ_BOTFATHER",
|
||||
"admin_ids": [123456789],
|
||||
"controller_api_id": 1234567,
|
||||
"controller_api_hash": "0123456789abcdef0123456789abcdef"
|
||||
}
|
||||
448
controller.py
Normal file
448
controller.py
Normal file
@ -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 <name|all>\n"
|
||||
"/resume <name|all>\n"
|
||||
"/setinterval <name> <часы>\n"
|
||||
"/settarget <name> <@бот>\n"
|
||||
"/setcommand <name> <команда>\n"
|
||||
"/farmnow <name>\n"
|
||||
"/removeaccount <name>\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 <name|all>")
|
||||
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 <name|all>")
|
||||
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 <name> <часы>")
|
||||
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 <name> <@бот_или_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 <name> <текст команды>")
|
||||
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 <name>")
|
||||
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 <name>")
|
||||
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()
|
||||
137
db.py
Normal file
137
db.py
Normal file
@ -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()
|
||||
124
farmer.py
Normal file
124
farmer.py
Normal file
@ -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())
|
||||
Loading…
Reference in New Issue
Block a user