package main // Feed Worker - subscription management, dedup, filtering, and pagination for the note feed. // // Shell forwards verified feed events (subID "feed" / "feed-more") to this worker. // This worker deduplicates, applies mute/follow filters, and sends render commands back to Shell. // // supervisor -> worker (from Shell via feed.Send): // ["F_EVENT", subID, evJSON] -- feed or feed-more event arrived // ["F_EOSE", subID] -- EOSE for feed or feed-more sub // ["F_SET_MODE", mode] -- "follows", "relays", or relay URL // ["F_SET_RELAYS", urlsJSON] // ["F_SET_FOLLOWS", pksJSON] -- current follow list (pubkeys) // ["F_SET_MUTES", pksJSON] -- current mute list (pubkeys) // ["F_SET_PUBKEY", pk] // ["F_LOAD_MORE"] -- user clicked "load more" // ["F_REFRESH"] -- user triggered refresh // // worker -> supervisor (to Shell via F_* prefix routing): // ["F_RENDER", evJSON, now] -- now=1: render immediately, 0: buffer // ["F_NEW"] -- new buffered events available (pulse) // ["F_EOSE_DONE", subID] -- signal Shell to update UI after EOSE // ["F_STATUS", count, exhausted] -- event count and exhaustion state // ["F_SUB", subID, filterJSON, urlsJSON] // ["F_CLOSE", subID] import ( "git.smesh.lol/smesh/web/common/helpers" "git.smesh.lol/smesh/web/common/jsbridge/feed" "git.smesh.lol/smesh/web/common/mw" "git.smesh.lol/smesh/web/common/nostr" ) var ( feedMode string relayURLs []string followList []string followSet map[string]bool muteSet map[string]bool myPK string seenEvents map[string]bool oldestFeedTs int64 eventCount int32 feedInitialLoad bool feedExhausted bool feedEmptyStreak int32 feedLoading bool feedMoreGot int32 feedMoreTimer int32 ) func main() { seenEvents = map[string]bool{} followSet = map[string]bool{} muteSet = map[string]bool{} feedInitialLoad = true feed.WorkerOnMessage(handleMessage) } func handleMessage(msg string) { w := mw.New(msg) switch w.Str() { case "F_EVENT": subID := w.Str() evJSON := w.Raw() handleFeedEvent(subID, evJSON) case "F_EOSE": subID := w.Str() handleEOSE(subID) case "F_SET_MODE": feedMode = w.Str() case "F_SET_RELAYS": relayURLs = parseStringArray(w.Raw()) case "F_SET_FOLLOWS": followList = parseStringArray(w.Raw()) followSet = map[string]bool{} for _, pk := range followList { followSet[pk] = true } case "F_SET_MUTES": pks := parseStringArray(w.Raw()) muteSet = map[string]bool{} for _, pk := range pks { muteSet[pk] = true } case "F_SET_PUBKEY": myPK = w.Str() case "F_LOAD_MORE": handleLoadMore() case "F_REFRESH": handleRefresh() } } func handleFeedEvent(subID, evJSON string) { ev := nostr.ParseEvent(evJSON) if ev == nil { return } if seenEvents[ev.ID] { return } seenEvents[ev.ID] = true if !feedPassesFilter(ev) { return } if muteSet[ev.PubKey] { return } if repliesToMuted(ev) { return } if (ev.Kind == 1 || ev.Kind == 1111) && looksLikeJSONSpam(ev.Content) { return } if oldestFeedTs == 0 || ev.CreatedAt < oldestFeedTs { oldestFeedTs = ev.CreatedAt } eventCount++ if subID == "feed-more" { feedMoreGot++ feed.WorkerPost(`["F_RENDER",` | evJSON | `,1]`) return } if feedInitialLoad { feed.WorkerPost(`["F_RENDER",` | evJSON | `,1]`) } else { feed.WorkerPost(`["F_RENDER",` | evJSON | `,0]`) } } func handleEOSE(subID string) { if subID == "feed" { feedInitialLoad = false feed.WorkerPost(`["F_EOSE_DONE","feed"]`) feed.WorkerPost(`["F_STATUS",` | helpers.Itoa(int64(eventCount)) | `,0]`) } else if subID == "feed-more" { if feedMoreTimer != 0 { feed.WorkerClearTimeout(feedMoreTimer) feedMoreTimer = 0 } feedLoading = false if feedMoreGot == 0 { feedEmptyStreak++ if feedEmptyStreak >= 3 { feedExhausted = true } } else { feedEmptyStreak = 0 } ex := int64(0) if feedExhausted { ex = 1 } feed.WorkerPost(`["F_STATUS",` | helpers.Itoa(int64(eventCount)) | `,` | helpers.Itoa(ex) | `]`) feed.WorkerPost(`["F_EOSE_DONE","feed-more"]`) feed.WorkerPost(`["F_CLOSE","feed-more"]`) } } func handleRefresh() { seenEvents = map[string]bool{} oldestFeedTs = 0 eventCount = 0 feedInitialLoad = true feedExhausted = false feedEmptyStreak = 0 feed.WorkerPost(`["F_CLOSE","feed"]`) feed.WorkerPost(`["F_CLOSE","feed-more"]`) subscribe() } func handleLoadMore() { if feedLoading || feedExhausted || oldestFeedTs == 0 { return } feedLoading = true feedMoreGot = 0 filter := buildFeedFilter(20) until := `,"until":` | helpers.Itoa(oldestFeedTs) | `}` filter = filter[:len(filter)-1] | until urls := feedRelays() feed.WorkerPost(`["F_SUB","feed-more",` | filter | `,` | buildURLsJSON(urls) | `]`) feedMoreTimer = feed.WorkerSetTimeout(10000, func() { feedMoreTimer = 0 feedLoading = false feed.WorkerPost(`["F_EOSE_DONE","feed-more"]`) }) } func subscribe() { if myPK == "" || len(relayURLs) == 0 { return } filter := buildFeedFilter(20) urls := feedRelays() feed.WorkerPost(`["F_SUB","feed",` | filter | `,` | buildURLsJSON(urls) | `]`) } func buildFeedFilter(limit int32) (s string) { if feedMode == "follows" && len(followList) > 0 { authors := jstr(myPK) for _, pk := range followList { if pk != myPK { authors = authors | "," | jstr(pk) } } return `{"kinds":[1,6,7,1111],"authors":[` | authors | `],"limit":` | helpers.Itoa(int64(limit)) | `}` } return `{"kinds":[1,6,7,1111],"limit":` | helpers.Itoa(int64(limit)) | `}` } func feedPassesFilter(ev *nostr.Event) (ok bool) { if feedMode != "follows" { return true } if len(followList) == 0 { return true } if ev.PubKey == myPK { return true } return followSet[ev.PubKey] } func feedRelays() (ss []string) { if feedMode != "" && feedMode != "follows" && feedMode != "relays" { return []string{feedMode} } return relayURLs } 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 looksLikeJSONSpam(content string) (ok bool) { i := 0 for i < len(content) { c := content[i] if c == ' ' || c == '\n' || c == '\r' || c == '\t' { i++ } else { break } } if i >= len(content) { return false } open := content[i] return open == '{' || open == '[' } 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) }