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