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("closed") { edit.ClosedValue = &y ok = true } else if evt.Tags.Has("open") { edit.ClosedValue = &n ok = true } if evt.Tags.Has("restricted") { edit.RestrictedValue = &y ok = true } else if evt.Tags.Has("unrestricted") { edit.RestrictedValue = &n ok = true } if evt.Tags.Has("hidden") { edit.HiddenValue = &y ok = true } else if evt.Tags.Has("visible") { edit.HiddenValue = &n ok = true } if evt.Tags.Has("private") { edit.PrivateValue = &y ok = true } else if evt.Tags.Has("public") { edit.PrivateValue = &n 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) missing = false } 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.LastAdminsUpdate = a.When } 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] } } group.LastMembersUpdate = a.When } } 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 { if roles, exists := group.Members[tpk]; exists { group.LastMembersUpdate = a.When if len(roles) > 0 { group.LastAdminsUpdate = a.When } } delete(group.Members, tpk) } } type EditMetadata struct { NameValue *string PictureValue *string AboutValue *string RestrictedValue *bool ClosedValue *bool HiddenValue *bool PrivateValue *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.RestrictedValue != nil { group.Restricted = *a.RestrictedValue } if a.ClosedValue != nil { group.Closed = *a.ClosedValue } if a.HiddenValue != nil { group.Hidden = *a.HiddenValue } if a.PrivateValue != nil { group.Private = *a.PrivateValue } } 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.Restricted = true group.Hidden = 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...) }