[raffle] Add Actor to enter user into raffle using channel-points

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2023-12-03 15:57:23 +01:00
parent 6df8fd42c2
commit a336772303
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
6 changed files with 153 additions and 15 deletions

View file

@ -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

View file

@ -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 (<i class="fas fa-user"></i>) for someone joined through the **Everyone** allowance, a heart (<i class="fas fa-heart"></i>) for a follower, a star (<i class="fas fa-star"></i>) for a subscriber and a diamond (<i class="fas fa-gem"></i>) 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 (<i class="fas fa-user"></i>) for someone joined through the **Everyone** allowance, a heart (<i class="fas fa-heart"></i>) for a follower, a star (<i class="fas fa-star"></i>) for a subscriber, a diamond (<i class="fas fa-gem"></i>) for a VIP and a coin (<i class="fas fa-coins"></i>) 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 "<the name you chose for the reward>" }}`
- 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.

View file

@ -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
}

View file

@ -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,

View file

@ -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
}

View file

@ -227,6 +227,12 @@
:icon="['fas', 'heart']"
title="Follower"
/>
<font-awesome-icon
v-else-if="entry.enteredAs === 'reward'"
fixed-width
:icon="['fas', 'coins']"
title="Subscriber"
/>
<font-awesome-icon
v-else-if="entry.enteredAs === 'subscriber'"
fixed-width