#!/usr/bin/env python3
"""Remoting.org quickstart: register (solving the Remoting captcha), log in, create a room,
send a message, upload + send a file, sync. Standard library only.

    python3 remoting_quickstart.py <username> <password>
"""
import base64, hashlib, json, sys, time, urllib.request, urllib.error, uuid

HS = "https://matrix.remoting.org"            # homeserver API base
CAPTCHA = "https://chat.remoting.org/signup/challenge"
UA = {"User-Agent": "remoting-quickstart/1.0"}

def call(method, path, body=None, token=None, raw=None, ctype="application/json", base=HS):
    h = dict(UA)
    if token: h["Authorization"] = f"Bearer {token}"
    data = raw if raw is not None else (json.dumps(body).encode() if body is not None else None)
    if data is not None: h["Content-Type"] = ctype
    req = urllib.request.Request(base + path, data=data, headers=h, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as r: return r.status, json.load(r)
    except urllib.error.HTTPError as e: return e.code, json.load(e)

def solve_captcha():
    """Remoting's captcha is proof-of-work: find a nonce so sha256(salt + nonce) starts
    with `difficulty` zero bits. Returns the token Synapse expects as the captcha response."""
    c = json.load(urllib.request.urlopen(urllib.request.Request(CAPTCHA, headers=UA), timeout=30))
    n = 0
    while True:
        h = hashlib.sha256(f"{c['salt']}{n}".encode()).digest()
        bits = 0
        for b in h:
            if b == 0: bits += 8; continue
            bits += 8 - b.bit_length(); break
        if bits >= c["difficulty"]: break
        n += 1
    tok = {"salt": c["salt"], "exp": c["exp"], "sig": c["sig"], "nonce": str(n)}
    return base64.urlsafe_b64encode(json.dumps(tok).encode()).decode().rstrip("=")

def register(user, pw):
    # 1. start: the server answers 401 with a session id and the required stages
    _, r = call("POST", "/_matrix/client/v3/register", {"username": user, "password": pw})
    session = r["session"]
    # 2. captcha stage (m.login.recaptcha on the wire, but it is Remoting's own proof-of-work)
    call("POST", "/_matrix/client/v3/register", {"username": user, "password": pw,
         "auth": {"type": "m.login.recaptcha", "session": session, "response": solve_captcha()}})
    # 3. dummy stage finishes it and returns an access token
    s, r = call("POST", "/_matrix/client/v3/register", {"username": user, "password": pw,
         "initial_device_display_name": "quickstart", "auth": {"type": "m.login.dummy", "session": session}})
    if s != 200: raise SystemExit(f"register failed: {s} {r}")
    return r

def login(user, pw):
    s, r = call("POST", "/_matrix/client/v3/login", {"type": "m.login.password",
         "identifier": {"type": "m.id.user", "user": user}, "password": pw})
    if s != 200: raise SystemExit(f"login failed: {s} {r}")
    return r["access_token"], r["user_id"]

def main():
    user, pw = sys.argv[1], sys.argv[2]
    s, _ = call("GET", f"/_matrix/client/v3/register/available?username={user}")
    if s == 200:
        print("registered:", register(user, pw)["user_id"])
    token, me = login(user, pw)
    print("logged in as", me)

    # create a private room (invite people with "invite": ["@friend:remoting.org"])
    _, room = call("POST", "/_matrix/client/v3/createRoom", {"name": "Quickstart room", "preset": "private_chat"}, token)
    room_id = room["room_id"]; print("room:", room_id)

    # join a room by alias or id (public rooms only, or rooms you were invited to)
    # call("POST", "/_matrix/client/v3/join/%23remoting.org%3Aremoting.org", {}, token)

    # send a text message (txn id makes retries safe)
    _, ev = call("PUT", f"/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{uuid.uuid4()}",
                 {"msgtype": "m.text", "body": "Hello from the Remoting quickstart!"}, token)
    print("message:", ev["event_id"])

    # upload a file, then send it into the room
    data = b"hello remoting\n"
    _, up = call("POST", "/_matrix/media/v3/upload?filename=hello.txt", raw=data, ctype="text/plain", token=token)
    _, ev = call("PUT", f"/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{uuid.uuid4()}",
                 {"msgtype": "m.file", "body": "hello.txt", "url": up["content_uri"],
                  "info": {"mimetype": "text/plain", "size": len(data)}}, token)
    print("file:", up["content_uri"], ev["event_id"])

    # read: one sync returns your rooms and their latest messages
    _, sync = call("GET", "/_matrix/client/v3/sync?timeout=0", token=token)
    msgs = [e["content"].get("body") for e in sync["rooms"]["join"][room_id]["timeline"]["events"] if e["type"] == "m.room.message"]
    print("timeline:", msgs)

if __name__ == "__main__":
    main()
