Files
njump/nostr.go
2023-10-03 15:39:45 -03:00

304 lines
7.1 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"net/url"
"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"
"golang.org/x/exp/slices"
)
var (
pool = nostr.NewSimplePool(context.Background())
serial int
always = []string{
"wss://relay.nostr.band",
}
everything = []string{
"wss://nostr-pub.wellorder.net",
"wss://relay.damus.io",
"wss://relay.nostr.bg",
"wss://nostr.wine",
"wss://nos.lol",
"wss://nostr.mom",
"wss://atlas.nostr.land",
"wss://relay.snort.social",
"wss://offchain.pub",
"wss://relay.primal.net",
"wss://public.relaying.io",
}
profiles = []string{
"wss://purplepag.es",
}
trustedPubKeys = []string{
"7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805", // dtonon
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf
"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", // hodlbod
"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49", // Michael Dilger
}
excludedRelays = []string{
"wss://filter.nostr.wine", // paid
}
)
type CachedEvent struct {
Event *nostr.Event `json:"e"`
Relays []string `json:"r"`
}
func getRelay() string {
if serial == 0 {
serial = rand.Intn(len(everything))
}
serial = (serial + 1) % len(everything)
return everything[serial]
}
func getEvent(ctx context.Context, code string) (*nostr.Event, []string, error) {
if b, ok := cache.Get(code); ok {
// at this point `b` may be a StoredEvent json or an old naked Event json
// TODO: after a week we can assume everything will be StoredEvent, so we can simplify this
v := CachedEvent{}
err := json.Unmarshal(b, &v)
if v.Event == nil {
v.Event = &nostr.Event{}
err = json.Unmarshal(b, v.Event)
}
return v.Event, v.Relays, err
}
withRelays := true
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
}
var author string
var filter nostr.Filter
relays := make([]string, 0, 25)
relays = append(relays, always...)
switch v := data.(type) {
case nostr.ProfilePointer:
author = v.PublicKey
filter.Authors = []string{v.PublicKey}
filter.Kinds = []int{0}
relays = append(relays, profiles...)
relays = append(relays, v.Relays...)
withRelays = false
case nostr.EventPointer:
author = v.Author
filter.IDs = []string{v.ID}
relays = append(relays, getRelay())
relays = append(relays, getRelay())
relays = append(relays, v.Relays...)
case nostr.EntityPointer:
author = v.PublicKey
filter.Authors = []string{v.PublicKey}
filter.Tags = nostr.TagMap{
"d": []string{v.Identifier},
}
if v.Kind != 0 {
filter.Kinds = append(filter.Kinds, v.Kind)
}
relays = append(relays, getRelay())
relays = append(relays, getRelay())
relays = append(relays, v.Relays...)
case string:
if prefix == "note" {
filter.IDs = []string{v}
relays = append(relays, getRelay())
relays = append(relays, getRelay())
relays = append(relays, getRelay())
} else if prefix == "npub" {
author = v
filter.Authors = []string{v}
filter.Kinds = []int{0}
relays = append(relays, profiles...)
withRelays = false
}
}
if author != "" {
// fetch relays for author
authorRelays := relaysForPubkey(ctx, author, relays...)
if len(authorRelays) > 5 {
authorRelays = authorRelays[:5]
}
relays = append(relays, authorRelays...)
}
for len(relays) < 5 {
relays = append(relays, getRelay())
}
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
if withRelays {
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
}
}()
for ie := range pool.SubManyEoseNonUnique(ctx, relays, nostr.Filters{filter}) {
if pu, err := url.Parse(ie.Relay.URL); err == nil {
successRelays = append(successRelays, pu.Host+pu.RawPath)
}
result = ie.Event
countdown = min(countdown, 1)
}
} else {
ie := pool.QuerySingle(ctx, relays, filter)
if ie != nil {
result = ie.Event
}
}
if result == nil {
return nil, nil, fmt.Errorf("couldn't find this %s", prefix)
}
cache.SetJSONWithTTL(code, CachedEvent{Event: result, Relays: successRelays}, time.Hour*24*7)
return result, successRelays, nil
}
func getLastNotes(ctx context.Context, code string, limit int) []*nostr.Event {
if limit <= 0 {
limit = 10
}
pp := sdk.InputToProfile(ctx, code)
if pp == nil {
return nil
}
pubkeyRelays := relaysForPubkey(ctx, pp.PublicKey, pp.Relays...)
relays := append(pp.Relays, pubkeyRelays...)
ctx, cancel := context.WithTimeout(ctx, time.Second*4)
defer cancel()
relays = append(relays, getRelay())
relays = append(relays, getRelay())
relays = unique(relays)
ch := pool.SubManyEose(ctx, relays, nostr.Filters{
{
Kinds: []int{nostr.KindTextNote},
Authors: []string{pp.PublicKey},
Limit: limit,
},
})
lastNotes := make([]*nostr.Event, 0, 20)
for {
select {
case ie, more := <-ch:
if !more {
goto end
}
lastNotes = append(lastNotes, ie.Event)
case <-ctx.Done():
goto end
}
}
end:
slices.SortFunc(lastNotes, func(a, b *nostr.Event) bool { return a.CreatedAt > b.CreatedAt })
return lastNotes
}
func relaysForPubkey(ctx context.Context, pubkey string, extraRelays ...string) []string {
pubkeyRelays := make([]string, 0, 12)
if ok := cache.GetJSON("io:"+pubkey, &pubkeyRelays); !ok {
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*1500)
for _, relay := range sdk.FetchRelaysForPubkey(ctx, pool, pubkey, extraRelays...) {
if relay.Outbox {
pubkeyRelays = append(pubkeyRelays, relay.URL)
}
}
cancel()
if len(pubkeyRelays) > 0 {
cache.SetJSONWithTTL("io:"+pubkey, pubkeyRelays, time.Hour*24*7)
}
}
pubkeyRelays = unique(pubkeyRelays)
return pubkeyRelays
}
func contactsForPubkey(ctx context.Context, pubkey string, extraRelays ...string) []string {
pubkeyContacts := make([]string, 0, 100)
relays := make([]string, 0, 12)
if ok := cache.GetJSON("cc:"+pubkey, &pubkeyContacts); !ok {
log.Debug().Msgf("searching contacts for %s", pubkey)
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
pubkeyRelays := relaysForPubkey(ctx, pubkey, relays...)
relays = append(relays, pubkeyRelays...)
relays = append(relays, always...)
relays = append(relays, profiles...)
ch := pool.SubManyEose(ctx, relays, nostr.Filters{
{
Kinds: []int{3},
Authors: []string{pubkey},
Limit: 2,
},
})
for {
select {
case evt, more := <-ch:
if !more {
goto end
}
for _, tag := range evt.Tags {
if tag[0] == "p" {
pubkeyContacts = append(pubkeyContacts, tag[1])
}
}
case <-ctx.Done():
goto end
}
}
end:
cancel()
if len(pubkeyContacts) > 0 {
cache.SetJSONWithTTL("cc:"+pubkey, pubkeyContacts, time.Hour*6)
}
}
pubkeyContacts = unique(pubkeyContacts)
return pubkeyContacts
}