From a33677230341177c051b6b1cf9233feee736a9b9 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sun, 3 Dec 2023 15:57:23 +0100 Subject: [PATCH] [raffle] Add Actor to enter user into raffle using channel-points Signed-off-by: Knut Ahlers --- docs/content/configuration/actors.md | 13 +++++ docs/content/modules/raffle.md | 22 +++++++- internal/apimodules/raffle/actor.go | 80 ++++++++++++++++++++++++++++ internal/apimodules/raffle/api.go | 24 +++++---- internal/apimodules/raffle/raffle.go | 23 ++++++-- src/raffle.vue | 6 +++ 6 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 internal/apimodules/raffle/actor.go diff --git a/docs/content/configuration/actors.md b/docs/content/configuration/actors.md index 3b83a99..db5e5f7 100644 --- a/docs/content/configuration/actors.md +++ b/docs/content/configuration/actors.md @@ -233,6 +233,19 @@ Uses link- and clip-scanner to detect links / clips and applies link protection stop_on_no_action: false ``` +## Enter User to Raffle + +Enter user to raffle through channelpoints + +```yaml +- type: enter-raffle + attributes: + # The keyword for the active raffle to enter the user into + # Optional: false + # Type: string + keyword: "" +``` + ## Execute Script / Command Execute external script / command diff --git a/docs/content/modules/raffle.md b/docs/content/modules/raffle.md index 4a5c68e..604fd98 100644 --- a/docs/content/modules/raffle.md +++ b/docs/content/modules/raffle.md @@ -18,7 +18,7 @@ The screenshot above shows one draft of a raffle together with one currently act You can access the entrants list through the "group of people" button in the raffle overview. This becomes available as soon as the raffle has started. -In this list you can see the status, the nickname and the time of entry for each entrant. The status will be a person () for someone joined through the **Everyone** allowance, a heart () for a follower, a star () for a subscriber and a diamond () for a VIP. The list will update itself when there are changes in the entree-list. +In this list you can see the status, the nickname and the time of entry for each entrant. The status will be a person () for someone joined through the **Everyone** allowance, a heart () for a follower, a star () for a subscriber, a diamond () for a VIP and a coin () for someone who joined through a channel-point redeem. The list will update itself when there are changes in the entree-list. ![]({{< static "raffle-entrants-closed.png" >}}) @@ -64,3 +64,23 @@ The texts do support templating and do have the same format like other templates - **Message on raffle close** will be posted when the raffle closes (either you closed it manually or the **Close At** time is reached). Within the templates you do have access to the variables `.user` and `.raffle` (which represents the raffle object). Have a look at the default templates for examples what you can do with them. + +## Using Channel-Point Rewards to join + +To create a raffle to be entered through channel-point rewards you'll do the basic setup of your raffle as usual but you'll do some special adjustments: + +- Set the raffle **Keyword** to something no user will ever use in chat (must be one word, can be a bunch of random characters), if a user can guess this, they can enter without using the channel points +- Doesn't matter what you select for **Allowed Entries** (the channel-point actor will ignore that setting) +- Ensure no text contains the `{{ .raffle.Keyword }}` template directive (you don't want to "leak" your keyword) +- Create a Channel-Point reward: + - Name it as you like (but make the name unique among all your rewards as we will use that to determine whether to trigger the rule), set the points to the amount of channel points you like, put limits on it as you like + - You can enable "Skip Queue" but in that case points will be lost when no raffle is active or if any user redeems it more than once per raffle, if you don't set this you can refund the points manually but also you need to mark all raffle entries completed manually. +- Create a new rule: + - Channel: Limit to your channel + - Event: `channelpoint_redeem` + - Disable on template: `{{ ne .reward_title "" }}` + - Action: **Enter User to Raffle**, for the keyword enter the same as in the raffle + +When an user redeems that reward, the rule will be triggered and if a raffle is active with that keyword, the user will be entered into that raffle as if they triggered the keyword themselves. + +**Tip:** If no raffle is active disable / pause the reward to prevent users to waste points on it while there is no raffle active. diff --git a/internal/apimodules/raffle/actor.go b/internal/apimodules/raffle/actor.go new file mode 100644 index 0000000..0ac9986 --- /dev/null +++ b/internal/apimodules/raffle/actor.go @@ -0,0 +1,80 @@ +package raffle + +import ( + "time" + + "github.com/Luzifer/twitch-bot/v3/plugins" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "gopkg.in/irc.v4" +) + +type ( + enterRaffleActor struct{} +) + +var ptrStrEmpty = ptrStr("") + +func ptrStr(v string) *string { return &v } + +func (a enterRaffleActor) Execute(_ *irc.Client, m *irc.Message, _ *plugins.Rule, evtData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) { + if m != nil || evtData.MustString("reward_id", ptrStrEmpty) == "" { + return false, errors.New("enter-raffle actor is only supposed to act on channelpoint redeems") + } + + r, err := dbc.GetByChannelAndKeyword( + evtData.MustString("channel", ptrStrEmpty), + attrs.MustString("keyword", ptrStrEmpty), + ) + if err != nil { + if errors.Is(err, errRaffleNotFound) { + // We don't need to care, that was no raffle input + return false, errors.Errorf("specified keyword %q does not belong to active raffle", attrs.MustString("keyword", ptrStrEmpty)) + } + return false, errors.Wrap(err, "fetching raffle") + } + + re := raffleEntry{ + EnteredAs: "reward", + RaffleID: r.ID, + UserID: evtData.MustString("user_id", ptrStrEmpty), + UserLogin: evtData.MustString("user", ptrStrEmpty), + UserDisplayName: evtData.MustString("user", ptrStrEmpty), + EnteredAt: time.Now().UTC(), + } + + raffleEventFields := plugins.FieldCollectionFromData(map[string]any{ + "user_id": re.UserID, + "user": re.UserLogin, + }) + + // We have everything we need to create an entry + if err = dbc.Enter(re); err != nil { + logrus.WithFields(logrus.Fields{ + "raffle": r.ID, + "user_id": re.UserID, + "user": re.UserLogin, + }).WithError(err).Error("creating raffle entry") + return false, errors.Wrap( + r.SendEvent(raffleMessageEventEntryFailed, raffleEventFields), + "sending entry-failed chat message", + ) + } + + return false, errors.Wrap( + r.SendEvent(raffleMessageEventEntry, raffleEventFields), + "sending entry chat message", + ) +} + +func (a enterRaffleActor) IsAsync() bool { return false } +func (a enterRaffleActor) Name() string { return "enter-raffle" } + +func (a enterRaffleActor) Validate(_ plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) { + keyword, err := attrs.String("keyword") + if err != nil || keyword == "" { + return errors.New("keyword must be non-empty string") + } + + return nil +} diff --git a/internal/apimodules/raffle/api.go b/internal/apimodules/raffle/api.go index 0e3f1d9..2f4c07a 100644 --- a/internal/apimodules/raffle/api.go +++ b/internal/apimodules/raffle/api.go @@ -15,6 +15,8 @@ import ( "github.com/Luzifer/twitch-bot/v3/plugins" ) +const moduleName = "raffle" + var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ { Description: "Lists all raffles known to the bot", @@ -23,7 +25,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return ras, errors.Wrap(err, "fetching raffles from database") }, nil), Method: http.MethodGet, - Module: actorName, + Module: moduleName, Name: "List Raffles", Path: "/", RequiresWriteAuth: true, @@ -41,7 +43,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.Create(ra), "creating raffle") }, nil), Method: http.MethodPost, - Module: actorName, + Module: moduleName, Name: "Create Raffle", Path: "/", RequiresWriteAuth: true, @@ -54,7 +56,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.Delete(ids["id"]), "fetching raffle from database") }, []string{"id"}), Method: http.MethodDelete, - Module: actorName, + Module: moduleName, Name: "Delete Raffle", Path: "/{id}", RequiresWriteAuth: true, @@ -74,7 +76,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return ra, errors.Wrap(err, "fetching raffle from database") }, []string{"id"}), Method: http.MethodGet, - Module: actorName, + Module: moduleName, Name: "Get Raffle", Path: "/{id}", RequiresWriteAuth: true, @@ -102,7 +104,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.Update(ra), "updating raffle") }, []string{"id"}), Method: http.MethodPut, - Module: actorName, + Module: moduleName, Name: "Update Raffle", Path: "/{id}", RequiresWriteAuth: true, @@ -121,7 +123,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.Clone(ids["id"]), "cloning raffle") }, []string{"id"}), Method: http.MethodPut, - Module: actorName, + Module: moduleName, Name: "Clone Raffle", Path: "/{id}/clone", RequiresWriteAuth: true, @@ -140,7 +142,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.Close(ids["id"]), "closing raffle") }, []string{"id"}), Method: http.MethodPut, - Module: actorName, + Module: moduleName, Name: "Close Raffle", Path: "/{id}/close", RequiresWriteAuth: true, @@ -159,7 +161,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.PickWinner(ids["id"]), "picking winner") }, []string{"id"}), Method: http.MethodPut, - Module: actorName, + Module: moduleName, Name: "Pick Raffle Winner", Path: "/{id}/pick", RequiresWriteAuth: true, @@ -183,7 +185,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.Reopen(ids["id"], time.Duration(dur)*time.Second), "reopening raffle") }, []string{"id"}), Method: http.MethodPut, - Module: actorName, + Module: moduleName, Name: "Reopen Raffle", Path: "/{id}/reopen", QueryParams: []plugins.HTTPRouteParamDocumentation{ @@ -210,7 +212,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.Start(ids["id"]), "starting raffle") }, []string{"id"}), Method: http.MethodPut, - Module: actorName, + Module: moduleName, Name: "Start Raffle", Path: "/{id}/start", RequiresWriteAuth: true, @@ -229,7 +231,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{ return nil, errors.Wrap(dbc.RedrawWinner(ids["id"], ids["winner"]), "re-picking winner") }, []string{"id", "winner"}), Method: http.MethodPut, - Module: actorName, + Module: moduleName, Name: "Re-Pick Raffle Winner", Path: "/{id}/repick/{winner}", RequiresWriteAuth: true, diff --git a/internal/apimodules/raffle/raffle.go b/internal/apimodules/raffle/raffle.go index 04c0b00..82c681a 100644 --- a/internal/apimodules/raffle/raffle.go +++ b/internal/apimodules/raffle/raffle.go @@ -12,8 +12,6 @@ import ( "github.com/Luzifer/twitch-bot/v3/plugins" ) -const actorName = "raffle" - var ( db database.Connector dbc *dbClient @@ -58,7 +56,7 @@ func Register(args plugins.RegistrationArguments) (err error) { } { if err := fn(); err != nil { logrus.WithFields(logrus.Fields{ - "actor": actorName, + "actor": moduleName, "cron": name, }).WithError(err).Error("executing cron action") } @@ -71,5 +69,24 @@ func Register(args plugins.RegistrationArguments) (err error) { return errors.Wrap(err, "registering raw message handler") } + args.RegisterActor(enterRaffleActor{}.Name(), func() plugins.Actor { return &enterRaffleActor{} }) + args.RegisterActorDocumentation(plugins.ActionDocumentation{ + Description: "Enter user to raffle through channelpoints", + Name: "Enter User to Raffle", + Type: enterRaffleActor{}.Name(), + + Fields: []plugins.ActionDocumentationField{ + { + Default: "", + Description: "The keyword for the active raffle to enter the user into", + Key: "keyword", + Name: "Keyword", + Optional: false, + SupportTemplate: false, + Type: plugins.ActionDocumentationFieldTypeString, + }, + }, + }) + return nil } diff --git a/src/raffle.vue b/src/raffle.vue index e61f903..2c4c27f 100644 --- a/src/raffle.vue +++ b/src/raffle.vue @@ -227,6 +227,12 @@ :icon="['fas', 'heart']" title="Follower" /> +