crawler.mx raw
1 // Package crawler discovers Nostr relays by extracting relay URLs
2 // from kind 10002 (relay list metadata) events stored locally.
3 package crawler
4
5 import (
6 "git.smesh.lol/moxie/pkg/mxutil"
7 "git.smesh.lol/nostr/pkg/filter"
8 "git.smesh.lol/nostr/pkg/kind"
9 "git.smesh.lol/morly/pkg/store"
10 )
11
12 // Crawler discovers relay URLs from the local store.
13 type Crawler struct {
14 store *store.Engine
15 }
16
17 // New creates a crawler.
18 func New(s *store.Engine) (c *Crawler) { return &Crawler{store: s} }
19
20 // Relay is a discovered relay with its usage hints.
21 type Relay struct {
22 URL string
23 Read bool
24 Write bool
25 }
26
27 // Discover scans the store for kind 10002 events and extracts relay URLs.
28 // Returns deduplicated relays.
29 func (c *Crawler) Discover() (rs []Relay) {
30 kind.Ensure()
31 f := &filter.F{
32 Kinds: kind.NewS(kind.RelayListMetadata),
33 }
34 events, err := c.store.QueryEvents(f)
35 if err != nil {
36 return nil
37 }
38
39 seen := map[string]*Relay{}
40 for _, ev := range events {
41 if ev.Tags == nil {
42 continue
43 }
44 rTags := ev.Tags.GetAll([]byte("r"))
45 for _, rt := range rTags {
46 if rt.Len() < 2 {
47 continue
48 }
49 url := string(rt.Value())
50 if url == "" {
51 continue
52 }
53 r, ok := seen[url]
54 if !ok {
55 r = &Relay{URL: url}
56 seen[url] = r
57 }
58 // Third element is optional: "read", "write", or absent (both).
59 hint := rt.Relay()
60 if len(hint) == 0 {
61 r.Read = true
62 r.Write = true
63 } else if string(hint) == "read" {
64 r.Read = true
65 } else if string(hint) == "write" {
66 r.Write = true
67 }
68 }
69 }
70
71 out := []Relay{:0:len(seen)}
72 for _, r := range seen {
73 out = mxutil.Ensure(out, 1)
74 out = push(out, *r)
75 }
76 return out
77 }
78
79 // DiscoverURLs returns just the relay URL strings.
80 func (c *Crawler) DiscoverURLs() (ss []string) {
81 relays := c.Discover()
82 urls := []string{:len(relays)}
83 for i, r := range relays {
84 urls[i] = r.URL
85 }
86 return urls
87 }
88