diff --git a/internal/actors/commercial/actor.go b/internal/actors/commercial/actor.go new file mode 100644 index 0000000..f5fb250 --- /dev/null +++ b/internal/actors/commercial/actor.go @@ -0,0 +1,132 @@ +package commercial + +import ( + "context" + "regexp" + "strconv" + "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" +) + +const ( + actorName = "commercial" + + maxCommercialDuration = 180 +) + +var ( + formatMessage plugins.MsgFormatter + permCheckFn plugins.ChannelPermissionCheckFunc + tcGetter func(string) (*twitch.Client, error) + + commercialChatcommandRegex = regexp.MustCompile(`^/commercial ([0-9]+)$`) +) + +func Register(args plugins.RegistrationArguments) error { + formatMessage = args.FormatMessage + permCheckFn = args.HasPermissionForChannel + tcGetter = args.GetTwitchClientForChannel + + args.RegisterActor(actorName, func() plugins.Actor { return &actor{} }) + + args.RegisterActorDocumentation(plugins.ActionDocumentation{ + Description: "Start Commercial", + Name: "Commercial", + Type: actorName, + + Fields: []plugins.ActionDocumentationField{ + { + Default: "", + Description: "Duration of the commercial (must not be longer than 180s and must yield an integer)", + Key: "duration", + Name: "Duration", + Optional: false, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + }, + }) + + args.RegisterMessageModFunc("/commercial", 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) { + ptrStringEmpty := func(v string) *string { return &v }("") + + durationStr, err := formatMessage(attrs.MustString("duration", ptrStringEmpty), m, r, eventData) + if err != nil { + return false, errors.Wrap(err, "executing duration template") + } + + return false, startCommercial(strings.TrimLeft(plugins.DeriveChannel(m, eventData), "#"), durationStr) +} + +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) { + durationTemplate, err := attrs.String("duration") + if err != nil || durationTemplate == "" { + return errors.New("duration must be non-empty string") + } + + if err = tplValidator(durationTemplate); err != nil { + return errors.Wrap(err, "validating duration template") + } + + return nil +} + +func handleChatCommand(m *irc.Message) error { + channel := strings.TrimLeft(plugins.DeriveChannel(m, nil), "#") + + matches := commercialChatcommandRegex.FindStringSubmatch(m.Trailing()) + if matches == nil { + return errors.New("ban message does not match required format") + } + + if err := startCommercial(channel, matches[1]); err != nil { + return err + } + + return plugins.ErrSkipSendingMessage +} + +func startCommercial(channel, durationStr string) error { + duration, err := strconv.ParseInt(durationStr, 10, 64) + if err != nil { + return errors.Wrap(err, "parsing duration to integer") + } + + if duration > maxCommercialDuration { + return errors.New("duration too long") + } + + ok, err := permCheckFn(channel, twitch.ScopeChannelEditCommercial) + if err != nil { + return errors.Wrap(err, "checking for channel permissions") + } + + if !ok { + return errors.Errorf("channel %q is missing permission %s", channel, twitch.ScopeChannelEditCommercial) + } + + tc, err := tcGetter(channel) + if err != nil { + return errors.Wrap(err, "getting channel twitch-client") + } + + return errors.Wrap( + tc.RunCommercial(context.Background(), channel, duration), + "running commercial", + ) +} diff --git a/pkg/twitch/channels.go b/pkg/twitch/channels.go index bae2730..cbdcae9 100644 --- a/pkg/twitch/channels.go +++ b/pkg/twitch/channels.go @@ -129,3 +129,36 @@ func (c *Client) RemoveChannelVIP(ctx context.Context, broadcasterName, userName "executing request", ) } + +// RunCommercial starts a commercial on the specified channel +func (c *Client) RunCommercial(ctx context.Context, channel string, duration int64) error { + channelID, err := c.GetIDForUsername(channel) + if err != nil { + return errors.Wrap(err, "getting ID for channel name") + } + + payload := struct { + BroadcasterID string `json:"broadcaster_id"` + Length int64 `json:"length"` + }{ + BroadcasterID: channelID, + Length: duration, + } + + body := new(bytes.Buffer) + if err := json.NewEncoder(body).Encode(payload); err != nil { + return errors.Wrap(err, "encoding payload") + } + + return errors.Wrap( + c.request(clientRequestOpts{ + AuthType: authTypeBearerToken, + Body: body, + Context: ctx, + Method: http.MethodPost, + OKStatus: http.StatusOK, + URL: "https://api.twitch.tv/helix/channels/commercial", + }), + "executing request", + ) +} diff --git a/plugins_core.go b/plugins_core.go index a5416f1..81db751 100644 --- a/plugins_core.go +++ b/plugins_core.go @@ -12,6 +12,7 @@ import ( "github.com/Luzifer/go_helpers/v2/str" "github.com/Luzifer/twitch-bot/v3/internal/actors/announce" "github.com/Luzifer/twitch-bot/v3/internal/actors/ban" + "github.com/Luzifer/twitch-bot/v3/internal/actors/commercial" "github.com/Luzifer/twitch-bot/v3/internal/actors/counter" "github.com/Luzifer/twitch-bot/v3/internal/actors/delay" deleteactor "github.com/Luzifer/twitch-bot/v3/internal/actors/delete" @@ -51,6 +52,7 @@ var ( // Actors announce.Register, ban.Register, + commercial.Register, counter.Register, delay.Register, deleteactor.Register, diff --git a/wiki/Actors.md b/wiki/Actors.md index 7ef296d..abe85ae 100644 --- a/wiki/Actors.md +++ b/wiki/Actors.md @@ -44,6 +44,19 @@ Ban user from chat reason: "" ``` +## Commercial + +Start Commercial + +```yaml +- type: commercial + attributes: + # Duration of the commercial (must not be longer than 180s and must yield an integer) + # Optional: false + # Type: string (Supports Templating) + duration: "" +``` + ## Custom Event Create a custom event