move sdk out to its own module.

This commit is contained in:
fiatjaf
2023-10-31 17:48:06 -03:00
parent 5847335506
commit 1789d43d51
12 changed files with 42 additions and 622 deletions

View File

@@ -1,10 +0,0 @@
package cache
import "time"
type Cache32[V any] interface {
Get(k string) (v V, ok bool)
Delete(k string)
Set(k string, v V) bool
SetWithTTL(k string, v V, d time.Duration) bool
}

View File

@@ -1,48 +0,0 @@
package cache_memory
import (
"encoding/binary"
"encoding/hex"
"time"
"github.com/dgraph-io/ristretto"
)
type RistrettoCache[V any] struct {
Cache *ristretto.Cache[string, V]
}
func New32[V any](max int64) *RistrettoCache[V] {
cache, _ := ristretto.NewCache(&ristretto.Config[string, V]{
NumCounters: max * 10,
MaxCost: max,
BufferItems: 64,
KeyToHash: func(key string) (uint64, uint64) { return h32(key), 0 },
})
return &RistrettoCache[V]{Cache: cache}
}
func (s RistrettoCache[V]) Get(k string) (v V, ok bool) { return s.Cache.Get(k) }
func (s RistrettoCache[V]) Delete(k string) { s.Cache.Del(k) }
func (s RistrettoCache[V]) Set(k string, v V) bool { return s.Cache.Set(k, v, 1) }
func (s RistrettoCache[V]) SetWithTTL(k string, v V, d time.Duration) bool {
return s.Cache.SetWithTTL(k, v, 1, d)
}
func h32(key string) uint64 {
// we get an event id or pubkey as hex,
// so just extract the last 8 bytes from it and turn them into a uint64
return shortUint64(key)
}
func shortUint64(idOrPubkey string) uint64 {
length := len(idOrPubkey)
if length < 8 {
return 0
}
b, err := hex.DecodeString(idOrPubkey[length-8:])
if err != nil {
return 0
}
return uint64(binary.BigEndian.Uint32(b))
}

View File

@@ -1,7 +0,0 @@
package sdk
type Follow struct {
Pubkey string
Relay string
Petname string
}

View File

@@ -1,63 +0,0 @@
package sdk
import (
"context"
"encoding/hex"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip05"
"github.com/nbd-wtf/go-nostr/nip19"
)
// InputToProfile turns any npub/nprofile/hex/nip05 input into a ProfilePointer (or nil).
func InputToProfile(ctx context.Context, input string) *nostr.ProfilePointer {
// handle if it is a hex string
if len(input) == 64 {
if _, err := hex.DecodeString(input); err == nil {
return &nostr.ProfilePointer{PublicKey: input}
}
}
// handle nip19 codes, if that's the case
prefix, data, _ := nip19.Decode(input)
switch prefix {
case "npub":
input = data.(string)
return &nostr.ProfilePointer{PublicKey: input}
case "nprofile":
pp := data.(nostr.ProfilePointer)
return &pp
}
// handle nip05 ids, if that's the case
pp, _ := nip05.QueryIdentifier(ctx, input)
if pp != nil {
return pp
}
return nil
}
// InputToEventPointer turns any note/nevent/hex input into a EventPointer (or nil).
func InputToEventPointer(input string) *nostr.EventPointer {
// handle if it is a hex string
if len(input) == 64 {
if _, err := hex.DecodeString(input); err == nil {
return &nostr.EventPointer{ID: input}
}
}
// handle nip19 codes, if that's the case
prefix, data, _ := nip19.Decode(input)
switch prefix {
case "note":
input = data.(string)
return &nostr.EventPointer{ID: input}
case "nevent":
ep := data.(nostr.EventPointer)
return &ep
}
// handle nip05 ids, if that's the case
return nil
}

View File

