diff --git a/internal/actors/shoutout/actor.go b/internal/actors/shoutout/actor.go new file mode 100644 index 0000000..d294f41 --- /dev/null +++ b/internal/actors/shoutout/actor.go @@ -0,0 +1,97 @@ +package shoutout + +import ( + "regexp" + + "github.com/go-irc/irc" + "github.com/pkg/errors" + + "github.com/Luzifer/twitch-bot/v3/pkg/twitch" + "github.com/Luzifer/twitch-bot/v3/plugins" +) + +const actorName = "shoutout" + +var ( + botTwitchClient *twitch.Client + formatMessage plugins.MsgFormatter + ptrStringEmpty = func(v string) *string { return &v }("") + + shoutoutChatcommandRegex = regexp.MustCompile(`^/shoutout +([^\s]+)$`) +) + +func Register(args plugins.RegistrationArguments) error { + botTwitchClient = args.GetTwitchClient() + formatMessage = args.FormatMessage + + args.RegisterActor(actorName, func() plugins.Actor { return &actor{} }) + + args.RegisterActorDocumentation(plugins.ActionDocumentation{ + Description: "Perform a Twitch-native shoutout", + Name: "Shoutout", + Type: actorName, + + Fields: []plugins.ActionDocumentationField{ + { + Default: "", + Description: "User to give the shoutout to", + Key: "user", + Name: "User", + Optional: false, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + }, + }) + + args.RegisterMessageModFunc("/shoutout", handleChatCommand) + + return nil +} + +type actor struct{} + +func (a actor) Execute(c *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) { + user, err := formatMessage(attrs.MustString("user", ptrStringEmpty), m, r, eventData) + if err != nil { + return false, errors.Wrap(err, "executing user template") + } + + return false, errors.Wrap( + botTwitchClient.SendShoutout( + plugins.DeriveChannel(m, eventData), + user, + ), + "executing shoutout", + ) +} + +func (a actor) IsAsync() bool { return false } +func (a actor) Name() string { return actorName } + +func (a actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { + if v, err := attrs.String("user"); err != nil || v == "" { + return errors.New("user must be non-empty string") + } + + if err = tplValidator(attrs.MustString("user", ptrStringEmpty)); err != nil { + return errors.Wrap(err, "validating user template") + } + + return nil +} + +func handleChatCommand(m *irc.Message) error { + channel := plugins.DeriveChannel(m, nil) + + matches := shoutoutChatcommandRegex.FindStringSubmatch(m.Trailing()) + if matches == nil { + return errors.New("shoutout message does not match required format") + } + + if err := botTwitchClient.SendShoutout(channel, matches[1]); err != nil { + return errors.Wrap(err, "executing shoutout") + } + + return plugins.ErrSkipSendingMessage +} diff --git a/pkg/twitch/chat.go b/pkg/twitch/chat.go index 77844ca..55930eb 100644 --- a/pkg/twitch/chat.go +++ b/pkg/twitch/chat.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "strings" "github.com/pkg/errors" @@ -53,3 +54,41 @@ func (c *Client) SendChatAnnouncement(channel, color, message string) error { "executing request", ) } + +// SendShoutout creates a Twitch-native shoutout in the given channel +// for the given user. This equals `/shoutout ` in the channel. +func (c *Client) SendShoutout(channel, user string) error { + botID, _, err := c.GetAuthorizedUser() + if err != nil { + return errors.Wrap(err, "getting bot user-id") + } + + channelID, err := c.GetIDForUsername(strings.TrimLeft(channel, "#@")) + if err != nil { + return errors.Wrap(err, "getting channel user-id") + } + + userID, err := c.GetIDForUsername(strings.TrimLeft(user, "#@")) + if err != nil { + return errors.Wrap(err, "getting user user-id") + } + + params := make(url.Values) + params.Set("from_broadcaster_id", channelID) + params.Set("moderator_id", botID) + params.Set("to_broadcaster_id", userID) + + return errors.Wrap( + c.request(clientRequestOpts{ + AuthType: authTypeBearerToken, + Context: context.Background(), + Method: http.MethodPost, + OKStatus: http.StatusNoContent, + URL: fmt.Sprintf( + "https://api.twitch.tv/helix/chat/shoutouts?%s", + params.Encode(), + ), + }), + "executing request", + ) +} diff --git a/pkg/twitch/scopes.go b/pkg/twitch/scopes.go index b033174..dcb25c3 100644 --- a/pkg/twitch/scopes.go +++ b/pkg/twitch/scopes.go @@ -16,6 +16,7 @@ const ( ScopeModeratorManageBannedUsers = "moderator:manage:banned_users" ScopeModeratorManageChatMessages = "moderator:manage:chat_messages" ScopeModeratorManageChatSettings = "moderator:manage:chat_settings" + ScopeModeratorManageShoutouts = "moderator:manage:shoutouts" ScopeUserManageChatColor = "user:manage:chat_color" // Deprecated v5 scope but used in chat diff --git a/plugins_core.go b/plugins_core.go index 13b4959..f53a87f 100644 --- a/plugins_core.go +++ b/plugins_core.go @@ -23,6 +23,7 @@ import ( "github.com/Luzifer/twitch-bot/v3/internal/actors/quotedb" "github.com/Luzifer/twitch-bot/v3/internal/actors/raw" "github.com/Luzifer/twitch-bot/v3/internal/actors/respond" + "github.com/Luzifer/twitch-bot/v3/internal/actors/shoutout" "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" @@ -58,6 +59,7 @@ var ( quotedb.Register, raw.Register, respond.Register, + shoutout.Register, timeout.Register, variables.Register, vip.Register, diff --git a/scopes.go b/scopes.go index 1bca4d1..f757a9e 100644 --- a/scopes.go +++ b/scopes.go @@ -19,6 +19,7 @@ var ( twitch.ScopeModeratorManageBannedUsers, twitch.ScopeModeratorManageChatMessages, twitch.ScopeModeratorManageChatSettings, + twitch.ScopeModeratorManageShoutouts, // Chat Scopes twitch.ScopeChatEdit,