ZaminDocs

Moderator bot (guruhlar uchun)

Telegram'dagi Rose kabi guruh moderatori: yangi a'zolarni kutib oladi, "qo'shildi / chiqdi" xabarlarini o'chiradi, /warn (3 ta β€” ban), /mute 10m, /ban, /unban, flood'ga qarshi himoya va /rules. Bitta fayl, faqat requests; holat JSON faylda saqlanadi.

1. Botni tayyorlash

  1. @ZaminBot'da /newbot bilan bot yarating va tokenni oling. /setjoingroups standart holatda yoqilgan β€” hech narsa qilish shart emas. /setprivacy'ni o'zgartirmang: kerak bo'lsa, "barcha xabarlarni o'qish"ni guruh egasi o'zi beradi.
  2. Ixtiyoriy: /setcommands bilan buyruqlar ro'yxatini kiriting (warn - Ogohlantirish, mute - Jim qilish, rules - Qoidalar …).
  3. Guruh egasi botni guruhga qo'shadi va admin qiladi: Xabarlarni o'chirish va Foydalanuvchilarni cheklash huquqlari bilan.
  4. Flood'ga qarshi himoya kerak bo'lsa, guruh egasi botga Barcha xabarlarni o'qish huquqini ham beradi. Busiz bot faqat buyruqlar, @eslatishlar va o'ziga javoblarni ko'radi, flood'ni esa ko'ra olmaydi. Bu huquq berilganda guruh a'zolari ogohlantirish ko'radi: bot ko'rgan xabarlarni Zamin serveri va bot egasi o'qiy oladi.

2. Kod

moderator_bot.py
"""Zamin guruhlari uchun moderator bot (Rose uslubida).

Buyruqlar (adminlar uchun): /warn, /unwarn, /mute 10m, /unmute, /ban, /unban, /setrules
Hamma uchun: /rules, /help
Avtomatik: yangi a'zolarni kutib olish, qo'shildi/chiqdi xabarlarini o'chirish, flood'ga qarshi himoya.
"""
import json
import os
import re
import time
from collections import defaultdict, deque

import requests

TOKEN = os.environ["ZAMIN_BOT_TOKEN"]
API = f"https://api.zamin.app/bot{TOKEN}"
STATE_FILE = "moderator_state.json"

MAX_WARNS = 3              # shuncha ogohlantirish β€” ban
FLOOD_MESSAGES = 5         # FLOOD_SECONDS ichida shuncha xabardan ko'pi β€” flood
FLOOD_SECONDS = 10
FLOOD_MUTE_SECONDS = 5 * 60
ADMIN_CACHE_SECONDS = 300  # getChatAdministrators natijasi shuncha soniya saqlanadi
DEFAULT_RULES = (
    "1. Bir-biringizni hurmat qiling.\n"
    "2. Reklama va spam yo'q.\n"
    "3. Mavzudan chetga chiqmang."
)
HELP = (
    "Men guruh moderatoriman. Meni guruhga qo'shing va admin qiling "
    "(\"Xabarlarni o'chirish\" va \"Foydalanuvchilarni cheklash\" huquqlari bilan).\n\n"
    "Adminlar uchun (xabarga javob qilib yozing):\n"
    "/warn β€” ogohlantirish (3 ta bo'lsa β€” ban)\n"
    "/unwarn β€” ogohlantirishlarni tozalash\n"
    "/mute 10m β€” jim qilish (s, m, h, d)\n"
    "/unmute β€” jim qilishni bekor qilish\n"
    "/ban β€” guruhdan chetlatish\n"
    "/unban β€” banni olish (javob qilib yoki /unban <id>)\n"
    "/setrules <matn> β€” qoidalarni o'rnatish\n\n"
    "Hamma uchun: /rules β€” guruh qoidalari"
)
ADMIN_COMMANDS = {"warn", "unwarn", "mute", "unmute", "ban", "unban", "setrules"}
NEEDED_RIGHTS = [
    ("can_delete_messages", "Xabarlarni o'chirish"),
    ("can_restrict_members", "Foydalanuvchilarni cheklash"),
]

http = requests.Session()


class ApiError(Exception):
    def __init__(self, code, description):
        super().__init__(f"{code}: {description}")
        self.code = code
        self.description = description


