eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3// The Mac app's RAIL record: which spaces show, in what order, plus legacy
4// fields from the webview era (the full-frame webview and its join are gone —
5// identity is `SavedInstance` in the Keychain, arriving via the vault).
6// Retiring SavedChat entirely in favour of SavedInstance is deliberate future
7// work; today it only drives rail order and carries legacy tokens.
8
9// The persisted session for one chat. The key never leaves the device — it lives in
10// the webview's own storage, not here; this just carries what the webview needs.
11struct Session: Codable, Equatable {
12 var baseURL: URL // e.g. https://chat.example.com — derived from the invite link
13 var token: String // invite token (transport auth only; NOT a decryption key)
14 var keyB64: String // base64url AES-GCM key from the link's #fragment (legacy)
15 var name: String // screen name
16
17 /// wss/ws URL for the live feed, matching baseURL's scheme.
18 var socketURL: URL {
19 var c = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
20 c.scheme = baseURL.scheme == "https" ? "wss" : "ws"
21 c.path = "/ws"
22 c.queryItems = [URLQueryItem(name: "token", value: token)]
23 return c.url!
24 }
25
26 /// The Origin header value for the WebSocket upgrade (same-origin check).
27 var origin: String {
28 var s = baseURL.scheme.map { "\($0)://" } ?? ""
29 s += baseURL.host ?? ""
30 if let port = baseURL.port { s += ":\(port)" }
31 return s
32 }
33}
34
35// A saved chat: the invite URL (which carries host, token and key in its #fragment
36// for legacy links) plus the screen name. New-format (server-blind) links don't
37// carry the key — the web join flow inside the WKWebView fetches + unwraps it and
38// hands back a resolved session, persisted as claimedToken/claimedKey.
39struct SavedChat: Codable, Identifiable, Equatable {
40 var url: String
41 var name: String
42 var lastSeenId: Int64 = 0
43 var claimedToken: String?
44 var claimedKey: String?
45
46 var id: String { url }
47
48 /// The origin (scheme://host[:port]) of the stored link.
49 var origin: URL? {
50 guard let c = URLComponents(string: url), let scheme = c.scheme, let host = c.host else { return nil }
51 var o = URLComponents()
52 o.scheme = scheme
53 o.host = host
54 o.port = c.port
55 return o.url
56 }
57
58 /// A usable Session, or nil if we don't have the key yet. Prefers a resolved
59 /// (claimed) new-format session; otherwise parses a legacy `#k=` link.
60 var session: Session? {
61 if let token = claimedToken, let key = claimedKey, let base = origin {
62 return Session(baseURL: base, token: token, keyB64: key, name: name)
63 }
64 guard let p = SavedChat.parseInvite(url) else { return nil }
65 return Session(baseURL: p.baseURL, token: p.token, keyB64: p.keyB64, name: name)
66 }
67
68 /// The URL the WKWebView should load: the chat origin (`/`) once we have a
69 /// session to seed, otherwise the raw `/join/…` link so the web claims it.
70 var loadURL: URL? { session?.baseURL ?? URL(string: url) }
71
72 /// De-dupe token (claimed device token, else the legacy invite token from the URL).
73 var token: String? { claimedToken ?? SavedChat.parseInvite(url)?.token }
74
75 struct ParsedInvite { let baseURL: URL; let token: String; let keyB64: String }
76
77 /// Parse a legacy `https://host/join/<token>#k=<key>` link. Lives here because
78 /// main's shared `LChatAPI` dropped this when iOS moved to native key management.
79 static func parseInvite(_ raw: String) -> ParsedInvite? {
80 let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
81 guard var comps = URLComponents(string: trimmed),
82 let scheme = comps.scheme, comps.host != nil,
83 scheme == "http" || scheme == "https"
84 else { return nil }
85
86 let token = comps.path.split(separator: "/").last.map(String.init)
87 // The key lives in the fragment as `k=…` (URLComponents leaves it raw).
88 var keyB64: String?
89 if let frag = comps.fragment {
90 for pair in frag.split(separator: "&") {
91 let kv = pair.split(separator: "=", maxSplits: 1)
92 if kv.first == "k", kv.count == 2 { keyB64 = String(kv[1]) }
93 }
94 }
95 guard let token, !token.isEmpty, let keyB64, !keyB64.isEmpty else { return nil }
96
97 // Reduce to a clean origin URL (scheme://host[:port]).
98 comps.path = ""
99 comps.query = nil
100 comps.fragment = nil
101 guard let base = comps.url else { return nil }
102 return ParsedInvite(baseURL: base, token: token, keyB64: keyB64)
103 }
104}
105