@@ -1,74 +0,0 @@
package sdk
import (
"context"
"encoding/json"
"fmt"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
)
type ProfileMetadata struct {
pubkey string
event *nostr.Event
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"`
}
func (p ProfileMetadata) Npub() string {
v, _ := nip19.EncodePublicKey(p.pubkey)
return v
}
func (p ProfileMetadata) Nprofile(ctx context.Context, sys *System, nrelays int) string {
v, _ := nip19.EncodeProfile(p.pubkey, sys.FetchOutboxRelays(ctx, p.pubkey))
return v
}
func FetchProfileMetadata(ctx context.Context, pool *nostr.SimplePool, pubkey string, relays ...string) ProfileMetadata {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch := pool.SubManyEose(ctx, relays, nostr.Filters{
{
Kinds: []int{nostr.KindProfileMetadata},
Authors: []string{pubkey},
Limit: 1,
},
})
for ie := range ch {
if m, err := ParseMetadata(ie.Event); err == nil {
m.pubkey = pubkey
m.event = ie.Event
return *m
}
}
return ProfileMetadata{pubkey: pubkey}
}
func ParseMetadata(event *nostr.Event) (*ProfileMetadata, error) {
if event.Kind != 0 {
return nil, fmt.Errorf("event %s is kind %d, not 0", event.ID, event.Kind)
}
var meta ProfileMetadata
if err := json.Unmarshal([]byte(event.Content), &meta); err != nil {
cont := event.Content
if len(cont) > 100 {
cont = cont[0:99]
}
return nil, fmt.Errorf("failed to parse metadata (%s) from event %s: %w", cont, event.ID, err)
}
return &meta, nil
}

View File

@@ -1,108 +0,0 @@
package sdk
import (
"regexp"
"strconv"
"strings"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
)
type Reference struct {
Text string
Start int
End int
Profile *nostr.ProfilePointer
Event *nostr.EventPointer
Entity *nostr.EntityPointer
}
var mentionRegex = regexp.MustCompile(`\bnostr:((note|npub|naddr|nevent|nprofile)1\w+)\b|#\[(\d+)\]`)
// ParseReferences parses both NIP-08 and NIP-27 references in a single unifying interface.
func ParseReferences(evt *nostr.Event) []*Reference {
var references []*Reference
content := evt.Content
for _, ref := range mentionRegex.FindAllStringSubmatchIndex(evt.Content, -1) {
reference := &Reference{
Text: content[ref[0]:ref[1]],
Start: ref[0],
End: ref[1],
}
if ref[6] == -1 {
// didn't find a NIP-10 #[0] reference, so it's a NIP-27 mention
nip19code := content[ref[2]:ref[3]]
if prefix, data, err := nip19.Decode(nip19code); err == nil {
switch prefix {
case "npub":
reference.Profile = &nostr.ProfilePointer{
PublicKey: data.(string), Relays: []string{},
}
case "nprofile":
pp := data.(nostr.ProfilePointer)
reference.Profile = &pp
case "note":
reference.Event = &nostr.EventPointer{ID: data.(string), Relays: []string{}}
case "nevent":
evp := data.(nostr.EventPointer)
reference.Event = &evp
case "naddr":
addr := data.(nostr.EntityPointer)
reference.Entity = &addr
}
}
} else {
// it's a NIP-10 mention.
// parse the number, get data from event tags.
n := content[ref[6]:ref[7]]
idx, err := strconv.Atoi(n)
if err != nil || len(evt.Tags) <= idx {
continue
}
if tag := evt.Tags[idx]; tag != nil && len(tag) >= 2 {
switch tag[0] {
case "p":
relays := make([]string, 0, 1)
if len(tag) > 2 && tag[2] != "" {
relays = append(relays, tag[2])
}
reference.Profile = &nostr.ProfilePointer{
PublicKey: tag[1],
Relays: relays,
}
case "e":
relays := make([]string, 0, 1)
if len(tag) > 2 && tag[2] != "" {
relays = append(relays, tag[2])
}
reference.Event = &nostr.EventPointer{
ID: tag[1],
Relays: relays,
}
case "a":
if parts := strings.Split(tag[1], ":"); len(parts) == 3 {
kind, _ := strconv.Atoi(parts[0])
relays := make([]string, 0, 1)
if len(tag) > 2 && tag[2] != "" {
relays = append(relays, tag[2])
}
reference.Entity = &nostr.EntityPointer{
Identifier: parts[2],
PublicKey: parts[1],
Kind: kind,
Relays: relays,
}
}
}
}
}
references = append(references, reference)
}
return references
}

