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