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