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

259
kinds.go
View File

@ -1,136 +1,135 @@
package nostr package nostr
const ( const (
KindProfileMetadata int = 0 KindProfileMetadata int = 0
KindTextNote int = 1 KindTextNote int = 1
KindRecommendServer int = 2 KindRecommendServer int = 2
KindFollowList int = 3 KindFollowList int = 3
KindEncryptedDirectMessage int = 4 KindEncryptedDirectMessage int = 4
KindDeletion int = 5 KindDeletion int = 5
KindRepost int = 6 KindRepost int = 6
KindReaction int = 7 KindReaction int = 7
KindBadgeAward int = 8 KindBadgeAward int = 8
KindSimpleGroupChatMessage int = 9 KindSimpleGroupChatMessage int = 9
KindSimpleGroupThreadedReply int = 10 KindSimpleGroupThreadedReply int = 10
KindSimpleGroupThread int = 11 KindSimpleGroupThread int = 11
KindSimpleGroupReply int = 12 KindSimpleGroupReply int = 12
KindSeal int = 13 KindSeal int = 13
KindDirectMessage int = 14 KindDirectMessage int = 14
KindGenericRepost int = 16 KindGenericRepost int = 16
KindReactionToWebsite int = 17 KindReactionToWebsite int = 17
KindChannelCreation int = 40 KindChannelCreation int = 40
KindChannelMetadata int = 41 KindChannelMetadata int = 41
KindChannelMessage int = 42 KindChannelMessage int = 42
KindChannelHideMessage int = 43 KindChannelHideMessage int = 43
KindChannelMuteUser int = 44 KindChannelMuteUser int = 44
KindChess int = 64 KindChess int = 64
KindMergeRequests int = 818 KindMergeRequests int = 818
KindBid int = 1021 KindBid int = 1021
KIndBidConfirmation int = 1022 KIndBidConfirmation int = 1022
KindOpenTimestamps int = 1040 KindOpenTimestamps int = 1040
KindGiftWrap int = 1059 KindGiftWrap int = 1059
KindFileMetadata int = 1063 KindFileMetadata int = 1063
KindLiveChatMessage int = 1311 KindLiveChatMessage int = 1311
KindPatch int = 1617 KindPatch int = 1617
KindIssue int = 1621 KindIssue int = 1621
KindReply int = 1622 KindReply int = 1622
KindStatusOpen int = 1630 KindStatusOpen int = 1630
KindStatusApplied int = 1631 KindStatusApplied int = 1631
KindStatusClosed int = 1632 KindStatusClosed int = 1632
KindStatusDraft int = 1633 KindStatusDraft int = 1633
KindProblemTracker int = 1971 KindProblemTracker int = 1971
KindReporting int = 1984 KindReporting int = 1984
KindLabel int = 1985 KindLabel int = 1985
KindRelayReviews int = 1986 KindRelayReviews int = 1986
KindAIEmbeddings int = 1987 KindAIEmbeddings int = 1987
KindTorrent int = 2003 KindTorrent int = 2003
KindTorrentComment int = 2004 KindTorrentComment int = 2004
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 KindSimpleGroupDeleteEvent int = 9005
KindSimpleGroupRemovePermission int = 9004 KindSimpleGroupCreateGroup int = 9007
KindSimpleGroupDeleteEvent int = 9005 KindSimpleGroupDeleteGroup int = 9008
KindSimpleGroupEditGroupStatus int = 9006 KindSimpleGroupCreateInvite int = 9009
KindSimpleGroupCreateGroup int = 9007 KindSimpleGroupJoinRequest int = 9021
KindSimpleGroupDeleteGroup int = 9008 KindSimpleGroupLeaveRequest int = 9022
KindSimpleGroupJoinRequest int = 9021 KindZapGoal int = 9041
KindSimpleGroupLeaveRequest int = 9022 KindTidalLogin int = 9467
KindZapGoal int = 9041 KindZapRequest int = 9734
KindTidalLogin int = 9467 KindZap int = 9735
KindZapRequest int = 9734 KindHighlights int = 9802
KindZap int = 9735 KindMuteList int = 10000
KindHighlights int = 9802 KindPinList int = 10001
KindMuteList int = 10000 KindRelayListMetadata int = 10002
KindPinList int = 10001 KindBookmarkList int = 10003
KindRelayListMetadata int = 10002 KindCommunityList int = 10004
KindBookmarkList int = 10003 KindPublicChatList int = 10005
KindCommunityList int = 10004 KindBlockedRelayList int = 10006
KindPublicChatList int = 10005 KindSearchRelayList int = 10007
KindBlockedRelayList int = 10006 KindSimpleGroupList int = 10009
KindSearchRelayList int = 10007 KindInterestList int = 10015
KindSimpleGroupList int = 10009 KindEmojiList int = 10030
KindInterestList int = 10015 KindDMRelayList int = 10050
KindEmojiList int = 10030 KindUserServerList int = 10063
KindDMRelayList int = 10050 KindFileStorageServerList int = 10096
KindUserServerList int = 10063 KindGoodWikiAuthorList int = 10101
KindFileStorageServerList int = 10096 KindGoodWikiRelayList int = 10102
KindGoodWikiAuthorList int = 10101 KindNWCWalletInfo int = 13194
KindGoodWikiRelayList int = 10102 KindLightningPubRPC int = 21000
KindNWCWalletInfo int = 13194 KindClientAuthentication int = 22242
KindLightningPubRPC int = 21000 KindNWCWalletRequest int = 23194
KindClientAuthentication int = 22242 KindNWCWalletResponse int = 23195
KindNWCWalletRequest int = 23194 KindNostrConnect int = 24133
KindNWCWalletResponse int = 23195 KindBlobs int = 24242
KindNostrConnect int = 24133 KindHTTPAuth int = 27235
KindBlobs int = 24242 KindCategorizedPeopleList int = 30000
KindHTTPAuth int = 27235 KindCategorizedBookmarksList int = 30001
KindCategorizedPeopleList int = 30000 KindRelaySets int = 30002
KindCategorizedBookmarksList int = 30001 KindBookmarkSets int = 30003
KindRelaySets int = 30002 KindCuratedSets int = 30004
KindBookmarkSets int = 30003 KindCuratedVideoSets int = 30005
KindCuratedSets int = 30004 KindMuteSets int = 30007
KindCuratedVideoSets int = 30005 KindProfileBadges int = 30008
KindMuteSets int = 30007 KindBadgeDefinition int = 30009
KindProfileBadges int = 30008 KindInterestSets int = 30015
KindBadgeDefinition int = 30009 KindStallDefinition int = 30017
KindInterestSets int = 30015 KindProductDefinition int = 30018
KindStallDefinition int = 30017 KindMarketplaceUI int = 30019
KindProductDefinition int = 30018 KindProductSoldAsAuction int = 30020
KindMarketplaceUI int = 30019 KindArticle int = 30023
KindProductSoldAsAuction int = 30020 KindDraftArticle int = 30024
KindArticle int = 30023 KindEmojiSets int = 30030
KindDraftArticle int = 30024 KindModularArticleHeader int = 30040
KindEmojiSets int = 30030 KindModularArticleContent int = 30041
KindModularArticleHeader int = 30040 KindReleaseArtifactSets int = 30063
KindModularArticleContent int = 30041 KindApplicationSpecificData int = 30078
KindReleaseArtifactSets int = 30063 KindLiveEvent int = 30311
KindApplicationSpecificData int = 30078 KindUserStatuses int = 30315
KindLiveEvent int = 30311 KindClassifiedListing int = 30402
KindUserStatuses int = 30315 KindDraftClassifiedListing int = 30403
KindClassifiedListing int = 30402 KindRepositoryAnnouncement int = 30617
KindDraftClassifiedListing int = 30403 KindRepositoryState int = 30618
KindRepositoryAnnouncement int = 30617 KindSimpleGroupMetadata int = 39000
KindRepositoryState int = 30618 KindSimpleGroupAdmins int = 39001
KindSimpleGroupMetadata int = 39000 KindSimpleGroupMembers int = 39002
KindWikiArticle int = 30818 KindSimpleGroupRoles int = 39003
KindRedirects int = 30819 KindWikiArticle int = 30818
KindFeed int = 31890 KindRedirects int = 30819
KindDateCalendarEvent int = 31922 KindFeed int = 31890
KindTimeCalendarEvent int = 31923 KindDateCalendarEvent int = 31922
KindCalendar int = 31924 KindTimeCalendarEvent int = 31923
KindCalendarEventRSVP int = 31925 KindCalendar int = 31924
KindHandlerRecommendation int = 31989 KindCalendarEventRSVP int = 31925
KindHandlerInformation int = 31990 KindHandlerRecommendation int = 31989
KindVideoEvent int = 34235 KindHandlerInformation int = 31990
KindShortVideoEvent int = 34236 KindVideoEvent int = 34235
KindVideoViewEvent int = 34237 KindShortVideoEvent int = 34236
KindCommunityDefinition int = 34550 KindVideoViewEvent int = 34237
KindSimpleGroupAdmins int = 39001 KindCommunityDefinition int = 34550
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,18 +162,20 @@ 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 an admin // is not an admin
tag := make([]string, 3, 3+len(role.Permissions)) continue
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)
} }
// 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 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]
}
}