def with_retry(send, retries=2, max_wait=10):
    """429 da parameters.retry_after soniya kutib, aynan o'sha so'rovni qayta yuboradi.

    Limit rad etgan so'rov hech narsa qilmagan, shuning uchun qayta yuborish xavfsiz.
    max_wait dan uzoq kutish kerak bo'lsa, javobni darhol qaytaradi: moderator
    boshqa yangilanishlarni kutib qolmasin.
    """
    for attempt in range(retries + 1):
        body = send()
        if body.get("error_code") != 429 or attempt == retries:
            return body
        wait = (body.get("parameters") or {}).get("retry_after") or 1
        if wait > max_wait:
            return body
        time.sleep(wait)


def call(method, params=None, http_timeout=15):
    """Bot API metodini chaqiradi va result'ni qaytaradi (429 bo'lsa β€” with_retry)."""

    def send():
        r = http.post(f"{API}/{method}", json=params or {}, timeout=http_timeout)
        try:
            return r.json()
        except ValueError:
            raise ApiError(r.status_code, f"JSON bo'lmagan javob (HTTP {r.status_code})")

    body = with_retry(send)
    if body.get("ok"):
        return body["result"]
    raise ApiError(body.get("error_code", 0), body.get("description", ""))


def try_call(method, params):
    """Xatoni log qiladi va None qaytaradi (masalan, botda huquq yetmasa)."""
    try:
        return call(method, params)
    except (ApiError, requests.RequestException) as e:
        print(f"{method}: {e}")
        return None


def say(chat_id, text, reply_to=None):
    params = {"chat_id": chat_id, "text": text}
    if reply_to:
        params["reply_to_message_id"] = reply_to
    return try_call("sendMessage", params)


def act(chat_id, method, params, done_text):
    """Admin amalini bajaradi va natijani guruhga yozadi. Muvaffaqiyatli bo'lsa True."""
    try:
        call(method, params)
    except ApiError as e:
        # Masalan: "not enough rights to restrict/unrestrict chat member",
        # "user is an administrator of the chat", "user not found"
        say(chat_id, f"❌ Bajarilmadi: {e.description}")
        return False
    except requests.RequestException as e:
        print(f"{method}: {e}")
        return False
    say(chat_id, done_text)
    return True


# ---------- holat: offset, ogohlantirishlar, qoidalar (JSON faylda) ----------

def load_state():
    try:
        with open(STATE_FILE, encoding="utf-8") as f:
            return json.load(f)
    except (FileNotFoundError, ValueError):
        return {"offset": None, "warns": {}, "rules": {}}


def save_state():
    tmp = STATE_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(state, f, ensure_ascii=False)
    os.replace(tmp, STATE_FILE)


state = load_state()
admin_cache = {}               # chat_id -> (vaqt, {admin user_id, ...})
recent = defaultdict(deque)    # (chat_id, user_id) -> oxirgi xabarlar vaqti
me = {}                        # getMe natijasi (main() to'ldiradi)


# ---------- yordamchilar ----------

def name_of(user):
    name = user.get("first_name", "")
    if user.get("last_name"):
        name += " " + user["last_name"]
    return name


def admins_of(chat_id):
    """Guruh egasi va adminlari (odamlar) id'lari. Psevdonim id'lar from.id bilan bir xil."""
    cached = admin_cache.get(chat_id)
    if cached and time.time() - cached[0] < ADMIN_CACHE_SECONDS:
        return cached[1]
    result = try_call("getChatAdministrators", {"chat_id": chat_id})
    if result is None:
        return cached[1] if cached else set()
    ids = {m["user"]["id"] for m in result if not m["user"]["is_bot"]}
    admin_cache[chat_id] = (time.time(), ids)
    return ids


def parse_command(text):
    """'/warn@ModBot spam' -> ('warn', 'spam'). Boshqa botga atalgan buyruq -> (None, '')."""
    if not text.startswith("/"):
        return None, ""
    parts = text.split(maxsplit=1)
    name, _, bot = parts[0][1:].partition("@")
    if bot and bot.lower() != me["username"].lower():
        return None, ""
    return name.lower(), (parts[1].strip() if len(parts) > 1 else "")


def parse_duration(text):
    """'10m' -> 600. Birliklar: s, m, h, d. Noto'g'ri bo'lsa None."""
    m = re.fullmatch(r"(\d{1,4})([smhd])", text.strip().lower())
    if not m:
        return None
    return int(m.group(1)) * {"s": 1, "m": 60, "h": 3600, "d": 86400}[m.group(2)]


