diff --git a/cache.go b/cache.go index bc60499..6d83870 100644 --- a/cache.go +++ b/cache.go @@ -8,20 +8,15 @@ import ( "time" "github.com/dgraph-io/badger/v4" - "github.com/fiatjaf/eventstore" - eventstore_badger "github.com/fiatjaf/eventstore/badger" ) -var ( - cache = Cache{} - db eventstore.Store = &eventstore_badger.BadgerBackend{} -) +var cache = Cache{} type Cache struct { *badger.DB } -func (c *Cache) initialize() func() { +func (c *Cache) initializeCache() func() { db, err := badger.Open(badger.DefaultOptions(s.DiskCachePath)) if err != nil { log.Fatal().Err(err).Str("path", s.DiskCachePath).Msg("failed to open badger") @@ -40,7 +35,9 @@ func (c *Cache) initialize() func() { } }() - return func() { db.Close() } + return func() { + db.Close() + } } func (c *Cache) Delete(key string) error { diff --git a/calendar_event.templ b/calendar_event.templ index 1605642..b23a46b 100644 --- a/calendar_event.templ +++ b/calendar_event.templ @@ -8,9 +8,9 @@ import ( "github.com/nbd-wtf/go-nostr/nip52" ) -func formatPartecipants(partecipants []nip52.Participant) string { +func formatParticipants(participants []nip52.Participant) string { var list = make([]string, 0) - for _, p := range partecipants { + for _, p := range participants { nreplace, _ := nip19.EncodePublicKey(p.PubKey) bytes := []byte(p.Role) bytes[0] = byte(unicode.ToUpper(rune(bytes[0]))) @@ -79,7 +79,7 @@ templ calendarEventTemplate(params CalendarPageParams) { if len(params.CalendarEvent.Participants) != 0 {
People: - @templ.Raw(formatPartecipants(params.CalendarEvent.Participants)) + @templ.Raw(formatParticipants(params.CalendarEvent.Participants))
} if params.CalendarEvent.Image != "" { diff --git a/data.go b/data.go index 8229676..3047192 100644 --- a/data.go +++ b/data.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "html/template" - "slices" "strings" "time" @@ -24,9 +23,7 @@ type Data struct { naddr string naddrNaked string createdAt string - modifiedAt string parentLink template.HTML - renderableLastNotes []EnhancedEvent kindDescription string kindNIP string video string @@ -41,20 +38,11 @@ type Data struct { Kind30818Metadata Kind30818Metadata } -func (d Data) authorRelaysPretty(ctx context.Context) []string { - s := make([]string, 0, 3) - for _, url := range sys.FetchOutboxRelays(ctx, d.event.PubKey, 3) { - trimmed := trimProtocolAndEndingSlash(url) - if slices.Contains(s, trimmed) { - continue - } - s = append(s, trimmed) - } - return s -} +func grabData(ctx context.Context, code string) (Data, error) { + ctx, span := tracer.Start(ctx, "grab-data") + defer span.End() -func grabData(ctx context.Context, code string, isProfileSitemap bool) (Data, error) { - // code can be a nevent, nprofile, npub or nip05 identifier, in which case we try to fetch the associated event + // code can be a nevent or naddr, in which case we try to fetch the associated event event, relays, err := getEvent(ctx, code) if err != nil { log.Warn().Err(err).Str("code", code).Msg("failed to fetch event for code") @@ -73,8 +61,11 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (Data, er } } + ee := NewEnhancedEvent(ctx, event) + ee.relays = relays + data := Data{ - event: NewEnhancedEvent(ctx, event, relays), + event: ee, } data.nevent, _ = nip19.EncodeEvent(event.ID, relaysForNip19, event.PubKey) @@ -82,7 +73,6 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (Data, er data.naddr = "" data.naddrNaked = "" data.createdAt = time.Unix(int64(event.CreatedAt), 0).Format("2006-01-02 15:04:05") - data.modifiedAt = time.Unix(int64(event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00") if event.Kind >= 30000 && event.Kind < 40000 { if d := event.Tags.GetFirst([]string{"d", ""}); d != nil { @@ -94,13 +84,6 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (Data, er data.alt = nip31.GetAlt(*event) switch event.Kind { - case 0: - data.templateId = Profile - lastNotes := authorLastNotes(ctx, event.PubKey, isProfileSitemap) - data.renderableLastNotes = make([]EnhancedEvent, len(lastNotes)) - for i, levt := range lastNotes { - data.renderableLastNotes[i] = NewEnhancedEvent(ctx, levt, []string{}) - } case 1, 7, 30023, 30024: data.templateId = Note data.content = event.Content diff --git a/enhanced_event.go b/enhanced_event.go index a2225ef..bcc276c 100644 --- a/enhanced_event.go +++ b/enhanced_event.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "fmt" "html" "html/template" @@ -29,12 +28,11 @@ type EnhancedEvent struct { func NewEnhancedEvent( ctx context.Context, event *nostr.Event, - relays []string, ) EnhancedEvent { - ee := EnhancedEvent{ - Event: event, - relays: relays, - } + ctx, span := tracer.Start(ctx, "make-enhanced-event") + defer span.End() + + ee := EnhancedEvent{Event: event} for _, tag := range event.Tags { if tag[0] == "subject" || tag[0] == "title" { @@ -52,6 +50,7 @@ func NewEnhancedEvent( } else { ctx, cancel := context.WithTimeout(ctx, time.Second*3) defer cancel() + ee.author = sys.FetchProfileMetadata(ctx, event.PubKey) } } @@ -201,46 +200,3 @@ func (ee EnhancedEvent) CreatedAtStr() string { func (ee EnhancedEvent) ModifiedAtStr() string { return time.Unix(int64(ee.Event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00") } - -func (ee EnhancedEvent) ToJSONHTML() template.HTML { - tagsHTML := "[" - for t, tag := range ee.Tags { - tagsHTML += "\n [" - for i, item := range tag { - cls := `"text-zinc-500 dark:text-zinc-50"` - if i == 0 { - cls = `"text-amber-500 dark:text-amber-200"` - } - itemJSON, _ := json.Marshal(item) - tagsHTML += "\n " + html.EscapeString(string(itemJSON)) - if i < len(tag)-1 { - tagsHTML += "," - } else { - tagsHTML += "\n " - } - } - tagsHTML += "]" - if t < len(ee.Tags)-1 { - tagsHTML += "," - } else { - tagsHTML += "\n " - } - } - tagsHTML += "]" - - contentJSON, _ := json.Marshal(ee.Content) - - keyCls := "text-purple-700 dark:text-purple-300" - - return template.HTML(fmt.Sprintf( - `{ - "id": "%s", - "pubkey": "%s", - "created_at": %d, - "kind": %d, - "tags": %s, - "content": %s, - "sig": "%s" -}`, ee.ID, ee.PubKey, ee.CreatedAt, ee.Kind, tagsHTML, html.EscapeString(string(contentJSON)), ee.Sig), - ) -} diff --git a/go.mod b/go.mod index ce20e31..c0ed872 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/microcosm-cc/bluemonday v1.0.24 github.com/nbd-wtf/emoji v0.0.3 github.com/nbd-wtf/go-nostr v0.34.5 - github.com/nbd-wtf/nostr-sdk v0.4.2 + github.com/nbd-wtf/nostr-sdk v0.5.0 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pelletier/go-toml v1.9.5 github.com/pemistahl/lingua-go v1.4.0 @@ -30,6 +30,10 @@ require ( github.com/stretchr/testify v1.9.0 github.com/texttheater/golang-levenshtein v1.0.1 github.com/tylermmorton/tmpl v0.0.0-20231025031313-5552ee818c6d + go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 + go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/trace v1.28.0 golang.org/x/image v0.17.0 mvdan.cc/xurls/v2 v2.5.0 ) @@ -42,7 +46,8 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -51,19 +56,23 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/fasthttp/websocket v1.5.7 // indirect github.com/fiatjaf/generic-ristretto v0.0.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.1.2 // indirect + github.com/golang/glog v1.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -84,14 +93,20 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.23.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0375f69..8f76ddd 100644 --- a/go.sum +++ b/go.sum @@ -45,10 +45,12 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bytesparadise/libasciidoc v0.8.0 h1:iWAlYR7gm4Aes3NSvuGQyzRavatQpUBAJZyU9uMmwm0= github.com/bytesparadise/libasciidoc v0.8.0/go.mod h1:Q2ZeBQ1fko5+NTUTs8rGu9gjTtbVaD6Qxg37GOPYdN4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -97,6 +99,11 @@ github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= @@ -113,8 +120,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= -github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -131,8 +138,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd h1:PppHBegd3uPZ3Y/Iax/2mlCFJm1w4Qf/zP1MdW4ju2o= @@ -151,10 +158,14 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 h1:CWyXh/jylQWp2dtiV33mY4iSSp6yf4lmn+c7/tN+ObI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0/go.mod h1:nCLIt0w3Ept2NwF8ThLmrppXsfT07oC8k0XNDxd8sVU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -190,8 +201,8 @@ github.com/nbd-wtf/emoji v0.0.3 h1:YtkT7MVPXvqU1SQjvC/CShlWexnREzqNCxmhUnL00CA= github.com/nbd-wtf/emoji v0.0.3/go.mod h1:tS6D9iI34qwBmWc5g8X7tVDkWXulqbTJRsvsM6QsS88= github.com/nbd-wtf/go-nostr v0.34.5 h1:vti8WqvGWbVoWAPniaz7li2TpCyC+7ZS62Gmy7ib/z0= github.com/nbd-wtf/go-nostr v0.34.5/go.mod h1:NZQkxl96ggbO8rvDpVjcsojJqKTPwqhP4i82O7K5DJs= -github.com/nbd-wtf/nostr-sdk v0.4.2 h1:xL3LGqCcGHjATtU3tynxlcBmARXCKuaKZOPVSWIf2zY= -github.com/nbd-wtf/nostr-sdk v0.4.2/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= +github.com/nbd-wtf/nostr-sdk v0.5.0 h1:zrMxcvMSxkw29RyfXEdF3XW5rUWLuT5Q9oBAhd5dyew= +github.com/nbd-wtf/nostr-sdk v0.5.0/go.mod h1:MJ7gYv3XiZKU6MHSM0N7oHqQAQhbvpgGQk4Q+XUdIUs= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -220,8 +231,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -272,13 +283,29 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= @@ -310,8 +337,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -346,8 +373,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -381,11 +408,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf h1:GillM0Ef0pkZPIB+5iO6SDK+4T9pf6TpaYR6ICD5rVE= +google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -397,8 +430,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/justfile b/justfile index ff32f55..8644f75 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ export PATH := "./node_modules/.bin:" + env_var('PATH') dev tags='': - fd 'go|templ|base.css' | entr -r bash -c 'TAILWIND_DEBUG=true SKIP_LANGUAGE_MODEL=true && templ generate && go build -tags={{tags}} -o /tmp/njump && /tmp/njump' + fd 'go|templ|base.css' | entr -r bash -c 'TAILWIND_DEBUG=true SKIP_LANGUAGE_MODEL=true && templ generate && go build -tags={{tags}} -o /tmp/njump && PORT=3001 /tmp/njump' build: templ tailwind go build -o ./njump diff --git a/main.go b/main.go index c89bf10..d642334 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,6 @@ import ( "os/signal" "strings" - eventstore_badger "github.com/fiatjaf/eventstore/badger" "github.com/fiatjaf/khatru" "github.com/kelseyhightower/envconfig" "github.com/nbd-wtf/go-nostr" @@ -51,6 +50,15 @@ func main() { } } + if os.Getenv("OTEL_RESOURCE_ATTRIBUTES") != "" { + shutdown, err := setupOTelSDK(context.Background()) + if err != nil { + log.Fatal().Err(err).Msg("otel error") + return + } + defer shutdown(context.Background()) + } + if len(s.TrustedPubKeys) == 0 { s.TrustedPubKeys = defaultTrustedPubKeys } @@ -102,9 +110,11 @@ func main() { // image rendering stuff initializeImageDrawingStuff() - // eventstore and internal db - deinitCache := initCache() - defer deinitCache() + // internal db + defer cache.initializeCache()() + + // eventstore and nostr system + defer initSystem()() // initialize routines ctx, cancel := context.WithCancel(context.Background()) @@ -115,8 +125,8 @@ func main() { // expose our internal cache as a relay (mostly for debugging purposes) relay := khatru.NewRelay() - relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents) - relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent) + relay.QueryEvents = append(relay.QueryEvents, sys.Store.QueryEvents) + relay.DeleteEvent = append(relay.DeleteEvent, sys.Store.DeleteEvent) relay.RejectEvent = append(relay.RejectEvent, func(context.Context, *nostr.Event) (bool, string) { return true, "this relay is not writable" @@ -126,6 +136,7 @@ func main() { // routes mux := relay.Router() mux.Handle("/njump/static/", http.StripPrefix("/njump/", http.FileServer(http.FS(static)))) + mux.HandleFunc("/relays-archive.xml", renderArchive) mux.HandleFunc("/npubs-archive.xml", renderArchive) mux.HandleFunc("/npubs-sitemaps.xml", renderSitemapIndex) @@ -141,8 +152,10 @@ func main() { mux.HandleFunc("/embed/", renderEmbedjs) mux.HandleFunc("/", renderEvent) + corsHandler := cors.Default().Handler(relay) + log.Print("listening at http://0.0.0.0:" + s.Port) - server := &http.Server{Addr: "0.0.0.0:" + s.Port, Handler: cors.Default().Handler(relay)} + server := &http.Server{Addr: "0.0.0.0:" + s.Port, Handler: corsHandler} go func() { if err := server.ListenAndServe(); err != nil { log.Error().Err(err).Msg("") @@ -154,20 +167,3 @@ func main() { <-sc server.Close() } - -func initCache() func() { - // initialize disk cache - deinit := cache.initialize() - - // initialize eventstore database - if badgerBackend, ok := db.(*eventstore_badger.BadgerBackend); ok { - // it may be NullStore, in which case we do nothing - badgerBackend.Path = s.EventStorePath - } - db.Init() - - return func() { - deinit() - db.Close() - } -} diff --git a/nostr.go b/nostr.go index 60149e4..bfb6702 100644 --- a/nostr.go +++ b/nostr.go @@ -4,14 +4,16 @@ import ( "context" "fmt" "slices" + "sync" "time" - "github.com/fiatjaf/eventstore" + "github.com/fiatjaf/eventstore/badger" "github.com/nbd-wtf/go-nostr" - "github.com/nbd-wtf/go-nostr/nip05" "github.com/nbd-wtf/go-nostr/nip19" sdk "github.com/nbd-wtf/nostr-sdk" cache_memory "github.com/nbd-wtf/nostr-sdk/cache/memory" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) type RelayConfig struct { @@ -21,10 +23,7 @@ type RelayConfig struct { } var ( - sys = sdk.NewSystem( - sdk.WithMetadataCache(cache_memory.New32[sdk.ProfileMetadata](10000)), - sdk.WithRelayListCache(cache_memory.New32[sdk.RelayList](10000)), - ) + sys *sdk.System serial int relayConfig = RelayConfig{ @@ -33,6 +32,7 @@ var ( JustIds: []string{ "wss://cache2.primal.net/v1", "wss://relay.noswhere.com", + "wss://relay.damus.io", }, } @@ -49,19 +49,31 @@ type CachedEvent struct { Relays []string `json:"r"` } +func initSystem() func() { + db := &badger.BadgerBackend{ + Path: s.EventStorePath, + } + db.Init() + + sys = sdk.NewSystem( + sdk.WithMetadataCache(cache_memory.New32[sdk.ProfileMetadata](10000)), + sdk.WithRelayListCache(cache_memory.New32[sdk.RelayList](10000)), + sdk.WithStore(db), + ) + + return db.Close +} + func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) { - wdb := eventstore.RelayWrapper{Store: db} + ctx, span := tracer.Start(ctx, "get-event", trace.WithAttributes(attribute.String("code", code))) + defer span.End() // this is for deciding what relays will go on nevent and nprofile later priorityRelays := make(map[string]int) prefix, data, err := nip19.Decode(code) if err != nil { - pp, _ := nip05.QueryIdentifier(ctx, code) - if pp == nil { - return nil, nil, fmt.Errorf("failed to decode %w", err) - } - data = *pp + return nil, nil, fmt.Errorf("failed to decode %w", err) } author := "" @@ -71,16 +83,6 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) relays := make([]string, 0, 10) switch v := data.(type) { - case nostr.ProfilePointer: - author = v.PublicKey - filter.Authors = []string{v.PublicKey} - filter.Kinds = []int{0} - relays = append(relays, v.Relays...) - authorRelaysPosition = len(v.Relays) // ensure author relays are checked after hinted relays - relays = append(relays, sys.MetadataRelays...) - for _, r := range v.Relays { - priorityRelays[r] = 2 - } case nostr.EventPointer: author = v.Author filter.IDs = []string{v.ID} @@ -104,16 +106,12 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) if prefix == "note" { filter.IDs = []string{v} relays = append(relays, relayConfig.JustIds...) - } else if prefix == "npub" { - author = v - filter.Authors = []string{v} - filter.Kinds = []int{0} - relays = append(relays, sys.MetadataRelays...) } } // try to fetch in our internal eventstore first - if res, _ := wdb.QuerySync(ctx, filter); len(res) != 0 { + ctx, span = tracer.Start(ctx, "query-eventstore") + if res, _ := sys.StoreRelay.QuerySync(ctx, filter); len(res) != 0 { evt := res[0] // keep this event in cache for a while more @@ -125,10 +123,13 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) return evt, getRelaysForEvent(evt.ID), nil } + span.End() if author != "" { // fetch relays for author + ctx, span = tracer.Start(ctx, "fetch-outbox-relays") authorRelays := sys.FetchOutboxRelays(ctx, author, 3) + span.End() relays = slices.Insert(relays, authorRelaysPosition, authorRelays...) for _, r := range authorRelays { priorityRelays[r] = 1 @@ -140,33 +141,44 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) } relays = unique(relays) - ctx, cancel := context.WithTimeout(ctx, time.Second*8) - defer cancel() - // actually fetch the event here var result *nostr.Event var successRelays []string = nil - // keep track of where we have actually found the event so we can show that - successRelays = make([]string, 0, len(relays)) - countdown := 7.5 - go func() { - for { - time.Sleep(500 * time.Millisecond) - if countdown <= 0 { - cancel() - break - } - countdown -= 0.5 - } - }() + { + // actually fetch the event here + subManyCtx, cancel := context.WithTimeout(ctx, time.Second*8) + defer cancel() - for ie := range sys.Pool.SubManyEoseNonUnique(ctx, relays, nostr.Filters{filter}) { - successRelays = append(successRelays, ie.Relay.URL) - if result == nil || ie.CreatedAt > result.CreatedAt { - result = ie.Event + // keep track of where we have actually found the event so we can show that + successRelays = make([]string, 0, len(relays)) + countdown := 7.5 + go func() { + for { + time.Sleep(500 * time.Millisecond) + if countdown <= 0 { + cancel() + break + } + countdown -= 0.5 + } + }() + + fetchProfileOnce := sync.Once{} + + ctx, span = tracer.Start(subManyCtx, "sub-many-eose-non-unique") + for ie := range sys.Pool.SubManyEoseNonUnique(subManyCtx, relays, nostr.Filters{filter}) { + fetchProfileOnce.Do(func() { + go sys.FetchProfileMetadata(ctx, ie.PubKey) + }) + + successRelays = append(successRelays, ie.Relay.URL) + if result == nil || ie.CreatedAt > result.CreatedAt { + result = ie.Event + } + countdown = min(countdown, 1) } - countdown = min(countdown, 1) + span.End() } if result == nil { @@ -175,7 +187,9 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) } // save stuff in cache and in internal store - wdb.Publish(ctx, *result) + ctx, span = tracer.Start(ctx, "save-local") + sys.StoreRelay.Publish(ctx, *result) + span.End() // save relays if we got them allRelays := attachRelaysToEvent(result.ID, successRelays...) // put priority relays first so they get used in nevent and nprofile @@ -190,14 +204,23 @@ func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) return result, allRelays, nil } -func authorLastNotes(ctx context.Context, pubkey string, isSitemap bool) []*nostr.Event { - limit := 100 - store := true - useLocalStore := true +func authorLastNotes(ctx context.Context, pubkey string, isSitemap bool) []EnhancedEvent { + ctx, span := tracer.Start(ctx, "author-last-notes") + defer span.End() + + var limit int + var store bool + var useLocalStore bool + if isSitemap { limit = 50000 store = false useLocalStore = false + } else { + limit = 100 + store = true + useLocalStore = true + go sys.FetchProfileMetadata(ctx, pubkey) // fetch this before so the cache is filled for later } filter := nostr.Filter{ @@ -205,23 +228,39 @@ func authorLastNotes(ctx context.Context, pubkey string, isSitemap bool) []*nost Authors: []string{pubkey}, Limit: limit, } - var lastNotes []*nostr.Event + + lastNotes := make([]EnhancedEvent, 0, filter.Limit) // fetch from local store if available if useLocalStore { - lastNotes, _ = eventstore.RelayWrapper{Store: db}.QuerySync(ctx, filter) + ch, err := sys.Store.QueryEvents(ctx, filter) + if err == nil { + for evt := range ch { + lastNotes = append(lastNotes, NewEnhancedEvent(ctx, evt)) + if store { + sys.Store.SaveEvent(ctx, evt) + scheduleEventExpiration(evt.ID, time.Hour*24) + } + } + } } if len(lastNotes) < 5 { // if we didn't get enough notes (or if we didn't even query the local store), wait for the external relays - lastNotes = make([]*nostr.Event, 0, filter.Limit) + ctx, span = tracer.Start(ctx, "querying-external") ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - relays := sys.FetchOutboxRelays(ctx, pubkey, 5) - for len(relays) < 4 { + _, span2 := tracer.Start(ctx, "fetch-outbox-relays-for-author-last-notes") + relays := sys.FetchOutboxRelays(ctx, pubkey, 3) + span2.End() + + for len(relays) < 3 { relays = unique(append(relays, getRandomRelay())) } + + ctx, span2 = tracer.Start(ctx, "sub-many-eose") ch := sys.Pool.SubManyEose(ctx, relays, nostr.Filters{filter}) + span2.End() out: for { select { @@ -229,9 +268,13 @@ func authorLastNotes(ctx context.Context, pubkey string, isSitemap bool) []*nost if !more { break out } - lastNotes = append(lastNotes, ie.Event) + + ee := NewEnhancedEvent(ctx, ie.Event) + ee.relays = unique(append([]string{ie.Relay.URL}, getRelaysForEvent(ie.Event.ID)...)) + lastNotes = append(lastNotes, ee) + if store { - db.SaveEvent(ctx, ie.Event) + sys.Store.SaveEvent(ctx, ie.Event) attachRelaysToEvent(ie.Event.ID, ie.Relay.URL) scheduleEventExpiration(ie.Event.ID, time.Hour*24) } @@ -239,10 +282,11 @@ func authorLastNotes(ctx context.Context, pubkey string, isSitemap bool) []*nost break out } } + span.End() } // sort before returning - slices.SortFunc(lastNotes, func(a, b *nostr.Event) int { return int(b.CreatedAt - a.CreatedAt) }) + slices.SortFunc(lastNotes, func(a, b EnhancedEvent) int { return int(b.CreatedAt - a.CreatedAt) }) return lastNotes } @@ -321,3 +365,17 @@ func contactsForPubkey(ctx context.Context, pubkey string) []string { } return unique(pubkeyContacts) } + +func relaysPretty(ctx context.Context, pubkey string) []string { + s := make([]string, 0, 3) + ctx, span := tracer.Start(ctx, "author-relays-pretty") + defer span.End() + for _, url := range sys.FetchOutboxRelays(ctx, pubkey, 3) { + trimmed := trimProtocolAndEndingSlash(url) + if slices.Contains(s, trimmed) { + continue + } + s = append(s, trimmed) + } + return s +} diff --git a/oembed.go b/oembed.go index 4a28c60..df3d718 100644 --- a/oembed.go +++ b/oembed.go @@ -7,6 +7,9 @@ import ( "net/http" "net/url" "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) type OEmbedResponse struct { @@ -37,6 +40,8 @@ type OEmbedResponse struct { } func renderOEmbed(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + targetURL, err := url.Parse(r.URL.Query().Get("url")) if err != nil { http.Error(w, "invalid url: "+err.Error(), 400) @@ -49,9 +54,12 @@ func renderOEmbed(w http.ResponseWriter, r *http.Request) { return } + ctx, span := tracer.Start(ctx, "render-oembed", trace.WithAttributes(attribute.String("code", code))) + defer span.End() + host := r.Header.Get("X-Forwarded-Host") - data, err := grabData(r.Context(), code, false) + data, err := grabData(ctx, code) if err != nil { w.Header().Set("Cache-Control", "max-age=60") http.Error(w, "error fetching event: "+err.Error(), 404) diff --git a/otel.go b/otel.go new file mode 100644 index 0000000..3397fda --- /dev/null +++ b/otel.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + "errors" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/trace" +) + +var tracer = otel.Tracer("njump") + +func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) { + var shutdownFuncs []func(context.Context) error + + // shutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + shutdown = func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) { + err = errors.Join(inErr, shutdown(ctx)) + } + + prop := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + otel.SetTextMapPropagator(prop) + + tracerProvider, err := newTraceProvider() + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + return +} + +func newTraceProvider() (*trace.TracerProvider, error) { + traceExporter, err := otlptracegrpc.New(context.Background(), otlptracegrpc.WithInsecure()) + if err != nil { + return nil, err + } + + traceProvider := trace.NewTracerProvider( + trace.WithBatcher(traceExporter, + // Default is 5s. Set to 1s for demonstrative purposes. + trace.WithBatchTimeout(time.Second)), + ) + return traceProvider, nil +} diff --git a/pages.go b/pages.go index 45104f1..7ee8319 100644 --- a/pages.go +++ b/pages.go @@ -167,6 +167,8 @@ func (e *ErrorPageParams) MessageHTML() template.HTML { strings.Contains(e.Errors, "invalid separator"), strings.Contains(e.Errors, "not part of charset"): return "You have typed a wrong event code, we need a URL path that starts with /npub1, /nprofile1, /nevent1, /naddr1, or something like /name@domain.com (or maybe just /domain.com) or an event id as hex (like /aef8b32af...)" + case strings.Contains(e.Errors, "profile metadata not found"): + return "We couldn't find the metadata (name, picture etc) for the specified user. Please check back here in 6 hours." default: return "I can't give any suggestions to solve the problem.
Please tag daniele and fiatjaf and complain!" } diff --git a/render_embedded.go b/render_embedded.go index 2cb0e14..9f60293 100644 --- a/render_embedded.go +++ b/render_embedded.go @@ -6,6 +6,8 @@ import ( "net/http" "github.com/a-h/templ" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) func renderEmbedjs(w http.ResponseWriter, r *http.Request) { @@ -15,11 +17,16 @@ func renderEmbedjs(w http.ResponseWriter, r *http.Request) { } func renderEmbedded(w http.ResponseWriter, r *http.Request, code string) { - data, err := grabData(r.Context(), code, false) + ctx := r.Context() + + ctx, span := tracer.Start(ctx, "render-embedded", trace.WithAttributes(attribute.String("code", code))) + defer span.End() + + data, err := grabData(ctx, code) if err != nil { w.Header().Set("Cache-Control", "max-age=60") w.WriteHeader(http.StatusNotFound) - errorTemplate(ErrorPageParams{Errors: err.Error()}).Render(r.Context(), w) + errorTemplate(ErrorPageParams{Errors: err.Error()}).Render(ctx, w) return } @@ -36,7 +43,7 @@ func renderEmbedded(w http.ResponseWriter, r *http.Request, code string) { // first we run basicFormatting, which turns URLs into their appropriate HTML tags data.content = basicFormatting(html.EscapeString(data.content), true, false, false) // then we render quotes as HTML, which will also apply basicFormatting to all the internal quotes - data.content = renderQuotesAsHTML(r.Context(), data.content, data.templateId == TelegramInstantView) + data.content = renderQuotesAsHTML(ctx, data.content, data.templateId == TelegramInstantView) // we must do this because inside we must treat s differently when telegram_instant_view } @@ -56,7 +63,7 @@ func renderEmbedded(w http.ResponseWriter, r *http.Request, code string) { Metadata: data.event.author, NormalizedAuthorWebsiteURL: normalizeWebsiteURL(data.event.author.Website), RenderedAuthorAboutText: template.HTML(basicFormatting(html.EscapeString(data.event.author.About), false, false, true)), - AuthorRelays: data.authorRelaysPretty(r.Context()), + AuthorRelays: relaysPretty(ctx, data.event.author.PubKey), }) default: log.Error().Int("templateId", int(data.templateId)).Msg("no way to render") @@ -64,7 +71,7 @@ func renderEmbedded(w http.ResponseWriter, r *http.Request, code string) { return } - if err := component.Render(r.Context(), w); err != nil { + if err := component.Render(ctx, w); err != nil { log.Warn().Err(err).Msg("error rendering tmpl") } return diff --git a/render_event.go b/render_event.go index 628f8f3..5db1f80 100644 --- a/render_event.go +++ b/render_event.go @@ -17,6 +17,8 @@ import ( "github.com/nbd-wtf/go-nostr/nip05" "github.com/nbd-wtf/go-nostr/nip19" "github.com/pelletier/go-toml" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) func isValidShortcode(s string) bool { @@ -29,6 +31,7 @@ func isValidShortcode(s string) bool { } func renderEvent(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() code := r.URL.Path[1:] // hopefully a nip19 code // it's the homepage @@ -60,14 +63,14 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { // it may be a NIP-05 if nip05.IsValidIdentifier(code) { - renderProfile(w, r, code) + renderProfile(ctx, w, code) return } // otherwise error w.Header().Set("Cache-Control", "max-age=60") w.WriteHeader(http.StatusNotFound) - errorTemplate(ErrorPageParams{Errors: err.Error()}).Render(r.Context(), w) + errorTemplate(ErrorPageParams{Errors: err.Error()}).Render(ctx, w) return } @@ -81,22 +84,19 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { // render npub and nprofile using a separate function if prefix == "npub" || prefix == "nprofile" { // it's a profile - renderProfile(w, r, code) + renderProfile(ctx, w, code) return } + ctx, span := tracer.Start(ctx, "render-event", trace.WithAttributes(attribute.String("code", code))) + defer span.End() + // get data for this event - data, err := grabData(r.Context(), code, false) + data, err := grabData(ctx, code) if err != nil { w.Header().Set("Cache-Control", "max-age=60") w.WriteHeader(http.StatusNotFound) - errorTemplate(ErrorPageParams{Errors: err.Error()}).Render(r.Context(), w) - return - } - - // if the result is a kind:0 render this as a profile - if data.event.Kind == 0 { - renderProfile(w, r, data.event.author.Npub()) + errorTemplate(ErrorPageParams{Errors: err.Error()}).Render(ctx, w) return } @@ -110,6 +110,9 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { // from here onwards we know we're rendering an event // + ctx, span = tracer.Start(ctx, "actually-render") + defer span.End() + // if it's porn we return a 404 hasURL := urlRegex.MatchString(data.event.Content) if hasURL && hasProhibitedWordOrTag(data.event.Event) { @@ -209,7 +212,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { } } else { // otherwise replace npub/nprofiles with names and trim length - description = replaceUserReferencesWithNames(r.Context(), []string{data.event.Content}, "")[0] + description = replaceUserReferencesWithNames(ctx, []string{data.event.Content}, "")[0] if len(description) > 240 { description = description[:240] } @@ -221,7 +224,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { strings.TrimSpace( strings.Replace( strings.Replace( - replaceUserReferencesWithNames(r.Context(), []string{data.event.Content}, "")[0], + replaceUserReferencesWithNames(ctx, []string{data.event.Content}, "")[0], "\r\n", " ", -1), "\n", " ", -1, ), @@ -272,7 +275,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { // first we run basicFormatting, which turns URLs into their appropriate HTML tags data.content = basicFormatting(html.EscapeString(data.content), true, false, false) // then we render quotes as HTML, which will also apply basicFormatting to all the internal quotes - data.content = renderQuotesAsHTML(r.Context(), data.content, data.templateId == TelegramInstantView) + data.content = renderQuotesAsHTML(ctx, data.content, data.templateId == TelegramInstantView) // we must do this because inside we must treat s differently when telegram_instant_view } @@ -305,7 +308,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { CreatedAt: data.createdAt, KindDescription: data.kindDescription, KindNIP: data.kindNIP, - EventJSON: data.event.ToJSONHTML(), + EventJSON: toJSONHTML(data.event.Event), Kind: data.event.Kind, SeenOn: data.event.relays, Metadata: data.event.author, @@ -463,28 +466,26 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { } } - var StartAtDate, StartAtTime string - var EndAtDate, EndAtTime string - var TimeZone string + var startAtDate, startAtTime string + var endAtDate, endAtTime string location, err := time.LoadLocation(data.kind31922Or31923Metadata.StartTzid) if err != nil { // Set default TimeZone to UTC location = time.UTC } - TimeZone = getUTCOffset(location) - StartAtDate = data.kind31922Or31923Metadata.Start.In(location).Format("02 Jan 2006") - EndAtDate = data.kind31922Or31923Metadata.End.In(location).Format("02 Jan 2006") + startAtDate = data.kind31922Or31923Metadata.Start.In(location).Format("02 Jan 2006") + endAtDate = data.kind31922Or31923Metadata.End.In(location).Format("02 Jan 2006") if data.kind31922Or31923Metadata.CalendarEventKind == 31923 { - StartAtTime = data.kind31922Or31923Metadata.Start.In(location).Format("15:04") - EndAtTime = data.kind31922Or31923Metadata.End.In(location).Format("15:04") + startAtTime = data.kind31922Or31923Metadata.Start.In(location).Format("15:04") + endAtTime = data.kind31922Or31923Metadata.End.In(location).Format("15:04") } // Reset EndDate/Time if it is non initialized (beginning of the Unix epoch) if data.kind31922Or31923Metadata.End == (time.Time{}) { - EndAtDate = "" - EndAtTime = "" + endAtDate = "" + endAtTime = "" } component = calendarEventTemplate(CalendarPageParams{ @@ -495,11 +496,11 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { NaddrNaked: data.naddrNaked, NeventNaked: data.neventNaked, }, - TimeZone: TimeZone, - StartAtDate: StartAtDate, - StartAtTime: StartAtTime, - EndAtDate: EndAtDate, - EndAtTime: EndAtTime, + TimeZone: getUTCOffset(location), + StartAtDate: startAtDate, + StartAtTime: startAtTime, + EndAtDate: endAtDate, + EndAtTime: endAtTime, CalendarEvent: *data.kind31922Or31923Metadata, Details: detailsData, Content: template.HTML(data.content), @@ -507,9 +508,6 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { }) case WikiEvent: - PublishedAt := data.Kind30818Metadata.PublishedAt.Format("02 Jan 2006") - npub, _ := nip19.EncodePublicKey(data.event.PubKey) - component = wikiEventTemplate(WikiPageParams{ BaseEventPageParams: baseEventPageParams, OpenGraphParams: opengraph, @@ -518,7 +516,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { NaddrNaked: data.naddrNaked, NeventNaked: data.neventNaked, }, - PublishedAt: PublishedAt, + PublishedAt: data.Kind30818Metadata.PublishedAt.Format("02 Jan 2006"), WikiEvent: data.Kind30818Metadata, Details: detailsData, Content: data.content, @@ -532,7 +530,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { return strings.Replace(url, "{authorPubkey}", data.event.PubKey, -1) }, func(client ClientReference, url string) string { - return strings.Replace(url, "{npub}", npub, -1) + return strings.Replace(url, "{npub}", data.event.author.Npub(), -1) }, ), }) @@ -558,7 +556,7 @@ func renderEvent(w http.ResponseWriter, r *http.Request) { return } - if err := component.Render(r.Context(), w); err != nil { + if err := component.Render(ctx, w); err != nil { log.Warn().Err(err).Msg("error rendering tmpl") } return diff --git a/render_image.go b/render_image.go index b740fcd..3ca8d51 100644 --- a/render_image.go +++ b/render_image.go @@ -18,6 +18,8 @@ import ( "github.com/golang/freetype/truetype" sdk "github.com/nbd-wtf/nostr-sdk" "github.com/nfnt/resize" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" xfont "golang.org/x/image/font" ) @@ -37,19 +39,24 @@ var fonts embed.FS var multiNewlineRe = regexp.MustCompile(`\n\n+`) func renderImage(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + code := r.URL.Path[1+len("njump/image/"):] if code == "" { fmt.Fprintf(w, "call /njump/image/") return } + ctx, span := tracer.Start(ctx, "render-image", trace.WithAttributes(attribute.String("code", code))) + defer span.End() + // Trim fake extensions extensions := []string{".png", ".jpg", ".jpeg"} for _, ext := range extensions { code = strings.TrimSuffix(code, ext) } - data, err := grabData(r.Context(), code, false) + data, err := grabData(ctx, code) if err != nil { http.Error(w, "error fetching event: "+err.Error(), 404) return @@ -63,8 +70,8 @@ func renderImage(w http.ResponseWriter, r *http.Request) { content = shortenURLs(content, true) // this turns the raw event.Content into a series of lines ready to drawn - paragraphs := replaceUserReferencesWithNames(r.Context(), - quotesAsBlockPrefixedText(r.Context(), + paragraphs := replaceUserReferencesWithNames(ctx, + quotesAsBlockPrefixedText(ctx, strings.Split(content, "\n"), ), string(INVISIBLE_SPACE), diff --git a/render_profile.go b/render_profile.go index 99348a5..9bb12b6 100644 --- a/render_profile.go +++ b/render_profile.go @@ -1,13 +1,20 @@ package main import ( + "context" "html" "html/template" "net/http" "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) -func renderProfile(w http.ResponseWriter, r *http.Request, code string) { +func renderProfile(ctx context.Context, w http.ResponseWriter, code string) { + ctx, span := tracer.Start(ctx, "render-profile", trace.WithAttributes(attribute.String("code", code))) + defer span.End() + isSitemap := false if strings.HasSuffix(code, ".xml") { code = code[:len(code)-4] @@ -20,22 +27,32 @@ func renderProfile(w http.ResponseWriter, r *http.Request, code string) { isRSS = true } - data, err := grabData(r.Context(), code, isSitemap) - if err != nil { + profile, err := sys.FetchProfileFromInput(ctx, code) + if err != nil || profile.Event == nil { w.Header().Set("Cache-Control", "max-age=60") w.WriteHeader(http.StatusNotFound) - errorTemplate(ErrorPageParams{Errors: err.Error()}).Render(r.Context(), w) + + errMsg := "profile metadata not found" + if err != nil { + errMsg = err.Error() + } + errorTemplate(ErrorPageParams{Errors: errMsg}).Render(ctx, w) return } + createdAt := profile.Event.CreatedAt.Time().Format("2006-01-02T15:04:05Z07:00") + modifiedAt := profile.Event.CreatedAt.Time().Format("2006-01-02T15:04:05Z07:00") + + lastNotes := authorLastNotes(ctx, profile.PubKey, isSitemap) + if isSitemap { w.Header().Add("content-type", "text/xml") w.Header().Set("Cache-Control", "max-age=86400") w.Write([]byte(XML_HEADER)) err = SitemapTemplate.Render(w, &SitemapPage{ Host: s.Domain, - ModifiedAt: data.modifiedAt, - LastNotes: data.renderableLastNotes, + ModifiedAt: modifiedAt, + LastNotes: lastNotes, }) } else if isRSS { w.Header().Add("content-type", "text/xml") @@ -43,32 +60,34 @@ func renderProfile(w http.ResponseWriter, r *http.Request, code string) { w.Write([]byte(XML_HEADER)) err = RSSTemplate.Render(w, &RSSPage{ Host: s.Domain, - ModifiedAt: data.modifiedAt, - Metadata: data.event.author, - LastNotes: data.renderableLastNotes, + ModifiedAt: modifiedAt, + Metadata: profile, + LastNotes: lastNotes, }) } else { w.Header().Add("content-type", "text/html") w.Header().Set("Cache-Control", "max-age=86400") - nprofile := data.event.author.Nprofile(r.Context(), sys, 2) + + nprofile := profile.Nprofile(ctx, sys, 2) + err = profileTemplate(ProfilePageParams{ HeadParams: HeadParams{IsProfile: true}, Details: DetailsParams{ HideDetails: true, - CreatedAt: data.createdAt, - KindDescription: data.kindDescription, - KindNIP: data.kindNIP, - EventJSON: data.event.ToJSONHTML(), - Kind: data.event.Kind, - Metadata: data.event.author, + CreatedAt: createdAt, + KindDescription: kindNames[0], + KindNIP: kindNIPs[0], + EventJSON: toJSONHTML(profile.Event), + Kind: 0, + Metadata: profile, }, - Metadata: data.event.author, - NormalizedAuthorWebsiteURL: normalizeWebsiteURL(data.event.author.Website), - RenderedAuthorAboutText: template.HTML(basicFormatting(html.EscapeString(data.event.author.About), false, false, false)), + Metadata: profile, + NormalizedAuthorWebsiteURL: normalizeWebsiteURL(profile.Website), + RenderedAuthorAboutText: template.HTML(basicFormatting(html.EscapeString(profile.About), false, false, false)), Nprofile: nprofile, - AuthorRelays: data.authorRelaysPretty(r.Context()), - LastNotes: data.renderableLastNotes, - Clients: generateClientList(data.event.Kind, nprofile, + AuthorRelays: relaysPretty(ctx, profile.PubKey), + LastNotes: lastNotes, + Clients: generateClientList(0, nprofile, func(c ClientReference, s string) string { if c == nostrudel { s = strings.Replace(s, "/n/", "/u/", 1) @@ -76,12 +95,12 @@ func renderProfile(w http.ResponseWriter, r *http.Request, code string) { if c == primalWeb { s = strings.Replace( strings.Replace(s, "/e/", "/p/", 1), - nprofile, data.event.author.Npub(), 1) + nprofile, profile.Npub(), 1) } return s }, ), - }).Render(r.Context(), w) + }).Render(ctx, w) } if err != nil { diff --git a/render_relay.go b/render_relay.go index 3730cdb..2811607 100644 --- a/render_relay.go +++ b/render_relay.go @@ -48,7 +48,9 @@ func renderRelayPage(w http.ResponseWriter, r *http.Request) { lastEventAt = time.Unix(int64(lastNotes[0].CreatedAt), 0) } for i, levt := range lastNotes { - renderableLastNotes[i] = NewEnhancedEvent(nil, levt, []string{"wss://" + hostname}) + ee := NewEnhancedEvent(nil, levt) + ee.relays = []string{"wss://" + hostname} + renderableLastNotes[i] = ee } if len(renderableLastNotes) != 0 { diff --git a/routines.go b/routines.go index f0655d8..569b691 100644 --- a/routines.go +++ b/routines.go @@ -5,7 +5,6 @@ import ( "strings" "time" - "github.com/fiatjaf/eventstore" "github.com/nbd-wtf/go-nostr" ) @@ -27,8 +26,6 @@ func updateArchives(ctx context.Context) { } func deleteOldCachedEvents(ctx context.Context) { - wdb := eventstore.RelayWrapper{Store: db} - for { select { case <-ctx.Done(): @@ -52,10 +49,10 @@ func deleteOldCachedEvents(ctx context.Context) { if expires < now { // time to delete this id := spl[1] - res, _ := wdb.QuerySync(ctx, nostr.Filter{IDs: []string{id}}) + res, _ := sys.StoreRelay.QuerySync(ctx, nostr.Filter{IDs: []string{id}}) if len(res) > 0 { log.Debug().Msgf("deleting %s", res[0].ID) - if err := db.DeleteEvent(ctx, res[0]); err != nil { + if err := sys.Store.DeleteEvent(ctx, res[0]); err != nil { log.Warn().Err(err).Stringer("event", res[0]).Msg("failed to delete") } } diff --git a/utils.go b/utils.go index cb65a52..a1f993e 100644 --- a/utils.go +++ b/utils.go @@ -2,15 +2,21 @@ package main import ( "context" + "encoding/json" "fmt" + "html" + "html/template" "math/rand" "net/http" "regexp" "slices" "strings" + "sync" "time" "github.com/microcosm-cc/bluemonday" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "mvdan.cc/xurls/v2" "github.com/nbd-wtf/go-nostr" @@ -234,14 +240,33 @@ func replaceURLsWithTags(input string, imageReplacementTemplate, videoReplacemen func replaceNostrURLsWithHTMLTags(matcher *regexp.Regexp, input string) string { // match and replace npup1, nprofile1, note1, nevent1, etc - return matcher.ReplaceAllStringFunc(input, func(match string) string { + names := make(map[string]string) + wg := sync.WaitGroup{} + + // first we run it without waiting for the results of getNameFromNip19() as they will be async + firstPass := matcher.ReplaceAllStringFunc(input, func(match string) string { + nip19 := match[len("nostr:"):] + if strings.HasPrefix(nip19, "npub1") || strings.HasPrefix(nip19, "nprofile1") { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*4) + defer cancel() + wg.Add(1) + go func() { + name, _ := getNameFromNip19(ctx, nip19) + names[nip19] = name + wg.Done() + }() + } + return match + }) + + // in the second time now that we got all the names we actually perform replacement + wg.Wait() + return matcher.ReplaceAllStringFunc(firstPass, func(match string) string { nip19 := match[len("nostr:"):] firstChars := nip19[:8] lastChars := nip19[len(nip19)-4:] if strings.HasPrefix(nip19, "npub1") || strings.HasPrefix(nip19, "nprofile1") { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*4) - defer cancel() - name, _ := getNameFromNip19(ctx, nip19) + name, _ := names[nip19] return fmt.Sprintf(``, nip19, name, firstChars+"…"+lastChars) } else { return fmt.Sprintf(``, nip19, firstChars+"…"+lastChars) @@ -263,23 +288,19 @@ func shortenNostrURLs(input string) string { }) } -func getNameFromNip19(ctx context.Context, nip19 string) (string, bool) { - author, _, err := getEvent(ctx, nip19) - if err != nil { - return nip19, false - } - metadata, err := sdk.ParseMetadata(author) - if err != nil { - return nip19, false - } +func getNameFromNip19(ctx context.Context, nip19code string) (string, bool) { + ctx, span := tracer.Start(ctx, "get-name-from-nip19", trace.WithAttributes(attribute.String("nip19", nip19code))) + defer span.End() + + metadata, _ := sys.FetchProfileFromInput(ctx, nip19code) if metadata.Name == "" { - return nip19, false + return nip19code, false } return metadata.Name, true } -// replaces an npub/nprofile with the name of the author, if possible -// meant to be used when plaintext is expected, not formatted HTML +// replaces an npub/nprofile with the name of the author, if possible. +// meant to be used when plaintext is expected, not formatted HTML. func replaceUserReferencesWithNames(ctx context.Context, input []string, prefix string) []string { // Match and replace npup1 or nprofile1 ctx, cancel := context.WithTimeout(ctx, time.Second*3) @@ -471,3 +492,46 @@ func getUTCOffset(loc *time.Location) string { } return fmt.Sprintf("UTC%s%d", sign, offsetHours) } + +func toJSONHTML(evt *nostr.Event) template.HTML { + tagsHTML := "[" + for t, tag := range evt.Tags { + tagsHTML += "\n [" + for i, item := range tag { + cls := `"text-zinc-500 dark:text-zinc-50"` + if i == 0 { + cls = `"text-amber-500 dark:text-amber-200"` + } + itemJSON, _ := json.Marshal(item) + tagsHTML += "\n " + html.EscapeString(string(itemJSON)) + if i < len(tag)-1 { + tagsHTML += "," + } else { + tagsHTML += "\n " + } + } + tagsHTML += "]" + if t < len(evt.Tags)-1 { + tagsHTML += "," + } else { + tagsHTML += "\n " + } + } + tagsHTML += "]" + + contentJSON, _ := json.Marshal(evt.Content) + + keyCls := "text-purple-700 dark:text-purple-300" + + return template.HTML(fmt.Sprintf( + `{ + "id": "%s", + "pubkey": "%s", + "created_at": %d, + "kind": %d, + "tags": %s, + "content": %s, + "sig": "%s" +}`, evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, tagsHTML, html.EscapeString(string(contentJSON)), evt.Sig), + ) +}