449 lines
18 KiB
Python
449 lines
18 KiB
Python
"""
|
||
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()
|