diff --git a/docs/content/configuration/actors.md b/docs/content/configuration/actors.md index b423a5b..4f91923 100644 --- a/docs/content/configuration/actors.md +++ b/docs/content/configuration/actors.md @@ -84,6 +84,23 @@ Triggers the creation of a Clip from the given channel owned by the creator (sub add_delay: false ``` +## Create Marker + +Creates a marker on the currently running stream of the given channel. The marker will be created on behalf of the channel owner and requires matching scope. + +```yaml +- type: marker + attributes: + # Channel to create the marker in, defaults to the channel of the event / message + # Optional: true + # Type: string (Supports Templating) + channel: "" + # Description of the marker to create (up to 140 chars) + # Optional: true + # Type: string (Supports Templating) + description: "" +``` + ## Custom Event Create a custom event diff --git a/internal/actors/marker/actor.go b/internal/actors/marker/actor.go new file mode 100644 index 0000000..81fb9c5 --- /dev/null +++ b/internal/actors/marker/actor.go @@ -0,0 +1,111 @@ +// Package marker contains an actor to create markers on the current +// running stream +package marker + +import ( + "context" + "fmt" + "strings" + + "github.com/Luzifer/go_helpers/v2/fieldcollection" + "github.com/Luzifer/twitch-bot/v3/internal/helpers" + "github.com/Luzifer/twitch-bot/v3/pkg/twitch" + "github.com/Luzifer/twitch-bot/v3/plugins" + "gopkg.in/irc.v4" +) + +const actorName = "marker" + +var ( + formatMessage plugins.MsgFormatter + hasPerm plugins.ChannelPermissionCheckFunc + tcGetter func(string) (*twitch.Client, error) +) + +// Register provides the plugins.RegisterFunc +func Register(args plugins.RegistrationArguments) error { + formatMessage = args.FormatMessage + hasPerm = args.HasPermissionForChannel + tcGetter = args.GetTwitchClientForChannel + + args.RegisterActor(actorName, func() plugins.Actor { return &actor{} }) + + args.RegisterActorDocumentation(plugins.ActionDocumentation{ + Description: "Creates a marker on the currently running stream of the given channel. The marker will be created on behalf of the channel owner and requires matching scope.", + Name: "Create Marker", + Type: actorName, + Fields: []plugins.ActionDocumentationField{ + { + Description: "Channel to create the marker in, defaults to the channel of the event / message", + Key: "channel", + Name: "Channel", + Optional: true, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + { + Description: "Description of the marker to create (up to 140 chars)", + Key: "description", + Name: "Description", + Optional: true, + SupportTemplate: true, + Type: plugins.ActionDocumentationFieldTypeString, + }, + }, + }) + + return nil +} + +type actor struct{} + +func (actor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *fieldcollection.FieldCollection, attrs *fieldcollection.FieldCollection) (preventCooldown bool, err error) { + channel := plugins.DeriveChannel(m, eventData) + if channel, err = formatMessage(attrs.MustString("channel", &channel), m, r, eventData); err != nil { + return false, fmt.Errorf("parsing channel: %w", err) + } + + var description string + if description, err = formatMessage(attrs.MustString("description", &description), m, r, eventData); err != nil { + return false, fmt.Errorf("parsing description: %w", err) + } + + channel = strings.TrimLeft(channel, "#") + + canCreate, err := hasPerm(channel, twitch.ScopeChannelManageBroadcast) + if err != nil { + return false, fmt.Errorf("checking for required permission: %w", err) + } + + if !canCreate { + return false, fmt.Errorf("creator has not given %s permission", twitch.ScopeChannelManageBroadcast) + } + + tc, err := tcGetter(channel) + if err != nil { + return false, fmt.Errorf("getting Twitch client for %q: %w", channel, err) + } + + if err = tc.CreateStreamMarker(context.TODO(), description); err != nil { + return false, fmt.Errorf("creating marker: %w", err) + } + + return false, nil +} + +func (actor) IsAsync() bool { return false } + +func (actor) Name() string { return actorName } + +func (actor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *fieldcollection.FieldCollection) (err error) { + if err = attrs.ValidateSchema( + fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "channel", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}), + fieldcollection.CanHaveField(fieldcollection.SchemaField{Name: "description", NonEmpty: true, Type: fieldcollection.SchemaFieldTypeString}), + fieldcollection.MustHaveNoUnknowFields, + helpers.SchemaValidateTemplateField(tplValidator, "channel", "description"), + ); err != nil { + return fmt.Errorf("validating attributes: %w", err) + } + + return nil +} diff --git a/pkg/twitch/streams.go b/pkg/twitch/streams.go index f53fe33..7b24084 100644 --- a/pkg/twitch/streams.go +++ b/pkg/twitch/streams.go @@ -1,7 +1,9 @@ package twitch import ( + "bytes" "context" + "encoding/json" "fmt" "net/http" "time" @@ -33,6 +35,40 @@ type ( // the fact there just is no stream found var ErrNoStreamsFound = errors.New("no streams found") +// CreateStreamMarker creates a marker for the currently running stream. +// The stream must be live, no VoD, no upload and no re-run. +// The description may be up to 140 chars and can be omitted. +func (c *Client) CreateStreamMarker(ctx context.Context, description string) (err error) { + body := new(bytes.Buffer) + + userID, _, err := c.GetAuthorizedUser(ctx) + if err != nil { + return fmt.Errorf("getting ID for current user: %w", err) + } + + if err = json.NewEncoder(body).Encode(struct { + UserID string `json:"user_id"` + Description string `json:"description,omitempty"` + }{ + UserID: userID, + Description: description, + }); err != nil { + return fmt.Errorf("encoding payload: %w", err) + } + + if err := c.Request(ctx, ClientRequestOpts{ + AuthType: AuthTypeBearerToken, + Body: body, + Method: http.MethodPost, + OKStatus: http.StatusOK, + URL: "https://api.twitch.tv/helix/streams/markers", + }); err != nil { + return fmt.Errorf("creating marker: %w", err) + } + + return nil +} + // GetCurrentStreamInfo returns the StreamInfo of the currently running // stream of the given username func (c *Client) GetCurrentStreamInfo(ctx context.Context, username string) (*StreamInfo, error) { diff --git a/plugins_core.go b/plugins_core.go index 91fa1b7..222fadb 100644 --- a/plugins_core.go +++ b/plugins_core.go @@ -25,6 +25,7 @@ import ( "github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector" "github.com/Luzifer/twitch-bot/v3/internal/actors/linkprotect" logActor "github.com/Luzifer/twitch-bot/v3/internal/actors/log" + "github.com/Luzifer/twitch-bot/v3/internal/actors/marker" "github.com/Luzifer/twitch-bot/v3/internal/actors/messagehook" "github.com/Luzifer/twitch-bot/v3/internal/actors/modchannel" "github.com/Luzifer/twitch-bot/v3/internal/actors/nuke" @@ -78,6 +79,7 @@ var ( linkdetector.Register, linkprotect.Register, logActor.Register, + marker.Register, messagehook.Register, modchannel.Register, nuke.Register, diff --git a/scopes.go b/scopes.go index fba2740..0abfac6 100644 --- a/scopes.go +++ b/scopes.go @@ -5,7 +5,7 @@ import "github.com/Luzifer/twitch-bot/v3/pkg/twitch" var ( channelExtendedScopes = map[string]string{ twitch.ScopeChannelEditCommercial: "run commercial", - twitch.ScopeChannelManageBroadcast: "modify category / title", + twitch.ScopeChannelManageBroadcast: "modify category / title, create markers", twitch.ScopeChannelManagePolls: "manage polls", twitch.ScopeChannelManagePredictions: "manage predictions", twitch.ScopeChannelManageRaids: "start raids",