View File

@@ -1,108 +0,0 @@
package sdk
import (
"fmt"
"testing"
"github.com/nbd-wtf/go-nostr"
)
func TestParseReferences(t *testing.T) {
evt := nostr.Event{
Tags: nostr.Tags{
{"p", "c9d556c6d3978d112d30616d0d20aaa81410e3653911dd67787b5aaf9b36ade8", "wss://nostr.com"},
{"e", "a84c5de86efc2ec2cff7bad077c4171e09146b633b7ad117fffe088d9579ac33", "wss://other.com", "reply"},
{"e", "31d7c2875b5fc8e6f9c8f9dc1f84de1b6b91d1947ea4c59225e55c325d330fa8", ""},
},
Content: "hello #[0], have you seen #[2]? it was made by nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg on nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4! broken #[3]",
}
expected := []Reference{
{
Text: "#[0]",
Start: 6,
End: 10,
Profile: &nostr.ProfilePointer{
PublicKey: "c9d556c6d3978d112d30616d0d20aaa81410e3653911dd67787b5aaf9b36ade8",
Relays: []string{"wss://nostr.com"},
},
},
{
Text: "#[2]",
Start: 26,
End: 30,
Event: &nostr.EventPointer{
ID: "31d7c2875b5fc8e6f9c8f9dc1f84de1b6b91d1947ea4c59225e55c325d330fa8",
Relays: []string{},
},
},
{
Text: "nostr:nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg",
Start: 47,
End: 123,
Profile: &nostr.ProfilePointer{
PublicKey: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393",
Relays: []string{},
},
},
{
Text: "nostr:nevent1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8ychxp5v4",
Start: 127,
End: 201,
Event: &nostr.EventPointer{
ID: "cc6b9fea033f59c3c39a0407c5f1bfee439b077508d918cfdc0d6fd431d39393",
Relays: []string{},
Author: "",
},
},
}
got := ParseReferences(&evt)
if len(got) != len(expected) {
t.Errorf("got %d references, expected %d", len(got), len(expected))
}
for i, g := range got {
e := expected[i]
if g.Text != e.Text {
t.Errorf("%d: got text %s, expected %s", i, g.Text, e.Text)
}
if g.Start != e.Start {
t.Errorf("%d: got start %d, expected %d", i, g.Start, e.Start)
}
if g.End != e.End {
t.Errorf("%d: got end %d, expected %d", i, g.End, e.End)
}
if (g.Entity == nil && e.Entity != nil) ||
(g.Event == nil && e.Event != nil) ||
(g.Profile == nil && e.Profile != nil) {
t.Errorf("%d: got some unexpected nil", i)
}
if g.Profile != nil && (g.Profile.PublicKey != e.Profile.PublicKey ||
len(g.Profile.Relays) != len(e.Profile.Relays) ||
(len(g.Profile.Relays) > 0 && g.Profile.Relays[0] != e.Profile.Relays[0])) {
t.Errorf("%d: profile value is wrong", i)
}
if g.Event != nil && (g.Event.ID != e.Event.ID ||
g.Event.Author != e.Event.Author ||
len(g.Event.Relays) != len(e.Event.Relays) ||
(len(g.Event.Relays) > 0 && g.Event.Relays[0] != e.Event.Relays[0])) {
fmt.Println(g.Event.ID, g.Event.Relays, len(g.Event.Relays), g.Event.Relays[0] == "")
fmt.Println(e.Event.Relays, len(e.Event.Relays))
t.Errorf("%d: event value is wrong", i)
}
if g.Entity != nil && (g.Entity.PublicKey != e.Entity.PublicKey ||
g.Entity.Identifier != e.Entity.Identifier ||
g.Entity.Kind != e.Entity.Kind ||
len(g.Entity.Relays) != len(g.Entity.Relays)) {
t.Errorf("%d: entity value is wrong", i)
}
}
}

