router.mx raw

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