| 1 | package client |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "net/url" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // invitePrefix is the versioned fingerprint every minted link carries |
| 10 | // (web/static/invite.js buildInviteFragment). Older links are bare T. |
| 11 | const invitePrefix = "e1." |
| 12 | |
| 13 | // InviteLink is a parsed /join/... link: where to join, the secret T (never |
| 14 | // sent to the server), and the inviter's suggested pre-fill name (rides in the |
| 15 | // fragment for the same reason — the server must not see it). |
| 16 | type InviteLink struct { |
| 17 | Base string // scheme://host origin |
| 18 | T string // the invite secret (base64url) |
| 19 | Name string // suggested name ("" when the link carries none) |
| 20 | } |
| 21 | |
| 22 | // ParseInviteLink parses a full invite link — https://host/join/<label>#<frag> |
| 23 | // — mirroring web/static/invite.js parseInviteFragment: the fragment is |
| 24 | // "e1.<T>" or "e1.<T>~<urlencoded name>" (bare T on older links). |
| 25 | func ParseInviteLink(link string) (InviteLink, error) { |
| 26 | u, err := url.Parse(strings.TrimSpace(link)) |
| 27 | if err != nil || u.Scheme == "" || u.Host == "" { |
| 28 | return InviteLink{}, fmt.Errorf("client: that doesn't look like an invite link (expected https://…/join/…#…)") |
| 29 | } |
| 30 | frag := u.Fragment |
| 31 | if frag == "" { |
| 32 | return InviteLink{}, fmt.Errorf("client: the link is missing its #secret — copy the whole invite link") |
| 33 | } |
| 34 | frag = strings.TrimPrefix(frag, invitePrefix) |
| 35 | t, name := frag, "" |
| 36 | if i := strings.Index(frag, "~"); i >= 0 { |
| 37 | t = frag[:i] |
| 38 | if n, err := url.QueryUnescape(frag[i+1:]); err == nil { |
| 39 | name = n |
| 40 | } |
| 41 | } |
| 42 | if t == "" { |
| 43 | return InviteLink{}, fmt.Errorf("client: the link is missing its #secret — copy the whole invite link") |
| 44 | } |
| 45 | return InviteLink{Base: u.Scheme + "://" + u.Host, T: t, Name: name}, nil |
| 46 | } |
| 47 | |