go-nostr/sdk/metadata.go

178 lines
5.0 KiB
Go
Raw Permalink Normal View History

package sdk
import (
"context"
"fmt"
"time"
"github.com/nbd-wtf/go-nostr"
2024-11-27 10:23:28 -03:00
"github.com/nbd-wtf/go-nostr/nip05"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/sdk/hints"
)
type ProfileMetadata struct {
PubKey string `json:"-"` // must always be set otherwise things will break
Event *nostr.Event `json:"-"` // may be empty if a profile metadata event wasn't found
// every one of these may be empty
Name string `json:"name,omitempty"`
DisplayName string `json:"display_name,omitempty"`
About string `json:"about,omitempty"`
Website string `json:"website,omitempty"`
Picture string `json:"picture,omitempty"`
Banner string `json:"banner,omitempty"`
NIP05 string `json:"nip05,omitempty"`
LUD16 string `json:"lud16,omitempty"`
2024-11-27 10:23:28 -03:00
nip05Valid bool
nip05LastAttempt time.Time
}
func (p ProfileMetadata) Npub() string {
v, _ := nip19.EncodePublicKey(p.PubKey)
return v
}
func (p ProfileMetadata) NpubShort() string {
npub := p.Npub()
return npub[0:7] + "…" + npub[58:]
}
func (p ProfileMetadata) Nprofile(ctx context.Context, sys *System, nrelays int) string {
v, _ := nip19.EncodeProfile(p.PubKey, sys.FetchOutboxRelays(ctx, p.PubKey, 2))
return v
}
func (p ProfileMetadata) ShortName() string {
if p.Name != "" {
return p.Name
}
if p.DisplayName != "" {
return p.DisplayName
}
return p.NpubShort()
}
2024-11-27 10:23:28 -03:00
func (p *ProfileMetadata) NIP05Valid(ctx context.Context) bool {
if p.NIP05 == "" {
return false
}
now := time.Now()
if p.nip05LastAttempt.Before(now.AddDate(0, 0, -7)) {
// must revalidate
p.nip05LastAttempt = now
pp, err := nip05.QueryIdentifier(ctx, p.NIP05)
if err != nil {
p.nip05Valid = false
} else {
p.nip05Valid = pp.PublicKey == p.PubKey
}
}
return p.nip05Valid
}
// FetchProfileFromInput takes an nprofile, npub, nip05 or hex pubkey and returns a ProfileMetadata,
// updating the hintsDB in the process with any eventual relay hints
func (sys System) FetchProfileFromInput(ctx context.Context, nip19OrNip05Code string) (ProfileMetadata, error) {
p := InputToProfile(ctx, nip19OrNip05Code)
if p == nil {
return ProfileMetadata{}, fmt.Errorf("couldn't decode profile reference")
}
for _, r := range p.Relays {
if !IsVirtualRelay(r) {
sys.Hints.Save(p.PublicKey, nostr.NormalizeURL(r), hints.LastInHint, nostr.Now())
}
}
pm := sys.FetchProfileMetadata(ctx, p.PublicKey)
return pm, nil
}
// FetchProfileMetadata fetches metadata for a given user from the local cache, or from the local store,
// or, failing these, from the target user's defined outbox relays -- then caches the result.
func (sys *System) FetchProfileMetadata(ctx context.Context, pubkey string) (pm ProfileMetadata) {
if v, ok := sys.MetadataCache.Get(pubkey); ok {
return v
}
pm.PubKey = pubkey
res, _ := sys.StoreRelay.QuerySync(ctx, nostr.Filter{Kinds: []int{0}, Authors: []string{pubkey}})
if len(res) != 0 {
// ok, we found something locally
pm, _ = ParseMetadata(res[0])
pm.PubKey = pubkey
pm.Event = res[0]
// but if we haven't tried fetching from the network recently we should do it
lastFetchKey := makeLastFetchKey(0, pubkey)
lastFetchData, _ := sys.KVStore.Get(lastFetchKey)
if lastFetchData == nil || nostr.Now()-decodeTimestamp(lastFetchData) > 7*24*60*60 {
newM := sys.tryFetchMetadataFromNetwork(ctx, pubkey)
if newM != nil && newM.Event.CreatedAt > pm.Event.CreatedAt {
pm = *newM
}
// even if we didn't find anything register this because we tried
// (and we still have the previous event in our local store)
sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now()))
}
// and finally save this to cache
sys.MetadataCache.SetWithTTL(pubkey, pm, time.Hour*6)
return pm
}
if newM := sys.tryFetchMetadataFromNetwork(ctx, pubkey); newM != nil {
pm = *newM
// we'll only save this if we got something which means we found at least one event
lastFetchKey := makeLastFetchKey(0, pubkey)
sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now()))
}
// save cache even if we didn't get anything
sys.MetadataCache.SetWithTTL(pubkey, pm, time.Hour*6)
return pm
}
func (sys *System) tryFetchMetadataFromNetwork(ctx context.Context, pubkey string) *ProfileMetadata {
thunk0 := sys.replaceableLoaders[kind_0].Load(ctx, pubkey)
evt, err := thunk0()
if err != nil {
return nil
}
pm, err := ParseMetadata(evt)
if err != nil {
return nil
}
pm.PubKey = pubkey
pm.Event = evt
sys.StoreRelay.Publish(ctx, *evt)
sys.MetadataCache.SetWithTTL(pubkey, pm, time.Hour*6)
return &pm
}
func ParseMetadata(event *nostr.Event) (meta ProfileMetadata, err error) {
if event.Kind != 0 {
err = fmt.Errorf("event %s is kind %d, not 0", event.ID, event.Kind)
} else if er := json.Unmarshal([]byte(event.Content), &meta); er != nil {
cont := event.Content
if len(cont) > 100 {
cont = cont[0:99]
}
err = fmt.Errorf("failed to parse metadata (%s) from event %s: %w", cont, event.ID, er)
}
meta.PubKey = event.PubKey
meta.Event = event
return meta, err
}