From f3f56491ad873ff0f33e113c1fc481c3af252f98 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 26 Sep 2025 13:42:52 -0700 Subject: [PATCH] Manage access just by storing ephemeral events --- zooid/access.go | 174 ---------------------------------------------- zooid/events.go | 16 +++-- zooid/instance.go | 127 ++++++++++++++++----------------- zooid/util.go | 6 +- 4 files changed, 78 insertions(+), 245 deletions(-) delete mode 100644 zooid/access.go diff --git a/zooid/access.go b/zooid/access.go deleted file mode 100644 index af80925..0000000 --- a/zooid/access.go +++ /dev/null @@ -1,174 +0,0 @@ -package zooid - -import ( - "fmt" - "time" - - "fiatjaf.com/nostr" - "github.com/Masterminds/squirrel" -) - -type Invite struct { - ID string - CreatedAt int - Pubkey nostr.PubKey - Claim string -} - -type Redemption struct { - ID string - InviteID string - CreatedAt int - Pubkey nostr.PubKey -} - -type AccessStore struct { - Config *Config - Schema *Schema -} - -func (access *AccessStore) Init() error { - schema := access.Schema.Render(` - CREATE TABLE IF NOT EXISTS {{.Prefix}}__invites ( - id TEXT PRIMARY KEY, - created_at INTEGER NOT NULL, - pubkey TEXT NOT NULL, - claim TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_invites_created_at ON {{.Prefix}}__invites(created_at); - CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_invites_pubkey ON {{.Prefix}}__invites(pubkey); - CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_invites_claim ON {{.Prefix}}__invites(claim); - - CREATE TABLE IF NOT EXISTS {{.Prefix}}__redemptions ( - id TEXT PRIMARY KEY, - invite_id TEXT NOT NULL, - created_at INTEGER NOT NULL, - pubkey TEXT NOT NULL, - FOREIGN KEY (invite_id) REFERENCES {{.Prefix}}__invites(id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_redemptions_invite_id ON {{.Prefix}}__redemptions(invite_id); - CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_redemptions_created_at ON {{.Prefix}}__redemptions(created_at); - CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_redemptions_pubkey ON {{.Prefix}}__redemptions(pubkey); - `) - - if _, err := GetDb().Exec(schema); err != nil { - return fmt.Errorf("failed to create schema: %w", err) - } - - return nil -} - -// Invite utils - -func (access *AccessStore) SelectInvites() squirrel.SelectBuilder { - return squirrel.Select("id", "created_at", "pubkey", "claim").From(access.Schema.Prefix("invites")) -} - -func (access *AccessStore) QueryInvites(builder squirrel.SelectBuilder) []Invite { - rows, err := builder.RunWith(GetDb()).Query() - if err != nil { - return []Invite{} - } - defer rows.Close() - - var invites []Invite - for rows.Next() { - var invite Invite - var pubkeyStr string - err := rows.Scan(&invite.ID, &invite.CreatedAt, &pubkeyStr, &invite.Claim) - if err != nil { - continue - } - - if pubkey, err := nostr.PubKeyFromHex(pubkeyStr); err == nil { - invite.Pubkey = pubkey - } else { - continue - } - - invites = append(invites, invite) - } - - return invites -} - -func (access *AccessStore) AddInvite(pubkey nostr.PubKey, claim string) error { - id := RandomString(32) - createdAt := int(time.Now().Unix()) - - insertQb := squirrel.Insert(access.Schema.Prefix("invites")). - Columns("id", "created_at", "pubkey", "claim"). - Values(id, createdAt, pubkey.Hex(), claim) - - _, err := insertQb.RunWith(GetDb()).Exec() - if err != nil { - return fmt.Errorf("failed to add invite: %w", err) - } - - return nil -} - -func (access *AccessStore) GetInvitesByClaim(claim string) []Invite { - return access.QueryInvites(access.SelectInvites().Where(squirrel.Eq{"claim": claim})) -} - -func (access *AccessStore) GetInvitesByPubkey(pubkey nostr.PubKey) []Invite { - return access.QueryInvites(access.SelectInvites().Where(squirrel.Eq{"pubkey": pubkey.Hex()})) -} - -// Redemption utils - -func (access *AccessStore) SelectRedemptions() squirrel.SelectBuilder { - return squirrel.Select("id", "invite_id", "created_at", "pubkey").From(access.Schema.Prefix("redemptions")) -} - -func (access *AccessStore) QueryRedemptions(builder squirrel.SelectBuilder) []Redemption { - rows, err := builder.RunWith(GetDb()).Query() - if err != nil { - return []Redemption{} - } - defer rows.Close() - - var redemptions []Redemption - for rows.Next() { - var redemption Redemption - var pubkeyStr string - - err := rows.Scan(&redemption.ID, &redemption.InviteID, &redemption.CreatedAt, &pubkeyStr) - if err != nil { - continue - } - - if pubkey, err := nostr.PubKeyFromHex(pubkeyStr); err == nil { - redemption.Pubkey = pubkey - } else { - continue - } - - redemptions = append(redemptions, redemption) - } - - return redemptions -} - -func (access *AccessStore) AddRedemption(pubkey nostr.PubKey, invite Invite) error { - id := RandomString(32) - createdAt := int(time.Now().Unix()) - - insertQb := squirrel.Insert(access.Schema.Prefix("redemptions")). - Columns("id", "invite_id", "created_at", "pubkey"). - Values(id, invite.ID, createdAt, pubkey.Hex()) - - _, err := insertQb.RunWith(GetDb()).Exec() - if err != nil { - return fmt.Errorf("failed to add invite: %w", err) - } - - return nil -} - -func (access *AccessStore) GetRedemptionsByPubkey(pubkey nostr.PubKey) []Invite { - return access.QueryInvites(access.SelectRedemptions().Where(squirrel.Eq{"pubkey": pubkey.Hex()})) -} diff --git a/zooid/events.go b/zooid/events.go index 079e119..e774b86 100644 --- a/zooid/events.go +++ b/zooid/events.go @@ -100,9 +100,9 @@ func (events *EventStore) QueryEvents(filter nostr.Filter, maxLimit int) iter.Se return } - if maxLimit > 0 && maxLimit < filter.Limit { - filter.Limit = maxLimit - } + if maxLimit > 0 && maxLimit < filter.Limit { + filter.Limit = maxLimit + } rows, err := events.buildSelectQuery(filter).RunWith(GetDb()).Query() if err != nil { @@ -205,9 +205,13 @@ func (events *EventStore) buildSelectQuery(filter nostr.Filter) squirrel.SelectB } for tagKey, tagValues := range filter.Tags { - if len(tagValues) == 0 { - continue - } + if len(tagValues) == 0 { + continue + } + + if len(tagKey) != 1 { + continue + } tagValueInterfaces := make([]interface{}, len(tagValues)) for i, tagValue := range tagValues { diff --git a/zooid/instance.go b/zooid/instance.go index 2a163e1..1db5f71 100644 --- a/zooid/instance.go +++ b/zooid/instance.go @@ -2,16 +2,16 @@ package zooid import ( "context" - "slices" "iter" "log" "net/http" + "slices" "sync" "fiatjaf.com/nostr" - "fiatjaf.com/nostr/nip29" "fiatjaf.com/nostr/eventstore" "fiatjaf.com/nostr/khatru" + "fiatjaf.com/nostr/nip29" "github.com/gosimple/slug" ) @@ -20,7 +20,6 @@ type Instance struct { Config *Config Secret nostr.SecretKey Events eventstore.Store - Access *AccessStore Blossom *BlossomStore Management *ManagementStore Relay *khatru.Relay @@ -52,12 +51,6 @@ func MakeInstance(hostname string) (*Instance, error) { Name: slug.Make(config.Self.Schema) + "_events", }, }, - Access: &AccessStore{ - Config: config, - Schema: &Schema{ - Name: slug.Make(config.Self.Schema) + "_access", - }, - }, Blossom: &BlossomStore{ Config: config, Schema: &Schema{ @@ -101,10 +94,6 @@ func MakeInstance(hostname string) (*Instance, error) { log.Fatal("Failed to initialize event store:", err) } - if err := instance.Access.Init(); err != nil { - log.Fatal("Failed to initialize access store:", err) - } - if err := instance.Blossom.Init(); err != nil { log.Fatal("Failed to initialize blossom store:", err) } @@ -171,7 +160,12 @@ func (instance *Instance) HasAccess(pubkey nostr.PubKey) bool { return true } - if len(instance.Access.GetRedemptionsByPubkey(pubkey)) > 0 { + filter := nostr.Filter{ + Kinds: []nostr.Kind{AUTH_JOIN}, + Authors: []nostr.PubKey{pubkey}, + } + + for range instance.Events.QueryEvents(filter, 1) { return true } @@ -210,7 +204,7 @@ func (instance *Instance) AllowRecipientEvent(event nostr.Event) bool { recipientTag := event.Tags.Find("p") if recipientTag != nil { - pubkey, err := nostr.PubKeyFromHex(recipientTag[1]) + pubkey, err := nostr.PubKeyFromHex(recipientTag[1]) if err == nil && instance.HasAccess(pubkey) { return true @@ -221,6 +215,35 @@ func (instance *Instance) AllowRecipientEvent(event nostr.Event) bool { return false } +func (instance *Instance) GenerateInviteEvent(pubkey nostr.PubKey) nostr.Event { + filter := nostr.Filter{ + Kinds: []nostr.Kind{AUTH_INVITE}, + Authors: []nostr.PubKey{pubkey}, + } + + for event := range instance.Events.QueryEvents(filter, 1) { + return event + } + + event := nostr.Event{ + Kind: AUTH_INVITE, + CreatedAt: nostr.Now(), + Tags: nostr.Tags{ + []string{"claim", RandomString(8)}, + []string{"p", pubkey.Hex()}, + }, + } + + event.Sign(instance.Secret) + + err := instance.Events.SaveEvent(event) + if err != nil { + log.Printf("Failed to generate invite event: %w", err) + } + + return event +} + func (instance *Instance) OnJoinEvent(event nostr.Event) (reject bool, msg string) { claimTag := event.Tags.Find("claim") @@ -230,22 +253,21 @@ func (instance *Instance) OnJoinEvent(event nostr.Event) (reject bool, msg strin filter := nostr.Filter{ Kinds: []nostr.Kind{AUTH_INVITE}, - Tags: nostr.TagMap{ - "claim": []string{claimTag[1]}, - }, } - for range instance.Events.QueryEvents(filter, 1) { - return false, "" + for event := range instance.Events.QueryEvents(filter, 0) { + if event.Tags.FindWithValue("claim", claimTag[1]) != nil { + return false, "" + } } return true, "invalid: failed to validate invite code" } func (instance *Instance) GetGroupMetadataEvent(h string) nostr.Event { - for event := range instance.Events.QueryEvents(MakeGroupMetadataFilter(h), 1) { - return event - } + for event := range instance.Events.QueryEvents(MakeGroupMetadataFilter(h), 1) { + return event + } return nostr.Event{} } @@ -358,12 +380,12 @@ func (instance *Instance) OnEventSaved(ctx context.Context, event nostr.Event) { } if event.Kind == nostr.KindSimpleGroupJoinRequest && instance.Config.Groups.AutoJoin { - h := GetGroupIDFromEvent(event) - meta := instance.GetGroupMetadataEvent(h) + h := GetGroupIDFromEvent(event) + meta := instance.GetGroupMetadataEvent(h) - if !HasTag(meta.Tags, "closed") { - addEvent(MakePutUserEvent(h, event.PubKey)) - } + if !HasTag(meta.Tags, "closed") { + addEvent(MakePutUserEvent(h, event.PubKey)) + } } if event.Kind == nostr.KindSimpleGroupLeaveRequest && instance.Config.Groups.AutoLeave { @@ -388,6 +410,9 @@ func (instance *Instance) OnEventSaved(ctx context.Context, event nostr.Event) { } func (instance *Instance) OnEphemeralEvent(ctx context.Context, event nostr.Event) { + if slices.Contains([]nostr.Kind{AUTH_INVITE, AUTH_JOIN}, event.Kind) { + instance.Events.SaveEvent(event) + } } func (instance *Instance) OnRequest(ctx context.Context, filter nostr.Filter) (reject bool, msg string) { @@ -414,7 +439,7 @@ func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) stripSignature := func(event nostr.Event) nostr.Event { if instance.Config.Policy.StripSignatures && !instance.IsAdmin(pubkey) { - var zeroSig [64]byte + var zeroSig [64]byte event.Sig = zeroSig } @@ -422,52 +447,30 @@ func (instance *Instance) QueryStored(ctx context.Context, filter nostr.Filter) } if slices.Contains(filter.Kinds, AUTH_INVITE) && instance.Config.CanInvite(pubkey) { - var claim string - - invites := instance.Access.GetInvitesByPubkey(pubkey) - - if len(invites) > 0 { - claim = First(invites).Claim - } else { - claim = RandomString(8) - instance.Access.AddInvite(pubkey, claim) - } - - event := nostr.Event{ - Kind: AUTH_INVITE, - CreatedAt: nostr.Now(), - Tags: nostr.Tags{ - nostr.Tag{"claim", claim}, - }, - } - - event.Sign(instance.Secret) - - if !yield(stripSignature(event)) { + if !yield(stripSignature(instance.GenerateInviteEvent(pubkey))) { return } } for event := range instance.Events.QueryEvents(filter, 1000) { - hTag := event.Tags.Find("h") + // We save some ephemeral events for bookkeeping, don't return them + if event.Kind.IsEphemeral() { + continue + } - // Prune group related events if groups are disabled - if !instance.Config.Groups.Enabled { - if slices.Contains(nip29.ModerationEventKinds, event.Kind) { + h := GetGroupIDFromEvent(event) + + if h != "" { + if !instance.Config.Groups.Enabled { continue } - if slices.Contains(nip29.MetadataEventKinds, event.Kind) { - continue - } - - if hTag != nil { + if !instance.HasGroupAccess(h, pubkey) { continue } } - // Prune events that the user doesn't have access to - if hTag != nil && !instance.HasGroupAccess(hTag[1], pubkey) { + if !instance.Config.Groups.Enabled && slices.Contains(nip29.MetadataEventKinds, event.Kind) { continue } diff --git a/zooid/util.go b/zooid/util.go index 86dfbe3..c64c078 100644 --- a/zooid/util.go +++ b/zooid/util.go @@ -1,9 +1,9 @@ package zooid import ( + "fiatjaf.com/nostr" "math/rand" "strings" - "fiatjaf.com/nostr" ) const ( @@ -71,7 +71,7 @@ func HasTag(tags nostr.Tags, key string) bool { } func IsEmptyEvent(event nostr.Event) bool { - var zeroID nostr.ID + var zeroID nostr.ID - return event.ID == zeroID + return event.ID == zeroID }