diff --git a/internal/actors/vip/vip.go b/internal/actors/vip/vip.go new file mode 100644 index 0000000..9434c19 --- /dev/null +++ b/internal/actors/vip/vip.go @@ -0,0 +1,196 @@ +package vip + +import ( + "context" + "strings" + + "github.com/go-irc/irc" + "github.com/pkg/errors" + + "github.com/Luzifer/twitch-bot/v3/pkg/twitch" + "github.com/Luzifer/twitch-bot/v3/plugins" +) + +var ( + formatMessage plugins.MsgFormatter + permCheckFn plugins.ChannelPermissionCheckFunc + tcGetter func(string) (*twitch.Client, error) + + ptrStringEmpty = func(s string) *string { return &s }("") +) + +func Register(args plugins.RegistrationArguments) error { + formatMessage = args.FormatMessage + permCheckFn = args.HasPermissionForChannel + tcGetter = args.GetTwitchClientForChannel + + args.RegisterActor("vip", func() plugins.Actor { return &vipActor{} }) + args.RegisterActor("unvip", func() plugins.Actor { return &unvipActor{} }) + + args.RegisterActorDocumentation(plugins.ActionDocumentation{ + Description: "Add VIP for the given channel", + Name: "Add VIP", + Type: "vip", + + Fields: []plugins.ActionDocumentationField{ + { + Default: "", + Description: "Channel to add the VIP to", + Key: "channel", + Name: "Channel", + Optional: false, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + { + Default: "", + Description: "User to add as VIP", + Key: "user", + Name: "User", + Optional: false, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + }, + }) + + args.RegisterActorDocumentation(plugins.ActionDocumentation{ + Description: "Remove VIP for the given channel", + Name: "Remove VIP", + Type: "unvip", + + Fields: []plugins.ActionDocumentationField{ + { + Default: "", + Description: "Channel to remove the VIP from", + Key: "channel", + Name: "Channel", + Optional: false, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + { + Default: "", + Description: "User to remove as VIP", + Key: "user", + Name: "User", + Optional: false, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + }, + }) + + args.RegisterMessageModFunc("/vip", handleAddVIP) + args.RegisterMessageModFunc("/unvip", handleRemoveVIP) + + return nil +} + +// Actor + +type ( + actor struct{} + unvipActor struct{ actor } + vipActor struct{ actor } +) + +func (actor) IsAsync() bool { return false } +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { + for _, field := range []string{"channel", "user"} { + if v, err := attrs.String(field); err != nil || v == "" { + return errors.Errorf("%s must be non-empty string", field) + } + + if err = tplValidator(attrs.MustString(field, ptrStringEmpty)); err != nil { + return errors.Wrapf(err, "validating %s template", field) + } + } + + return nil +} + +func (a actor) getParams(m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (channel, user string, err error) { + if channel, err = formatMessage(attrs.MustString("channel", nil), m, r, eventData); err != nil { + return "", "", errors.Wrap(err, "parsing channel") + } + + if user, err = formatMessage(attrs.MustString("user", nil), m, r, eventData); err != nil { + return "", "", errors.Wrap(err, "parsing user") + } + + return strings.TrimLeft(channel, "#"), user, nil +} + +func (u unvipActor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) { + channel, user, err := u.getParams(m, r, eventData, attrs) + if err != nil { + return false, errors.Wrap(err, "getting parameters") + } + + return false, errors.Wrap( + executeModVIP(channel, func(tc *twitch.Client) error { return tc.RemoveChannelVIP(context.Background(), channel, user) }), + "removing VIP", + ) +} + +func (unvipActor) Name() string { return "unvip" } + +func (v vipActor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) { + channel, user, err := v.getParams(m, r, eventData, attrs) + if err != nil { + return false, errors.Wrap(err, "getting parameters") + } + + return false, errors.Wrap( + executeModVIP(channel, func(tc *twitch.Client) error { return tc.AddChannelVIP(context.Background(), channel, user) }), + "adding VIP", + ) +} + +func (vipActor) Name() string { return "vip" } + +// Generic helper + +func executeModVIP(channel string, modFn func(tc *twitch.Client) error) error { + ok, err := permCheckFn(channel, twitch.ScopeChannelManageVIPS) + if err != nil { + return errors.Wrap(err, "checking for channel permissions") + } + + if !ok { + return errors.Errorf("channel %q is missing permission %s", channel, twitch.ScopeChannelManageVIPS) + } + + tc, err := tcGetter(channel) + if err != nil { + return errors.Wrap(err, "getting channel twitch-client") + } + + return modFn(tc) +} + +// Chat-Commands + +func handleAddVIP(m *irc.Message) error { + return handleModVIP(m, func(tc *twitch.Client, channel, user string) error { + return errors.Wrap(tc.AddChannelVIP(context.Background(), channel, user), "adding VIP") + }) +} + +func handleModVIP(m *irc.Message, modFn func(tc *twitch.Client, channel, user string) error) error { + channel := strings.TrimLeft(plugins.DeriveChannel(m, nil), "#") + + parts := strings.Split(m.Trailing(), " ") + if len(parts) != 2 { //nolint:gomnd // Just a count, makes no sense as a constant + return errors.Errorf("wrong command usage, must consist of 2 words") + } + + return executeModVIP(channel, func(tc *twitch.Client) error { return modFn(tc, channel, parts[1]) }) +} + +func handleRemoveVIP(m *irc.Message) error { + return handleModVIP(m, func(tc *twitch.Client, channel, user string) error { + return errors.Wrap(tc.RemoveChannelVIP(context.Background(), channel, user), "removing VIP") + }) +} diff --git a/plugins_core.go b/plugins_core.go index 334bf79..4302dbb 100644 --- a/plugins_core.go +++ b/plugins_core.go @@ -24,6 +24,7 @@ import ( "github.com/Luzifer/twitch-bot/v3/internal/actors/respond" "github.com/Luzifer/twitch-bot/v3/internal/actors/timeout" "github.com/Luzifer/twitch-bot/v3/internal/actors/variables" + "github.com/Luzifer/twitch-bot/v3/internal/actors/vip" "github.com/Luzifer/twitch-bot/v3/internal/actors/whisper" "github.com/Luzifer/twitch-bot/v3/internal/apimodules/customevent" "github.com/Luzifer/twitch-bot/v3/internal/apimodules/msgformat" @@ -57,6 +58,7 @@ var ( respond.Register, timeout.Register, variables.Register, + vip.Register, whisper.Register, // Template functions diff --git a/wiki/Actors.md b/wiki/Actors.md index 5102a4a..695dbd7 100644 --- a/wiki/Actors.md +++ b/wiki/Actors.md @@ -1,6 +1,23 @@ # Available Actions +## Add VIP + +Add VIP for the given channel + +```yaml +- type: vip + attributes: + # Channel to add the VIP to + # Optional: false + # Type: string (Supports Templating) + channel: "" + # User to add as VIP + # Optional: false + # Type: string (Supports Templating) + user: "" +``` + ## Ban User Ban user from chat @@ -225,6 +242,23 @@ Manage a database of quotes in your channel format: "Quote #{{ .index }}: {{ .quote }}" ``` +## Remove VIP + +Remove VIP for the given channel + +```yaml +- type: unvip + attributes: + # Channel to remove the VIP from + # Optional: false + # Type: string (Supports Templating) + channel: "" + # User to remove as VIP + # Optional: false + # Type: string (Supports Templating) + user: "" +``` + ## Reset User Punishment Reset punishment level for user