package nip29 import ( "fmt" "slices" "strconv" "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} y := true n := false hasName := false // DEPRECATED: remove all the fields not tagged with Replace = true eventually // edit-metadata to become a PUT rather than a PATCH for _, tag := range evt.Tags { if len(tag) >= 1 { switch tag[0] { case "name": if len(tag) >= 2 { edit.NameValue = &tag[1] if ok { edit.Replace = true } ok = true hasName = true } case "picture": if len(tag) >= 2 { edit.PictureValue = &tag[1] if hasName { edit.Replace = true } ok = true } case "about": if len(tag) >= 2 { edit.AboutValue = &tag[1] if hasName { edit.Replace = true } ok = true } case "supported_kinds": kinds := make([]nostr.Kind, 0, len(tag)-1) for _, kstr := range tag[1:] { if kind, err := strconv.ParseUint(kstr, 10, 16); err != nil { return nil, fmt.Errorf("invalid kind: %w", err) } else { kinds = append(kinds, nostr.Kind(kind)) } } edit.SupportedKindsValue = &kinds edit.Replace = true case "closed": edit.ClosedValue = &y if hasName { edit.Replace = true } ok = true case "open": edit.ClosedValue = &n ok = true case "restricted": edit.RestrictedValue = &y if hasName { edit.Replace = true } ok = true case "unrestricted": edit.RestrictedValue = &n ok = true case "hidden": edit.HiddenValue = &y if hasName { edit.Replace = true } ok = true case "visible": edit.HiddenValue = &n ok = true case "private": edit.PrivateValue = &y if hasName { edit.Replace = true } ok = true case "public": edit.PrivateValue = &n ok = true case "livekit": edit.LiveKitValue = &y edit.Replace = true ok = true case "no-livekit": edit.LiveKitValue = &n ok = true case "no-text": edit.SupportedKindsValue = nil 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 LiveKitValue *bool SupportedKindsValue *[]nostr.Kind Replace bool When nostr.Timestamp } func (_ EditMetadata) Name() string { return "edit-metadata" } func (a EditMetadata) Apply(group *Group) { group.LastMetadataUpdate = a.When if a.Replace { group.Name = "" group.Picture = "" group.About = "" group.Restricted = false group.Closed = false group.Hidden = false group.Private = false group.LiveKit = false group.SupportedKinds = nil } 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 } if a.LiveKitValue != nil { group.LiveKit = *a.LiveKitValue } if a.SupportedKindsValue != nil { group.SupportedKinds = *a.SupportedKindsValue } } 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 group.LastLiveKitParticipantsUpdate = 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.LiveKitParticipants = make([]nostr.PubKey, 0) group.Closed = true group.Private = true group.Restricted = true group.Hidden = true group.Name = "[deleted]" group.About = "" group.Picture = "" group.LiveKit = false group.LastMetadataUpdate = a.When group.LastAdminsUpdate = a.When group.LastMembersUpdate = a.When group.LastLiveKitParticipantsUpdate = 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...) }