mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-18 13:53:03 +01:00
178 lines
5.0 KiB
Go
178 lines
5.0 KiB
Go
package sdk
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/nbd-wtf/go-nostr"
|
|
"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"`
|
|
|
|
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()
|
|
}
|
|
|
|
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
|
|
}
|