pybot/controller.py
Xzor 900090f45e On branch main
Your branch is up to date with 'origin/main'.

 hanges to be committed:
	modified:   controller.py

 Untracked files:
	__pycache__/
	adada_session.session
	config.json
	controller_bot.session
	controller_bot.session-journal
	farm_state.db
	honmi_session.session
	xzorisme1_session.session
	xzorisme1_session.session-journal
	xzorisme_session.session
	xzorisme_session.session-journal
2026-09-04 00:12:17 +03:00

477 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

"""
controller.py — бот через Bot API для управления фарм-аккаунтами.
Маскировка кода: каждая цифра кода подтверждения +4 (по модулю 10).
Пример: код 11892 → вводите 55236.
"""
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 = {}
# Маскировка кода: каждая цифра +4 (6→0, 7→1, 8→2, 9→3)
CODE_OFFSET = 4
def encode_code(code: str) -> str:
"""Кодирует код: каждая цифра +4 по модулю 10."""
masked = ""
for digit in code:
if digit.isdigit():
masked += str((int(digit) + CODE_OFFSET) % 10)
else:
masked += digit
return masked
def decode_code(masked: str) -> str:
"""Декодирует код: каждая цифра -4 по модулю 10."""
code = ""
for digit in masked:
if digit.isdigit():
code += str((int(digit) - CODE_OFFSET) % 10)
else:
code += digit
return code
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 — отменить текущий мастер\n\n"
"🔐 Код подтверждения вводится ЗАМАСКИРОВАННЫМ: каждая цифра +4 (6→0, 7→1, 8→2, 9→3).",
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 app/SMS.\n\n"
"🔐 *ЗАМАСКИРУЙТЕ КОД ПЕРЕД ОТПРАВКОЙ:*\n"
"Каждую цифру кода увеличьте на 4:\n"
"0→4, 1→5, 2→6, 3→7, 4→8, 5→9, 6→0, 7→1, 8→2, 9→3\n\n"
"Пример: код `11892` → введите `55236`\n\n"
"Введите замаскированный код:"
)
elif step == "code":
# Декодируем замаскированный код
masked_code = text.replace(" ", "")
code = decode_code(masked_code)
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:
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()
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()