def target_of(message, arg):
    """Buyruq kimga: javob qilingan xabar muallifi yoki birinchi so'z (raqamli id).
    (user_id, ism, qolgan matn) qaytaradi; topilmasa user_id = None."""
    author = (message.get("reply_to_message") or {}).get("from")
    if author and not author.get("is_bot"):
        return author["id"], name_of(author), arg
    first, _, rest = arg.partition(" ")
    if first.isdigit():
        return int(first), first, rest.strip()
    return None, "", arg


# ---------- buyruqlar ----------

def handle_command(message, cmd, arg):
    chat_id = message["chat"]["id"]
    mid = message["message_id"]

    if cmd in ("start", "help"):
        say(chat_id, HELP, mid)
        return
    if cmd == "rules":
        rules = state["rules"].get(str(chat_id), DEFAULT_RULES)
        say(chat_id, "πŸ“œ Guruh qoidalari:\n" + rules, mid)
        return
    if cmd not in ADMIN_COMMANDS:
        return
    if message["from"]["id"] not in admins_of(chat_id):
        say(chat_id, "β›” Bu buyruq faqat adminlar uchun.", mid)
        return

    if cmd == "setrules":
        if not arg:
            say(chat_id, "Foydalanish: /setrules <qoidalar matni>", mid)
            return
        state["rules"][str(chat_id)] = arg[:3000]
        save_state()
        say(chat_id, "βœ… Qoidalar saqlandi. Ko'rish: /rules", mid)
        return

    user_id, name, rest = target_of(message, arg)
    if user_id is None:
        say(chat_id, f"Kimga? Odamning xabariga javob qilib /{cmd} yozing.", mid)
        return
    if user_id in admins_of(chat_id):
        say(chat_id, "Adminlarga bu buyruq qo'llanmaydi.", mid)
        return
    key = f"{chat_id}:{user_id}"

    if cmd == "warn":
        count = state["warns"].get(key, 0) + 1
        state["warns"][key] = count
        save_state()
        if count < MAX_WARNS:
            say(chat_id, f"⚠️ {name}: ogohlantirish {count}/{MAX_WARNS}. Qoidalar: /rules")
        elif act(chat_id, "banChatMember", {"chat_id": chat_id, "user_id": user_id},
                 f"🚫 {name} {MAX_WARNS} ta ogohlantirish oldi va guruhdan chetlatildi."):
            state["warns"].pop(key, None)
            save_state()
    elif cmd == "unwarn":
        state["warns"].pop(key, None)
        save_state()
        say(chat_id, f"βœ… {name}: ogohlantirishlar tozalandi.")
    elif cmd == "mute":
        text = rest or "1h"
        seconds = parse_duration(text)
        # Server 30 soniyadan qisqa yoki 366 kundan uzun muddatni "muddatsiz" deb tushunadi
        if seconds is None or not 60 <= seconds <= 366 * 86400:
            say(chat_id, "Muddat: 10m, 2h, 1d ko'rinishida (1 daqiqadan 366 kungacha).", mid)
            return
        # Berilmagan ruxsat false hisoblanadi ({} ham jim qiladi); aniqlik uchun yozib qo'yamiz
        act(chat_id, "restrictChatMember", {
            "chat_id": chat_id,
            "user_id": user_id,
            "permissions": {"can_send_messages": False},
            "until_date": int(time.time()) + seconds,
        }, f"πŸ”‡ {name} {text} ga jim qilindi.")
    elif cmd == "unmute":
        # Cheklovni faqat can_send_messages: True olib tashlaydi ({} esa jim qilardi)
        act(chat_id, "restrictChatMember", {
            "chat_id": chat_id,
            "user_id": user_id,
            "permissions": {"can_send_messages": True},
        }, f"πŸ”Š {name} yana yoza oladi.")
    elif cmd == "ban":
        act(chat_id, "banChatMember", {"chat_id": chat_id, "user_id": user_id},
            f"🚫 {name} guruhdan chetlatildi.")
    elif cmd == "unban":
        # only_if_banned: True β€” aks holda guruhdagi odam chiqarib yuboriladi
        if act(chat_id, "unbanChatMember", {"chat_id": chat_id, "user_id": user_id, "only_if_banned": True},
               f"βœ… {name} bandan chiqarildi: endi qaytib qo'shilishi mumkin."):
            state["warns"].pop(key, None)
            save_state()


# ---------- flood'ga qarshi ----------

