package main // MLS Worker - marmot (MLS-over-Nostr) messaging. // // Owns marmot state: KeyPairPackage, group states, peer key package cache. // Communicates through supervisor (wasm-host.mjs) via typed JSON messages. // // supervisor -> worker: // ["M_SET_PUBKEY", pk] // ["M_INIT", urlsJSON] -- generate/load KPP, load groups, subscribe // ["M_SEND_DM", peer, content, urlsJSON] -- send MLS DM // ["M_INCOMING", evJSON] -- raw kind 443/445/1059 from fetch worker // ["M_CRYPTO_RESULT", reqID, result, err] -- signer response // ["M_STORE_RESULT", tag, reqID, data] -- store response // ["M_FETCH_KP_RESULT", peer, evJSON] -- key package fetch result // // worker -> supervisor: // ["M_CRYPTO_REQ", reqID, method, peer, data] -- to signer // ["M_STORE_REQ", tag, reqID, ...] -- to store worker // ["M_PUBLISH", evJSON, urlsJSON] -- publish via relay-proxy // ["M_FETCH_KP", peer, urlsJSON] -- request key package from fetch worker // ["M_SUBSCRIBE", urlsJSON, groupIDsJSON] -- trigger fetch worker subscription // ["M_UPDATE_GROUPS", groupIDsJSON] -- update fetch worker kind 445 filter // ["MLS_DM_RECEIVED", dmJSON] -- to app worker // ["MLS_DM_SENT", peer, ok, err] -- to app worker // ["MLS_UNREAD_COUNT", count] -- to app worker // ["MLS_DM_LIST", listJSON] -- to app worker // ["MLS_DM_HISTORY", peer, msgsJSON] -- to app worker import ( "git.smesh.lol/smesh/web/common/helpers" "git.smesh.lol/smesh/web/common/jsbridge/mlsw" "git.smesh.lol/smesh/web/common/marmot" "git.smesh.lol/smesh/web/common/mls" "git.smesh.lol/smesh/web/common/mw" "git.smesh.lol/smesh/web/common/nostr" ) var ( myPK string myKPP *mls.KeyPairPackage peerKPs map[string]*mls.KeyPackage mlsGroups map[string]*marmot.GroupState // nostrGroupIDHex -> GroupState initStarted bool initDone bool relayURLs []string unreadCount int32 pendingIncoming []string // events queued before initDone cryptoCBs map[int32]func(string, string) nextCryptoID int32 storeCBs map[int32]func(string, string) // reqID -> fn(data, err) nextStoreID int32 // peer -> queued (content, urls) waiting for KP fetch pendingKPFetch map[string][]pendingSend kpFetchTimers map[string]int32 // groupIDHex -> queued event JSONs for messages that arrived before Welcome pendingGroupMsgs map[string][]string // peer -> recent DM records (JSON strings), ring buffer per peer dmHistory map[string][]string ) type pendingSend struct { content string urls []string } func main() { peerKPs = map[string]*mls.KeyPackage{} mlsGroups = map[string]*marmot.GroupState{} cryptoCBs = map[int32]func(string, string){} storeCBs = map[int32]func(string, string){} pendingKPFetch = map[string][]pendingSend{} kpFetchTimers = map[string]int32{} pendingGroupMsgs = map[string][]string{} dmHistory = map[string][]string{} mlsw.WorkerOnMessage(handleMessage) } func handleMessage(msg string) { w := mw.New(msg) cmd := w.Str() println("[mls-worker] handleMessage cmd=" | cmd) switch cmd { case "M_SET_PUBKEY": myPK = w.Str() println("[mls-worker] SET_PUBKEY pk=" | myPK[:16] | "...") case "M_INIT", "MLS_INIT": urlsJSON := w.Raw() relayURLs = parseStrArr(urlsJSON) println("[mls-worker] INIT urls=" | urlsJSON) handleInit(relayURLs) case "M_SEND_DM", "MLS_SEND_DM": peer := w.Str() content := w.Str() urlsJSON := w.Raw() urls := parseStrArr(urlsJSON) if len(urls) == 0 { urls = relayURLs } println("[mls-worker] SEND_DM peer=" | peer[:16] | "... content_len=" | itoi(len(content)) | " urls=" | itoi(len(urls))) handleSendDM(peer, content, urls) case "M_INCOMING": evJSON := w.Raw() if !initDone { println("[mls-worker] INCOMING queued (initDone=false), pending=" | itoi(len(pendingIncoming)+1)) pendingIncoming = append(pendingIncoming, evJSON) return } println("[mls-worker] INCOMING processing") processIncoming(evJSON) case "M_CRYPTO_RESULT": reqID := int32(w.Num()) result := mw.Unescape(w.Str()) errMsg := mw.Unescape(w.Str()) println("[mls-worker] CRYPTO_RESULT reqID=" | itoi(reqID) | " err=" | errMsg) if fn, ok := cryptoCBs[reqID]; ok { delete(cryptoCBs, reqID) fn(result, errMsg) } case "M_STORE_RESULT": _ = w.Str() // tag (unused - keyed by reqID) reqID := int32(w.Num()) data := w.Str() println("[mls-worker] STORE_RESULT reqID=" | itoi(reqID) | " data_len=" | itoi(len(data))) if fn, ok := storeCBs[reqID]; ok { delete(storeCBs, reqID) fn(data, "") } else { println("[mls-worker] STORE_RESULT: no callback for reqID=" | itoi(reqID)) } case "M_FETCH_KP_RESULT": peer := w.Str() evJSON := w.Raw() println("[mls-worker] FETCH_KP_RESULT peer=" | peer[:16] | "... evJSON_len=" | itoi(len(evJSON))) handleFetchKPResult(peer, evJSON) case "MLS_DM_LIST": println("[mls-worker] DM_LIST requested") handleDMList() case "MLS_DM_HISTORY": peer := w.Str() println("[mls-worker] DM_HISTORY peer=" | peer[:16] | "...") handleDMHistory(peer) default: println("[mls-worker] UNHANDLED cmd=" | cmd) } } // --- Init --- func handleInit(urls []string) { if initStarted { println("[mls-worker] handleInit: already started, skipping") return } if myPK == "" { println("[mls-worker] handleInit: no pubkey, aborting") return } initStarted = true println("[mls-worker] handleInit: loading KPP from store") storeReq("S_MLS_LOAD_KPP", "", func(kppBase64, _ string) { if kppBase64 != "" { println("[mls-worker] handleInit: got stored KPP, len=" | itoi(len(kppBase64))) raw := helpers.Base64Decode(kppBase64) if raw != nil { if kpp, err := mls.UnmarshalKeyPairPackage(raw); err == nil { myKPP = kpp println("[mls-worker] handleInit: KPP loaded from store") } else { println("[mls-worker] handleInit: KPP unmarshal err=" | err.Error()) } } } if myKPP == nil { println("[mls-worker] handleInit: generating new KPP") pubBytes := helpers.HexDecode(myPK) if pubBytes == nil { println("[mls-worker] handleInit: invalid pubkey hex") return } now := mlsw.WorkerNowSeconds() kpp, err := marmot.GenerateKeyPackage(pubBytes, now) if err != nil { println("[mls-worker] handleInit: generate KPP err=" | err.Error()) return } myKPP = kpp println("[mls-worker] handleInit: KPP generated, saving") raw, err := kpp.Marshal() if err == nil { storeFireForget("S_MLS_SAVE_KPP", jstr(helpers.Base64Encode(raw))) } } println("[mls-worker] handleInit: publishing key package") publishKeyPackage(urls, func() { println("[mls-worker] handleInit: KP published, loading groups") loadGroups(func() { initDone = true println("[mls-worker] handleInit: INIT DONE, groups=" | itoi(len(mlsGroups)) | " pending=" | itoi(len(pendingIncoming))) pending := pendingIncoming pendingIncoming = nil for _, ev := range pending { processIncoming(ev) } groupIDsJSON := buildGroupIDsJSON() urlsJSON := buildURLsJSON(urls) println("[mls-worker] handleInit: subscribing groups=" | groupIDsJSON) mlsw.WorkerPost(`["M_SUBSCRIBE",` | urlsJSON | `,` | groupIDsJSON | `]`) mlsw.WorkerPost(`["MLS_UNREAD_COUNT",0]`) }) }) }) } func publishKeyPackage(urls []string, done func()) { now := mlsw.WorkerNowSeconds() ev, err := marmot.BuildKeyPackageEvent(myKPP, myPK, now, urls) if err != nil { println("[mls-worker] publishKP: build err=" | err.Error()) done() return } evJSON := ev.ToJSON() println("[mls-worker] publishKP: unsigned kind=" | itoi(int32(ev.Kind)) | " evJSON(80)=" | evJSON[:80]) requestCrypto("signEvent", "", evJSON, func(signed, errMsg string) { if errMsg != "" { println("[mls-worker] publishKP: sign err=" | errMsg) } else if signed != "" { println("[mls-worker] publishKP: signed(80)=" | signed[:80]) mlsw.WorkerPost(`["M_PUBLISH",` | signed | `,` | buildURLsJSON(urls) | `]`) } else { println("[mls-worker] publishKP: sign returned empty") } done() }) } func loadGroups(done func()) { storeReq("S_MLS_LIST_GROUPS", "", func(groupsJSON, _ string) { ids := parseStrArr(groupsJSON) if len(ids) == 0 { done() return } remaining := len(ids) for _, id := range ids { gid := id storeReq("S_MLS_LOAD_GROUP", jstr(gid), func(stateBase64, _ string) { if stateBase64 != "" { raw := helpers.Base64Decode(stateBase64) if raw != nil { if gs, err := marmot.UnmarshalGroupState(raw); err == nil { mlsGroups[gid] = gs } } } remaining-- if remaining == 0 { done() } }) } }) } // --- Incoming events --- func processIncoming(evJSON string) { ev := nostr.ParseEvent(evJSON) if ev == nil { println("[mls-worker] processIncoming: parse failed") return } println("[mls-worker] processIncoming: kind=" | itoi(int32(ev.Kind)) | " from=" | ev.PubKey[:16] | "... id=" | ev.ID[:16] | "...") switch ev.Kind { case marmot.KindKeyPackage: if kp, err := marmot.EventToKeyPackage(ev); err == nil { cs := kp.CipherSuiteValue() if cs != 3 { println("[mls-worker] processIncoming: skip KP cs=" | itoi(int32(cs)) | " from " | ev.PubKey[:16] | "...") break } if len(peerKPs) > 200 { for k := range peerKPs { delete(peerKPs, k) break } } peerKPs[ev.PubKey] = kp println("[mls-worker] processIncoming: cached KP for " | ev.PubKey[:16] | "...") } else { println("[mls-worker] processIncoming: KP parse err=" | err.Error()) } case marmot.KindGiftWrap: println("[mls-worker] processIncoming: gift wrap, decrypting") requestCrypto("nip44.decrypt", myPK, ev.Content, func(plaintext, errMsg string) { if errMsg != "" { println("[mls-worker] processIncoming: gift wrap decrypt err=" | errMsg) return } if plaintext == "" { println("[mls-worker] processIncoming: gift wrap decrypt empty") return } inner := nostr.ParseEvent(plaintext) if inner == nil { println("[mls-worker] processIncoming: inner event parse failed") return } println("[mls-worker] processIncoming: inner kind=" | itoi(int32(inner.Kind))) switch inner.Kind { case marmot.KindWelcome: processWelcome(inner, ev.PubKey) case marmot.KindGroupMessage: processGroupMessage(inner) default: println("[mls-worker] processIncoming: unexpected inner kind=" | itoi(int32(inner.Kind))) } }) case marmot.KindGroupMessage: processGroupMessage(ev) default: println("[mls-worker] processIncoming: unhandled kind=" | itoi(int32(ev.Kind))) } } func processWelcome(rumorEv *nostr.Event, senderPubHex string) { println("[mls-worker] processWelcome: from=" | senderPubHex[:16] | "...") if myKPP == nil { println("[mls-worker] processWelcome: no KPP, aborting") return } welcome, err := marmot.RumorToWelcome(rumorEv) if err != nil { println("[mls-worker] processWelcome: parse err=" | err.Error()) return } peerPub := helpers.HexDecode(senderPubHex) if peerPub == nil { println("[mls-worker] processWelcome: bad sender hex") return } now := mlsw.WorkerNowSeconds() gs, err := marmot.JoinDMGroup(welcome, myKPP, peerPub, now) if err != nil { println("[mls-worker] processWelcome: join err=" | err.Error()) return } groupIDHex := helpers.HexEncode(gs.NostrGroupID) println("[mls-worker] processWelcome: joined group=" | groupIDHex[:16] | "...") mlsGroups[groupIDHex] = gs saveGroup(groupIDHex, gs) mlsw.WorkerPost(`["M_UPDATE_GROUPS",` | buildGroupIDsJSON() | `]`) if queued, ok := pendingGroupMsgs[groupIDHex]; ok { println("[mls-worker] processWelcome: draining " | itoi(len(queued)) | " pending messages") delete(pendingGroupMsgs, groupIDHex) for _, evJSON := range queued { processIncoming(evJSON) } } } func processGroupMessage(ev *nostr.Event) { hTag := ev.Tags.GetFirst("h") if hTag == nil { println("[mls-worker] processGroupMessage: no 'h' tag") return } groupIDHex := string(hTag.Value()) println("[mls-worker] processGroupMessage: group=" | groupIDHex[:16] | "... from=" | ev.PubKey[:16] | "...") gs, ok := mlsGroups[groupIDHex] if !ok { q := pendingGroupMsgs[groupIDHex] if len(q) >= 64 { println("[mls-worker] processGroupMessage: unknown group, pending queue full, dropping") return } pendingGroupMsgs[groupIDHex] = append(q, ev.ToJSON()) println("[mls-worker] processGroupMessage: unknown group, queued (pending=" | itoi(len(pendingGroupMsgs[groupIDHex])) | ")") return } secret, err := marmot.DeriveExporterSecret(gs.Group) if err != nil { println("[mls-worker] processGroupMessage: derive secret err=" | err.Error()) return } _, mlsCipher, err := marmot.EventToMessage(ev, secret) if err != nil { println("[mls-worker] processGroupMessage: EventToMessage err=" | err.Error()) return } plaintext, _, err := gs.Group.UnmarshalAndProcessMessage(mlsCipher) if err != nil { println("[mls-worker] processGroupMessage: process err=" | err.Error()) return } saveGroup(groupIDHex, gs) peer := helpers.HexEncode(gs.PeerPub) println("[mls-worker] processGroupMessage: decrypted msg from " | peer[:16] | "... len=" | itoi(len(plaintext))) dmJSON := buildDMRecord(peer, ev.PubKey, plaintext, ev.CreatedAt, "mls", ev.ID) storeDM(peer, dmJSON) mlsw.WorkerPost(`["MLS_DM_RECEIVED",` | dmJSON | `]`) unreadCount++ mlsw.WorkerPost(`["MLS_UNREAD_COUNT",` | itoi(unreadCount) | `]`) } // --- Send DM --- func handleSendDM(peer, content string, urls []string) { groupIDHex := dmGroupIDHex(peer) println("[mls-worker] handleSendDM: peer=" | peer[:16] | "... groupID=" | groupIDHex[:16] | "...") if gs, ok := mlsGroups[groupIDHex]; ok { println("[mls-worker] handleSendDM: existing group found, sending") sendToExistingGroup(peer, content, urls, gs, groupIDHex) return } if kp, ok := peerKPs[peer]; ok { println("[mls-worker] handleSendDM: have peer KP, creating group") createGroupAndSend(peer, content, urls, kp) return } println("[mls-worker] handleSendDM: no KP, fetching from relay") pendingKPFetch[peer] = append(pendingKPFetch[peer], pendingSend{content: content, urls: urls}) if _, fetching := kpFetchTimers[peer]; !fetching { startKPFetch(peer, urls) } } func startKPFetch(peer string, urls []string) { mlsw.WorkerPost(`["M_FETCH_KP",` | jstr(peer) | `,` | buildURLsJSON(urls) | `]`) p := peer handle := mlsw.WorkerSetTimeout(15000, func() { delete(kpFetchTimers, p) if pending, ok := pendingKPFetch[p]; ok { delete(pendingKPFetch, p) for range pending { mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(p) | `,0,"peer does not support MLS messaging"]`) } } }) kpFetchTimers[peer] = handle } func handleFetchKPResult(peer, evJSON string) { if h, ok := kpFetchTimers[peer]; ok { mlsw.WorkerClearTimeout(h) delete(kpFetchTimers, peer) } if evJSON != "" && evJSON != "null" { ev := nostr.ParseEvent(evJSON) if ev != nil { if kp, err := marmot.EventToKeyPackage(ev); err == nil { cs := kp.CipherSuiteValue() if cs != 3 { println("[mls-worker] handleFetchKPResult: skip KP cs=" | itoi(int32(cs)) | " for " | peer[:16] | "...") } else { peerKPs[peer] = kp println("[mls-worker] handleFetchKPResult: got KP for " | peer[:16] | "...") } } else { println("[mls-worker] handleFetchKPResult: parse KP err=" | err.Error()) } } else { println("[mls-worker] handleFetchKPResult: parse event failed") } } else { println("[mls-worker] handleFetchKPResult: no event returned for " | peer[:16] | "...") } pending, ok := pendingKPFetch[peer] if !ok { println("[mls-worker] handleFetchKPResult: no pending sends") return } delete(pendingKPFetch, peer) kp, hasKP := peerKPs[peer] println("[mls-worker] handleFetchKPResult: hasKP=" | bstr(hasKP) | " pending=" | itoi(len(pending))) for _, ps := range pending { if !hasKP { mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,"peer does not support MLS messaging"]`) continue } createGroupAndSend(peer, ps.content, ps.urls, kp) } } func bstr(b bool) (s string) { if b { return "true" } return "false" } func createGroupAndSend(peer, content string, urls []string, peerKP *mls.KeyPackage) { if myKPP == nil { println("[mls-worker] createGroupAndSend: no KPP") mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,"not initialized"]`) return } selfPub := helpers.HexDecode(myPK) peerPub := helpers.HexDecode(peer) if selfPub == nil || peerPub == nil { println("[mls-worker] createGroupAndSend: bad pubkey hex") mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,"invalid pubkey"]`) return } now := mlsw.WorkerNowSeconds() peerCS := peerKP.CipherSuiteValue() println("[mls-worker] createGroupAndSend: selfCS=" | itoi(int32(myKPP.Public.CipherSuiteValue())) | " peerCS=" | itoi(int32(peerCS))) if peerCS != 3 { println("[mls-worker] createGroupAndSend: unsupported peer cipher suite " | itoi(int32(peerCS))) mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,"unsupported peer cipher suite"]`) return } gs, welcome, err := marmot.CreateDMGroup(myKPP, peerKP, selfPub, peerPub, "", urls) if err != nil { println("[mls-worker] createGroupAndSend: create err=" | err.Error()) mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,` | jstr(err.Error()) | `]`) return } groupIDHex := helpers.HexEncode(gs.NostrGroupID) println("[mls-worker] createGroupAndSend: group=" | groupIDHex[:16] | "... sending welcome") mlsGroups[groupIDHex] = gs rumorEv := marmot.WelcomeToRumor(welcome, selfPub, now, "", urls) rumorJSON := rumorEv.ToJSON() requestCrypto("nip44.encrypt", peer, rumorJSON, func(sealed, errMsg string) { if errMsg != "" { println("[mls-worker] createGroupAndSend: gift-wrap encrypt err=" | errMsg) mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,"gift-wrap encrypt failed"]`) return } println("[mls-worker] createGroupAndSend: gift-wrap encrypted, signing") wrapEv := &nostr.Event{ PubKey: myPK, CreatedAt: mlsw.WorkerNowSeconds(), Kind: marmot.KindGiftWrap, Tags: nostr.Tags{nostr.Tag{"p", peer}}, Content: sealed, } wrapJSON := wrapEv.ToJSON() requestCrypto("signEvent", "", wrapJSON, func(signedWrap, errMsg string) { if errMsg != "" { println("[mls-worker] createGroupAndSend: sign wrap err=" | errMsg) } else if signedWrap != "" { println("[mls-worker] createGroupAndSend: publishing welcome (kind 1059)") mlsw.WorkerPost(`["M_PUBLISH",` | signedWrap | `,` | buildURLsJSON(urls) | `]`) } p := peer sendToExistingGroup(p, content, urls, gs, groupIDHex) }) }) saveGroup(groupIDHex, gs) mlsw.WorkerPost(`["M_UPDATE_GROUPS",` | buildGroupIDsJSON() | `]`) } func sendToExistingGroup(peer, content string, urls []string, gs *marmot.GroupState, groupIDHex string) { now := mlsw.WorkerNowSeconds() println("[mls-worker] sendToExistingGroup: creating app message") appMsg, err := gs.Group.CreateApplicationMessage([]byte(content)) if err != nil { println("[mls-worker] sendToExistingGroup: createAppMsg err=" | err.Error()) mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,` | jstr(err.Error()) | `]`) return } secret, err := marmot.DeriveExporterSecret(gs.Group) if err != nil { println("[mls-worker] sendToExistingGroup: deriveSecret err=" | err.Error()) mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,` | jstr(err.Error()) | `]`) return } ev445, err := marmot.MessageToEvent(gs.NostrGroupID, appMsg, secret, now) if err != nil { println("[mls-worker] sendToExistingGroup: MessageToEvent err=" | err.Error()) mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,0,` | jstr(err.Error()) | `]`) return } println("[mls-worker] sendToExistingGroup: publishing kind 445 id=" | ev445.ID[:16] | "...") mlsw.WorkerPost(`["M_PUBLISH",` | ev445.ToJSON() | `,` | buildURLsJSON(urls) | `]`) saveGroup(groupIDHex, gs) dmJSON := buildDMRecord(peer, myPK, content, now, "mls", ev445.ID) storeDM(peer, dmJSON) mlsw.WorkerPost(`["MLS_DM_SENT",` | jstr(peer) | `,1,""]`) mlsw.WorkerPost(`["MLS_DM_RECEIVED",` | dmJSON | `]`) } // --- Store helpers --- func saveGroup(groupIDHex string, gs *marmot.GroupState) { raw, err := marmot.MarshalGroupState(gs) if err != nil { return } b64 := helpers.Base64Encode(raw) storeFireForget("S_MLS_SAVE_GROUP", jstr(groupIDHex) | `,` | jstr(b64)) } func storeReq(op, extraArgs string, fn func(string, string)) { nextStoreID++ id := nextStoreID storeCBs[id] = fn msg := `["M_STORE_REQ",` | jstr(op) | `,` | itoi(id) if extraArgs != "" { msg = msg | `,` | extraArgs } mlsw.WorkerPost(msg | `]`) } func storeFireForget(op, args string) { nextStoreID++ id := nextStoreID msg := `["M_STORE_REQ",` | jstr(op) | `,` | itoi(id) if args != "" { msg = msg | `,` | args } mlsw.WorkerPost(msg | `]`) } func requestCrypto(method, peer, data string, fn func(string, string)) { nextCryptoID++ id := nextCryptoID cryptoCBs[id] = fn mlsw.WorkerPost(`["M_CRYPTO_REQ",` | itoi(id) | `,` | jstr(method) | `,` | jstr(peer) | `,` | jstr(data) | `]`) cbID := id mlsw.WorkerSetTimeout(15000, func() { if _, ok := cryptoCBs[cbID]; ok { delete(cryptoCBs, cbID) fn("", "timeout") } }) } // --- DM list / history --- func handleDMList() { // Build conversation list from in-memory group states. list := "[" first := true for _, gs := range mlsGroups { if !first { list = list | "," } first = false peer := helpers.HexEncode(gs.PeerPub) list = list | `{"peer":` | jstr(peer) | `}` } list = list | "]" mlsw.WorkerPost(`["MLS_DM_LIST",` | list | `]`) } func handleDMHistory(peer string) { msgs := dmHistory[peer] if len(msgs) == 0 { mlsw.WorkerPost(`["MLS_DM_HISTORY",` | jstr(peer) | `,[]` | `]`) return } // renderer expects newest-first (reverses to oldest-first for display) arr := "[" for i := len(msgs) - 1; i >= 0; i-- { if i < len(msgs)-1 { arr = arr | "," } arr = arr | msgs[i] } arr = arr | "]" mlsw.WorkerPost(`["MLS_DM_HISTORY",` | jstr(peer) | `,` | arr | `]`) } func storeDM(peer, dmJSON string) { h := dmHistory[peer] if len(h) >= 256 { h = h[1:] } dmHistory[peer] = append(h, dmJSON) } // --- Helpers --- func dmGroupIDHex(peer string) (s string) { selfPub := helpers.HexDecode(myPK) peerPub := helpers.HexDecode(peer) if selfPub == nil || peerPub == nil { return "" } id := marmot.DMGroupID(selfPub, peerPub) return helpers.HexEncode(id) } func buildGroupIDsJSON() (s string) { s := "[" first := true for id := range mlsGroups { if !first { s = s | "," } first = false s = s | jstr(id) } return s | "]" } func buildURLsJSON(urls []string) (s string) { s := "[" for i, u := range urls { if i > 0 { s = s | "," } s = s | jstr(u) } return s | "]" } func buildDMRecord(peer, from, content string, createdAt int64, protocol, eventID string) (s string) { return `{"id":` | jstr(eventID) | `,"peer":` | jstr(peer) | `,"from":` | jstr(from) | `,"content":` | jstr(content) | `,"created_at":` | itoa(createdAt) | `,"protocol":` | jstr(protocol) | `,"eventId":` | jstr(eventID) | `}` } func parseStrArr(json string) (ss []string) { var out []string i := 0 for i < len(json) && json[i] != '[' { i++ } if i >= len(json) { return nil } i++ for { for i < len(json) && (json[i] == ' ' || json[i] == ',' || json[i] == '\n') { i++ } if i >= len(json) || json[i] == ']' { break } if json[i] != '"' { break } i++ start := i for i < len(json) && json[i] != '"' { if json[i] == '\\' { i++ } i++ } if i >= len(json) { break } out = append(out, json[start:i]) i++ } return out } func itoa(n int64) (s string) { return helpers.Itoa(n) } func itoi(n int32) (s string) { return helpers.Itoa(int64(n)) } func jstr(s string) (sv string) { return helpers.JsonString(s) }