""" Invitation links: `https:///#/invite?v=1&g=&t=&n=&c=`. The shape `static/invite-link.js` reads and `api/invite_links.py` writes (docs/MESHBAY_DESIGN.md ยง3.4). Two secrets: the ticket is for the hub, which makes the first account to redeem it a member; the code is for the node the link names, and goes to no other node and never to the hub. Every field is checked for its exact shape, as the application checks it. """ import re from dataclasses import dataclass from urllib.parse import parse_qs, urlsplit _GROUP = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") _TICKET = re.compile(r"^[A-Za-z0-9_-]{22}$") _NODE_PK = re.compile(r"^[A-Za-z0-9_-]{43}$") _CODE = re.compile(r"^[0-9A-Za-z]{4}-[0-9A-Za-z]{4}$") @dataclass class InviteLink: origin: str # the hub the link was made on, '' when only the fragment was given group_id: str ticket: str node_pk: str # standard base64, as the hub lists node keys code: str def node_pk_from_link(n: str) -> str: """The link's URL-safe, unpadded key, as the hub and the transport write it.""" b64 = n.replace("-", "+").replace("_", "/") return b64 + "=" * (-len(b64) % 4) def parse_link(text: str) -> InviteLink | None: """The link in `text`, or None when it holds none (or a malformed one).""" s = text.strip() at = s.find("/invite?") if at < 0: return None q = {k: v[0] for k, v in parse_qs(s[at + len("/invite?"):]).items()} g, t, n = q.get("g", ""), q.get("t", ""), q.get("n", "") c = q.get("c", "").upper() if q.get("v") != "1" or not (_GROUP.match(g) and _TICKET.match(t) and _NODE_PK.match(n) and _CODE.match(c)): return None parts = urlsplit(s) origin = f"{parts.scheme}://{parts.netloc}" if parts.scheme and parts.netloc else "" return InviteLink(origin, g, t, node_pk_from_link(n), c)