nip29: moderation actions and invite code support.
(from pyramid implementation.)
This commit is contained in:
@@ -41,11 +41,12 @@ type Group struct {
|
|||||||
Name string
|
Name string
|
||||||
Picture string
|
Picture string
|
||||||
About string
|
About string
|
||||||
Members map[string][]*Role
|
Members map[nostr.PubKey][]*Role
|
||||||
Private bool
|
Private bool
|
||||||
Closed bool
|
Closed bool
|
||||||
|
|
||||||
Roles []*Role
|
Roles []*Role
|
||||||
|
InviteCodes []string
|
||||||
|
|
||||||
LastMetadataUpdate nostr.Timestamp
|
LastMetadataUpdate nostr.Timestamp
|
||||||
LastAdminsUpdate nostr.Timestamp
|
LastAdminsUpdate nostr.Timestamp
|
||||||
@@ -67,7 +68,7 @@ func (group Group) String() string {
|
|||||||
members := make([]string, len(group.Members))
|
members := make([]string, len(group.Members))
|
||||||
i := 0
|
i := 0
|
||||||
for pubkey, roles := range group.Members {
|
for pubkey, roles := range group.Members {
|
||||||
members[i] = pubkey
|
members[i] = pubkey.Hex()
|
||||||
if len(roles) > 0 {
|
if len(roles) > 0 {
|
||||||
members[i] += ":"
|
members[i] += ":"
|
||||||
}
|
}
|
||||||
@@ -103,7 +104,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[nostr.PubKey][]*Role),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,15 +115,15 @@ 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[nostr.PubKey][]*Role),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := g.MergeInMetadataEvent(evt)
|
err := g.MergeInMetadataEvent(evt)
|
||||||
return g, err
|
return g, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group Group) ToMetadataEvent() *nostr.Event {
|
func (group Group) ToMetadataEvent() nostr.Event {
|
||||||
evt := &nostr.Event{
|
evt := nostr.Event{
|
||||||
Kind: nostr.KindSimpleGroupMetadata,
|
Kind: nostr.KindSimpleGroupMetadata,
|
||||||
CreatedAt: group.LastMetadataUpdate,
|
CreatedAt: group.LastMetadataUpdate,
|
||||||
Tags: nostr.Tags{
|
Tags: nostr.Tags{
|
||||||
@@ -154,8 +155,8 @@ func (group Group) ToMetadataEvent() *nostr.Event {
|
|||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group Group) ToAdminsEvent() *nostr.Event {
|
func (group Group) ToAdminsEvent() nostr.Event {
|
||||||
evt := &nostr.Event{
|
evt := nostr.Event{
|
||||||
Kind: nostr.KindSimpleGroupAdmins,
|
Kind: nostr.KindSimpleGroupAdmins,
|
||||||
CreatedAt: group.LastAdminsUpdate,
|
CreatedAt: group.LastAdminsUpdate,
|
||||||
Tags: make(nostr.Tags, 1, 1+len(group.Members)/3),
|
Tags: make(nostr.Tags, 1, 1+len(group.Members)/3),
|
||||||
@@ -171,7 +172,7 @@ func (group Group) ToAdminsEvent() *nostr.Event {
|
|||||||
// is an admin
|
// is an admin
|
||||||
tag := make([]string, 2, 2+len(roles))
|
tag := make([]string, 2, 2+len(roles))
|
||||||
tag[0] = "p"
|
tag[0] = "p"
|
||||||
tag[1] = member
|
tag[1] = member.Hex()
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
tag = append(tag, role.Name)
|
tag = append(tag, role.Name)
|
||||||
}
|
}
|
||||||
@@ -181,8 +182,8 @@ func (group Group) ToAdminsEvent() *nostr.Event {
|
|||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group Group) ToMembersEvent() *nostr.Event {
|
func (group Group) ToMembersEvent() nostr.Event {
|
||||||
evt := &nostr.Event{
|
evt := nostr.Event{
|
||||||
Kind: nostr.KindSimpleGroupMembers,
|
Kind: nostr.KindSimpleGroupMembers,
|
||||||
CreatedAt: group.LastMembersUpdate,
|
CreatedAt: group.LastMembersUpdate,
|
||||||
Tags: make(nostr.Tags, 1, 1+len(group.Members)),
|
Tags: make(nostr.Tags, 1, 1+len(group.Members)),
|
||||||
@@ -191,14 +192,14 @@ func (group Group) ToMembersEvent() *nostr.Event {
|
|||||||
|
|
||||||
for member := range group.Members {
|
for member := range group.Members {
|
||||||
// include both admins and normal 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
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group Group) ToRolesEvent() *nostr.Event {
|
func (group Group) ToRolesEvent() nostr.Event {
|
||||||
evt := &nostr.Event{
|
evt := nostr.Event{
|
||||||
Kind: nostr.KindSimpleGroupRoles,
|
Kind: nostr.KindSimpleGroupRoles,
|
||||||
CreatedAt: group.LastMembersUpdate,
|
CreatedAt: group.LastMembersUpdate,
|
||||||
Tags: make(nostr.Tags, 1, 1+len(group.Members)),
|
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" {
|
if tag[0] != "p" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !nostr.IsValid32ByteHex(tag[1]) {
|
|
||||||
|
member, err := nostr.PubKeyFromHex(tag[1])
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, roleName := range tag[2:] {
|
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" {
|
if tag[0] != "p" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !nostr.IsValid32ByteHex(tag[1]) {
|
|
||||||
|
member, err := nostr.PubKeyFromHex(tag[1])
|
||||||
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, exists := group.Members[tag[1]]
|
_, exists := group.Members[member]
|
||||||
if !exists {
|
if !exists {
|
||||||
group.Members[tag[1]] = nil
|
group.Members[member] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
272
nip29/moderation_actions.go
Normal file
272
nip29/moderation_actions.go
Normal file
@@ -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...)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user