diff --git a/nip29/group.go b/nip29/group.go index e37bdc0..1a4acdf 100644 --- a/nip29/group.go +++ b/nip29/group.go @@ -41,11 +41,12 @@ type Group struct { Name string Picture string About string - Members map[string][]*Role + Members map[nostr.PubKey][]*Role Private bool Closed bool - Roles []*Role + Roles []*Role + InviteCodes []string LastMetadataUpdate nostr.Timestamp LastAdminsUpdate nostr.Timestamp @@ -67,7 +68,7 @@ func (group Group) String() string { members := make([]string, len(group.Members)) i := 0 for pubkey, roles := range group.Members { - members[i] = pubkey + members[i] = pubkey.Hex() if len(roles) > 0 { members[i] += ":" } @@ -103,7 +104,7 @@ func NewGroup(gadstr string) (Group, error) { return Group{ Address: gad, Name: gad.ID, - Members: make(map[string][]*Role), + Members: make(map[nostr.PubKey][]*Role), }, nil } @@ -114,15 +115,15 @@ 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[nostr.PubKey][]*Role), } err := g.MergeInMetadataEvent(evt) return g, err } -func (group Group) ToMetadataEvent() *nostr.Event { - evt := &nostr.Event{ +func (group Group) ToMetadataEvent() nostr.Event { + evt := nostr.Event{ Kind: nostr.KindSimpleGroupMetadata, CreatedAt: group.LastMetadataUpdate, Tags: nostr.Tags{ @@ -154,8 +155,8 @@ func (group Group) ToMetadataEvent() *nostr.Event { return evt } -func (group Group) ToAdminsEvent() *nostr.Event { - evt := &nostr.Event{ +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), @@ -171,7 +172,7 @@ func (group Group) ToAdminsEvent() *nostr.Event { // is an admin tag := make([]string, 2, 2+len(roles)) tag[0] = "p" - tag[1] = member + tag[1] = member.Hex() for _, role := range roles { tag = append(tag, role.Name) } @@ -181,8 +182,8 @@ func (group Group) ToAdminsEvent() *nostr.Event { return evt } -func (group Group) ToMembersEvent() *nostr.Event { - evt := &nostr.Event{ +func (group Group) ToMembersEvent() nostr.Event { + evt := nostr.Event{ Kind: nostr.KindSimpleGroupMembers, CreatedAt: group.LastMembersUpdate, Tags: make(nostr.Tags, 1, 1+len(group.Members)), @@ -191,14 +192,14 @@ func (group Group) ToMembersEvent() *nostr.Event { for member := range group.Members { // include both admins and normal members - evt.Tags = append(evt.Tags, nostr.Tag{"p", member}) + evt.Tags = append(evt.Tags, nostr.Tag{"p", member.Hex()}) } return evt } -func (group Group) ToRolesEvent() *nostr.Event { - evt := &nostr.Event{ +func (group Group) ToRolesEvent() nostr.Event { + evt := nostr.Event{ Kind: nostr.KindSimpleGroupRoles, CreatedAt: group.LastMembersUpdate, Tags: make(nostr.Tags, 1, 1+len(group.Members)), @@ -260,12 +261,14 @@ func (group *Group) MergeInAdminsEvent(evt *nostr.Event) error { if tag[0] != "p" { continue } - if !nostr.IsValid32ByteHex(tag[1]) { + + member, err := nostr.PubKeyFromHex(tag[1]) + if err != nil { continue } for _, roleName := range tag[2:] { - group.Members[tag[1]] = append(group.Members[tag[1]], group.GetRoleByName(roleName)) + group.Members[member] = append(group.Members[member], group.GetRoleByName(roleName)) } } @@ -288,13 +291,15 @@ func (group *Group) MergeInMembersEvent(evt *nostr.Event) error { if tag[0] != "p" { continue } - if !nostr.IsValid32ByteHex(tag[1]) { + + member, err := nostr.PubKeyFromHex(tag[1]) + if err != nil { continue } - _, exists := group.Members[tag[1]] + _, exists := group.Members[member] if !exists { - group.Members[tag[1]] = nil + group.Members[member] = nil } } diff --git a/nip29/moderation_actions.go b/nip29/moderation_actions.go new file mode 100644 index 0000000..7c66069 --- /dev/null +++ b/nip29/moderation_actions.go @@ -0,0 +1,272 @@ +package nip29 + +import ( + "fmt" + "slices" + + "fiatjaf.com/nostr" +) + +var PTagNotValidPublicKey = fmt.Errorf("'p' tag value is not a valid public key") + +type Action interface { + Apply(group *Group) + Name() string +} + +var ( + _ Action = PutUser{} + _ Action = RemoveUser{} + _ Action = CreateGroup{} + _ Action = DeleteEvent{} + _ Action = EditMetadata{} + _ Action = CreateInvite{} +) + +func PrepareModerationAction(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[nostr.Kind]func(nostr.Event) (Action, error){ + nostr.KindSimpleGroupPutUser: func(evt nostr.Event) (Action, error) { + targets := make([]PubKeyRoles, 0, len(evt.Tags)) + for tag := range evt.Tags.FindAll("p") { + target, err := nostr.PubKeyFromHex(tag[1]) + if err != nil { + return nil, PTagNotValidPublicKey + } + + targets = append(targets, PubKeyRoles{ + PubKey: target, + RoleNames: tag[2:], + }) + } + + var inviteCode string + if ctag := evt.Tags.Find("code"); ctag != nil { + inviteCode = ctag[1] + } + + if len(targets) > 0 { + return PutUser{ + Targets: targets, + InviteCode: inviteCode, + When: evt.CreatedAt, + }, nil + } + return nil, fmt.Errorf("missing 'p' tags") + }, + nostr.KindSimpleGroupRemoveUser: func(evt nostr.Event) (Action, error) { + targets := make([]nostr.PubKey, 0, len(evt.Tags)) + for tag := range evt.Tags.FindAll("p") { + target, err := nostr.PubKeyFromHex(tag[1]) + if err != nil { + return nil, PTagNotValidPublicKey + } + + targets = append(targets, target) + } + if len(targets) > 0 { + return RemoveUser{Targets: targets, When: evt.CreatedAt}, 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.Find("name"); t != nil { + edit.NameValue = &t[1] + ok = true + } + if t := evt.Tags.Find("picture"); t != nil { + edit.PictureValue = &t[1] + ok = true + } + if t := evt.Tags.Find("about"); t != nil { + edit.AboutValue = &t[1] + ok = true + } + + y := true + n := false + + if evt.Tags.Has("public") { + edit.PrivateValue = &n + ok = true + } else if evt.Tags.Has("private") { + edit.PrivateValue = &y + ok = true + } + + if evt.Tags.Has("open") { + edit.ClosedValue = &n + ok = true + } else if evt.Tags.Has("closed") { + edit.ClosedValue = &y + ok = true + } + + if ok { + return edit, nil + } + return nil, fmt.Errorf("missing metadata tags") + }, + nostr.KindSimpleGroupDeleteEvent: func(evt nostr.Event) (Action, error) { + missing := true + targets := make([]nostr.ID, 0, 2) + for tag := range evt.Tags.FindAll("e") { + id, err := nostr.IDFromHex(tag[1]) + if err != nil { + return nil, fmt.Errorf("invalid event id hex") + } + + targets = append(targets, id) + } + + if missing { + return nil, fmt.Errorf("missing 'e' tag") + } + + return DeleteEvent{Targets: targets}, nil + }, + nostr.KindSimpleGroupCreateGroup: func(evt nostr.Event) (Action, error) { + return CreateGroup{Creator: evt.PubKey, When: evt.CreatedAt}, nil + }, + nostr.KindSimpleGroupDeleteGroup: func(evt nostr.Event) (Action, error) { + return DeleteGroup{When: evt.CreatedAt}, nil + }, + nostr.KindSimpleGroupCreateInvite: func(evt nostr.Event) (Action, error) { + codes := make([]string, 0) + for tag := range evt.Tags.FindAll("code") { + codes = append(codes, tag[1]) + } + if len(codes) == 0 { + return nil, fmt.Errorf("missing 'code' tags") + } else if len(codes) > 10 { + return nil, fmt.Errorf("too many 'code' tags") + } + return CreateInvite{Codes: codes}, nil + }, +} + +type DeleteEvent struct { + Targets []nostr.ID +} + +func (_ DeleteEvent) Name() string { return "delete-event" } +func (a DeleteEvent) Apply(group *Group) {} + +type PubKeyRoles struct { + PubKey nostr.PubKey + RoleNames []string +} + +type PutUser struct { + Targets []PubKeyRoles + InviteCode string + When nostr.Timestamp +} + +func (_ PutUser) Name() string { return "put-user" } +func (a PutUser) Apply(group *Group) { + for _, target := range a.Targets { + roles := make([]*Role, 0, len(target.RoleNames)) + for _, roleName := range target.RoleNames { + if slices.IndexFunc(roles, func(r *Role) bool { return r.Name == roleName }) != -1 { + continue + } + roles = append(roles, group.GetRoleByName(roleName)) + } + group.Members[target.PubKey] = roles + + if a.InviteCode != "" { + if idx := slices.Index(group.InviteCodes, a.InviteCode); idx != -1 { + group.InviteCodes[idx] = group.InviteCodes[len(group.InviteCodes)-1] + group.InviteCodes = group.InviteCodes[0 : len(group.InviteCodes)-1] + } + } + } +} + +type RemoveUser struct { + Targets []nostr.PubKey + When nostr.Timestamp +} + +func (_ RemoveUser) Name() string { return "remove-user" } +func (a RemoveUser) Apply(group *Group) { + for _, tpk := range a.Targets { + delete(group.Members, tpk) + } +} + +type EditMetadata struct { + NameValue *string + PictureValue *string + AboutValue *string + PrivateValue *bool + ClosedValue *bool + When nostr.Timestamp +} + +func (_ EditMetadata) Name() string { return "edit-metadata" } +func (a EditMetadata) Apply(group *Group) { + group.LastMetadataUpdate = a.When + if a.NameValue != nil { + group.Name = *a.NameValue + } + if a.PictureValue != nil { + group.Picture = *a.PictureValue + } + if a.AboutValue != nil { + group.About = *a.AboutValue + } + if a.PrivateValue != nil { + group.Private = *a.PrivateValue + } + if a.ClosedValue != nil { + group.Closed = *a.ClosedValue + } +} + +type CreateGroup struct { + Creator nostr.PubKey + When nostr.Timestamp +} + +func (_ CreateGroup) Name() string { return "create-group" } +func (a CreateGroup) Apply(group *Group) { + group.LastMetadataUpdate = a.When + group.LastAdminsUpdate = a.When + group.LastMembersUpdate = a.When +} + +type DeleteGroup struct { + When nostr.Timestamp +} + +func (_ DeleteGroup) Name() string { return "delete-group" } +func (a DeleteGroup) Apply(group *Group) { + group.Members = make(map[nostr.PubKey][]*Role) + group.Closed = true + group.Private = true + group.Name = "[deleted]" + group.About = "" + group.Picture = "" + group.LastMetadataUpdate = a.When + group.LastAdminsUpdate = a.When + group.LastMembersUpdate = a.When +} + +type CreateInvite struct { + Codes []string +} + +func (_ CreateInvite) Name() string { return "create-invite" } +func (a CreateInvite) Apply(group *Group) { + group.InviteCodes = append(group.InviteCodes, a.Codes...) +}