From 21ee8c7361be89dcfc0582c3b70679fd5c474075 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 24 Oct 2025 11:36:56 -0700 Subject: [PATCH] Generate member lists --- zooid/config.go | 18 ++--- zooid/events.go | 2 +- zooid/groups.go | 184 +++++++++++++++++++++++++++++++++----------- zooid/instance.go | 5 +- zooid/management.go | 38 +++++++-- zooid/util.go | 10 +++ 6 files changed, 194 insertions(+), 63 deletions(-) diff --git a/zooid/config.go b/zooid/config.go index 5f74fd2..764d866 100644 --- a/zooid/config.go +++ b/zooid/config.go @@ -79,6 +79,10 @@ func LoadConfig(filename string) (*Config, error) { return &config, nil } +func (config *Config) Sign(event *nostr.Event) error { + return event.Sign(config.secret) +} + func (config *Config) GetSelf() nostr.PubKey { return config.secret.Public() } @@ -87,16 +91,12 @@ func (config *Config) IsSelf(pubkey nostr.PubKey) bool { return pubkey == config.GetSelf() } -func (config *Config) Sign(event *nostr.Event) error { - return event.Sign(config.secret) +func (config *Config) GetOwner() nostr.PubKey { + return nostr.MustPubKeyFromHex(config.Info.Pubkey) } func (config *Config) IsOwner(pubkey nostr.PubKey) bool { - return pubkey.Hex() == config.Info.Pubkey -} - -func (config *Config) IsAdmin(pubkey nostr.PubKey) bool { - return config.IsOwner(pubkey) || config.IsSelf(pubkey) + return pubkey == config.GetOwner() } func (config *Config) GetAssignedRoles(pubkey nostr.PubKey) []Role { @@ -124,7 +124,7 @@ func (config *Config) GetAllRoles(pubkey nostr.PubKey) []Role { } func (config *Config) CanInvite(pubkey nostr.PubKey) bool { - if config.IsAdmin(pubkey) { + if config.IsOwner(pubkey) || config.IsSelf(pubkey) { return true } @@ -138,7 +138,7 @@ func (config *Config) CanInvite(pubkey nostr.PubKey) bool { } func (config *Config) CanManage(pubkey nostr.PubKey) bool { - if config.IsAdmin(pubkey) { + if config.IsOwner(pubkey) || config.IsSelf(pubkey) { return true } diff --git a/zooid/events.go b/zooid/events.go index f15e2fe..d2b63a1 100644 --- a/zooid/events.go +++ b/zooid/events.go @@ -387,7 +387,7 @@ func (events *EventStore) GetOrCreateApplicationSpecificData(d string) nostr.Eve } } -func (events *EventStore) GetOrCreateMemberList() nostr.Event { +func (events *EventStore) GetOrCreateRelayMembersList() nostr.Event { filter := nostr.Filter{ Kinds: []nostr.Kind{RELAY_MEMBERS}, } diff --git a/zooid/groups.go b/zooid/groups.go index 575c60b..a9a7450 100644 --- a/zooid/groups.go +++ b/zooid/groups.go @@ -4,6 +4,8 @@ import ( "fiatjaf.com/nostr" ) +// Utils + func GetGroupIDFromEvent(event nostr.Event) string { tag := event.Tags.Find("h") @@ -14,11 +16,16 @@ func GetGroupIDFromEvent(event nostr.Event) string { return "" } +// Struct definition + type GroupStore struct { - Config *Config - Events *EventStore + Config *Config + Events *EventStore + Management *ManagementStore } +// Metadata + func (g *GroupStore) GetMetadata(h string) nostr.Event { filter := nostr.Filter{ Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata}, @@ -34,6 +41,81 @@ func (g *GroupStore) GetMetadata(h string) nostr.Event { return nostr.Event{} } +func (g *GroupStore) SetMetadataFromEvent(event nostr.Event) error { + tags := nostr.Tags{} + + for _, tag := range event.Tags { + if len(tag) >= 2 && tag[0] == "h" { + tags = append(tags, nostr.Tag{"d", tag[1]}) + } else { + tags = append(tags, tag) + } + } + + metadataEvent := nostr.Event{ + Kind: nostr.KindSimpleGroupMetadata, + CreatedAt: event.CreatedAt, + Tags: tags, + } + + return g.Events.SignAndStoreEvent(&metadataEvent, true) +} + +// Deletion + +func (g *GroupStore) DeleteGroup(h string) { + filters := []nostr.Filter{ + { + Tags: nostr.TagMap{ + "d": []string{h}, + }, + }, + { + Tags: nostr.TagMap{ + "h": []string{h}, + }, + }, + } + + for _, filter := range filters { + for event := range g.Events.QueryEvents(filter, 0) { + g.Events.DeleteEvent(event.ID) + } + } +} + +// Admins + +func (g *GroupStore) IsAdmin(h string, pubkey nostr.PubKey) bool { + return g.Management.IsAdmin(pubkey) +} + +func (g *GroupStore) GetAdmins(h string) []nostr.PubKey { + return g.Management.GetAdmins() +} + +func (g *GroupStore) GenerateAdminsEvent(h string) nostr.Event { + tags := nostr.Tags{ + nostr.Tag{"-"}, + } + + for _, pubkey := range g.GetAdmins(h) { + tags = append(tags, nostr.Tag{"p", pubkey.Hex()}) + } + + event := nostr.Event{ + Kind: nostr.KindSimpleGroupAdmins, + CreatedAt: nostr.Now(), + Tags: tags, + } + + g.Config.Sign(&event) + + return event +} + +// Membership + func (g *GroupStore) AddMember(h string, pubkey nostr.PubKey) error { event := nostr.Event{ Kind: nostr.KindSimpleGroupPutUser, @@ -60,47 +142,6 @@ func (g *GroupStore) RemoveMember(h string, pubkey nostr.PubKey) error { return g.Events.SignAndStoreEvent(&event, true) } -func (g *GroupStore) SetMetadataFromEvent(event nostr.Event) error { - tags := nostr.Tags{} - - for _, tag := range event.Tags { - if len(tag) >= 2 && tag[0] == "h" { - tags = append(tags, nostr.Tag{"d", tag[1]}) - } else { - tags = append(tags, tag) - } - } - - metadataEvent := nostr.Event{ - Kind: nostr.KindSimpleGroupMetadata, - CreatedAt: event.CreatedAt, - Tags: tags, - } - - return g.Events.SignAndStoreEvent(&metadataEvent, true) -} - -func (g *GroupStore) DeleteGroup(h string) { - filters := []nostr.Filter{ - { - Tags: nostr.TagMap{ - "d": []string{h}, - }, - }, - { - Tags: nostr.TagMap{ - "h": []string{h}, - }, - }, - } - - for _, filter := range filters { - for event := range g.Events.QueryEvents(filter, 0) { - g.Events.DeleteEvent(event.ID) - } - } -} - func (g *GroupStore) IsMember(h string, pubkey nostr.PubKey) bool { filter := nostr.Filter{ Kinds: []nostr.Kind{nostr.KindSimpleGroupPutUser, nostr.KindSimpleGroupRemoveUser}, @@ -123,10 +164,65 @@ func (g *GroupStore) IsMember(h string, pubkey nostr.PubKey) bool { return false } +func (g *GroupStore) GetMembers(h string) []nostr.PubKey { + filter := nostr.Filter{ + Kinds: []nostr.Kind{nostr.KindSimpleGroupPutUser, nostr.KindSimpleGroupRemoveUser}, + Tags: nostr.TagMap{ + "h": []string{h}, + }, + } + + members := make([]nostr.PubKey, 0) + + for event := range g.Events.QueryEvents(filter, 0) { + for hex := range event.Tags.FindAll("p") { + if pubkey, err := nostr.PubKeyFromHex(hex[1]); err != nil { + if event.Kind == nostr.KindSimpleGroupPutUser { + members = append(members, pubkey) + } else { + members = Remove(members, pubkey) + } + } + } + } + + return members +} + +func (g *GroupStore) GenerateMembersEvent(h string) nostr.Event { + tags := nostr.Tags{ + nostr.Tag{"-"}, + } + + for _, pubkey := range g.GetMembers(h) { + tags = append(tags, nostr.Tag{"p", pubkey.Hex()}) + } + + event := nostr.Event{ + Kind: nostr.KindSimpleGroupMembers, + CreatedAt: nostr.Now(), + Tags: tags, + } + + g.Config.Sign(&event) + + return event +} + +// Other stuff + func (g *GroupStore) HasAccess(h string, pubkey nostr.PubKey) bool { if !HasTag(g.GetMetadata(h).Tags, "closed") { return true } - return g.IsMember(h, pubkey) + if g.IsAdmin(h, pubkey) { + return true + } + + if g.IsMember(h, pubkey) { + return true + } + + return false } diff --git a/zooid/instance.go b/zooid/instance.go index 774bda6..53c7eaf 100644 --- a/zooid/instance.go +++ b/zooid/instance.go @@ -50,8 +50,9 @@ func MakeInstance(filename string) (*Instance, error) { } groups := &GroupStore{ - Config: config, - Events: events, + Config: config, + Events: events, + Management: management, } instance := &Instance{ diff --git a/zooid/management.go b/zooid/management.go index 879e7ab..193ad40 100644 --- a/zooid/management.go +++ b/zooid/management.go @@ -120,11 +120,35 @@ func (m *ManagementStore) PubkeyIsBanned(pubkey nostr.PubKey) bool { return tag != nil } +// Admins + +func (m *ManagementStore) IsAdmin(pubkey nostr.PubKey) bool { + return m.Config.IsOwner(pubkey) || m.Config.IsSelf(pubkey) +} + +func (m *ManagementStore) GetAdmins() []nostr.PubKey { + members := make([]nostr.PubKey, 0) + + members = append(members, m.Config.GetOwner()) + + members = append(members, m.Config.GetSelf()) + + for _, role := range m.Config.Roles { + if role.CanManage { + for _, pubkey := range role.Pubkeys { + members = append(members, nostr.MustPubKeyFromHex(pubkey)) + } + } + } + + return members +} + // Membership func (m *ManagementStore) GetMembers() []nostr.PubKey { pubkeys := make([]nostr.PubKey, 0) - for tag := range m.Events.GetOrCreateMemberList().Tags.FindAll("member") { + for tag := range m.Events.GetOrCreateRelayMembersList().Tags.FindAll("member") { pubkey, err := nostr.PubKeyFromHex(tag[1]) if err == nil { @@ -136,11 +160,11 @@ func (m *ManagementStore) GetMembers() []nostr.PubKey { } func (m *ManagementStore) IsMember(pubkey nostr.PubKey) bool { - return m.Events.GetOrCreateMemberList().Tags.FindWithValue("member", pubkey.Hex()) != nil + return m.Events.GetOrCreateRelayMembersList().Tags.FindWithValue("member", pubkey.Hex()) != nil } func (m *ManagementStore) AddMember(pubkey nostr.PubKey) error { - membersEvent := m.Events.GetOrCreateMemberList() + membersEvent := m.Events.GetOrCreateRelayMembersList() if membersEvent.Tags.FindWithValue("member", pubkey.Hex()) == nil { addMemberEvent := nostr.Event{ @@ -167,7 +191,7 @@ func (m *ManagementStore) AddMember(pubkey nostr.PubKey) error { } func (m *ManagementStore) RemoveMember(pubkey nostr.PubKey) error { - membersEvent := m.Events.GetOrCreateMemberList() + membersEvent := m.Events.GetOrCreateRelayMembersList() if membersEvent.Tags.FindWithValue("member", pubkey.Hex()) != nil { removeMemberEvent := nostr.Event{ @@ -224,7 +248,7 @@ func (m *ManagementStore) GetAllowedPubkeyItems() []nip86.PubKeyReason { reasons := make([]nip86.PubKeyReason, 0) reasons = append(reasons, nip86.PubKeyReason{ - PubKey: nostr.MustPubKeyFromHex(m.Config.Info.Pubkey), + PubKey: m.Config.GetOwner(), Reason: "relay owner", }) @@ -242,7 +266,7 @@ func (m *ManagementStore) GetAllowedPubkeyItems() []nip86.PubKeyReason { } } - for tag := range m.Events.GetOrCreateMemberList().Tags.FindAll("member") { + for tag := range m.Events.GetOrCreateRelayMembersList().Tags.FindAll("member") { pubkey, err := nostr.PubKeyFromHex(tag[1]) if err != nil { @@ -276,7 +300,7 @@ func (m *ManagementStore) AllowPubkey(pubkey nostr.PubKey) error { } func (m *ManagementStore) HasAccess(pubkey nostr.PubKey) bool { - if m.Config.IsAdmin(pubkey) { + if m.IsAdmin(pubkey) { return true } diff --git a/zooid/util.go b/zooid/util.go index 7e913d2..949d18d 100644 --- a/zooid/util.go +++ b/zooid/util.go @@ -48,6 +48,16 @@ func Filter[T any](ss []T, test func(T) bool) (ret []T) { return } +func Remove[T comparable](slice []T, element T) []T { + for i, v := range slice { + if v == element { + return append(slice[:i], slice[i+1:]...) + } + } + + return slice +} + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" func RandomString(n int) string {