main.mx raw

   1  package main
   2  
   3  // Notif Worker - notification subscription, dedup, filtering, watermark tracking.
   4  //
   5  //   supervisor -> worker:
   6  //     ["N_EVENT", subID, evJSON]
   7  //     ["N_EOSE", subID]
   8  //     ["N_SET_PUBKEY", pk]
   9  //     ["N_SET_RELAYS", urlsJSON]
  10  //     ["N_SET_MUTES", pksJSON]
  11  //     ["N_LOAD_MORE"]
  12  //     ["N_MARK_READ"]
  13  //     ["N_SET_FILTER", filter]        -- "all", "mentions", "reactions", "zaps"
  14  //     ["N_READ_TS", ts]               -- initial notifReadTs from IDB
  15  //
  16  //   worker -> supervisor (routed to Shell):
  17  //     ["N_RENDER", evJSON, mode]      -- mode: "prepend" or "append"
  18  //     ["N_DOT", show]                 -- show=1 or 0
  19  //     ["N_EOSE_DONE", subID]
  20  //     ["N_STATUS", count, exhausted]
  21  //     ["N_SUB", subID, filterJSON, urlsJSON]
  22  //     ["N_CLOSE", subID]
  23  //     ["N_FETCH_REFS", idsJSON]       -- Shell should fetch these referenced events
  24  //     ["N_STORE_READ_TS", ts]         -- save notif-read-ts to IDB
  25  
  26  import (
  27  	"git.smesh.lol/smesh/web/common/helpers"
  28  	"git.smesh.lol/smesh/web/common/jsbridge/notif"
  29  	"git.smesh.lol/smesh/web/common/mw"
  30  	"git.smesh.lol/smesh/web/common/nostr"
  31  )
  32  
  33  var (
  34  	myPK         string
  35  	relayURLs    []string
  36  	muteSet      map[string]bool
  37  	notifFilter  string
  38  	notifReadTs  int64
  39  	notifSeen    map[string]bool
  40  	oldestTs     int64
  41  	eventCount   int32
  42  	exhausted    bool
  43  	emptyStreak  int32
  44  	initLoad     bool
  45  	loading      bool
  46  	moreGot      int32
  47  	moreTimer    int32
  48  	notifMissing []string
  49  	refTimer     int32
  50  )
  51  
  52  func main() {
  53  	muteSet = map[string]bool{}
  54  	notifSeen = map[string]bool{}
  55  	notifFilter = "all"
  56  	initLoad = true
  57  	notif.WorkerOnMessage(handleMessage)
  58  }
  59  
  60  func handleMessage(msg string) {
  61  	w := mw.New(msg)
  62  	switch w.Str() {
  63  	case "N_SET_PUBKEY":
  64  		myPK = w.Str()
  65  	case "N_SET_RELAYS":
  66  		relayURLs = parseStringArray(w.Raw())
  67  	case "N_SET_MUTES":
  68  		pks := parseStringArray(w.Raw())
  69  		muteSet = map[string]bool{}
  70  		for _, pk := range pks { muteSet[pk] = true }
  71  	case "N_SET_FILTER":
  72  		notifFilter = w.Str()
  73  	case "N_READ_TS":
  74  		notifReadTs = w.Num()
  75  	case "N_EVENT":
  76  		_ = w.Str() // subID consumed
  77  		evJSON := w.Raw()
  78  		handleNotifEvent(evJSON)
  79  	case "N_EOSE":
  80  		subID := w.Str()
  81  		handleEOSE(subID)
  82  	case "N_SUBSCRIBE":
  83  		handleSubscribe()
  84  	case "N_LOAD_MORE":
  85  		handleLoadMore()
  86  	case "N_MARK_READ":
  87  		handleMarkRead()
  88  	}
  89  }
  90  
  91  func handleNotifEvent(evJSON string) {
  92  	ev := nostr.ParseEvent(evJSON)
  93  	if ev == nil { return }
  94  	if notifSeen == nil { notifSeen = map[string]bool{} }
  95  	if notifSeen[ev.ID] { return }
  96  	notifSeen[ev.ID] = true
  97  	if oldestTs == 0 || ev.CreatedAt < oldestTs { oldestTs = ev.CreatedAt }
  98  	if ev.PubKey == myPK { return }
  99  	if muteSet[notifSenderPK(ev)] { return }
 100  	if repliesToMuted(ev) { return }
 101  
 102  	// Queue missing referenced events
 103  	if ev.Kind != 1 {
 104  		refID := notifRefID(ev)
 105  		if refID != "" {
 106  			notifMissing = append(notifMissing, refID)
 107  		}
 108  	}
 109  
 110  	// Check against read watermark for dot display
 111  	if ev.CreatedAt > notifReadTs {
 112  		notif.WorkerPost(`["N_DOT",1]`)
 113  	}
 114  
 115  	if !passesFilter(ev) { return }
 116  
 117  	mode := "append"
 118  	if !initLoad { mode = "prepend" }
 119  	notif.WorkerPost(`["N_RENDER",` | evJSON | `,"` | mode | `"]`)
 120  	eventCount++
 121  }
 122  
 123  func handleEOSE(subID string) {
 124  	if subID == "ntf" {
 125  		initLoad = false
 126  		if len(notifMissing) > 0 {
 127  			notif.WorkerPost(`["N_FETCH_REFS",` | buildIDsJSON(notifMissing) | `]`)
 128  			notifMissing = nil
 129  		}
 130  		notif.WorkerPost(`["N_EOSE_DONE","ntf"]`)
 131  		notif.WorkerPost(`["N_STATUS",` | helpers.Itoa(int64(eventCount)) | `,0]`)
 132  	} else if subID == "ntf-more" {
 133  		if moreTimer != 0 { notif.WorkerClearTimeout(moreTimer); moreTimer = 0 }
 134  		loading = false
 135  		if moreGot == 0 {
 136  			emptyStreak++
 137  			if emptyStreak >= 3 { exhausted = true }
 138  		} else {
 139  			emptyStreak = 0
 140  		}
 141  		ex := int64(0)
 142  		if exhausted { ex = 1 }
 143  		notif.WorkerPost(`["N_STATUS",` | helpers.Itoa(int64(eventCount)) | `,` | helpers.Itoa(ex) | `]`)
 144  		notif.WorkerPost(`["N_EOSE_DONE","ntf-more"]`)
 145  		notif.WorkerPost(`["N_CLOSE","ntf-more"]`)
 146  	}
 147  }
 148  
 149  func handleSubscribe() {
 150  	if myPK == "" || len(relayURLs) == 0 {
 151  		return
 152  	}
 153  	// Close any existing ntf sub, then open fresh.
 154  	notif.WorkerPost(`["N_CLOSE","ntf"]`)
 155  	filter := `{"kinds":[1,6,7,9735],"#p":[` | jstr(myPK) | `],"limit":20}`
 156  	notif.WorkerPost(`["N_SUB","ntf",` | filter | `,` | buildURLsJSON(relayURLs) | `]`)
 157  }
 158  
 159  func handleLoadMore() {
 160  	if loading || exhausted || oldestTs == 0 || myPK == "" { return }
 161  	loading = true
 162  	moreGot = 0
 163  	filter := `{"kinds":[1,6,7,9735],"#p":[` | jstr(myPK) | `],"until":` | helpers.Itoa(oldestTs) | `,"limit":20}`
 164  	notif.WorkerPost(`["N_SUB","ntf-more",` | filter | `,` | buildURLsJSON(relayURLs) | `]`)
 165  	moreTimer = notif.WorkerSetTimeout(10000, func() {
 166  		moreTimer = 0
 167  		loading = false
 168  		notif.WorkerPost(`["N_EOSE_DONE","ntf-more"]`)
 169  	})
 170  }
 171  
 172  func handleMarkRead() {
 173  	ts := notif.WorkerNowSeconds()
 174  	notifReadTs = ts
 175  	notif.WorkerPost(`["N_STORE_READ_TS",` | helpers.Itoa(ts) | `]`)
 176  	notif.WorkerPost(`["N_DOT",0]`)
 177  }
 178  
 179  func passesFilter(ev *nostr.Event) (ok bool) {
 180  	switch notifFilter {
 181  	case "mentions":
 182  		return ev.Kind == 1
 183  	case "reactions":
 184  		return ev.Kind == 7
 185  	case "zaps":
 186  		return ev.Kind == 9735
 187  	default:
 188  		return true
 189  	}
 190  }
 191  
 192  func notifRefID(ev *nostr.Event) (s string) {
 193  	switch ev.Kind {
 194  	case 7, 9735, 6:
 195  		tag := ev.Tags.GetFirst([]byte("e"))
 196  		if tag != nil { return string(tag.Value()) }
 197  	}
 198  	return ""
 199  }
 200  
 201  func notifSenderPK(ev *nostr.Event) (s string) {
 202  	if ev.Kind == 9735 {
 203  		pTag := ev.Tags.GetFirst([]byte("P"))
 204  		if pTag != nil { return string(pTag.Value()) }
 205  	}
 206  	return ev.PubKey
 207  }
 208  
 209  func repliesToMuted(ev *nostr.Event) (ok bool) {
 210  	for _, tag := range ev.Tags.GetAll("p") {
 211  		if v := string(tag.Value()); v != "" && muteSet[v] { return true }
 212  	}
 213  	return false
 214  }
 215  
 216  func buildIDsJSON(ids []string) (s string) {
 217  	s := "["
 218  	for i, id := range ids {
 219  		if i > 0 { s = s | "," }
 220  		s = s | jstr(id)
 221  	}
 222  	return s | "]"
 223  }
 224  
 225  func buildURLsJSON(urls []string) (s string) {
 226  	s := "["
 227  	for i, u := range urls {
 228  		if i > 0 { s = s | "," }
 229  		s = s | jstr(u)
 230  	}
 231  	return s | "]"
 232  }
 233  
 234  func parseStringArray(json string) (ss []string) {
 235  	var out []string
 236  	i := 0
 237  	for i < len(json) && json[i] != '[' { i++ }
 238  	if i >= len(json) { return nil }
 239  	i++
 240  	for {
 241  		for i < len(json) && (json[i] == ' ' || json[i] == ',' || json[i] == '\n') { i++ }
 242  		if i >= len(json) || json[i] == ']' { break }
 243  		if json[i] != '"' { break }
 244  		i++
 245  		start := i
 246  		for i < len(json) && json[i] != '"' {
 247  			if json[i] == '\\' { i++ }
 248  			i++
 249  		}
 250  		if i >= len(json) { break }
 251  		out = append(out, json[start:i])
 252  		i++
 253  	}
 254  	return out
 255  }
 256  
 257  func jstr(s string) (sv string) { return helpers.JsonString(s) }
 258