def is_flood(chat_id, user_id):
    """Oxirgi FLOOD_SECONDS ichida FLOOD_MESSAGES dan ko'p xabar bo'lsa True."""
    q = recent[(chat_id, user_id)]
    now = time.time()
    q.append(now)
    while q and now - q[0] > FLOOD_SECONDS:
        q.popleft()
    return len(q) > FLOOD_MESSAGES


def punish_flood(message):
    chat_id = message["chat"]["id"]
    user = message["from"]
    recent.pop((chat_id, user["id"]), None)
    try_call("deleteMessage", {"chat_id": chat_id, "message_id": message["message_id"]})
    act(chat_id, "restrictChatMember", {
        "chat_id": chat_id,
        "user_id": user["id"],
        "permissions": {"can_send_messages": False},
        "until_date": int(time.time()) + FLOOD_MUTE_SECONDS,
    }, f"🌊 {name_of(user)}: juda tez yozyapsiz. {FLOOD_MUTE_SECONDS // 60} daqiqa jim turing.")


# ---------- yangilanishlar ----------

def handle_service(message):
    """Qo'shildi / chiqdi xizmat xabarlari: salom va xabarni o'chirish."""
    chat_id = message["chat"]["id"]
    people = [u for u in message.get("new_chat_members", []) if not u["is_bot"]]
    if people:
        names = ", ".join(name_of(u) for u in people)
        say(chat_id, f"πŸ‘‹ Xush kelibsiz, {names}! Iltimos, qoidalarni o'qing: /rules")
    # "can_delete_messages" bo'lmasa, o'chirmaydi (xato log'ga yoziladi)
    try_call("deleteMessage", {"chat_id": chat_id, "message_id": message["message_id"]})


def handle_my_chat_member(update):
    chat = update["chat"]
    new = update["new_chat_member"]
    if chat["type"] != "group":
        return
    if new["status"] in ("left", "kicked"):
        admin_cache.pop(chat["id"], None)
        return
    if new["status"] == "member":
        say(chat["id"], "Salom! Ishlashim uchun meni admin qiling: \"Xabarlarni o'chirish\" va "
                        "\"Foydalanuvchilarni cheklash\" huquqlari bilan. /help")
    elif new["status"] == "administrator":
        missing = [title for field, title in NEEDED_RIGHTS if not new.get(field)]
        if missing:
            say(chat["id"], "Rahmat! Yana kerak: " + ", ".join(missing) + ".")
        elif not new.get("can_read_all_group_messages"):
            say(chat["id"], "βœ… Tayyor. Flood'ga qarshi himoya uchun guruh egasi menga "
                            "\"Barcha xabarlarni o'qish\" huquqini berishi kerak (ixtiyoriy).")
        else:
            say(chat["id"], "βœ… Tayyor. Flood'ga qarshi himoya yoqildi.")


def handle(update):
    if "my_chat_member" in update:
        handle_my_chat_member(update["my_chat_member"])
        return
    message = update.get("message")
    if not message:
        return
    chat = message["chat"]
    if chat["type"] == "private":
        say(chat["id"], HELP)
        return
    if chat["type"] != "group":
        return  # kanal izohlari va boshqalar
    if "new_chat_members" in message or "left_chat_member" in message:
        handle_service(message)
        return
    user = message.get("from")
    if not user or "new_chat_title" in message:
        return
    if user["id"] not in admins_of(chat["id"]) and is_flood(chat["id"], user["id"]):
        punish_flood(message)
        return
    cmd, arg = parse_command(message.get("text") or message.get("caption") or "")
    if cmd:
        handle_command(message, cmd, arg)


def main():
    me.update(call("getMe"))
    call("deleteWebhook")  # webhook yoqilgan bo'lsa getUpdates 409 qaytaradi
    print(f"@{me['username']} ishga tushdi")
    while True:
        params = {"timeout": 30, "allowed_updates": ["message", "my_chat_member"]}
        if state["offset"] is not None:
            params["offset"] = state["offset"]
        try:
            updates = call("getUpdates", params, http_timeout=40)
        except ApiError as e:
            if e.code in (401, 409):
                raise SystemExit(f"To'xtadi: {e}")  # token noto'g'ri yoki boshqa nusxa ishlayapti
            time.sleep(3)
            continue
        except requests.RequestException:
            time.sleep(3)
            continue
        for update in updates:
            state["offset"] = update["update_id"] + 1
            try:
                handle(update)
            except Exception as e:  # bitta yangilanish butun botni to'xtatmasin
                print("Xato:", update["update_id"], e)
        if updates:
            save_state()


