init comit

This commit is contained in:
xzorisme 2026-09-03 23:15:10 +03:00
commit 2026da4bd0
5 changed files with 799 additions and 0 deletions

84
README.md Normal file
View 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
View File

@ -0,0 +1,6 @@
{
"bot_token": "СЮДА_ТОКЕН_ОТ_BOTFATHER",
"admin_ids": [123456789],
"controller_api_id": 1234567,
"controller_api_hash": "0123456789abcdef0123456789abcdef"
}

448
controller.py Normal file
View 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
View 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
View 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())