router.go raw
1 package main
2
3 import (
4 "common/helpers"
5 "common/jsbridge/registry"
6 "common/jsbridge/sw"
7 "common/nostr"
8 )
9
10 // Subscription Router domain — central dispatcher.
11 // Extension calls (crypto, marmot) go through the registry.
12
13 var (
14 clientSubs map[string]*clientSub
15 proxySubs map[string]*proxySub
16 )
17
18 type clientSub struct {
19 filter *nostr.Filter
20 filterRaw string
21 clientID string
22 }
23
24 type proxySub struct {
25 remoteIDs map[string]bool
26 relayCount int
27 eoseCount int
28 timer sw.Timer
29 done bool
30 }
31
32 func initRouter() {
33 clientSubs = make(map[string]*clientSub)
34 proxySubs = make(map[string]*proxySub)
35 }
36
37 func routeMessage(clientID string, w *mw, msgType string) {
38 switch msgType {
39 case "REQ":
40 subID := w.str()
41 filterRaw := w.raw()
42 routerReq(clientID, subID, filterRaw)
43
44 case "CLOSE":
45 subID := w.str()
46 routerClose(subID)
47
48 case "EVENT":
49 eventRaw := w.raw()
50 routerPublish(clientID, eventRaw)
51
52 case "PROXY":
53 subID := w.str()
54 filterRaw := w.raw()
55 relayURLs := w.strs()
56 routerProxy(clientID, subID, filterRaw, relayURLs)
57
58 case "RELAY_INFO":
59 relayURL := w.str()
60 handleRelayInfo(clientID, relayURL)
61
62 case "SET_KEY":
63 hexKey := w.str()
64 identitySetKey(hexKey)
65 sendToClient(clientID, "[\"KEY_SET\"]")
66
67 case "SET_PUBKEY":
68 identitySetPubkey(w.str())
69
70 case "CLEAR_KEY":
71 identityClearKey()
72 writeRelays = nil
73
74 case "SET_WRITE_RELAYS":
75 writeRelays = w.strs()
76
77 case "SIGN":
78 requestID := w.str()
79 eventRaw := w.raw()
80 routerSign(clientID, requestID, eventRaw)
81
82 case "BROADCAST":
83 pubkey := w.str()
84 relayURLs := w.strs()
85 routerBroadcast(clientID, pubkey, relayURLs)
86
87 case "SEND_DM":
88 recipientPubkey := w.str()
89 content := w.str()
90 relayURLs := w.strs()
91 routerSendDM(clientID, recipientPubkey, content, relayURLs)
92
93 case "DM_SUB":
94 relayURLs := w.strs()
95 routerDMSub(clientID, relayURLs)
96
97 case "DM_LIST":
98 routerDMList(clientID)
99
100 case "DM_HISTORY":
101 peer := w.str()
102 limit := int(w.num())
103 until := w.num()
104 routerDMHistory(clientID, peer, limit, until)
105
106 case "MLS_INIT":
107 relayURLs := w.strs()
108 json := stringsToJSON(relayURLs)
109 if !registry.HasHook("marmotInit") {
110 registry.LoadModule("swmarmot", func() { registry.MarmotInit(json) })
111 } else {
112 registry.MarmotInit(json)
113 }
114
115 case "MLS_SEND":
116 recipient := w.str()
117 content := w.str()
118 if !registry.HasHook("marmotSend") {
119 registry.LoadModule("swmarmot", func() { registry.MarmotSend(recipient, content) })
120 } else {
121 registry.MarmotSend(recipient, content)
122 }
123
124 case "MLS_SUB":
125 if !registry.HasHook("marmotSubscribe") {
126 registry.LoadModule("swmarmot", func() { registry.MarmotSubscribe() })
127 } else {
128 registry.MarmotSubscribe()
129 }
130
131 case "MLS_PUBLISH_KP":
132 relayURLs := w.strs()
133 json := stringsToJSON(relayURLs)
134 if !registry.HasHook("marmotPublishKP") {
135 registry.LoadModule("swmarmot", func() { registry.MarmotPublishKP(json) })
136 } else {
137 registry.MarmotPublishKP(json)
138 }
139
140 case "MLS_LIST_GROUPS":
141 if !registry.HasHook("marmotListGroups") {
142 registry.LoadModule("swmarmot", func() { registry.MarmotListGroups(clientID) })
143 } else {
144 registry.MarmotListGroups(clientID)
145 }
146
147 case "CRYPTO_RESULT":
148 id := int(w.num())
149 result := w.str()
150 errMsg := w.str()
151 if fn, ok := cryptoCBs[id]; ok {
152 delete(cryptoCBs, id)
153 fn(result, errMsg)
154 }
155
156 case "PAGE":
157 page := w.str()
158 routerPageHint(page)
159 }
160 }
161
162 // --- REQ / CLOSE / EVENT ---
163
164 func routerReq(clientID, subID, filterRaw string) {
165 f := nostr.ParseFilter(filterRaw)
166 if f == nil {
167 return
168 }
169 clientSubs[subID] = &clientSub{filter: f, filterRaw: filterRaw, clientID: clientID}
170
171 cacheQuery(filterRaw, func(eventsJSON string) {
172 events := nostr.ParseEventsJSON(eventsJSON)
173 for _, ev := range events {
174 sendToClient(clientID, "[\"EVENT\","+jstr(subID)+","+ev.ToJSON()+"]")
175 }
176 sendToClient(clientID, "[\"EOSE\","+jstr(subID)+"]")
177 })
178 }
179
180 func routerClose(subID string) {
181 delete(clientSubs, subID)
182 routerCleanupProxy(subID)
183 }
184
185 func routerPublish(clientID, eventRaw string) {
186 ev := nostr.ParseEvent(eventRaw)
187 if ev == nil {
188 return
189 }
190
191 cacheStore(eventRaw, func(saved bool) {
192 if saved {
193 pushToMatchingSubs(ev)
194 }
195 })
196
197 relayPublish(ev)
198 sendToClient(clientID, "[\"OK\","+jstr(ev.ID)+",true,\"\"]")
199 }
200
201 // --- PROXY subscriptions ---
202
203 func routerProxy(clientID, subID, filterRaw string, relayURLs []string) {
204 routerCleanupProxy(subID)
205
206 f := nostr.ParseFilter(filterRaw)
207 if f == nil {
208 return
209 }
210 clientSubs[subID] = &clientSub{filter: f, filterRaw: filterRaw, clientID: clientID}
211
212 remoteIDs := make(map[string]bool)
213 base := "p_" + subID + "_"
214
215 proxySubs[subID] = &proxySub{
216 remoteIDs: remoteIDs,
217 relayCount: len(relayURLs),
218 }
219
220 for _, url := range relayURLs {
221 suffix := urlSuffix(url)
222 rSubID := base + suffix
223 remoteIDs[rSubID] = true
224 c := getConn(url)
225 c.Subscribe(rSubID, []*nostr.Filter{f})
226 }
227
228 proxyID := subID
229 proxySubs[subID].timer = sw.SetTimeout(5000, func() {
230 info, ok := proxySubs[proxyID]
231 if ok && !info.done {
232 info.done = true
233 if cs, ok := clientSubs[proxyID]; ok {
234 sendToClient(cs.clientID, "[\"EOSE\","+jstr(proxyID)+"]")
235 }
236 }
237 })
238 }
239
240 func routerCleanupProxy(proxyID string) {
241 info, ok := proxySubs[proxyID]
242 if !ok {
243 return
244 }
245 sw.ClearTimeout(info.timer)
246
247 if !info.done {
248 if cs, ok := clientSubs[proxyID]; ok {
249 sendToClient(cs.clientID, "[\"EOSE\","+jstr(proxyID)+"]")
250 }
251 }
252
253 for rSubID := range info.remoteIDs {
254 for _, url := range rpool.URLs() {
255 c := rpool.Get(url)
256 if c != nil && c.IsOpen() {
257 c.CloseSubscription(rSubID)
258 }
259 }
260 }
261 delete(proxySubs, proxyID)
262 delete(clientSubs, proxyID)
263 }
264
265 // --- Relay event callbacks ---
266
267 func routerOnRelayEvent(relayURL string, ev *nostr.Event) {
268 evJSON := ev.ToJSON()
269
270 pushToMatchingSubs(ev)
271
272 cacheStore(evJSON, func(saved bool) {
273 if saved {
274 if ev.Kind != 4 && ev.Kind != 1059 {
275 relayPublishExcept(ev, relayURL)
276 }
277 }
278 if ev.Kind == 4 || ev.Kind == 1059 {
279 decryptFn := func() {
280 registry.DecryptDM(evJSON, func(dmRecJSON string) {
281 if dmRecJSON != "" {
282 routerSaveDMRecordJSON(dmRecJSON)
283 }
284 })
285 }
286 if !registry.HasHook("decryptDM") {
287 registry.LoadModule("swcrypto", func() { decryptFn() })
288 } else {
289 decryptFn()
290 }
291 }
292 })
293 }
294
295 func routerOnRelayEOSE(subID string) {
296 for proxyID, info := range proxySubs {
297 if info.remoteIDs[subID] {
298 info.eoseCount++
299 if info.eoseCount >= info.relayCount && !info.done {
300 info.done = true
301 sw.ClearTimeout(info.timer)
302 if cs, ok := clientSubs[proxyID]; ok {
303 sendToClient(cs.clientID, "[\"EOSE\","+jstr(proxyID)+"]")
304 }
305 }
306 }
307 }
308 }
309
310 func pushToMatchingSubs(ev *nostr.Event) {
311 for subID, cs := range clientSubs {
312 if cs.filter.Matches(ev) {
313 sendToClient(cs.clientID, "[\"EVENT\","+jstr(subID)+","+ev.ToJSON()+"]")
314 }
315 }
316 }
317
318 // --- Signing ---
319
320 func routerSign(clientID, requestID, eventRaw string) {
321 if !hasKey {
322 sendToClient(clientID, "[\"SIGN_ERROR\","+jstr(requestID)+",\"no key\"]")
323 return
324 }
325 ev := nostr.ParseEvent(eventRaw)
326 if ev == nil {
327 sendToClient(clientID, "[\"SIGN_ERROR\","+jstr(requestID)+",\"parse error\"]")
328 return
329 }
330 if identitySignEvent(ev) {
331 sendToClient(clientID, "[\"SIGNED\","+jstr(requestID)+","+ev.ToJSON()+"]")
332 } else {
333 sendToClient(clientID, "[\"SIGN_ERROR\","+jstr(requestID)+",\"sign failed\"]")
334 }
335 }
336
337 // --- DM routing ---
338
339 // routerSaveDMRecordJSON stores a DM record (JSON) and notifies clients.
340 func routerSaveDMRecordJSON(dmJSON string) {
341 cacheSaveDM(dmJSON, func(result string) {
342 if result != "duplicate" {
343 broadcastToClients("[\"DM_RECEIVED\"," + dmJSON + "]")
344 }
345 })
346 }
347
348 func routerSendDM(clientID, recipientPubkey, content string, relayURLs []string) {
349 if myPubkey == "" || !hasKey {
350 return
351 }
352 if !registry.HasHook("encryptNip04") {
353 registry.LoadModule("swcrypto", func() {
354 routerSendDM(clientID, recipientPubkey, content, relayURLs)
355 })
356 return
357 }
358
359 // NIP-04 via crypto extension.
360 registry.EncryptNip04(recipientPubkey, content, func(ev04JSON string) {
361 cacheStore(ev04JSON, func(_ bool) {})
362 ev04 := nostr.ParseEvent(ev04JSON)
363 if ev04 != nil {
364 for _, url := range relayURLs {
365 getConn(url).Publish(ev04)
366 }
367 }
368 })
369
370 // NIP-17 via crypto extension.
371 registry.EncryptNip17(recipientPubkey, content, func(recipientJSON, senderJSON string) {
372 if recipientJSON != "" {
373 rw := nostr.ParseEvent(recipientJSON)
374 if rw != nil {
375 for _, url := range relayURLs {
376 getConn(url).Publish(rw)
377 }
378 }
379 }
380 if senderJSON != "" {
381 swEv := nostr.ParseEvent(senderJSON)
382 if swEv != nil {
383 for _, url := range relayURLs {
384 getConn(url).Publish(swEv)
385 }
386 }
387 }
388 })
389
390 // Save sent DM record.
391 now := sw.NowSeconds()
392 recJSON := registry.MakeDMRecord(recipientPubkey, myPubkey, content, now, "nip17", "")
393 if recJSON != "" {
394 routerSaveDMRecordJSON(recJSON)
395 }
396 sendToClient(clientID, "[\"DM_SENT\","+jstr(recipientPubkey)+",true,\"\"]")
397 }
398
399 func routerDMSub(_ string, relayURLs []string) {
400 if myPubkey == "" || len(relayURLs) == 0 {
401 return
402 }
403 dmRelayURLs = relayURLs
404
405 for rSubID := range dmSubIDs {
406 for _, url := range rpool.URLs() {
407 c := rpool.Get(url)
408 if c != nil && c.IsOpen() {
409 c.CloseSubscription(rSubID)
410 }
411 }
412 }
413 dmSubIDs = make(map[string]bool)
414
415 for _, url := range relayURLs {
416 suffix := urlSuffix(url)
417 id1 := "dm4in_" + suffix
418 id2 := "dm4out_" + suffix
419 id3 := "dm17_" + suffix
420 dmSubIDs[id1] = true
421 dmSubIDs[id2] = true
422 dmSubIDs[id3] = true
423
424 c := getConn(url)
425 c.Subscribe(id1, []*nostr.Filter{{Kinds: []int{4}, Tags: map[string][]string{"#p": {myPubkey}}, Limit: 100}})
426 c.Subscribe(id2, []*nostr.Filter{{Kinds: []int{4}, Authors: []string{myPubkey}, Limit: 100}})
427 c.Subscribe(id3, []*nostr.Filter{{Kinds: []int{1059}, Tags: map[string][]string{"#p": {myPubkey}}, Limit: 100}})
428 }
429 }
430
431 func routerDMList(clientID string) {
432 cacheGetConversationList(func(listJSON string) {
433 sendToClient(clientID, "[\"DM_LIST\","+listJSON+"]")
434 })
435 }
436
437 func routerDMHistory(clientID, peer string, limit int, until int64) {
438 if limit <= 0 {
439 limit = 50
440 }
441 cacheQueryDMs(peer, limit, until, func(msgsJSON string) {
442 sendToClient(clientID, "[\"DM_HISTORY\","+jstr(peer)+","+msgsJSON+"]")
443 })
444 }
445
446 // --- Broadcast ---
447
448 func routerBroadcast(clientID, pubkey string, relayURLs []string) {
449 filterJSON := "{\"authors\":[" + jstr(pubkey) + "],\"kinds\":[0,3,10002,10050,10051]}"
450 cacheQuery(filterJSON, func(eventsJSON string) {
451 events := nostr.ParseEventsJSON(eventsJSON)
452 byKind := make(map[int]*nostr.Event)
453 for _, ev := range events {
454 if prev, ok := byKind[ev.Kind]; !ok || ev.CreatedAt > prev.CreatedAt {
455 byKind[ev.Kind] = ev
456 }
457 }
458
459 userRelays := relayURLs
460 if relayEv, ok := byKind[10002]; ok {
461 userRelays = nil
462 for _, t := range relayEv.Tags.GetAll("r") {
463 userRelays = append(userRelays, t.Value())
464 }
465 }
466 if len(userRelays) == 0 {
467 userRelays = writeRelays
468 }
469
470 if _, ok := byKind[10050]; !ok && hasKey && len(userRelays) > 0 {
471 ev := createRelayListEvent(10050, pubkey, userRelays)
472 if ev != nil {
473 cacheStore(ev.ToJSON(), func(_ bool) {})
474 byKind[10050] = ev
475 }
476 }
477 if _, ok := byKind[10051]; !ok && hasKey && len(userRelays) > 0 {
478 ev := createRelayListEvent(10051, pubkey, userRelays)
479 if ev != nil {
480 cacheStore(ev.ToJSON(), func(_ bool) {})
481 byKind[10051] = ev
482 }
483 }
484
485 count := 0
486 for _, ev := range byKind {
487 for _, url := range relayURLs {
488 getConn(url).Publish(ev)
489 }
490 count++
491 }
492 sendToClient(clientID, "[\"BROADCAST_DONE\","+helpers.Itoa(int64(count))+","+helpers.Itoa(int64(len(relayURLs)))+"]")
493 })
494 }
495
496 func createRelayListEvent(kind int, _ string, relays []string) *nostr.Event {
497 tagKey := "relay"
498 var tags nostr.Tags
499 for _, r := range relays {
500 tags = append(tags, nostr.Tag{tagKey, r})
501 }
502 ev := &nostr.Event{
503 Kind: kind,
504 Content: "",
505 Tags: tags,
506 CreatedAt: sw.NowSeconds(),
507 }
508 if !identitySignEvent(ev) {
509 return nil
510 }
511 return ev
512 }
513
514 // --- Page hints (Phase 4) ---
515
516 func routerPageHint(page string) {
517 if page == "messaging" {
518 if !registry.HasHook("encryptNip04") {
519 registry.LoadModule("swcrypto", nil)
520 }
521 if !registry.HasHook("marmotInit") {
522 registry.LoadModule("swmarmot", nil)
523 }
524 }
525 }
526
527 // --- Helpers ---
528
529 func stringsToJSON(ss []string) string {
530 if len(ss) == 0 {
531 return "[]"
532 }
533 b := "["
534 for i, s := range ss {
535 if i > 0 {
536 b += ","
537 }
538 b += jstr(s)
539 }
540 return b + "]"
541 }
542