View File

@@ -1,112 +0,0 @@
package sdk
import (
"context"
"encoding/json"
"github.com/nbd-wtf/go-nostr"
)
type Relay struct {
URL string
Inbox bool
Outbox bool
}
func FetchRelaysForPubkey(ctx context.Context, pool *nostr.SimplePool, pubkey string, relays ...string) []Relay {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch := pool.SubManyEose(ctx, relays, nostr.Filters{
{
Kinds: []int{
nostr.KindRelayListMetadata,
nostr.KindContactList,
},
Authors: []string{pubkey},
Limit: 2,
},
})
result := make([]Relay, 0, 20)
i := 0
for ie := range ch {
switch ie.Event.Kind {
case nostr.KindRelayListMetadata:
result = append(result, ParseRelaysFromKind10002(ie.Event)...)
case nostr.KindContactList:
result = append(result, ParseRelaysFromKind3(ie.Event)...)
}
i++
if i >= 2 {
break
}
}
return result
}
func ParseRelaysFromKind10002(evt *nostr.Event) []Relay {
result := make([]Relay, 0, len(evt.Tags))
for _, tag := range evt.Tags {
if u := tag.Value(); u != "" && tag[0] == "r" {
if !nostr.IsValidRelayURL(u) {
continue
}
u := nostr.NormalizeURL(u)
relay := Relay{
URL: u,
}
if len(tag) == 2 {
relay.Inbox = true
relay.Outbox = true
} else if tag[2] == "write" {
relay.Outbox = true
} else if tag[2] == "read" {
relay.Inbox = true
}
result = append(result, relay)
}
}
return result
}
func ParseRelaysFromKind3(evt *nostr.Event) []Relay {
type Item struct {
Read bool `json:"read"`
Write bool `json:"write"`
}
items := make(map[string]Item, 20)
json.Unmarshal([]byte(evt.Content), &items)
results := make([]Relay, len(items))
i := 0
for u, item := range items {
if !nostr.IsValidRelayURL(u) {
continue
}
u := nostr.NormalizeURL(u)
relay := Relay{
URL: u,
}
if item.Read {
relay.Inbox = true
}
if item.Write {
relay.Outbox = true
}
results = append(results, relay)
i++
}
return results
}

View File

@@ -1,59 +0,0 @@
package sdk
import (
"context"
"time"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/sdk/cache"
)
type System struct {
RelaysCache cache.Cache32[[]Relay]
FollowsCache cache.Cache32[[]Follow]
MetadataCache cache.Cache32[ProfileMetadata]
Pool *nostr.SimplePool
RelayListRelays []string
FollowListRelays []string
MetadataRelays []string
}
func (sys System) FetchRelays(ctx context.Context, pubkey string) []Relay {
if v, ok := sys.RelaysCache.Get(pubkey); ok {
return v
}
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
res := FetchRelaysForPubkey(ctx, sys.Pool, pubkey, sys.RelayListRelays...)
sys.RelaysCache.SetWithTTL(pubkey, res, time.Hour*6)
return res
}
func (sys System) FetchOutboxRelays(ctx context.Context, pubkey string) []string {
relays := sys.FetchRelays(ctx, pubkey)
result := make([]string, 0, len(relays))
for _, relay := range relays {
if relay.Outbox {
result = append(result, relay.URL)
}
}
return result
}
func (sys System) FetchProfileMetadata(ctx context.Context, pubkey string) ProfileMetadata {
if v, ok := sys.MetadataCache.Get(pubkey); ok {
return v
}
ctxRelays, cancel := context.WithTimeout(ctx, time.Second*2)
relays := sys.FetchOutboxRelays(ctxRelays, pubkey)
cancel()
ctx, cancel = context.WithTimeout(ctx, time.Second*3)
res := FetchProfileMetadata(ctx, sys.Pool, pubkey, append(relays, sys.MetadataRelays...)...)
cancel()
sys.MetadataCache.SetWithTTL(pubkey, res, time.Hour*6)
return res
}