From e4b4654e66931bc89570412c88db196974de4e5a Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 4 Jan 2024 11:29:21 -0300 Subject: [PATCH] nip29: bring in helpers for managing groups to be used on the relay side. --- nip29/group.go | 95 ++++++++++++++- nip29/nip29.go | 13 +-- nip29/relay.go | 308 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 405 insertions(+), 11 deletions(-) create mode 100644 nip29/relay.go diff --git a/nip29/group.go b/nip29/group.go index cf16463..df92841 100644 --- a/nip29/group.go +++ b/nip29/group.go @@ -16,11 +16,13 @@ type Group struct { Closed bool LastMetadataUpdate nostr.Timestamp + LastAdminsUpdate nostr.Timestamp + LastMembersUpdate nostr.Timestamp } func (group Group) ToMetadataEvent() *nostr.Event { evt := &nostr.Event{ - Kind: 39000, + Kind: nostr.KindSimpleGroupMetadata, CreatedAt: group.LastMetadataUpdate, Content: group.About, Tags: nostr.Tags{ @@ -52,6 +54,47 @@ func (group Group) ToMetadataEvent() *nostr.Event { return evt } +func (group Group) ToAdminsEvent() *nostr.Event { + evt := &nostr.Event{ + Kind: nostr.KindSimpleGroupAdmins, + CreatedAt: group.LastAdminsUpdate, + Tags: make(nostr.Tags, 1, 1+len(group.Members)/3), + } + evt.Tags[0] = nostr.Tag{"d", group.ID} + + for member, role := range group.Members { + if role != nil { + // is an admin + tag := make([]string, 0, 3+len(role.Permissions)) + tag[0] = "p" + tag[1] = member + tag[2] = role.Name + for perm := range role.Permissions { + tag = append(tag, string(perm)) + } + evt.Tags = append(evt.Tags, tag) + } + } + + return evt +} + +func (group Group) ToMembersEvent() *nostr.Event { + evt := &nostr.Event{ + Kind: nostr.KindSimpleGroupMembers, + CreatedAt: group.LastMembersUpdate, + Tags: make(nostr.Tags, 1, 1+len(group.Members)), + } + evt.Tags[0] = nostr.Tag{"d", group.ID} + + for member := range group.Members { + // include both admins and normal members + evt.Tags = append(evt.Tags, nostr.Tag{"p", member}) + } + + return evt +} + func (group *Group) MergeInMetadataEvent(evt *nostr.Event) error { if evt.Kind != nostr.KindSimpleGroupMetadata { return fmt.Errorf("expected kind %d, got %d", nostr.KindSimpleGroupMetadata, evt.Kind) @@ -84,3 +127,53 @@ func (group *Group) MergeInMetadataEvent(evt *nostr.Event) error { return nil } + +func (group *Group) MergeInAdminsEvent(evt *nostr.Event) error { + if evt.Kind != nostr.KindSimpleGroupAdmins { + return fmt.Errorf("expected kind %d, got %d", nostr.KindSimpleGroupAdmins, evt.Kind) + } + + if evt.CreatedAt <= group.LastAdminsUpdate { + return fmt.Errorf("event is older than our last update (%d vs %d)", evt.CreatedAt, group.LastAdminsUpdate) + } + + group.LastAdminsUpdate = evt.CreatedAt + for _, tag := range evt.Tags { + if len(tag) < 3 { + continue + } + if tag[0] != "p" { + continue + } + if !nostr.IsValidPublicKeyHex(tag[1]) { + continue + } + } + + return nil +} + +func (group *Group) MergeInMembersEvent(evt *nostr.Event) error { + if evt.Kind != nostr.KindSimpleGroupMembers { + return fmt.Errorf("expected kind %d, got %d", nostr.KindSimpleGroupMembers, evt.Kind) + } + + if evt.CreatedAt <= group.LastMembersUpdate { + return fmt.Errorf("event is older than our last update (%d vs %d)", evt.CreatedAt, group.LastMembersUpdate) + } + + group.LastMembersUpdate = evt.CreatedAt + for _, tag := range evt.Tags { + if len(tag) < 2 { + continue + } + if tag[0] != "p" { + continue + } + if !nostr.IsValidPublicKeyHex(tag[1]) { + continue + } + } + + return nil +} diff --git a/nip29/nip29.go b/nip29/nip29.go index 48a9dc2..ea45651 100644 --- a/nip29/nip29.go +++ b/nip29/nip29.go @@ -2,6 +2,7 @@ package nip29 import ( "github.com/nbd-wtf/go-nostr" + "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) @@ -10,7 +11,7 @@ type Role struct { Permissions map[Permission]struct{} } -type Permission = string +type Permission string const ( PermAddUser Permission = "add-user" @@ -24,15 +25,7 @@ const ( type KindRange []int -var ModerationEventKinds = KindRange{ - nostr.KindSimpleGroupAddUser, - nostr.KindSimpleGroupRemoveUser, - nostr.KindSimpleGroupEditMetadata, - nostr.KindSimpleGroupAddPermission, - nostr.KindSimpleGroupRemovePermission, - nostr.KindSimpleGroupDeleteEvent, - nostr.KindSimpleGroupEditGroupStatus, -} +var ModerationEventKinds = KindRange(maps.Keys(moderationActionFactories)) var MetadataEventKinds = KindRange{ nostr.KindSimpleGroupMetadata, diff --git a/nip29/relay.go b/nip29/relay.go new file mode 100644 index 0000000..a95107f --- /dev/null +++ b/nip29/relay.go @@ -0,0 +1,308 @@ +package nip29 + +import ( + "fmt" + + "github.com/nbd-wtf/go-nostr" +) + +var ( + // used for the default role, the actual relay, hidden otherwise + MasterRole *Role = &Role{ + Name: "master", + Permissions: map[Permission]struct{}{ + PermAddUser: {}, + PermEditMetadata: {}, + PermDeleteEvent: {}, + PermRemoveUser: {}, + PermAddPermission: {}, + PermRemovePermission: {}, + PermEditGroupStatus: {}, + }, + } + + // used for normal members without admin powers, not displayed + EmptyRole *Role = nil + + PermissionsMap = map[Permission]struct{}{ + PermAddUser: {}, + PermEditMetadata: {}, + PermDeleteEvent: {}, + PermRemoveUser: {}, + PermAddPermission: {}, + PermRemovePermission: {}, + PermEditGroupStatus: {}, + } +) + +type Action interface { + Apply(group *Group) + PermissionName() Permission +} + +func GetModerationAction(evt *nostr.Event) (Action, error) { + factory, ok := moderationActionFactories[evt.Kind] + if !ok { + return nil, fmt.Errorf("event kind %d is not a supported moderation action", evt.Kind) + } + return factory(evt) +} + +var moderationActionFactories = map[int]func(*nostr.Event) (Action, error){ + nostr.KindSimpleGroupAddUser: func(evt *nostr.Event) (Action, error) { + targets := make([]string, 0, len(evt.Tags)) + for _, tag := range evt.Tags.GetAll([]string{"p", ""}) { + if !nostr.IsValidPublicKeyHex(tag[1]) { + return nil, fmt.Errorf("") + } + targets = append(targets, tag[1]) + } + if len(targets) > 0 { + return &AddUser{Targets: targets}, nil + } + return nil, fmt.Errorf("missing 'p' tags") + }, + nostr.KindSimpleGroupRemoveUser: func(evt *nostr.Event) (Action, error) { + targets := make([]string, 0, len(evt.Tags)) + for _, tag := range evt.Tags.GetAll([]string{"p", ""}) { + if !nostr.IsValidPublicKeyHex(tag[1]) { + return nil, fmt.Errorf("invalid public key hex") + } + targets = append(targets, tag[1]) + } + if len(targets) > 0 { + return &RemoveUser{Targets: targets}, nil + } + return nil, fmt.Errorf("missing 'p' tags") + }, + nostr.KindSimpleGroupEditMetadata: func(evt *nostr.Event) (Action, error) { + ok := false + edit := EditMetadata{When: evt.CreatedAt} + if t := evt.Tags.GetFirst([]string{"name", ""}); t != nil { + edit.NameValue = (*t)[1] + ok = true + } + if t := evt.Tags.GetFirst([]string{"picture", ""}); t != nil { + edit.PictureValue = (*t)[1] + ok = true + } + if t := evt.Tags.GetFirst([]string{"about", ""}); t != nil { + edit.AboutValue = (*t)[1] + ok = true + } + if ok { + return &edit, nil + } + return nil, fmt.Errorf("missing metadata tags") + }, + nostr.KindSimpleGroupAddPermission: func(evt *nostr.Event) (Action, error) { + nTags := len(evt.Tags) + + permissions := make([]Permission, 0, nTags-1) + for _, tag := range evt.Tags.GetAll([]string{"permission", ""}) { + perm := Permission(tag[1]) + if _, ok := PermissionsMap[perm]; !ok { + return nil, fmt.Errorf("unknown permission '%s'", tag[1]) + } + permissions = append(permissions, perm) + } + + targets := make([]string, 0, nTags-1) + for _, tag := range evt.Tags.GetAll([]string{"p", ""}) { + if !nostr.IsValidPublicKeyHex(tag[1]) { + return nil, fmt.Errorf("invalid public key hex") + } + targets = append(targets, tag[1]) + } + + if len(permissions) > 0 && len(targets) > 0 { + return &AddPermission{Targets: targets, Permissions: permissions}, nil + } + + return nil, fmt.Errorf("") + }, + nostr.KindSimpleGroupRemovePermission: func(evt *nostr.Event) (Action, error) { + nTags := len(evt.Tags) + + permissions := make([]Permission, 0, nTags-1) + for _, tag := range evt.Tags.GetAll([]string{"permission", ""}) { + perm := Permission(tag[1]) + if _, ok := PermissionsMap[perm]; !ok { + return nil, fmt.Errorf("unknown permission '%s'", tag[1]) + } + permissions = append(permissions, perm) + } + + targets := make([]string, 0, nTags-1) + for _, tag := range evt.Tags.GetAll([]string{"p", ""}) { + if !nostr.IsValidPublicKeyHex(tag[1]) { + return nil, fmt.Errorf("invalid public key hex") + } + targets = append(targets, tag[1]) + } + + if len(permissions) > 0 && len(targets) > 0 { + return &RemovePermission{Targets: targets, Permissions: permissions}, nil + } + + return nil, fmt.Errorf("") + }, + nostr.KindSimpleGroupDeleteEvent: func(evt *nostr.Event) (Action, error) { + tags := evt.Tags.GetAll([]string{"e", ""}) + if len(tags) == 0 { + return nil, fmt.Errorf("missing 'e' tag") + } + + targets := make([]string, len(tags)) + for i, tag := range tags { + if nostr.IsValidPublicKeyHex(tag[1]) { + targets[i] = tag[1] + } else { + return nil, fmt.Errorf("invalid event id hex") + } + } + + return &DeleteEvent{Targets: targets}, nil + }, + nostr.KindSimpleGroupEditGroupStatus: func(evt *nostr.Event) (Action, error) { + egs := EditGroupStatus{When: evt.CreatedAt} + + egs.Public = evt.Tags.GetFirst([]string{"public"}) != nil + egs.Private = evt.Tags.GetFirst([]string{"private"}) != nil + egs.Open = evt.Tags.GetFirst([]string{"open"}) != nil + egs.Closed = evt.Tags.GetFirst([]string{"closed"}) != nil + + // disallow contradictions + if egs.Public && egs.Private { + return nil, fmt.Errorf("contradiction") + } + if egs.Open && egs.Closed { + return nil, fmt.Errorf("contradiction") + } + + // TODO remove this once we start supporting private groups + if egs.Private { + return nil, fmt.Errorf("private groups not yet supported") + } + + return egs, nil + }, +} + +type DeleteEvent struct { + Targets []string +} + +func (DeleteEvent) PermissionName() Permission { return PermDeleteEvent } +func (a DeleteEvent) Apply(group *Group) {} + +type AddUser struct { + Targets []string +} + +func (AddUser) PermissionName() Permission { return PermAddUser } +func (a AddUser) Apply(group *Group) { + for _, target := range a.Targets { + group.Members[target] = EmptyRole + } +} + +type RemoveUser struct { + Targets []string +} + +func (RemoveUser) PermissionName() Permission { return PermRemoveUser } +func (a RemoveUser) Apply(group *Group) { + for _, target := range a.Targets { + delete(group.Members, target) + } +} + +type EditMetadata struct { + NameValue string + PictureValue string + AboutValue string + When nostr.Timestamp +} + +func (EditMetadata) PermissionName() Permission { return PermEditMetadata } +func (a EditMetadata) Apply(group *Group) { + group.Name = a.NameValue + group.Picture = a.PictureValue + group.About = a.AboutValue + group.LastMetadataUpdate = a.When +} + +type AddPermission struct { + Targets []string + Permissions []Permission +} + +func (AddPermission) PermissionName() Permission { return PermAddPermission } +func (a AddPermission) Apply(group *Group) { + for _, target := range a.Targets { + role, ok := group.Members[target] + + // if it's a normal user, create a new permissions object thing for this user + // instead of modifying the global EmptyRole + if !ok || role == EmptyRole { + role = &Role{Permissions: make(map[Permission]struct{})} + group.Members[target] = role + } + + // add all permissions listed + for _, perm := range a.Permissions { + role.Permissions[perm] = struct{}{} + } + } +} + +type RemovePermission struct { + Targets []string + Permissions []Permission +} + +func (RemovePermission) PermissionName() Permission { return PermRemovePermission } +func (a RemovePermission) Apply(group *Group) { + for _, target := range a.Targets { + role, ok := group.Members[target] + if !ok || role == EmptyRole { + continue + } + + // remove all permissions listed + for _, perm := range a.Permissions { + delete(role.Permissions, perm) + } + + // if no more permissions are available, change this guy to be a normal user + if role.Name == "" && len(role.Permissions) == 0 { + group.Members[target] = EmptyRole + } + } +} + +type EditGroupStatus struct { + Public bool + Private bool + Open bool + Closed bool + When nostr.Timestamp +} + +func (EditGroupStatus) PermissionName() Permission { return PermEditGroupStatus } +func (a EditGroupStatus) Apply(group *Group) { + if a.Public { + group.Private = false + } else if a.Private { + group.Private = true + } + + if a.Open { + group.Closed = false + } else if a.Closed { + group.Closed = true + } + + group.LastMetadataUpdate = a.When +}