Start on group store

This commit is contained in:
Jon Staab
2025-09-26 09:39:24 -07:00
parent f8a7b003c1
commit 174e1a361f
5 changed files with 337 additions and 10 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ See `justfile` for defined commands.
## TODO
- [ ] Sync claims to management db, pull directly from management db when checking access
- [ ] See if we can build groups directly on top of the event store by generating events eagerly rather than lazily
- [ ] Add admin/owner/etc to list allowed pubkeys
- [ ] Watch configuration files and hot reload
- [ ] Free up resources after instance inactivity
+3
View File
@@ -15,6 +15,7 @@ type Role struct {
}
type Config struct {
Host string
Self struct {
Name string `toml:"name"`
Icon string `toml:"icon"`
@@ -50,6 +51,8 @@ func LoadConfig(hostname string) (*Config, error) {
return nil, fmt.Errorf("failed to parse config file %s: %w", path, err)
}
config.Host = hostname
return &config, nil
}
+317 -2
View File
@@ -1,5 +1,320 @@
package zooid
func EnableGroups(instance *Instance) {
instance.Relay.Info.SupportedNIPs = append(instance.Relay.Info.SupportedNIPs, 29)
import (
"context"
"encoding/json"
"fmt"
"log"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip29"
)
type GroupsStore struct {
Host string
Config *Config
Schema *Schema
}
func (groups *GroupsStore) Init() error {
schema := groups.Schema.Render(`
CREATE TABLE IF NOT EXISTS {{.Prefix}}__groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
about TEXT NOT NULL,
closed BOOLEAN NOT NULL,
private BOOLEAN NOT NULL,
last_metadata_update INTEGER,
last_admins_update INTEGER,
last_members_update INTEGER
);
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_groups_id ON {{.Prefix}}__groups(id);
CREATE TABLE IF NOT EXISTS {{.Prefix}}__group_members (
id TEXT PRIMARY KEY,
group_id TEXT NOT NULL,
pubkey TEXT NOT NULL,
FOREIGN KEY (group_id) REFERENCES {{.Prefix}}__groups(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_group_members_group_id ON {{.Prefix}}__group_members(group_id);
CREATE INDEX IF NOT EXISTS {{.Prefix}}__idx_group_members_pubkey ON {{.Prefix}}__group_members(pubkey);
`)
if _, err := GetDb().Exec(schema); err != nil {
return fmt.Errorf("failed to create schema: %w", err)
}
return nil
}
// Group CRUD
func (groups *GroupsStore) SelectGroups() squirrel.SelectBuilder {
return squirrel.Select("id", "name", "about", "closed", "private", "last_metadata_update", "last_admins_update", "last_members_update").From(groups.Schema.Prefix("groups"))
}
func (groups *GroupsStore) QueryGroups(builder squirrel.SelectBuilder) []Group {
rows, err := builder.RunWith(GetDb()).Query()
if err != nil {
return []Group{}
}
defer rows.Close()
var groups []Group
for rows.Next() {
var group Group
var id string
err := rows.Scan(&id, &group.Name, &group.About, &group.Closed, &group.Private, &group.LastMetadataUpdate, &group.LastAdminsUpdate, &group.LastMembersUpdate)
if err != nil {
continue
}
group.Address = nip29.GroupAddress{
ID: id
Relay: groups.Config.Host
}
groups = append(groups, group)
}
return groups
}
func (groups *GroupStore) PutGroup(group *nip29.Group) {
// Insert, on duplicate update
}
func (groups *GroupStore) DeleteGroup(id string) {
// Delete group
}
func (groups *GroupsStore) GetGroups() []nip29.Group {
return groups.QueryGroups(groups.SelectGroups())
}
func (groups *GroupsStore) GetGroupByID(id string) (nip29.Group, bool) {
groupList := groups.QueryGroups(groups.SelectGroups().Where(squirrel.Eq{"id": id}))
return First(groupList), len(groupList) > 0
}
// Group Utils
func (groups *GroupStore) MakeGroup(h string) *nip29.Group {
qualifiedID := fmt.Sprintf("%s'%s", groups.Config.Host, h)
group, err := nip29.NewGroup(qualifiedID)
if err != nil {
log.Printf("Failed to create group with qualified ID %s", qualifiedID)
return nil
}
return &group
}
func (groups *GroupStore) GetGroupIDFromEvent(event *nostr.Event) string {
hTag := event.Tags.GetFirst([]string{"h"})
if hTag == nil {
return ""
}
return hTag.Value()
}
func (groups *GroupStore) GetGroupFromEvent(event *nostr.Event) *nip29.Group {
id = GetGroupIDFromEvent(event)
if id == "" {
return nil
}
return GetGroupByID(id)
}
func (groups *GroupStore) IsGroupMember(ctx context.Context, id string, pubkey string) bool {
filter := nostr.Filter{
Kinds: []int{nostr.KindSimpleGroupPutUser, nostr.KindSimpleGroupRemoveUser},
Tags: nostr.TagMap{
"p": []string{pubkey},
"h": []string{id},
},
}
events, err := GetBackend().QueryEvents(ctx, filter)
if err != nil {
log.Println(err)
}
for event := range events {
if event.Kind == nostr.KindSimpleGroupPutUser {
return true
}
if event.Kind == nostr.KindSimpleGroupRemoveUser {
return false
}
}
return false
}
func HandleCreateGroup(event *nostr.Event) {
group := MakeGroup(GetGroupIDFromEvent(event))
if group != nil {
PutGroup(group)
}
}
func HandleEditMetadata(event *nostr.Event) {
group := GetGroupFromEvent(event)
if group == nil {
group = MakeGroup(GetGroupIDFromEvent(event))
}
group.LastMetadataUpdate = event.CreatedAt
group.Name = group.Address.ID
if tag := event.Tags.GetFirst([]string{"name", ""}); tag != nil {
group.Name = (*tag)[1]
}
if tag := event.Tags.GetFirst([]string{"about", ""}); tag != nil {
group.About = (*tag)[1]
}
if tag := event.Tags.GetFirst([]string{"picture", ""}); tag != nil {
group.Picture = (*tag)[1]
}
if tag := event.Tags.GetFirst([]string{"private"}); tag != nil {
group.Private = true
}
if tag := event.Tags.GetFirst([]string{"closed"}); tag != nil {
group.Closed = true
}
PutGroup(group)
}
func HandleDeleteGroup(event *nostr.Event) {
ctx := context.Background()
id := GetGroupIDFromEvent(event)
DeleteGroup(id)
hFilter := nostr.Filter{
Tags: nostr.TagMap{
"h": []string{id},
},
}
hCh, err := GetBackend().QueryEvents(ctx, hFilter)
if err != nil {
log.Println(err)
} else {
for event := range hCh {
DeleteEvent(ctx, event)
}
}
dFilter := nostr.Filter{
Tags: nostr.TagMap{
"d": []string{id},
},
}
dCh, err := GetBackend().QueryEvents(ctx, dFilter)
if err != nil {
log.Println(err)
} else {
for event := range dCh {
DeleteEvent(ctx, event)
}
}
}
func GenerateGroupMetadataEvents(ctx context.Context, filter nostr.Filter) []*nostr.Event {
result := make([]*nostr.Event, 0)
for _, group := range ListGroups() {
event := group.ToMetadataEvent()
if !filter.Matches(event) {
continue
}
if err := event.Sign(RELAY_SECRET); err != nil {
log.Println("Failed to sign metadata event", err)
} else {
result = append(result, event)
}
}
return result
}
func GenerateGroupAdminsEvents(ctx context.Context, filter nostr.Filter) []*nostr.Event {
result := make([]*nostr.Event, 0)
for _, group := range ListGroups() {
event := nostr.Event{
Kind: nostr.KindSimpleGroupAdmins,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"d", group.Address.ID},
},
}
for _, pubkey := range RELAY_ADMINS {
event.Tags = append(event.Tags, nostr.Tag{"p", pubkey})
}
if !filter.Matches(&event) {
continue
}
if err := event.Sign(RELAY_SECRET); err != nil {
log.Println("Failed to sign admins event", err)
} else {
result = append(result, &event)
}
}
return result
}
func MakePutUserEvent(event *nostr.Event) *nostr.Event {
putUser := nostr.Event{
Kind: nostr.KindSimpleGroupPutUser,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"p", event.PubKey},
nostr.Tag{"h", GetGroupIDFromEvent(event)},
},
}
if err := putUser.Sign(RELAY_SECRET); err != nil {
log.Println(err)
}
return &putUser
}
func MakeRemoveUserEvent(event *nostr.Event) *nostr.Event {
removeUser := nostr.Event{
Kind: nostr.KindSimpleGroupRemoveUser,
CreatedAt: nostr.Now(),
Tags: nostr.Tags{
nostr.Tag{"p", event.PubKey},
nostr.Tag{"h", GetGroupIDFromEvent(event)},
},
}
if err := removeUser.Sign(RELAY_SECRET); err != nil {
log.Println(err)
}
return &removeUser
}
+16 -5
View File
@@ -19,6 +19,7 @@ type Instance struct {
Secret nostr.SecretKey
Events eventstore.Store
Access *AccessStore
Groups *GroupsStore
Blossom *BlossomStore
Management *ManagementStore
Relay *khatru.Relay
@@ -47,25 +48,31 @@ func MakeInstance(hostname string) (*Instance, error) {
Events: &EventStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "__events",
Name: slug.Make(config.Self.Schema) + "_events",
},
},
Access: &AccessStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "__access",
Name: slug.Make(config.Self.Schema) + "_access",
},
},
Groups: &GroupsStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "_groups",
},
},
Blossom: &BlossomStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "__blossom",
Name: slug.Make(config.Self.Schema) + "_blossom",
},
},
Management: &ManagementStore{
Config: config,
Schema: &Schema{
Name: slug.Make(config.Self.Schema) + "__management",
Name: slug.Make(config.Self.Schema) + "_management",
},
},
Relay: khatru.NewRelay(),
@@ -103,6 +110,10 @@ func MakeInstance(hostname string) (*Instance, error) {
log.Fatal("Failed to initialize access store:", err)
}
if err := instance.Groups.Init(); err != nil {
log.Fatal("Failed to initialize groups store:", err)
}
if err := instance.Blossom.Init(); err != nil {
log.Fatal("Failed to initialize blossom store:", err)
}
@@ -112,7 +123,7 @@ func MakeInstance(hostname string) (*Instance, error) {
}
if config.Groups.Enabled {
EnableGroups(instance)
instance.Groups.Enable(instance)
}
if config.Blossom.Enabled {
-2
View File
@@ -178,8 +178,6 @@ func (m *ManagementStore) EventHasStatus(id nostr.ID, status string) bool {
return false
}
// Handlers
// Middleware
func (m *ManagementStore) Enable(instance *Instance) {