nip29: update to latest NIP version (change some kinds and remove permissions and moderation stuff).

This commit is contained in:
fiatjaf 2024-11-09 14:25:30 -03:00
parent 7fc2f88e79
commit 456f8687ed
5 changed files with 251 additions and 214 deletions

View File

@ -48,15 +48,13 @@ const (
KindCoinjoinPool int = 2022 KindCoinjoinPool int = 2022
KindCommunityPostApproval int = 4550 KindCommunityPostApproval int = 4550
KindJobFeedback int = 7000 KindJobFeedback int = 7000
KindSimpleGroupAddUser int = 9000 KindSimpleGroupPutUser int = 9000
KindSimpleGroupRemoveUser int = 9001 KindSimpleGroupRemoveUser int = 9001
KindSimpleGroupEditMetadata int = 9002 KindSimpleGroupEditMetadata int = 9002
KindSimpleGroupAddPermission int = 9003
KindSimpleGroupRemovePermission int = 9004
KindSimpleGroupDeleteEvent int = 9005 KindSimpleGroupDeleteEvent int = 9005
KindSimpleGroupEditGroupStatus int = 9006
KindSimpleGroupCreateGroup int = 9007 KindSimpleGroupCreateGroup int = 9007
KindSimpleGroupDeleteGroup int = 9008 KindSimpleGroupDeleteGroup int = 9008
KindSimpleGroupCreateInvite int = 9009
KindSimpleGroupJoinRequest int = 9021 KindSimpleGroupJoinRequest int = 9021
KindSimpleGroupLeaveRequest int = 9022 KindSimpleGroupLeaveRequest int = 9022
KindZapGoal int = 9041 KindZapGoal int = 9041
@ -116,6 +114,9 @@ const (
KindRepositoryAnnouncement int = 30617 KindRepositoryAnnouncement int = 30617
KindRepositoryState int = 30618 KindRepositoryState int = 30618
KindSimpleGroupMetadata int = 39000 KindSimpleGroupMetadata int = 39000
KindSimpleGroupAdmins int = 39001
KindSimpleGroupMembers int = 39002
KindSimpleGroupRoles int = 39003
KindWikiArticle int = 30818 KindWikiArticle int = 30818
KindRedirects int = 30819 KindRedirects int = 30819
KindFeed int = 31890 KindFeed int = 31890
@ -129,8 +130,6 @@ const (
KindShortVideoEvent int = 34236 KindShortVideoEvent int = 34236
KindVideoViewEvent int = 34237 KindVideoViewEvent int = 34237
KindCommunityDefinition int = 34550 KindCommunityDefinition int = 34550
KindSimpleGroupAdmins int = 39001
KindSimpleGroupMembers int = 39002
) )
func IsRegularKind(kind int) bool { func IsRegularKind(kind int) bool {

View File

@ -3,6 +3,7 @@ package nip29
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"slices"
"strings" "strings"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
@ -40,13 +41,56 @@ type Group struct {
Name string Name string
Picture string Picture string
About string About string
Members map[string]*Role Members map[string][]*Role
Private bool Private bool
Closed bool Closed bool
Roles []*Role
LastMetadataUpdate nostr.Timestamp LastMetadataUpdate nostr.Timestamp
LastAdminsUpdate nostr.Timestamp LastAdminsUpdate nostr.Timestamp
LastMembersUpdate nostr.Timestamp LastMembersUpdate nostr.Timestamp
LastRolesUpdate nostr.Timestamp
}
func (group Group) String() string {
maybePrivate := ""
maybeClosed := ""
if group.Private {
maybePrivate = " private"
}
if group.Closed {
maybeClosed = " closed"
}
members := make([]string, len(group.Members))
i := 0
for pubkey, roles := range group.Members {
members[i] = pubkey
if len(roles) > 0 {
members[i] += ":"
}
for _, role := range roles {
members[i] += role.Name
if slices.Contains(group.Roles, role) {
members[i] += "*"
}
members[i] += "/"
}
members[i] = strings.TrimRight(members[i], "/")
i++
}
return fmt.Sprintf(`<Group %s name="%s"%s%s picture="%s" about="%s" members=[%v]>`,
group.Address,
group.Name,
maybePrivate,
maybeClosed,
group.Picture,
group.About,
strings.Join(members, " "),
)
} }
// NewGroup takes a group address in the form "<id>'<relay-hostname>" // NewGroup takes a group address in the form "<id>'<relay-hostname>"
@ -59,7 +103,7 @@ func NewGroup(gadstr string) (Group, error) {
return Group{ return Group{
Address: gad, Address: gad,
Name: gad.ID, Name: gad.ID,
Members: make(map[string]*Role), Members: make(map[string][]*Role),
}, nil }, nil
} }
@ -70,7 +114,7 @@ func NewGroupFromMetadataEvent(relayURL string, evt *nostr.Event) (Group, error)
ID: evt.Tags.GetD(), ID: evt.Tags.GetD(),
}, },
Name: evt.Tags.GetD(), Name: evt.Tags.GetD(),
Members: make(map[string]*Role), Members: make(map[string][]*Role),
} }
err := g.MergeInMetadataEvent(evt) err := g.MergeInMetadataEvent(evt)
@ -118,19 +162,21 @@ func (group Group) ToAdminsEvent() *nostr.Event {
} }
evt.Tags[0] = nostr.Tag{"d", group.Address.ID} evt.Tags[0] = nostr.Tag{"d", group.Address.ID}
for member, role := range group.Members { for member, roles := range group.Members {
if role != nil { if len(roles) == 0 {
// is not an admin
continue
}
// is an admin // is an admin
tag := make([]string, 3, 3+len(role.Permissions)) tag := make([]string, 2, 2+len(roles))
tag[0] = "p" tag[0] = "p"
tag[1] = member tag[1] = member
tag[2] = role.Name for _, role := range roles {
for perm := range role.Permissions { tag = append(tag, role.Name)
tag = append(tag, string(perm))
} }
evt.Tags = append(evt.Tags, tag) evt.Tags = append(evt.Tags, tag)
} }
}
return evt return evt
} }
@ -151,6 +197,22 @@ func (group Group) ToMembersEvent() *nostr.Event {
return evt return evt
} }
func (group Group) ToRolesEvent() *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.Address.ID}
for _, role := range group.Roles {
// include both admins and normal members
evt.Tags = append(evt.Tags, nostr.Tag{"role", role.Name, role.Description})
}
return evt
}
func (group *Group) MergeInMetadataEvent(evt *nostr.Event) error { func (group *Group) MergeInMetadataEvent(evt *nostr.Event) error {
if evt.Kind != nostr.KindSimpleGroupMetadata { if evt.Kind != nostr.KindSimpleGroupMetadata {
return fmt.Errorf("expected kind %d, got %d", nostr.KindSimpleGroupMetadata, evt.Kind) return fmt.Errorf("expected kind %d, got %d", nostr.KindSimpleGroupMetadata, evt.Kind)
@ -202,16 +264,8 @@ func (group *Group) MergeInAdminsEvent(evt *nostr.Event) error {
continue continue
} }
role := group.Members[tag[1]] for _, roleName := range tag[2:] {
if role == nil { group.Members[tag[1]] = append(group.Members[tag[1]], group.GetRoleByName(roleName))
role = &Role{Name: tag[2]}
group.Members[tag[1]] = role
}
if role.Permissions == nil {
role.Permissions = make(map[Permission]struct{}, len(tag)-3)
}
for _, perm := range tag[2:] {
role.Permissions[Permission(perm)] = struct{}{}
} }
} }
@ -240,7 +294,7 @@ func (group *Group) MergeInMembersEvent(evt *nostr.Event) error {
_, exists := group.Members[tag[1]] _, exists := group.Members[tag[1]]
if !exists { if !exists {
group.Members[tag[1]] = EmptyRole group.Members[tag[1]] = nil
} }
} }

