"""Your first Yapper bot in Python. Nothing to install (standard library only). Python 3.9+.

  1. Put your token below (Settings -> Developer -> Create).
  2. Add the bot to a server you manage (same place), then run:  python bot.py
"""
import json, os, random, time, urllib.error, urllib.request

TOKEN = os.environ.get("YAPPER_BOT_TOKEN", "PASTE_YOUR_TOKEN_HERE")
BASE = "https://www.yapper.website/api/bot/v1"


def api(method, path, body=None, tries=3):
    req = urllib.request.Request(BASE + path, method=method, data=None if body is None else json.dumps(body).encode(),
                                 headers={"Authorization": f"Bot {TOKEN}", "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=40) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        try: err = json.load(e)
        except Exception: err = {}
        msg = err.get("error") if isinstance(err, dict) else None
        # 429 rate limited, or Yapper's own 503 "busy" (it says retry_after, and nothing was done): wait, try again.
        busy = e.code == 503 and isinstance(err, dict) and "retry_after" in err
        wait = int(e.headers.get("Retry-After") or 0) if e.code == 429 or busy else 0
        if 0 < wait <= 60 and tries > 1:
            print(f"{'busy' if busy else 'rate limited'}, retrying in {wait}s"); time.sleep(wait * (1 + random.random() * 0.5))
            return api(method, path, body, tries - 1)
        raise RuntimeError(f"{method} {path} failed ({e.code}): {msg or e.reason}") from None


def send(channel_id, content, reply_to=None):
    return api("POST", f"/channels/{channel_id}/messages", {"content": content, "reply_to": reply_to})


# Commands show up in the "/" menu for everyone in the server.
api("PUT", "/commands", {"commands": [
    {"name": "ping", "description": "Is the bot alive?"},
    {"name": "roll", "description": "Roll a die, like /roll 20"},
]})
print("online as", api("GET", "/me")["username"])

def handle(ev):
    d = ev["data"]
    if ev["type"] == "message":
        if d.get("author_bot"):                          # another bot or a webhook: don't answer (no bot-to-bot loops)
            return
        if d.get("command") == "ping":
            send(d["channel_id"], "pong 🏓", reply_to=d["id"])
        elif d.get("command") == "roll":
            first = (d.get("args") or "6").split()[0]
            sides = max(2, min(1000, int(first) if first.isdigit() else 6))
            send(d["channel_id"], f"🎲 {random.randint(1, sides)} (d{sides})", reply_to=d["id"])
        elif d.get("mentioned"):
            send(d["channel_id"], f"hey <@{d['author_id']}>, try /ping", reply_to=d["id"])
    elif ev["type"] == "member_join":
        chans = api("GET", f"/servers/{d['server_id']}/channels")["channels"]
        text = [c for c in chans if c["kind"] == "text"]
        if text:
            send(text[0]["id"], f"welcome <@{d['user_id']}>! you're member #{d['member_count']}")


last = api("GET", "/events?after=0&wait=0")["last"]   # skip old events
fails = 0
while True:
    try:
        out = api("GET", f"/events?after={last}&wait=25")
        fails = 0
    except Exception as e:                               # network blip: wait longer each time, then retry
        fails += 1
        if "(401)" in str(e):
            raise SystemExit("Token rejected: make a new one in Settings -> Developer")
        wait = min(30, 2 ** fails)
        print(f"retrying in {wait}s:", e); time.sleep(wait); continue
    for ev in out["events"]:
        if ev["id"] <= last:                             # already handled
            continue
        last = ev["id"]
        try:
            handle(ev)
        except Exception as e:                           # one failed reply shouldn't stop the bot
            print("error handling", ev["type"], "event:", e)
    last = max(last, out.get("last") or 0)
