From 456f8687ed5a0fc6bd30f8239359aaf3cb441571 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sat, 9 Nov 2024 14:25:30 -0300 Subject: [PATCH] nip29: update to latest NIP version (change some kinds and remove permissions and moderation stuff). --- kinds.go | 259 ++++++++++++++++++++++---------------------- nip29/group.go | 104 +++++++++++++----- nip29/nip29.go | 37 +------ nip29/nip29_test.go | 53 ++++----- nip29/utils.go | 12 ++ 5 files changed, 251 insertions(+), 214 deletions(-) create mode 100644 nip29/utils.go diff --git a/kinds.go b/kinds.go index 177b2fd..feb79b3 100644 --- a/kinds.go +++ b/kinds.go @@ -1,136 +1,135 @@ package nostr const ( - KindProfileMetadata int = 0 - KindTextNote int = 1 - KindRecommendServer int = 2 - KindFollowList int = 3 - KindEncryptedDirectMessage int = 4 - KindDeletion int = 5 - KindRepost int = 6 - KindReaction int = 7 - KindBadgeAward int = 8 - KindSimpleGroupChatMessage int = 9 - KindSimpleGroupThreadedReply int = 10 - KindSimpleGroupThread int = 11 - KindSimpleGroupReply int = 12 - KindSeal int = 13 - KindDirectMessage int = 14 - KindGenericRepost int = 16 - KindReactionToWebsite int = 17 - KindChannelCreation int = 40 - KindChannelMetadata int = 41 - KindChannelMessage int = 42 - KindChannelHideMessage int = 43 - KindChannelMuteUser int = 44 - KindChess int = 64 - KindMergeRequests int = 818 - KindBid int = 1021 - KIndBidConfirmation int = 1022 - KindOpenTimestamps int = 1040 - KindGiftWrap int = 1059 - KindFileMetadata int = 1063 - KindLiveChatMessage int = 1311 - KindPatch int = 1617 - KindIssue int = 1621 - KindReply int = 1622 - KindStatusOpen int = 1630 - KindStatusApplied int = 1631 - KindStatusClosed int = 1632 - KindStatusDraft int = 1633 - KindProblemTracker int = 1971 - KindReporting int = 1984 - KindLabel int = 1985 - KindRelayReviews int = 1986 - KindAIEmbeddings int = 1987 - KindTorrent int = 2003 - KindTorrentComment int = 2004 - KindCoinjoinPool int = 2022 - KindCommunityPostApproval int = 4550 - KindJobFeedback int = 7000 - KindSimpleGroupAddUser int = 9000 - KindSimpleGroupRemoveUser int = 9001 - KindSimpleGroupEditMetadata int = 9002 - KindSimpleGroupAddPermission int = 9003 - KindSimpleGroupRemovePermission int = 9004 - KindSimpleGroupDeleteEvent int = 9005 - KindSimpleGroupEditGroupStatus int = 9006 - KindSimpleGroupCreateGroup int = 9007 - KindSimpleGroupDeleteGroup int = 9008 - KindSimpleGroupJoinRequest int = 9021 - KindSimpleGroupLeaveRequest int = 9022 - KindZapGoal int = 9041 - KindTidalLogin int = 9467 - KindZapRequest int = 9734 - KindZap int = 9735 - KindHighlights int = 9802 - KindMuteList int = 10000 - KindPinList int = 10001 - KindRelayListMetadata int = 10002 - KindBookmarkList int = 10003 - KindCommunityList int = 10004 - KindPublicChatList int = 10005 - KindBlockedRelayList int = 10006 - KindSearchRelayList int = 10007 - KindSimpleGroupList int = 10009 - KindInterestList int = 10015 - KindEmojiList int = 10030 - KindDMRelayList int = 10050 - KindUserServerList int = 10063 - KindFileStorageServerList int = 10096 - KindGoodWikiAuthorList int = 10101 - KindGoodWikiRelayList int = 10102 - KindNWCWalletInfo int = 13194 - KindLightningPubRPC int = 21000 - KindClientAuthentication int = 22242 - KindNWCWalletRequest int = 23194 - KindNWCWalletResponse int = 23195 - KindNostrConnect int = 24133 - KindBlobs int = 24242 - KindHTTPAuth int = 27235 - KindCategorizedPeopleList int = 30000 - KindCategorizedBookmarksList int = 30001 - KindRelaySets int = 30002 - KindBookmarkSets int = 30003 - KindCuratedSets int = 30004 - KindCuratedVideoSets int = 30005 - KindMuteSets int = 30007 - KindProfileBadges int = 30008 - KindBadgeDefinition int = 30009 - KindInterestSets int = 30015 - KindStallDefinition int = 30017 - KindProductDefinition int = 30018 - KindMarketplaceUI int = 30019 - KindProductSoldAsAuction int = 30020 - KindArticle int = 30023 - KindDraftArticle int = 30024 - KindEmojiSets int = 30030 - KindModularArticleHeader int = 30040 - KindModularArticleContent int = 30041 - KindReleaseArtifactSets int = 30063 - KindApplicationSpecificData int = 30078 - KindLiveEvent int = 30311 - KindUserStatuses int = 30315 - KindClassifiedListing int = 30402 - KindDraftClassifiedListing int = 30403 - KindRepositoryAnnouncement int = 30617 - KindRepositoryState int = 30618 - KindSimpleGroupMetadata int = 39000 - KindWikiArticle int = 30818 - KindRedirects int = 30819 - KindFeed int = 31890 - KindDateCalendarEvent int = 31922 - KindTimeCalendarEvent int = 31923 - KindCalendar int = 31924 - KindCalendarEventRSVP int = 31925 - KindHandlerRecommendation int = 31989 - KindHandlerInformation int = 31990 - KindVideoEvent int = 34235 - KindShortVideoEvent int = 34236 - KindVideoViewEvent int = 34237 - KindCommunityDefinition int = 34550 - KindSimpleGroupAdmins int = 39001 - KindSimpleGroupMembers int = 39002 + KindProfileMetadata int = 0 + KindTextNote int = 1 + KindRecommendServer int = 2 + KindFollowList int = 3 + KindEncryptedDirectMessage int = 4 + KindDeletion int = 5 + KindRepost int = 6 + KindReaction int = 7 + KindBadgeAward int = 8 + KindSimpleGroupChatMessage int = 9 + KindSimpleGroupThreadedReply int = 10 + KindSimpleGroupThread int = 11 + KindSimpleGroupReply int = 12 + KindSeal int = 13 + KindDirectMessage int = 14 + KindGenericRepost int = 16 + KindReactionToWebsite int = 17 + KindChannelCreation int = 40 + KindChannelMetadata int = 41 + KindChannelMessage int = 42 + KindChannelHideMessage int = 43 + KindChannelMuteUser int = 44 + KindChess int = 64 + KindMergeRequests int = 818 + KindBid int = 1021 + KIndBidConfirmation int = 1022 + KindOpenTimestamps int = 1040 + KindGiftWrap int = 1059 + KindFileMetadata int = 1063 + KindLiveChatMessage int = 1311 + KindPatch int = 1617 + KindIssue int = 1621 + KindReply int = 1622 + KindStatusOpen int = 1630 + KindStatusApplied int = 1631 + KindStatusClosed int = 1632 + KindStatusDraft int = 1633 + KindProblemTracker int = 1971 + KindReporting int = 1984 + KindLabel int = 1985 + KindRelayReviews int = 1986 + KindAIEmbeddings int = 1987 + KindTorrent int = 2003 + KindTorrentComment int = 2004 + KindCoinjoinPool int = 2022 + KindCommunityPostApproval int = 4550 + KindJobFeedback int = 7000 + KindSimpleGroupPutUser int = 9000 + KindSimpleGroupRemoveUser int = 9001 + KindSimpleGroupEditMetadata int = 9002 + KindSimpleGroupDeleteEvent int = 9005 + KindSimpleGroupCreateGroup int = 9007 + KindSimpleGroupDeleteGroup int = 9008 + KindSimpleGroupCreateInvite int = 9009 + KindSimpleGroupJoinRequest int = 9021 + KindSimpleGroupLeaveRequest int = 9022 + KindZapGoal int = 9041 + KindTidalLogin int = 9467 + KindZapRequest int = 9734 + KindZap int = 9735 + KindHighlights int = 9802 + KindMuteList int = 10000 + KindPinList int = 10001 + KindRelayListMetadata int = 10002 + KindBookmarkList int = 10003 + KindCommunityList int = 10004 + KindPublicChatList int = 10005 + KindBlockedRelayList int = 10006 + KindSearchRelayList int = 10007 + KindSimpleGroupList int = 10009 + KindInterestList int = 10015 + KindEmojiList int = 10030 + KindDMRelayList int = 10050 + KindUserServerList int = 10063 + KindFileStorageServerList int = 10096 + KindGoodWikiAuthorList int = 10101 + KindGoodWikiRelayList int = 10102 + KindNWCWalletInfo int = 13194 + KindLightningPubRPC int = 21000 + KindClientAuthentication int = 22242 + KindNWCWalletRequest int = 23194 + KindNWCWalletResponse int = 23195 + KindNostrConnect int = 24133 + KindBlobs int = 24242 + KindHTTPAuth int = 27235 + KindCategorizedPeopleList int = 30000 + KindCategorizedBookmarksList int = 30001 + KindRelaySets int = 30002 + KindBookmarkSets int = 30003 + KindCuratedSets int = 30004 + KindCuratedVideoSets int = 30005 + KindMuteSets int = 30007 + KindProfileBadges int = 30008 + KindBadgeDefinition int = 30009 + KindInterestSets int = 30015 + KindStallDefinition int = 30017 + KindProductDefinition int = 30018 + KindMarketplaceUI int = 30019 + KindProductSoldAsAuction int = 30020 + KindArticle int = 30023 + KindDraftArticle int = 30024 + KindEmojiSets int = 30030 + KindModularArticleHeader int = 30040 + KindModularArticleContent int = 30041 + KindReleaseArtifactSets int = 30063 + KindApplicationSpecificData int = 30078 + KindLiveEvent int = 30311 + KindUserStatuses int = 30315 + KindClassifiedListing int = 30402 + KindDraftClassifiedListing int = 30403 + KindRepositoryAnnouncement int = 30617 + KindRepositoryState int = 30618 + KindSimpleGroupMetadata int = 39000 + KindSimpleGroupAdmins int = 39001 + KindSimpleGroupMembers int = 39002 + KindSimpleGroupRoles int = 39003 + KindWikiArticle int = 30818 + KindRedirects int = 30819 + KindFeed int = 31890 + KindDateCalendarEvent int = 31922 + KindTimeCalendarEvent int = 31923 + KindCalendar int = 31924 + KindCalendarEventRSVP int = 31925 + KindHandlerRecommendation int = 31989 + KindHandlerInformation int = 31990 + KindVideoEvent int = 34235 + KindShortVideoEvent int = 34236 + KindVideoViewEvent int = 34237 + KindCommunityDefinition int = 34550 ) func IsRegularKind(kind int) bool { diff --git a/nip29/group.go b/nip29/group.go index 177514e..6a5ecde 100644 --- a/nip29/group.go +++ b/nip29/group.go @@ -3,6 +3,7 @@ package nip29 import ( "fmt" "net/url" + "slices" "strings" "github.com/nbd-wtf/go-nostr" @@ -40,13 +41,56 @@ type Group struct { Name string Picture string About string - Members map[string]*Role + Members map[string][]*Role Private bool Closed bool + Roles []*Role + LastMetadataUpdate nostr.Timestamp LastAdminsUpdate 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.Address, + group.Name, + maybePrivate, + maybeClosed, + group.Picture, + group.About, + strings.Join(members, " "), + ) } // NewGroup takes a group address in the form "'" @@ -59,7 +103,7 @@ func NewGroup(gadstr string) (Group, error) { return Group{ Address: gad, Name: gad.ID, - Members: make(map[string]*Role), + Members: make(map[string][]*Role), }, nil } @@ -70,7 +114,7 @@ func NewGroupFromMetadataEvent(relayURL string, evt *nostr.Event) (Group, error) ID: evt.Tags.GetD(), }, Name: evt.Tags.GetD(), - Members: make(map[string]*Role), + Members: make(map[string][]*Role), } err := g.MergeInMetadataEvent(evt) @@ -118,18 +162,20 @@ func (group Group) ToAdminsEvent() *nostr.Event { } evt.Tags[0] = nostr.Tag{"d", group.Address.ID} - for member, role := range group.Members { - if role != nil { - // is an admin - tag := make([]string, 3, 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) + for member, roles := range group.Members { + if len(roles) == 0 { + // is not an admin + continue } + + // is an admin + tag := make([]string, 2, 2+len(roles)) + tag[0] = "p" + tag[1] = member + for _, role := range roles { + tag = append(tag, role.Name) + } + evt.Tags = append(evt.Tags, tag) } return evt @@ -151,6 +197,22 @@ func (group Group) ToMembersEvent() *nostr.Event { 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 { if evt.Kind != nostr.KindSimpleGroupMetadata { 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 } - role := group.Members[tag[1]] - if role == nil { - 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{}{} + for _, roleName := range tag[2:] { + group.Members[tag[1]] = append(group.Members[tag[1]], group.GetRoleByName(roleName)) } } @@ -240,7 +294,7 @@ func (group *Group) MergeInMembersEvent(evt *nostr.Event) error { _, exists := group.Members[tag[1]] if !exists { - group.Members[tag[1]] = EmptyRole + group.Members[tag[1]] = nil } } diff --git a/nip29/nip29.go b/nip29/nip29.go index 8e4b0ed..e0b92c9 100644 --- a/nip29/nip29.go +++ b/nip29/nip29.go @@ -8,58 +8,29 @@ import ( type Role struct { 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 var ModerationEventKinds = KindRange{ - nostr.KindSimpleGroupAddUser, + nostr.KindSimpleGroupPutUser, nostr.KindSimpleGroupRemoveUser, nostr.KindSimpleGroupEditMetadata, - nostr.KindSimpleGroupAddPermission, - nostr.KindSimpleGroupRemovePermission, nostr.KindSimpleGroupDeleteEvent, - nostr.KindSimpleGroupEditGroupStatus, nostr.KindSimpleGroupCreateGroup, nostr.KindSimpleGroupDeleteGroup, + nostr.KindSimpleGroupCreateInvite, } var MetadataEventKinds = KindRange{ nostr.KindSimpleGroupMetadata, nostr.KindSimpleGroupAdmins, nostr.KindSimpleGroupMembers, + nostr.KindSimpleGroupRoles, } func (kr KindRange) Includes(kind int) bool { _, ok := slices.BinarySearch(kr, kind) return ok } - -var ( - // used for normal members without admin powers - EmptyRole *Role = nil - - PermissionsMap = map[Permission]struct{}{ - PermAddUser: {}, - PermEditMetadata: {}, - PermDeleteEvent: {}, - PermRemoveUser: {}, - PermAddPermission: {}, - PermRemovePermission: {}, - PermEditGroupStatus: {}, - } -) diff --git a/nip29/nip29_test.go b/nip29/nip29_test.go index 61e5d5d..3bb3ddd 100644 --- a/nip29/nip29_test.go +++ b/nip29/nip29_test.go @@ -3,7 +3,7 @@ package nip29 import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -19,41 +19,42 @@ func TestGroupEventBackAndForth(t *testing.T) { group1.Private = true meta1 := group1.ToMetadataEvent() - assert.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) - assert.NotNil(t, meta1.Tags.GetFirst([]string{"private"}), "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) + require.NotNil(t, meta1.Tags.GetFirst([]string{"name", "banana"}), "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.Members[ALICE] = &Role{Name: "nada", Permissions: map[Permission]struct{}{PermAddUser: {}}} - group2.Members[BOB] = &Role{Name: "nada", Permissions: map[Permission]struct{}{PermEditMetadata: {}}} - group2.Members[CAROL] = EmptyRole - group2.Members[DEREK] = EmptyRole + group2.Members[ALICE] = []*Role{{Name: "nada"}} + group2.Members[BOB] = []*Role{{Name: "nada"}} + group2.Members[CAROL] = nil + group2.Members[DEREK] = nil admins2 := group2.ToAdminsEvent() - assert.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") - assert.NotNil(t, admins2.Tags.GetFirst([]string{"p", ALICE, "nada", "add-user"}), "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.Equal(t, "abc", admins2.Tags.GetD(), "translation of group2 to admins event failed") + require.Equal(t, 3, len(admins2.Tags), "translation of group2 to admins event failed") + require.NotNil(t, admins2.Tags.GetFirst([]string{"p", ALICE, "nada"}), "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() - assert.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") - assert.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") - assert.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.Equal(t, "abc", members2.Tags.GetD(), "translation of group2 to members2 event failed") + require.Equal(t, 5, len(members2.Tags), "translation of group2 to members2 event failed") + require.NotNil(t, members2.Tags.GetFirst([]string{"p", ALICE}), "translation of group2 to members2 event failed") + require.NotNil(t, members2.Tags.GetFirst([]string{"p", BOB}), "translation of group2 to members2 event failed") + require.NotNil(t, members2.Tags.GetFirst([]string{"p", CAROL}), "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) - assert.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") - assert.Equal(t, EmptyRole, group1.Members[DEREK], "merge of members2 into group1 failed") + require.Equal(t, 4, len(group1.Members), "merge of members2 into group1 failed") + require.Len(t, group1.Members[ALICE], 0, "merge of members2 into group1 failed") + require.Len(t, group1.Members[DEREK], 0, "merge of members2 into group1 failed") group1.MergeInAdminsEvent(admins2) - assert.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, 4, len(group1.Members), "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) - assert.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, "banana", group2.Name, "merge of meta1 into group2 failed") + require.Equal(t, "abc", group2.Address.ID, "merge of meta1 into group2 failed") } diff --git a/nip29/utils.go b/nip29/utils.go new file mode 100644 index 0000000..1990919 --- /dev/null +++ b/nip29/utils.go @@ -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] + } +}