fix slowness of previous refactors that injected nostr-sdk into this codebase + refactors.

- adjusting many small things related to nostr-sdk usage
- fetching profiles in a smarter way
- decoupling the logic for rendering profile pages from the `grabData`/`getEvent` flow of other event pages.
- incorporating nostr-sdk more holistically, including more hints stuff
- improving nostr-sdk itself after some bugs and weird behaviors observed here
- set up opentelemetry (should probably remove this later)
This commit is contained in:
fiatjaf
2024-08-01 15:24:22 -03:00
parent 289d097078
commit c0004f67a2
19 changed files with 496 additions and 289 deletions

View File

@@ -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 {

View File

@@ -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 {
<div class="pb-4">
<span class="font-medium">People</span>:
@templ.Raw(formatPartecipants(params.CalendarEvent.Participants))
@templ.Raw(formatParticipants(params.CalendarEvent.Participants))
</div>
}
if params.CalendarEvent.Image != "" {

33
data.go
View File

@@ -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

View File

@@ -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 <span class=" + cls + ">" + 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(
`{
<span class="`+keyCls+`">"id":</span> <span class="text-zinc-500 dark:text-zinc-50">"%s"</span>,
<span class="`+keyCls+`">"pubkey":</span> <span class="text-zinc-500 dark:text-zinc-50">"%s"</span>,
<span class="`+keyCls+`">"created_at":</span> <span class="text-green-600">%d</span>,
<span class="`+keyCls+`">"kind":</span> <span class="text-amber-500 dark:text-amber-200">%d</span>,
<span class="`+keyCls+`">"tags":</span> %s,
<span class="`+keyCls+`">"content":</span> <span class="text-zinc-500 dark:text-zinc-50">%s</span>,
<span class="`+keyCls+`">"sig":</span> <span class="text-zinc-500 dark:text-zinc-50 content">"%s"</span>
}`, ee.ID, ee.PubKey, ee.CreatedAt, ee.Kind, tagsHTML, html.EscapeString(string(contentJSON)), ee.Sig),
)
}

31
go.mod
View File

@@ -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
)

69
go.sum
View File

@@ -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=

View File

@@ -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

44
main.go
View File

@@ -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()
}
}

182
nostr.go
View File

@@ -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
}

View File

@@ -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)

65
otel.go Normal file
View File

@@ -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
}

View File

@@ -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.<br> Please tag <a href='/dtonon.com'>daniele</a> and <a href='/fiatjaf.com'>fiatjaf</a> and complain!"
}

View File

@@ -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 <blockquotes> we must treat <img>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

View File

@@ -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 <blockquotes> we must treat <img>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

View File

@@ -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/<nip19 code>")
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),

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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(`<span itemprop="mentions" itemscope itemtype="https://schema.org/Person"><a itemprop="url" href="/%s" class="bg-lavender dark:prose:text-neutral-50 dark:text-neutral-50 dark:bg-garnet px-1"><span>%s</span> (<span class="italic">%s</span>)</a></span>`, nip19, name, firstChars+"…"+lastChars)
} else {
return fmt.Sprintf(`<span itemprop="mentions" itemscope itemtype="https://schema.org/Article"><a itemprop="url" href="/%s" class="bg-lavender dark:prose:text-neutral-50 dark:text-neutral-50 dark:bg-garnet px-1">%s</a></span>`, 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 <span class=" + cls + ">" + 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(
`{
<span class="`+keyCls+`">"id":</span> <span class="text-zinc-500 dark:text-zinc-50">"%s"</span>,
<span class="`+keyCls+`">"pubkey":</span> <span class="text-zinc-500 dark:text-zinc-50">"%s"</span>,
<span class="`+keyCls+`">"created_at":</span> <span class="text-green-600">%d</span>,
<span class="`+keyCls+`">"kind":</span> <span class="text-amber-500 dark:text-amber-200">%d</span>,
<span class="`+keyCls+`">"tags":</span> %s,
<span class="`+keyCls+`">"content":</span> <span class="text-zinc-500 dark:text-zinc-50">%s</span>,
<span class="`+keyCls+`">"sig":</span> <span class="text-zinc-500 dark:text-zinc-50 content">"%s"</span>
}`, evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, tagsHTML, html.EscapeString(string(contentJSON)), evt.Sig),
)
}