package main // Notif Worker - notification subscription, dedup, filtering, watermark tracking. // // supervisor -> worker: // ["N_EVENT", subID, evJSON] // ["N_EOSE", subID] // ["N_SET_PUBKEY", pk] // ["N_SET_RELAYS", urlsJSON] // ["N_SET_MUTES", pksJSON] // ["N_LOAD_MORE"] // ["N_MARK_READ"] // ["N_SET_FILTER", filter] -- "all", "mentions", "reactions", "zaps" // ["N_READ_TS", ts] -- initial notifReadTs from IDB // // worker -> supervisor (routed to Shell): // ["N_RENDER", evJSON, mode] -- mode: "prepend" or "append" // ["N_DOT", show] -- show=1 or 0 // ["N_EOSE_DONE", subID] // ["N_STATUS", count, exhausted] // ["N_SUB", subID, filterJSON, urlsJSON] // ["N_CLOSE", subID] // ["N_FETCH_REFS", idsJSON] -- Shell should fetch these referenced events // ["N_STORE_READ_TS", ts] -- save notif-read-ts to IDB import ( "git.smesh.lol/smesh/web/common/helpers" "git.smesh.lol/smesh/web/common/jsbridge/notif" "git.smesh.lol/smesh/web/common/mw" "git.smesh.lol/smesh/web/common/nostr" ) var ( myPK string relayURLs []string muteSet map[string]bool notifFilter string notifReadTs int64 notifSeen map[string]bool oldestTs int64 eventCount int32 exhausted bool emptyStreak int32 initLoad bool loading bool moreGot int32 moreTimer int32 notifMissing []string refTimer int32 ) func main() { muteSet = map[string]bool{} notifSeen = map[string]bool{} notifFilter = "all" initLoad = true notif.WorkerOnMessage(handleMessage) } func handleMessage(msg string) { w := mw.New(msg) switch w.Str() { case "N_SET_PUBKEY": myPK = w.Str() case "N_SET_RELAYS": relayURLs = parseStringArray(w.Raw()) case "N_SET_MUTES": pks := parseStringArray(w.Raw()) muteSet = map[string]bool{} for _, pk := range pks { muteSet[pk] = true } case "N_SET_FILTER": notifFilter = w.Str() case "N_READ_TS": notifReadTs = w.Num() case "N_EVENT": _ = w.Str() // subID consumed evJSON := w.Raw() handleNotifEvent(evJSON) case "N_EOSE": subID := w.Str() handleEOSE(subID) case "N_SUBSCRIBE": handleSubscribe() case "N_LOAD_MORE": handleLoadMore() case "N_MARK_READ": handleMarkRead() } } func handleNotifEvent(evJSON string) { ev := nostr.ParseEvent(evJSON) if ev == nil { return } if notifSeen == nil { notifSeen = map[string]bool{} } if notifSeen[ev.ID] { return } notifSeen[ev.ID] = true if oldestTs == 0 || ev.CreatedAt < oldestTs { oldestTs = ev.CreatedAt } if ev.PubKey == myPK { return } if muteSet[notifSenderPK(ev)] { return } if repliesToMuted(ev) { return } // Queue missing referenced events if ev.Kind != 1 { refID := notifRefID(ev) if refID != "" { notifMissing = append(notifMissing, refID) } } // Check against read watermark for dot display if ev.CreatedAt > notifReadTs { notif.WorkerPost(`["N_DOT",1]`) } if !passesFilter(ev) { return } mode := "append" if !initLoad { mode = "prepend" } notif.WorkerPost(`["N_RENDER",` | evJSON | `,"` | mode | `"]`) eventCount++ } func handleEOSE(subID string) { if subID == "ntf" { initLoad = false if len(notifMissing) > 0 { notif.WorkerPost(`["N_FETCH_REFS",` | buildIDsJSON(notifMissing) | `]`) notifMissing = nil } notif.WorkerPost(`["N_EOSE_DONE","ntf"]`) notif.WorkerPost(`["N_STATUS",` | helpers.Itoa(int64(eventCount)) | `,0]`) } else if subID == "ntf-more" { if moreTimer != 0 { notif.WorkerClearTimeout(moreTimer); moreTimer = 0 } loading = false if moreGot == 0 { emptyStreak++ if emptyStreak >= 3 { exhausted = true } } else { emptyStreak = 0 } ex := int64(0) if exhausted { ex = 1 } notif.WorkerPost(`["N_STATUS",` | helpers.Itoa(int64(eventCount)) | `,` | helpers.Itoa(ex) | `]`) notif.WorkerPost(`["N_EOSE_DONE","ntf-more"]`) notif.WorkerPost(`["N_CLOSE","ntf-more"]`) } } func handleSubscribe() { if myPK == "" || len(relayURLs) == 0 { return } // Close any existing ntf sub, then open fresh. notif.WorkerPost(`["N_CLOSE","ntf"]`) filter := `{"kinds":[1,6,7,9735],"#p":[` | jstr(myPK) | `],"limit":20}` notif.WorkerPost(`["N_SUB","ntf",` | filter | `,` | buildURLsJSON(relayURLs) | `]`) } func handleLoadMore() { if loading || exhausted || oldestTs == 0 || myPK == "" { return } loading = true moreGot = 0 filter := `{"kinds":[1,6,7,9735],"#p":[` | jstr(myPK) | `],"until":` | helpers.Itoa(oldestTs) | `,"limit":20}` notif.WorkerPost(`["N_SUB","ntf-more",` | filter | `,` | buildURLsJSON(relayURLs) | `]`) moreTimer = notif.WorkerSetTimeout(10000, func() { moreTimer = 0 loading = false notif.WorkerPost(`["N_EOSE_DONE","ntf-more"]`) }) } func handleMarkRead() { ts := notif.WorkerNowSeconds() notifReadTs = ts notif.WorkerPost(`["N_STORE_READ_TS",` | helpers.Itoa(ts) | `]`) notif.WorkerPost(`["N_DOT",0]`) } func passesFilter(ev *nostr.Event) (ok bool) { switch notifFilter { case "mentions": return ev.Kind == 1 case "reactions": return ev.Kind == 7 case "zaps": return ev.Kind == 9735 default: return true } } func notifRefID(ev *nostr.Event) (s string) { switch ev.Kind { case 7, 9735, 6: tag := ev.Tags.GetFirst([]byte("e")) if tag != nil { return string(tag.Value()) } } return "" } func notifSenderPK(ev *nostr.Event) (s string) { if ev.Kind == 9735 { pTag := ev.Tags.GetFirst([]byte("P")) if pTag != nil { return string(pTag.Value()) } } return ev.PubKey } func repliesToMuted(ev *nostr.Event) (ok bool) { for _, tag := range ev.Tags.GetAll("p") { if v := string(tag.Value()); v != "" && muteSet[v] { return true } } return false } func buildIDsJSON(ids []string) (s string) { s := "[" for i, id := range ids { if i > 0 { s = s | "," } 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 parseStringArray(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 jstr(s string) (sv string) { return helpers.JsonString(s) }