router.go raw
1 package main
2
3 import (
4 "common/helpers"
5 "common/jsbridge/sw"
6 )
7
8 // Subscription Router — thin forwarder to relay SW via bus.
9 // All relay operations, subscriptions, and caching are handled by the relay SW.
10
11 // pendingSentDMs holds MLS_SEND saves deferred because myPubkey wasn't set yet.
12 // Replayed when SET_PUBKEY arrives.
13 type pendingSentDM struct {
14 recipient string
15 content string
16 }
17
18 var pendingSentDMs []pendingSentDM
19 var mlsRelays []string
20 var marmotSubs map[string]bool // tracks active marmot proxy subscriptions for cleanup
21
22 // sentDMDedup tracks recently sent DM content to filter self-echoes.
23 // MLS handleGroupMessage always attributes messages to PeerPub, so when
24 // our own outbound echoes back (seenIDs lost on WASM restart), it appears
25 // as a message from the peer. This ring buffer catches that.
26 var sentDMDedup [32]string
27 var sentDMIdx int
28
29 func markSentDM(peer, content string) {
30 sentDMDedup[sentDMIdx%32] = peer + "\x00" + content
31 sentDMIdx++
32 }
33
34 func isSentDMEcho(peer, content string) bool {
35 needle := peer + "\x00" + content
36 for _, s := range sentDMDedup {
37 if s == needle {
38 return true
39 }
40 }
41 return false
42 }
43
44 func flushPendingSentDMs() {
45 if myPubkey == "" || len(pendingSentDMs) == 0 {
46 return
47 }
48 for _, p := range pendingSentDMs {
49 now := sw.NowSeconds()
50 rec := makeDMRecord(p.recipient, myPubkey, p.content, now, "marmot", "")
51 busSend("relay", "[\"SAVE_DM_QUIET\","+rec.ToJSON()+"]")
52 }
53 pendingSentDMs = nil
54 }
55
56 func routeMessage(clientID string, w *mw, msgType string) {
57 sw.Log("shell: page→" + msgType)
58 switch msgType {
59 // Identity — handle locally + forward to relay SW.
60 case "SET_PUBKEY":
61 pk := w.str()
62 identitySetPubkey(pk)
63 busSend("relay", "[\"SET_PUBKEY\","+jstr(pk)+"]")
64 flushPendingSentDMs()
65 case "CLEAR_KEY":
66 identityClearKey()
67 busSend("relay", "[\"CLEAR_KEY\"]")
68
69 // Relay operations — forward to relay SW.
70 case "REQ":
71 subID := w.str()
72 filterRaw := w.raw()
73 busSend("relay", "[\"REQ\","+jstr(clientID)+","+jstr(subID)+","+filterRaw+"]")
74 case "CLOSE":
75 subID := w.str()
76 busSend("relay", "[\"CLOSE\","+jstr(subID)+"]")
77 case "EVENT":
78 eventRaw := w.raw()
79 busSend("relay", "[\"EVENT\","+jstr(clientID)+","+eventRaw+"]")
80 case "PROXY":
81 subID := w.str()
82 filterRaw := w.raw()
83 relayURLs := w.strs()
84 busSend("relay", "[\"PROXY\","+jstr(clientID)+","+jstr(subID)+","+filterRaw+","+strsJSON(relayURLs)+"]")
85 case "RELAY_INFO":
86 relayURL := w.str()
87 busSend("relay", "[\"RELAY_INFO\","+jstr(clientID)+","+jstr(relayURL)+"]")
88 case "SET_WRITE_RELAYS":
89 busSend("relay", "[\"SET_WRITE_RELAYS\","+strsJSON(w.strs())+"]")
90 case "SIGN":
91 requestID := w.str()
92 eventRaw := w.raw()
93 busSend("relay", "[\"SIGN\","+jstr(clientID)+","+jstr(requestID)+","+eventRaw+"]")
94 case "BROADCAST":
95 pubkey := w.str()
96 relayURLs := w.strs()
97 busSend("relay", "[\"BROADCAST\","+jstr(clientID)+","+jstr(pubkey)+","+strsJSON(relayURLs)+"]")
98
99 // DM operations — forward to relay SW for IDB.
100 case "DM_LIST":
101 busSend("relay", "[\"DM_LIST\","+jstr(clientID)+"]")
102 case "DM_HISTORY":
103 peer := w.str()
104 limit := int(w.num())
105 until := w.num()
106 busSend("relay", "[\"DM_HISTORY\","+jstr(clientID)+","+jstr(peer)+","+helpers.Itoa(int64(limit))+","+helpers.Itoa(until)+"]")
107 case "CLEAR_DM_HISTORY":
108 peer := w.str()
109 busSend("relay", "[\"CLEAR_DM_HISTORY\","+jstr(peer)+"]")
110
111 // MLS — proxy through page to signer extension (marmot WASM runs inside signer).
112 case "MLS_INIT":
113 relays := w.strs()
114 mlsRelays = relays
115 sendToClient(clientID, "[\"MLS_PROXY\",\"init\","+strsJSON(relays)+"]")
116 case "MLS_SEND":
117 recipient := w.str()
118 content := w.str()
119 markSentDM(recipient, content)
120 sendToClient(clientID, "[\"MLS_PROXY\",\"sendDM\","+jstr(recipient)+","+jstr(content)+"]")
121 // Save sent DM to relay's IDB (quiet — no DM_RECEIVED broadcast).
122 if myPubkey == "" {
123 pendingSentDMs = append(pendingSentDMs, pendingSentDM{recipient, content})
124 } else {
125 now := sw.NowSeconds()
126 rec := makeDMRecord(recipient, myPubkey, content, now, "marmot", "")
127 busSend("relay", "[\"SAVE_DM_QUIET\","+rec.ToJSON()+"]")
128 }
129 case "MLS_SUB":
130 sendToClient(clientID, "[\"MLS_PROXY\",\"subscribe\"]")
131 case "MLS_PUBLISH_KP":
132 sendToClient(clientID, "[\"MLS_PROXY\",\"publishKP\"]")
133 case "MLS_LIST_GROUPS":
134 sendToClient(clientID, "[\"MLS_PROXY\",\"listGroups\"]")
135 case "MLS_BACKUP":
136 sendToClient(clientID, "[\"MLS_PROXY\",\"backupGroups\"]")
137 case "MLS_RESTORE":
138 sendToClient(clientID, "[\"MLS_PROXY\",\"restoreGroups\"]")
139 case "MLS_RATCHET":
140 peer := w.str()
141 sendToClient(clientID, "[\"MLS_PROXY\",\"ratchetGroup\","+jstr(peer)+"]")
142
143 // MLS results from page (mls-bridge.mjs routes signer extension outputs here).
144 // Relay URLs may come from mlsRelays (set by MLS_INIT) or inline in the message
145 // (set by mls-bridge.mjs). The inline URLs ensure routing works even before the
146 // Go WASM app sends MLS_INIT.
147 case "MLS_PUBLISH":
148 eventRaw := w.str()
149 relays := w.strs()
150 if len(relays) == 0 {
151 relays = mlsRelays
152 }
153 if len(relays) > 0 {
154 busSend("relay", "[\"MLS_RELAY_PUBLISH\","+eventRaw+","+strsJSON(relays)+"]")
155 } else {
156 busSend("relay", "[\"EVENT\",\"\","+eventRaw+"]")
157 }
158 case "MLS_SUBSCRIBE":
159 subID := w.str()
160 filterRaw := w.raw()
161 relays := w.strs()
162 if len(relays) == 0 {
163 relays = mlsRelays
164 }
165 // Pass filters as-is (array or single object) — relay SW's parseFilters handles both.
166 mSubID := "marmot-sub-" + subID
167 if marmotSubs == nil {
168 marmotSubs = make(map[string]bool)
169 }
170 marmotSubs[mSubID] = true
171 if len(relays) > 0 {
172 busSend("relay", "[\"PROXY\",\"\","+jstr(mSubID)+","+filterRaw+","+strsJSON(relays)+"]")
173 } else {
174 busSend("relay", "[\"REQ\",\"\","+jstr(mSubID)+","+filterRaw+"]")
175 }
176 case "MLS_DM":
177 dmJSON := w.raw()
178 // mls-bridge sends {peer, sender, content, ts, source, eventId}
179 // but IDB expects {id, peer, from, content, created_at, protocol, eventId}.
180 peer := jsonField(dmJSON, "peer")
181 sender := jsonField(dmJSON, "sender")
182 content := jsonField(dmJSON, "content")
183 // Self-echo filter: MLS handleGroupMessage always sets sender=PeerPub.
184 // If we recently sent this exact content to this peer, it's our own echo.
185 if isSentDMEcho(peer, content) {
186 return
187 }
188 ts := parseTS(jsonFieldRaw(dmJSON, "ts"))
189 source := jsonField(dmJSON, "source")
190 eventID := jsonField(dmJSON, "eventId")
191 rec := makeDMRecord(peer, sender, content, ts, source, eventID)
192 recJSON := rec.ToJSON()
193 busSend("relay", "[\"SAVE_DM_QUIET\","+recJSON+"]")
194 fwdDM(recJSON)
195 case "MLS_GROUPS":
196 groupsJSON := w.raw()
197 broadcastToClients("[\"MLS_GROUPS\"," + groupsJSON + "]")
198 case "MLS_STATUS":
199 statusMsg := w.str()
200 broadcastToClients("[\"MLS_STATUS\"," + jstr(statusMsg) + "]")
201
202 // Crypto result from page — dispatch to waiting callback.
203 case "CRYPTO_RESULT":
204 id := int(w.num())
205 result := w.str()
206 errMsg := w.str()
207 if fn, ok := cryptoCBs[id]; ok {
208 delete(cryptoCBs, id)
209 fn(result, errMsg)
210 }
211 }
212 }
213
214 // fwdDM broadcasts a DM_RECEIVED message to all page clients.
215 func fwdDM(dmJSON string) {
216 broadcastToClients("[\"DM_RECEIVED\"," + dmJSON + "]")
217 }
218