ZaminDocs

Eslatma bot

/eslatma 10m Choy ichish β€” 10 daqiqadan keyin bot eslatadi. Zamin'ning namunaviy @yordamchi_bot'idagi /eslatma kabi. Eslatmalar va offset JSON faylda saqlanadi, shuning uchun bot qayta ishga tushsa ham hech narsa yo'qolmaydi.

Kod

reminder_bot.py
import json
import os
import re
import time

import requests

TOKEN = os.environ["ZAMIN_BOT_TOKEN"]
API = f"https://api.zamin.app/bot{TOKEN}"
DB_FILE = "reminders.json"
UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
MAX_SECONDS = 30 * 86400
HELP = (
    "⏰ Eslatma bot\n\n"
    "/eslatma 10m Choy ichish β€” 10 daqiqadan keyin eslataman\n"
    "Birliklar: s (soniya), m (daqiqa), h (soat), d (kun), 30 kungacha\n"
    "/eslatmalar β€” rejalashtirilgan eslatmalar\n"
    "/tozalash β€” hammasini o'chirish"
)


def call(method, params=None, http_timeout=15):
    for _ in range(3):
        r = requests.post(f"{API}/{method}", json=params or {}, timeout=http_timeout)
        body = r.json()
        if body.get("ok"):
            return body["result"]
        retry = (body.get("parameters") or {}).get("retry_after")
        if body.get("error_code") == 429 and retry:
            time.sleep(retry)
            continue
        raise RuntimeError(f"{method}: {body.get('error_code')} {body.get('description')}")
    raise RuntimeError(f"{method}: juda ko'p urinish")


def load():
    try:
        with open(DB_FILE, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return {"offset": None, "reminders": []}


def save(db):
    tmp = DB_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(db, f, ensure_ascii=False)
    os.replace(tmp, DB_FILE)  # yarim yozilgan fayl qolmasin


db = load()


def send(chat_id, text):
    call("sendMessage", {"chat_id": chat_id, "text": text})


def on_message(message):
    chat_id = message["chat"]["id"]
    text = message.get("text", "").strip()
    mine = [r for r in db["reminders"] if r["chat_id"] == chat_id]

    m = re.match(r"^/eslatma(?:@\w+)?\s+(\d{1,4})([smhd])\s+(.+)$", text, re.S)
    if m:
        seconds = int(m.group(1)) * UNITS[m.group(2)]
        if not 0 < seconds <= MAX_SECONDS:
            return send(chat_id, "Muddat 1 soniyadan 30 kungacha bo'lsin.")
        if len(mine) >= 50:
            return send(chat_id, "Eslatmalar soni 50 tadan oshmasin.")
        db["reminders"].append({"chat_id": chat_id, "at": time.time() + seconds, "text": m.group(3)[:500]})
        save(db)
        return send(chat_id, f"πŸ‘Œ {m.group(1)}{m.group(2)} dan keyin eslataman: {m.group(3)[:500]}")

    if text.startswith("/eslatmalar"):
        if not mine:
            return send(chat_id, "Rejalashtirilgan eslatma yo'q.")
        lines = []
        for r in sorted(mine, key=lambda r: r["at"]):
            minutes = max(0, int((r["at"] - time.time()) // 60))
            lines.append(f"β€’ {minutes} daqiqadan keyin β€” {r['text']}")
        return send(chat_id, "⏰ Eslatmalar:\n" + "\n".join(lines))

    if text.startswith("/tozalash"):
        db["reminders"] = [r for r in db["reminders"] if r["chat_id"] != chat_id]
        save(db)
        return send(chat_id, "πŸ—‘ Barcha eslatmalar o'chirildi.")

    send(chat_id, HELP)


def forget(chat_id):
    """Foydalanuvchi botni blokladi: uning ma'lumotlarini saqlamaymiz."""
    db["reminders"] = [r for r in db["reminders"] if r["chat_id"] != chat_id]
    save(db)


def fire_due():
    now = time.time()
    due = [r for r in db["reminders"] if r["at"] <= now]
    for r in due:
        try:
            send(r["chat_id"], f"⏰ Eslatma: {r['text']}")
        except (requests.RequestException, RuntimeError, ValueError) as e:
            print("Yuborilmadi:", e)  # masalan, chat not found: foydalanuvchi bloklagan
        db["reminders"].remove(r)
    if due:
        save(db)


def main():
    call("deleteWebhook")
    call("setMyCommands", {"commands": [
        {"command": "eslatma", "description": "Eslatma: /eslatma 10m Choy ichish"},
        {"command": "eslatmalar", "description": "Eslatmalarim"},
        {"command": "tozalash", "description": "Hammasini o'chirish"},
    ]})
    while True:
        fire_due()
        # Long poll eng yaqin eslatma vaqtidan oshmasin
        upcoming = [r["at"] for r in db["reminders"]]
        wait = 25 if not upcoming else max(0, min(25, int(min(upcoming) - time.time())))
        params = {"timeout": wait, "allowed_updates": ["message", "my_chat_member"]}
        if db["offset"] is not None:
            params["offset"] = db["offset"]
        try:
            updates = call("getUpdates", params, http_timeout=wait + 10)
        except (requests.RequestException, RuntimeError, ValueError) as e:
            print("getUpdates xatosi:", e)
            time.sleep(3)
            continue
        for update in updates:
            db["offset"] = update["update_id"] + 1
            try:
                if "message" in update:
                    on_message(update["message"])
                elif "my_chat_member" in update:
                    member = update["my_chat_member"]
                    if member["new_chat_member"]["status"] == "kicked":
                        forget(member["chat"]["id"])
            except (requests.RequestException, RuntimeError, ValueError) as e:
                print("Xato:", e)
        if updates:
            save(db)  # offset ham saqlanadi: qayta ishga tushganda takrorlanmaydi


if __name__ == "__main__":
    main()

Ishga tushirish

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

Qanday ishlaydi

  • Alohida taymer yoki thread yo'q: long poll'ning timeout'i eng yaqin eslatmagacha qisqartiriladi, so'ng fire_due() vaqti kelganlarini yuboradi.
  • Fayl avval .tmp'ga yoziladi va os.replace bilan almashtiriladi β€” elektr o'chsa ham fayl buzilmaydi.
  • my_chat_member β†’ kicked kelsa, foydalanuvchining barcha eslatmalari o'chiriladi: bloklagan odamga baribir yozib bo'lmaydi, ma'lumotini saqlash esa keraksiz.
  • Foydalanuvchi bloklagan bo'lsa, sendMessage chat not found qaytaradi β€” eslatma tashlab yuboriladi.