main.mx raw

   1  package main
   2  
   3  // Feed Worker - subscription management, dedup, filtering, and pagination for the note feed.
   4  //
   5  // Shell forwards verified feed events (subID "feed" / "feed-more") to this worker.
   6  // This worker deduplicates, applies mute/follow filters, and sends render commands back to Shell.
   7  //
   8  //   supervisor -> worker (from Shell via feed.Send):
   9  //     ["F_EVENT", subID, evJSON]          -- feed or feed-more event arrived
  10  //     ["F_EOSE", subID]                   -- EOSE for feed or feed-more sub
  11  //     ["F_SET_MODE", mode]                -- "follows", "relays", or relay URL
  12  //     ["F_SET_RELAYS", urlsJSON]
  13  //     ["F_SET_FOLLOWS", pksJSON]          -- current follow list (pubkeys)
  14  //     ["F_SET_MUTES", pksJSON]            -- current mute list (pubkeys)
  15  //     ["F_SET_PUBKEY", pk]
  16  //     ["F_LOAD_MORE"]                     -- user clicked "load more"
  17  //     ["F_REFRESH"]                       -- user triggered refresh
  18  //
  19  //   worker -> supervisor (to Shell via F_* prefix routing):
  20  //     ["F_RENDER", evJSON, now]           -- now=1: render immediately, 0: buffer
  21  //     ["F_NEW"]                           -- new buffered events available (pulse)
  22  //     ["F_EOSE_DONE", subID]              -- signal Shell to update UI after EOSE
  23  //     ["F_STATUS", count, exhausted]      -- event count and exhaustion state
  24  //     ["F_SUB", subID, filterJSON, urlsJSON]
  25  //     ["F_CLOSE", subID]
  26  
  27  import (
  28  	"git.smesh.lol/smesh/web/common/helpers"
  29  	"git.smesh.lol/smesh/web/common/jsbridge/feed"
  30  	"git.smesh.lol/smesh/web/common/mw"
  31  	"git.smesh.lol/smesh/web/common/nostr"
  32  )
  33  
  34  var (
  35  	feedMode        string
  36  	relayURLs       []string
  37  	followList      []string
  38  	followSet       map[string]bool
  39  	muteSet         map[string]bool
  40  	myPK            string
  41  	seenEvents      map[string]bool
  42  	oldestFeedTs    int64
  43  	eventCount      int32
  44  	feedInitialLoad bool
  45  	feedExhausted   bool
  46  	feedEmptyStreak int32
  47  	feedLoading     bool
  48  	feedMoreGot     int32
  49  	feedMoreTimer   int32
  50  )
  51  
  52  func main() {
  53  	seenEvents = map[string]bool{}
  54  	followSet = map[string]bool{}
  55  	muteSet = map[string]bool{}
  56  	feedInitialLoad = true
  57  	feed.WorkerOnMessage(handleMessage)
  58  }
  59  
  60  func handleMessage(msg string) {
  61  	w := mw.New(msg)
  62  	switch w.Str() {
  63  	case "F_EVENT":
  64  		subID := w.Str()
  65  		evJSON := w.Raw()
  66  		handleFeedEvent(subID, evJSON)
  67  	case "F_EOSE":
  68  		subID := w.Str()
  69  		handleEOSE(subID)
  70  	case "F_SET_MODE":
  71  		feedMode = w.Str()
  72  	case "F_SET_RELAYS":
  73  		relayURLs = parseStringArray(w.Raw())
  74  	case "F_SET_FOLLOWS":
  75  		followList = parseStringArray(w.Raw())
  76  		followSet = map[string]bool{}
  77  		for _, pk := range followList {
  78  			followSet[pk] = true
  79  		}
  80  	case "F_SET_MUTES":
  81  		pks := parseStringArray(w.Raw())
  82  		muteSet = map[string]bool{}
  83  		for _, pk := range pks {
  84  			muteSet[pk] = true
  85  		}
  86  	case "F_SET_PUBKEY":
  87  		myPK = w.Str()
  88  	case "F_LOAD_MORE":
  89  		handleLoadMore()
  90  	case "F_REFRESH":
  91  		handleRefresh()
  92  	}
  93  }
  94  
  95  func handleFeedEvent(subID, evJSON string) {
  96  	ev := nostr.ParseEvent(evJSON)
  97  	if ev == nil {
  98  		return
  99  	}
 100  	if seenEvents[ev.ID] {
 101  		return
 102  	}
 103  	seenEvents[ev.ID] = true
 104  	if !feedPassesFilter(ev) {
 105  		return
 106  	}
 107  	if muteSet[ev.PubKey] {
 108  		return
 109  	}
 110  	if repliesToMuted(ev) {
 111  		return
 112  	}
 113  	if (ev.Kind == 1 || ev.Kind == 1111) && looksLikeJSONSpam(ev.Content) {
 114  		return
 115  	}
 116  	if oldestFeedTs == 0 || ev.CreatedAt < oldestFeedTs {
 117  		oldestFeedTs = ev.CreatedAt
 118  	}
 119  	eventCount++
 120  	if subID == "feed-more" {
 121  		feedMoreGot++
 122  		feed.WorkerPost(`["F_RENDER",` | evJSON | `,1]`)
 123  		return
 124  	}
 125  	if feedInitialLoad {
 126  		feed.WorkerPost(`["F_RENDER",` | evJSON | `,1]`)
 127  	} else {
 128  		feed.WorkerPost(`["F_RENDER",` | evJSON | `,0]`)
 129  	}
 130  }
 131  
 132  func handleEOSE(subID string) {
 133  	if subID == "feed" {
 134  		feedInitialLoad = false
 135  		feed.WorkerPost(`["F_EOSE_DONE","feed"]`)
 136  		feed.WorkerPost(`["F_STATUS",` | helpers.Itoa(int64(eventCount)) | `,0]`)
 137  	} else if subID == "feed-more" {
 138  		if feedMoreTimer != 0 {
 139  			feed.WorkerClearTimeout(feedMoreTimer)
 140  			feedMoreTimer = 0
 141  		}
 142  		feedLoading = false
 143  		if feedMoreGot == 0 {
 144  			feedEmptyStreak++
 145  			if feedEmptyStreak >= 3 {
 146  				feedExhausted = true
 147  			}
 148  		} else {
 149  			feedEmptyStreak = 0
 150  		}
 151  		ex := int64(0)
 152  		if feedExhausted {
 153  			ex = 1
 154  		}
 155  		feed.WorkerPost(`["F_STATUS",` | helpers.Itoa(int64(eventCount)) | `,` | helpers.Itoa(ex) | `]`)
 156  		feed.WorkerPost(`["F_EOSE_DONE","feed-more"]`)
 157  		feed.WorkerPost(`["F_CLOSE","feed-more"]`)
 158  	}
 159  }
 160  
 161  func handleRefresh() {
 162  	seenEvents = map[string]bool{}
 163  	oldestFeedTs = 0
 164  	eventCount = 0
 165  	feedInitialLoad = true
 166  	feedExhausted = false
 167  	feedEmptyStreak = 0
 168  	feed.WorkerPost(`["F_CLOSE","feed"]`)
 169  	feed.WorkerPost(`["F_CLOSE","feed-more"]`)
 170  	subscribe()
 171  }
 172  
 173  func handleLoadMore() {
 174  	if feedLoading || feedExhausted || oldestFeedTs == 0 {
 175  		return
 176  	}
 177  	feedLoading = true
 178  	feedMoreGot = 0
 179  	filter := buildFeedFilter(20)
 180  	until := `,"until":` | helpers.Itoa(oldestFeedTs) | `}`
 181  	filter = filter[:len(filter)-1] | until
 182  	urls := feedRelays()
 183  	feed.WorkerPost(`["F_SUB","feed-more",` | filter | `,` | buildURLsJSON(urls) | `]`)
 184  	feedMoreTimer = feed.WorkerSetTimeout(10000, func() {
 185  		feedMoreTimer = 0
 186  		feedLoading = false
 187  		feed.WorkerPost(`["F_EOSE_DONE","feed-more"]`)
 188  	})
 189  }
 190  
 191  func subscribe() {
 192  	if myPK == "" || len(relayURLs) == 0 {
 193  		return
 194  	}
 195  	filter := buildFeedFilter(20)
 196  	urls := feedRelays()
 197  	feed.WorkerPost(`["F_SUB","feed",` | filter | `,` | buildURLsJSON(urls) | `]`)
 198  }
 199  
 200  func buildFeedFilter(limit int32) (s string) {
 201  	if feedMode == "follows" && len(followList) > 0 {
 202  		authors := jstr(myPK)
 203  		for _, pk := range followList {
 204  			if pk != myPK {
 205  				authors = authors | "," | jstr(pk)
 206  			}
 207  		}
 208  		return `{"kinds":[1,6,7,1111],"authors":[` | authors | `],"limit":` | helpers.Itoa(int64(limit)) | `}`
 209  	}
 210  	return `{"kinds":[1,6,7,1111],"limit":` | helpers.Itoa(int64(limit)) | `}`
 211  }
 212  
 213  func feedPassesFilter(ev *nostr.Event) (ok bool) {
 214  	if feedMode != "follows" {
 215  		return true
 216  	}
 217  	if len(followList) == 0 {
 218  		return true
 219  	}
 220  	if ev.PubKey == myPK {
 221  		return true
 222  	}
 223  	return followSet[ev.PubKey]
 224  }
 225  
 226  func feedRelays() (ss []string) {
 227  	if feedMode != "" && feedMode != "follows" && feedMode != "relays" {
 228  		return []string{feedMode}
 229  	}
 230  	return relayURLs
 231  }
 232  
 233  func repliesToMuted(ev *nostr.Event) (ok bool) {
 234  	for _, tag := range ev.Tags.GetAll("p") {
 235  		if v := string(tag.Value()); v != "" && muteSet[v] {
 236  			return true
 237  		}
 238  	}
 239  	return false
 240  }
 241  
 242  func looksLikeJSONSpam(content string) (ok bool) {
 243  	i := 0
 244  	for i < len(content) {
 245  		c := content[i]
 246  		if c == ' ' || c == '\n' || c == '\r' || c == '\t' {
 247  			i++
 248  		} else {
 249  			break
 250  		}
 251  	}
 252  	if i >= len(content) {
 253  		return false
 254  	}
 255  	open := content[i]
 256  	return open == '{' || open == '['
 257  }
 258  
 259  func buildURLsJSON(urls []string) (s string) {
 260  	s := "["
 261  	for i, u := range urls {
 262  		if i > 0 { s = s | "," }
 263  		s = s | jstr(u)
 264  	}
 265  	return s | "]"
 266  }
 267  
 268  func parseStringArray(json string) (ss []string) {
 269  	var out []string
 270  	i := 0
 271  	for i < len(json) && json[i] != '[' { i++ }
 272  	if i >= len(json) { return nil }
 273  	i++
 274  	for {
 275  		for i < len(json) && (json[i] == ' ' || json[i] == ',' || json[i] == '\n') { i++ }
 276  		if i >= len(json) || json[i] == ']' { break }
 277  		if json[i] != '"' { break }
 278  		i++
 279  		start := i
 280  		for i < len(json) && json[i] != '"' {
 281  			if json[i] == '\\' { i++ }
 282  			i++
 283  		}
 284  		if i >= len(json) { break }
 285  		out = append(out, json[start:i])
 286  		i++
 287  	}
 288  	return out
 289  }
 290  
 291  func jstr(s string) (sv string) { return helpers.JsonString(s) }
 292