View File

@ -8,58 +8,29 @@ import (
type Role struct { type Role struct {
Name string Name string
Permissions map[Permission]struct{} Description string
} }
type Permission string
const (
PermAddUser Permission = "add-user"
PermEditMetadata Permission = "edit-metadata"
PermDeleteEvent Permission = "delete-event"
PermRemoveUser Permission = "remove-user"
PermAddPermission Permission = "add-permission"
PermRemovePermission Permission = "remove-permission"
PermEditGroupStatus Permission = "edit-group-status"
PermDeleteGroup Permission = "delete-group"
)
type KindRange []int type KindRange []int
var ModerationEventKinds = KindRange{ var ModerationEventKinds = KindRange{
nostr.KindSimpleGroupAddUser, nostr.KindSimpleGroupPutUser,
nostr.KindSimpleGroupRemoveUser, nostr.KindSimpleGroupRemoveUser,
nostr.KindSimpleGroupEditMetadata, nostr.KindSimpleGroupEditMetadata,
nostr.KindSimpleGroupAddPermission,
nostr.KindSimpleGroupRemovePermission,
nostr.KindSimpleGroupDeleteEvent, nostr.KindSimpleGroupDeleteEvent,
nostr.KindSimpleGroupEditGroupStatus,
nostr.KindSimpleGroupCreateGroup, nostr.KindSimpleGroupCreateGroup,
nostr.KindSimpleGroupDeleteGroup, nostr.KindSimpleGroupDeleteGroup,
nostr.KindSimpleGroupCreateInvite,
} }
var MetadataEventKinds = KindRange{ var MetadataEventKinds = KindRange{
nostr.KindSimpleGroupMetadata, nostr.KindSimpleGroupMetadata,
nostr.KindSimpleGroupAdmins, nostr.KindSimpleGroupAdmins,
nostr.KindSimpleGroupMembers, nostr.KindSimpleGroupMembers,
nostr.KindSimpleGroupRoles,
} }
func (kr KindRange) Includes(kind int) bool { func (kr KindRange) Includes(kind int) bool {
_, ok := slices.BinarySearch(kr, kind) _, ok := slices.BinarySearch(kr, kind)
return ok return ok
} }
var (
// used for normal members without admin powers
EmptyRole *Role = nil
PermissionsMap = map[Permission]struct{}{
PermAddUser: {},
PermEditMetadata: {},
PermDeleteEvent: {},
PermRemoveUser: {},
PermAddPermission: {},
PermRemovePermission: {},
PermEditGroupStatus: {},
}
)

View File

@ -3,7 +3,7 @@ package nip29
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
) )
const ( const (
@ -19,41 +19,42 @@ func TestGroupEventBackAndForth(t *testing.T) {
group1.Private = true group1.Private = true
meta1 := group1.ToMetadataEvent() meta1 := group1.ToMetadataEvent()
assert.Equal(t, "xyz", meta1.Tags.GetD(), "translation of group1 to metadata event failed: %s", meta1) require.Equal(t, "xyz", meta1.Tags.GetD(), "translation of group1 to metadata event failed: %s", meta1)
assert.NotNil(t, meta1.Tags.GetFirst([]string{"name", "banana"}), "translation of group1 to metadata event failed: %s", meta1) require.NotNil(t, meta1.Tags.GetFirst([]string{"name", "banana"}), "translation of group1 to metadata event failed: %s", meta1)
assert.NotNil(t, meta1.Tags.GetFirst([]string{"private"}), "translation of group1 to metadata event failed: %s", meta1) require.NotNil(t, meta1.Tags.GetFirst([]string{"private"}), "translation of group1 to metadata event failed: %s", meta1)
group2, _ := NewGroup("groups.com'abc") group2, _ := NewGroup("groups.com'abc")
group2.Members[ALICE] = &Role{Name: "nada", Permissions: map[Permission]struct{}{PermAddUser: {}}} group2.Members[ALICE] = []*Role{{Name: "nada"}}
group2.Members[BOB] = &Role{Name: "nada", Permissions: map[Permission]struct{}{PermEditMetadata: {}}} group2.Members[BOB] = []*Role{{Name: "nada"}}
group2.Members[CAROL] = EmptyRole group2.Members[CAROL] = nil
group2.Members[DEREK] = EmptyRole group2.Members[DEREK] = nil
admins2 := group2.ToAdminsEvent() admins2 := group2.ToAdminsEvent()
assert.Equal(t, "abc", admins2.Tags.GetD(), "translation of group2 to admins event failed") require.Equal(t, "abc", admins2.Tags.GetD(), "translation of group2 to admins event failed")
assert.Equal(t, 3, len(admins2.Tags), "translation of group2 to admins event failed") require.Equal(t, 3, len(admins2.Tags), "translation of group2 to admins event failed")
assert.NotNil(t, admins2.Tags.GetFirst([]string{"p", ALICE, "nada", "add-user"}), "translation of group2 to admins event failed") require.NotNil(t, admins2.Tags.GetFirst([]string{"p", ALICE, "nada"}), "translation of group2 to admins event failed")
assert.NotNil(t, admins2.Tags.GetFirst([]string{"p", BOB, "nada", "edit-metadata"}), "translation of group2 to admins event failed") require.NotNil(t, admins2.Tags.GetFirst([]string{"p", BOB, "nada"}), "translation of group2 to admins event failed")
members2 := group2.ToMembersEvent() members2 := group2.ToMembersEvent()
assert.Equal(t, "abc", members2.Tags.GetD(), "translation of group2 to members2 event failed") require.Equal(t, "abc", members2.Tags.GetD(), "translation of group2 to members2 event failed")
assert.Equal(t, 5, len(members2.Tags), "translation of group2 to members2 event failed") require.Equal(t, 5, len(members2.Tags), "translation of group2 to members2 event failed")
assert.NotNil(t, members2.Tags.GetFirst([]string{"p", ALICE}), "translation of group2 to members2 event failed") require.NotNil(t, members2.Tags.GetFirst([]string{"p", ALICE}), "translation of group2 to members2 event failed")
assert.NotNil(t, members2.Tags.GetFirst([]string{"p", BOB}), "translation of group2 to members2 event failed") require.NotNil(t, members2.Tags.GetFirst([]string{"p", BOB}), "translation of group2 to members2 event failed")
assert.NotNil(t, members2.Tags.GetFirst([]string{"p", CAROL}), "translation of group2 to members2 event failed") require.NotNil(t, members2.Tags.GetFirst([]string{"p", CAROL}), "translation of group2 to members2 event failed")
assert.NotNil(t, members2.Tags.GetFirst([]string{"p", DEREK}), "translation of group2 to members2 event failed") require.NotNil(t, members2.Tags.GetFirst([]string{"p", DEREK}), "translation of group2 to members2 event failed")
group1.MergeInMembersEvent(members2) group1.MergeInMembersEvent(members2)
assert.Equal(t, 4, len(group1.Members), "merge of members2 into group1 failed") require.Equal(t, 4, len(group1.Members), "merge of members2 into group1 failed")
assert.Equal(t, EmptyRole, group1.Members[ALICE], "merge of members2 into group1 failed") require.Len(t, group1.Members[ALICE], 0, "merge of members2 into group1 failed")
assert.Equal(t, EmptyRole, group1.Members[DEREK], "merge of members2 into group1 failed") require.Len(t, group1.Members[DEREK], 0, "merge of members2 into group1 failed")
group1.MergeInAdminsEvent(admins2) group1.MergeInAdminsEvent(admins2)
assert.Equal(t, 4, len(group1.Members), "merge of admins2 into group1 failed") require.Equal(t, 4, len(group1.Members), "merge of admins2 into group1 failed")
assert.Equal(t, "nada", group1.Members[ALICE].Name, "merge of admins2 into group1 failed")
assert.Equal(t, EmptyRole, group1.Members[DEREK], "merge of admins2 into group1 failed") require.Equal(t, "nada", group1.Members[ALICE][0].Name, "merge of admins2 into group1 failed")
require.Len(t, group1.Members[DEREK], 0, "merge of admins2 into group1 failed")
group2.MergeInMetadataEvent(meta1) group2.MergeInMetadataEvent(meta1)
assert.Equal(t, "banana", group2.Name, "merge of meta1 into group2 failed") require.Equal(t, "banana", group2.Name, "merge of meta1 into group2 failed")
assert.Equal(t, "abc", group2.Address.ID, "merge of meta1 into group2 failed") require.Equal(t, "abc", group2.Address.ID, "merge of meta1 into group2 failed")
} }

12
nip29/utils.go Normal file
View File

@ -0,0 +1,12 @@
package nip29
import "slices"
func (group Group) GetRoleByName(name string) *Role {
idx := slices.IndexFunc(group.Roles, func(role *Role) bool { return role.Name == name })
if idx == -1 {
return &Role{Name: name}
} else {
return group.Roles[idx]
}
}