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
- @ZaminBot'da
/newbotbilan bot yarating va tokenni oling./setjoingroupsstandart holatda yoqilgan β hech narsa qilish shart emas./setprivacy'ni o'zgartirmang: kerak bo'lsa, "barcha xabarlarni o'qish"ni guruh egasi o'zi beradi. - Ixtiyoriy:
/setcommandsbilan buyruqlar ro'yxatini kiriting (warn - Ogohlantirish,mute - Jim qilish,rules - Qoidalarβ¦). - Guruh egasi botni guruhga qo'shadi va admin qiladi: Xabarlarni o'chirish va Foydalanuvchilarni cheklash huquqlari bilan.
- 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.pyGuruhda 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
/warnbuyruq bo'lgani uchun botga keladi.reply_to_message'da boshqa odamning xabari mazmunsiz, lekinfrombilan keladi β shufrom.idkerakli odam./warn@BoshqaBote'tiborsiz qoldiriladi, oddiy/warnesa guruhdagi har bir botga boradi. - Adminlarni aniqlash.
getChatAdministratorsnatijasi 5 daqiqaga keshlanadi. Undagi id'larmessage.from.idbilan 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_membersvaleft_chat_memberxabarlari privacy mode'dan qat'i nazar keladi. Bot salom yozadi va xizmat xabarinideleteMessagebilan o'chiradi (48 soat ichida,can_delete_messagesbilan). - /warn β ban. Ogohlantirishlar
"chat_id:user_id"kaliti bilan faylda saqlanadi; uchinchisidabanChatMember. Ban muddatsiz (until_datee'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 ruxsatfalsehisoblanadi, ya'ni{}a'zoni jim qiladi. - /unban doim
only_if_banned: truebilan: 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
400qaytaradi β bot xato matnini guruhga yozadi (not enough rights to restrict/unrestrict chat member,user is an administrator of the chatβ¦).
Eslatmalar
- 429.
with_retryparameters.retry_aftersoniya (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 holdauser not found). Boshqa botdagi id bu yerda ishlamaydi.- Ilovaning eski versiyalaridan yozilgan buyruqlar botga yetib bormaydi.
recentlug'ati faqat xotirada; juda katta guruhlarda eski yozuvlarni vaqti-vaqti bilan tozalang. Ogohlantirishlar va qoidalarmoderator_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).