if __name__ == "__main__":
    main()

3. Ishga tushirish

Terminal
pip3 install requests
export ZAMIN_BOT_TOKEN="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
python3 moderator_bot.py

Guruhda biror odamning xabariga javob qilib /warn yozing β€” bot ogohlantirishni sanaydi. /mute 10m, /ban ham javob qilib yoziladi. /unban uchun banlangan odamning eski xabariga javob qiling yoki /unban <id> yozing.

Qanday ishlaydi

  • Buyruqlar. Admin javob qilib yozgan /warn buyruq bo'lgani uchun botga keladi. reply_to_message'da boshqa odamning xabari mazmunsiz, lekin from bilan keladi β€” shu from.id kerakli odam. /warn@BoshqaBot e'tiborsiz qoldiriladi, oddiy /warn esa guruhdagi har bir botga boradi.
  • Adminlarni aniqlash. getChatAdministrators natijasi 5 daqiqaga keshlanadi. Undagi id'lar message.from.id bilan bir xil psevdonim raqamlar, shuning uchun to'g'ridan-to'g'ri solishtirish mumkin. Server ham egasi va adminlarga admin amallarini qo'llamaydi.
  • Salom va xizmat xabarlari. new_chat_members va left_chat_member xabarlari privacy mode'dan qat'i nazar keladi. Bot salom yozadi va xizmat xabarini deleteMessage bilan o'chiradi (48 soat ichida, can_delete_messages bilan).
  • /warn β†’ ban. Ogohlantirishlar "chat_id:user_id" kaliti bilan faylda saqlanadi; uchinchisida banChatMember. Ban muddatsiz (until_date e'tiborga olinmaydi).
  • /mute 10m. restrictChatMember + {"can_send_messages": false} + until_date. 30 soniyadan qisqa yoki 366 kundan uzun muddat server uchun "muddatsiz", shuning uchun kod 1 daqiqa – 366 kun oralig'ini talab qiladi. /unmute β€” {"can_send_messages": true}: cheklovni faqat shunday olish mumkin. Berilmagan ruxsat false hisoblanadi, ya'ni {} a'zoni jim qiladi.
  • /unban doim only_if_banned: true bilan: aks holda guruhdagi odam chiqarib yuborilardi.
  • Flood. 10 soniyada 5 tadan ko'p xabar yozgan a'zo (admin emas) 5 daqiqaga jim qilinadi, oxirgi xabari o'chiriladi. Faqat "barcha xabarlarni o'qish" huquqi bilan ishlaydi.
  • Xatolar. Huquq yetmasa yoki odam admin bo'lsa, server 400 qaytaradi β€” bot xato matnini guruhga yozadi (not enough rights to restrict/unrestrict chat member, user is an administrator of the chat …).

Eslatmalar

  • 429. with_retry parameters.retry_after soniya (10 gacha) kutib, o'sha so'rovni qayta yuboradi: rad etilgan so'rov hech narsa qilmagan, shuning uchun ogohlantirish yoki ban ikki marta bo'lmaydi.
  • Bitta guruhga daqiqasiga 20 tadan ortiq xabar yuborib bo'lmaydi (429). Bot ortiqcha gapirmasin: ko'p odam birdan qo'shilsa, salomni bitta xabarga birlashtirish yoki o'chirib qo'yish mumkin.
  • user_id β€” psevdonim id: /unban <id> faqat bot ilgari ko'rgan odam uchun ishlaydi (aks holda user not found). Boshqa botdagi id bu yerda ishlamaydi.
  • Ilovaning eski versiyalaridan yozilgan buyruqlar botga yetib bormaydi.
  • recent lug'ati faqat xotirada; juda katta guruhlarda eski yozuvlarni vaqti-vaqti bilan tozalang. Ogohlantirishlar va qoidalar moderator_state.json'da (uni maxfiy saqlang β€” unda guruh id'lari bor).
  • Guruhdan chiqarilganda (my_chat_member β†’ kicked) bot shu guruh keshini tozalaydi; kerak bo'lsa, ogohlantirishlarni ham o'chiring.
  • Production uchun bu kodni webhookka o'tkazish oson: handle(update) o'zgarmaydi (Webhook darsligi).