package main import ( "common/helpers" "common/jsbridge/sw" ) // Subscription Router — thin forwarder to relay SW via bus. // All relay operations, subscriptions, and caching are handled by the relay SW. // pendingSentDMs holds MLS_SEND saves deferred because myPubkey wasn't set yet. // Replayed when SET_PUBKEY arrives. type pendingSentDM struct { recipient string content string } var pendingSentDMs []pendingSentDM var mlsRelays []string var marmotSubs map[string]bool // tracks active marmot proxy subscriptions for cleanup // sentDMDedup tracks recently sent DM content to filter self-echoes. // MLS handleGroupMessage always attributes messages to PeerPub, so when // our own outbound echoes back (seenIDs lost on WASM restart), it appears // as a message from the peer. This ring buffer catches that. var sentDMDedup [32]string var sentDMIdx int func markSentDM(peer, content string) { sentDMDedup[sentDMIdx%32] = peer + "\x00" + content sentDMIdx++ } func isSentDMEcho(peer, content string) bool { needle := peer + "\x00" + content for _, s := range sentDMDedup { if s == needle { return true } } return false } func flushPendingSentDMs() { if myPubkey == "" || len(pendingSentDMs) == 0 { return } for _, p := range pendingSentDMs { now := sw.NowSeconds() rec := makeDMRecord(p.recipient, myPubkey, p.content, now, "marmot", "") busSend("relay", "[\"SAVE_DM_QUIET\","+rec.ToJSON()+"]") } pendingSentDMs = nil } func routeMessage(clientID string, w *mw, msgType string) { sw.Log("shell: page→" + msgType) switch msgType { // Identity — handle locally + forward to relay SW. case "SET_PUBKEY": pk := w.str() identitySetPubkey(pk) busSend("relay", "[\"SET_PUBKEY\","+jstr(pk)+"]") flushPendingSentDMs() case "CLEAR_KEY": identityClearKey() busSend("relay", "[\"CLEAR_KEY\"]") // Relay operations — forward to relay SW. case "REQ": subID := w.str() filterRaw := w.raw() busSend("relay", "[\"REQ\","+jstr(clientID)+","+jstr(subID)+","+filterRaw+"]") case "CLOSE": subID := w.str() busSend("relay", "[\"CLOSE\","+jstr(subID)+"]") case "EVENT": eventRaw := w.raw() busSend("relay", "[\"EVENT\","+jstr(clientID)+","+eventRaw+"]") case "PROXY": subID := w.str() filterRaw := w.raw() relayURLs := w.strs() busSend("relay", "[\"PROXY\","+jstr(clientID)+","+jstr(subID)+","+filterRaw+","+strsJSON(relayURLs)+"]") case "RELAY_INFO": relayURL := w.str() busSend("relay", "[\"RELAY_INFO\","+jstr(clientID)+","+jstr(relayURL)+"]") case "SET_WRITE_RELAYS": busSend("relay", "[\"SET_WRITE_RELAYS\","+strsJSON(w.strs())+"]") case "SIGN": requestID := w.str() eventRaw := w.raw() busSend("relay", "[\"SIGN\","+jstr(clientID)+","+jstr(requestID)+","+eventRaw+"]") case "BROADCAST": pubkey := w.str() relayURLs := w.strs() busSend("relay", "[\"BROADCAST\","+jstr(clientID)+","+jstr(pubkey)+","+strsJSON(relayURLs)+"]") // DM operations — forward to relay SW for IDB. case "DM_LIST": busSend("relay", "[\"DM_LIST\","+jstr(clientID)+"]") case "DM_HISTORY": peer := w.str() limit := int(w.num()) until := w.num() busSend("relay", "[\"DM_HISTORY\","+jstr(clientID)+","+jstr(peer)+","+helpers.Itoa(int64(limit))+","+helpers.Itoa(until)+"]") case "CLEAR_DM_HISTORY": peer := w.str() busSend("relay", "[\"CLEAR_DM_HISTORY\","+jstr(peer)+"]") // MLS — proxy through page to signer extension (marmot WASM runs inside signer). case "MLS_INIT": relays := w.strs() mlsRelays = relays sendToClient(clientID, "[\"MLS_PROXY\",\"init\","+strsJSON(relays)+"]") case "MLS_SEND": recipient := w.str() content := w.str() markSentDM(recipient, content) sendToClient(clientID, "[\"MLS_PROXY\",\"sendDM\","+jstr(recipient)+","+jstr(content)+"]") // Save sent DM to relay's IDB (quiet — no DM_RECEIVED broadcast). if myPubkey == "" { pendingSentDMs = append(pendingSentDMs, pendingSentDM{recipient, content}) } else { now := sw.NowSeconds() rec := makeDMRecord(recipient, myPubkey, content, now, "marmot", "") busSend("relay", "[\"SAVE_DM_QUIET\","+rec.ToJSON()+"]") } case "MLS_SUB": sendToClient(clientID, "[\"MLS_PROXY\",\"subscribe\"]") case "MLS_PUBLISH_KP": sendToClient(clientID, "[\"MLS_PROXY\",\"publishKP\"]") case "MLS_LIST_GROUPS": sendToClient(clientID, "[\"MLS_PROXY\",\"listGroups\"]") case "MLS_BACKUP": sendToClient(clientID, "[\"MLS_PROXY\",\"backupGroups\"]") case "MLS_RESTORE": sendToClient(clientID, "[\"MLS_PROXY\",\"restoreGroups\"]") case "MLS_RATCHET": peer := w.str() sendToClient(clientID, "[\"MLS_PROXY\",\"ratchetGroup\","+jstr(peer)+"]") // MLS results from page (mls-bridge.mjs routes signer extension outputs here). // Relay URLs may come from mlsRelays (set by MLS_INIT) or inline in the message // (set by mls-bridge.mjs). The inline URLs ensure routing works even before the // Go WASM app sends MLS_INIT. case "MLS_PUBLISH": eventRaw := w.str() relays := w.strs() if len(relays) == 0 { relays = mlsRelays } if len(relays) > 0 { busSend("relay", "[\"MLS_RELAY_PUBLISH\","+eventRaw+","+strsJSON(relays)+"]") } else { busSend("relay", "[\"EVENT\",\"\","+eventRaw+"]") } case "MLS_SUBSCRIBE": subID := w.str() filterRaw := w.raw() relays := w.strs() if len(relays) == 0 { relays = mlsRelays } // Pass filters as-is (array or single object) — relay SW's parseFilters handles both. mSubID := "marmot-sub-" + subID if marmotSubs == nil { marmotSubs = make(map[string]bool) } marmotSubs[mSubID] = true if len(relays) > 0 { busSend("relay", "[\"PROXY\",\"\","+jstr(mSubID)+","+filterRaw+","+strsJSON(relays)+"]") } else { busSend("relay", "[\"REQ\",\"\","+jstr(mSubID)+","+filterRaw+"]") } case "MLS_DM": dmJSON := w.raw() // mls-bridge sends {peer, sender, content, ts, source, eventId} // but IDB expects {id, peer, from, content, created_at, protocol, eventId}. peer := jsonField(dmJSON, "peer") sender := jsonField(dmJSON, "sender") content := jsonField(dmJSON, "content") // Self-echo filter: MLS handleGroupMessage always sets sender=PeerPub. // If we recently sent this exact content to this peer, it's our own echo. if isSentDMEcho(peer, content) { return } ts := parseTS(jsonFieldRaw(dmJSON, "ts")) source := jsonField(dmJSON, "source") eventID := jsonField(dmJSON, "eventId") rec := makeDMRecord(peer, sender, content, ts, source, eventID) recJSON := rec.ToJSON() busSend("relay", "[\"SAVE_DM_QUIET\","+recJSON+"]") fwdDM(recJSON) case "MLS_GROUPS": groupsJSON := w.raw() broadcastToClients("[\"MLS_GROUPS\"," + groupsJSON + "]") case "MLS_STATUS": statusMsg := w.str() broadcastToClients("[\"MLS_STATUS\"," + jstr(statusMsg) + "]") // Crypto result from page — dispatch to waiting callback. case "CRYPTO_RESULT": id := int(w.num()) result := w.str() errMsg := w.str() if fn, ok := cryptoCBs[id]; ok { delete(cryptoCBs, id) fn(result, errMsg) } } } // fwdDM broadcasts a DM_RECEIVED message to all page clients. func fwdDM(dmJSON string) { broadcastToClients("[\"DM_RECEIVED\"," + dmJSON + "]") }