mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-11-09 08:40:01 +00:00
[raffle] Add Actor to enter user into raffle using channel-points
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
6df8fd42c2
commit
a336772303
6 changed files with 153 additions and 15 deletions
|
@ -233,6 +233,19 @@ Uses link- and clip-scanner to detect links / clips and applies link protection
|
||||||
stop_on_no_action: false
|
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 Script / Command
|
||||||
|
|
||||||
Execute external script / command
|
Execute external script / command
|
||||||
|
|
|
@ -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.
|
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" >}})
|
![]({{< 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).
|
- **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.
|
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.
|
||||||
|
|
80
internal/apimodules/raffle/actor.go
Normal file
80
internal/apimodules/raffle/actor.go
Normal 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
|
||||||
|
}
|
|
@ -15,6 +15,8 @@ import (
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const moduleName = "raffle"
|
||||||
|
|
||||||
var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
{
|
{
|
||||||
Description: "Lists all raffles known to the bot",
|
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")
|
return ras, errors.Wrap(err, "fetching raffles from database")
|
||||||
}, nil),
|
}, nil),
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "List Raffles",
|
Name: "List Raffles",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -41,7 +43,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.Create(ra), "creating raffle")
|
return nil, errors.Wrap(dbc.Create(ra), "creating raffle")
|
||||||
}, nil),
|
}, nil),
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Create Raffle",
|
Name: "Create Raffle",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -54,7 +56,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.Delete(ids["id"]), "fetching raffle from database")
|
return nil, errors.Wrap(dbc.Delete(ids["id"]), "fetching raffle from database")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodDelete,
|
Method: http.MethodDelete,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Delete Raffle",
|
Name: "Delete Raffle",
|
||||||
Path: "/{id}",
|
Path: "/{id}",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -74,7 +76,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return ra, errors.Wrap(err, "fetching raffle from database")
|
return ra, errors.Wrap(err, "fetching raffle from database")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Get Raffle",
|
Name: "Get Raffle",
|
||||||
Path: "/{id}",
|
Path: "/{id}",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -102,7 +104,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.Update(ra), "updating raffle")
|
return nil, errors.Wrap(dbc.Update(ra), "updating raffle")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Update Raffle",
|
Name: "Update Raffle",
|
||||||
Path: "/{id}",
|
Path: "/{id}",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -121,7 +123,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.Clone(ids["id"]), "cloning raffle")
|
return nil, errors.Wrap(dbc.Clone(ids["id"]), "cloning raffle")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Clone Raffle",
|
Name: "Clone Raffle",
|
||||||
Path: "/{id}/clone",
|
Path: "/{id}/clone",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -140,7 +142,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.Close(ids["id"]), "closing raffle")
|
return nil, errors.Wrap(dbc.Close(ids["id"]), "closing raffle")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Close Raffle",
|
Name: "Close Raffle",
|
||||||
Path: "/{id}/close",
|
Path: "/{id}/close",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -159,7 +161,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.PickWinner(ids["id"]), "picking winner")
|
return nil, errors.Wrap(dbc.PickWinner(ids["id"]), "picking winner")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Pick Raffle Winner",
|
Name: "Pick Raffle Winner",
|
||||||
Path: "/{id}/pick",
|
Path: "/{id}/pick",
|
||||||
RequiresWriteAuth: true,
|
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")
|
return nil, errors.Wrap(dbc.Reopen(ids["id"], time.Duration(dur)*time.Second), "reopening raffle")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Reopen Raffle",
|
Name: "Reopen Raffle",
|
||||||
Path: "/{id}/reopen",
|
Path: "/{id}/reopen",
|
||||||
QueryParams: []plugins.HTTPRouteParamDocumentation{
|
QueryParams: []plugins.HTTPRouteParamDocumentation{
|
||||||
|
@ -210,7 +212,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.Start(ids["id"]), "starting raffle")
|
return nil, errors.Wrap(dbc.Start(ids["id"]), "starting raffle")
|
||||||
}, []string{"id"}),
|
}, []string{"id"}),
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Start Raffle",
|
Name: "Start Raffle",
|
||||||
Path: "/{id}/start",
|
Path: "/{id}/start",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
@ -229,7 +231,7 @@ var apiRoutes = []plugins.HTTPRouteRegistrationArgs{
|
||||||
return nil, errors.Wrap(dbc.RedrawWinner(ids["id"], ids["winner"]), "re-picking winner")
|
return nil, errors.Wrap(dbc.RedrawWinner(ids["id"], ids["winner"]), "re-picking winner")
|
||||||
}, []string{"id", "winner"}),
|
}, []string{"id", "winner"}),
|
||||||
Method: http.MethodPut,
|
Method: http.MethodPut,
|
||||||
Module: actorName,
|
Module: moduleName,
|
||||||
Name: "Re-Pick Raffle Winner",
|
Name: "Re-Pick Raffle Winner",
|
||||||
Path: "/{id}/repick/{winner}",
|
Path: "/{id}/repick/{winner}",
|
||||||
RequiresWriteAuth: true,
|
RequiresWriteAuth: true,
|
||||||
|
|
|
@ -12,8 +12,6 @@ import (
|
||||||
"github.com/Luzifer/twitch-bot/v3/plugins"
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const actorName = "raffle"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
db database.Connector
|
db database.Connector
|
||||||
dbc *dbClient
|
dbc *dbClient
|
||||||
|
@ -58,7 +56,7 @@ func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
} {
|
} {
|
||||||
if err := fn(); err != nil {
|
if err := fn(); err != nil {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"actor": actorName,
|
"actor": moduleName,
|
||||||
"cron": name,
|
"cron": name,
|
||||||
}).WithError(err).Error("executing cron action")
|
}).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")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,6 +227,12 @@
|
||||||
:icon="['fas', 'heart']"
|
:icon="['fas', 'heart']"
|
||||||
title="Follower"
|
title="Follower"
|
||||||
/>
|
/>
|
||||||
|
<font-awesome-icon
|
||||||
|
v-else-if="entry.enteredAs === 'reward'"
|
||||||
|
fixed-width
|
||||||
|
:icon="['fas', 'coins']"
|
||||||
|
title="Subscriber"
|
||||||
|
/>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-else-if="entry.enteredAs === 'subscriber'"
|
v-else-if="entry.enteredAs === 'subscriber'"
|
||||||
fixed-width
|
fixed-width
|
||||||
|
|
Loading…
Reference in a new issue