mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-20 20:01:17 +00:00
[kofi] Add kofi_donation
event and Ko-fi webhook handler
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
6826f507f6
commit
99c366ada0
6 changed files with 228 additions and 0 deletions
|
@ -100,6 +100,21 @@ Fields:
|
||||||
- `channel` - The channel the event occurred in
|
- `channel` - The channel the event occurred in
|
||||||
- `user` - The login-name of the user who joined
|
- `user` - The login-name of the user who joined
|
||||||
|
|
||||||
|
## `kofi_donation`
|
||||||
|
|
||||||
|
A Ko-fi donation was received through the API-Webhook.
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
- `channel` - The channel the event occurred for
|
||||||
|
- `from` - The name submitted by Ko-fi (can be arbitrarily entered)
|
||||||
|
- `amount` - The amount donated as submitted by Ko-fi (i.e. 27.95)
|
||||||
|
- `currency` - The currency of the amount (i.e. USD)
|
||||||
|
- `isSubscription` - Boolean, true on monthly subscriptions, false on single-donations
|
||||||
|
- `isFirstSubPayment` - Boolean, true on first montly payment, false otherwise
|
||||||
|
- `message` - The message entered by the donator (**not** present when donation was marked as private!)
|
||||||
|
- `tier` - The tier the subscriber subscribed to (seems not to be filled on the first transaction?)
|
||||||
|
|
||||||
## `outbound_raid`
|
## `outbound_raid`
|
||||||
|
|
||||||
The channel has raided another channel. (The event is issued in the moment the raid is executed, not when the raid timer starts!)
|
The channel has raided another channel. (The event is issued in the moment the raid is executed, not when the raid timer starts!)
|
||||||
|
|
36
docs/content/modules/kofi.md
Normal file
36
docs/content/modules/kofi.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
title: "Ko-fi Event-Integration"
|
||||||
|
---
|
||||||
|
|
||||||
|
If you are an active user of Ko-fi you probably want to have the Ko-fi events sent to your bot to trigger chat-messages, alerts in overlays or just to have the event registered in the bot for overlays or other purposes. To do so you can use the Ko-fi integration of the bot to receive events for donations and subscriptions (shop-orders currently are not supported).
|
||||||
|
|
||||||
|
## Setting up
|
||||||
|
|
||||||
|
You will need
|
||||||
|
|
||||||
|
- a Ko-fi account
|
||||||
|
- an instance of the bot with access to the configuration
|
||||||
|
- the verification token available in the "API" menu entry in your settings page on Ko-fi
|
||||||
|
|
||||||
|
Given the bot web-interface is available at `https://example.com/` your webhook URL would be `https://example.com/kofi/webhook/<your channel>` so for example `https://example.com/kofi/webhook/luziferus`. You will later enter this into the "Webhook URL" field in the "API" settings-panel.
|
||||||
|
|
||||||
|
At first copy your "Verification Token" from the "API" settings-panel (and don't tell anyone!). You need to create a new block within your bots configuration file (at the moment there is no way to configure this through the web-interface):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Module configuration by channel or defining bot-wide defaults. See
|
||||||
|
# module specific documentation for options to configure in this
|
||||||
|
# section. All modules come with internal defaults so there is no
|
||||||
|
# need to configure this but you can overwrite the internal defaults.
|
||||||
|
module_config:
|
||||||
|
kofi:
|
||||||
|
luziferus: # put your channel name here, is the same as in the URL
|
||||||
|
verification_token: 'your verification token'
|
||||||
|
```
|
||||||
|
|
||||||
|
You can configure one token per channel and **should not** use the `default` entry as that would be used for all channels. This for example applies if your bot instance manages multiple channels with different Ko-fi accounts attached to them.
|
||||||
|
|
||||||
|
Now that we know about the verification token, you can put the URL into the "API" settings-panel and click the "Update" button.
|
||||||
|
|
||||||
|
As soon as you now click the "Send Single Donation Test", "Send First Monthly Test" or "Send Membership Tier Test" buttons you should see a `kofi_donation` event in the [debug overlay]({{< ref "../overlays/_index.md" >}}).
|
||||||
|
|
||||||
|
From here you can create rules using the [`kofi_donation` event]({{< ref "../configuration/events.md" >}}#kofi_donation) doing stuff.
|
|
@ -27,6 +27,7 @@ var (
|
||||||
eventTypeFollow = ptrStr("follow")
|
eventTypeFollow = ptrStr("follow")
|
||||||
eventTypeGiftPaidUpgrade = ptrStr("giftpaidupgrade")
|
eventTypeGiftPaidUpgrade = ptrStr("giftpaidupgrade")
|
||||||
eventTypeJoin = ptrStr("join")
|
eventTypeJoin = ptrStr("join")
|
||||||
|
eventKoFiDonation = ptrStr("kofi_donation")
|
||||||
eventTypeOutboundRaid = ptrStr("outbound_raid")
|
eventTypeOutboundRaid = ptrStr("outbound_raid")
|
||||||
eventTypePart = ptrStr("part")
|
eventTypePart = ptrStr("part")
|
||||||
eventTypePermit = ptrStr("permit")
|
eventTypePermit = ptrStr("permit")
|
||||||
|
@ -61,6 +62,7 @@ var (
|
||||||
eventTypeFollow,
|
eventTypeFollow,
|
||||||
eventTypeGiftPaidUpgrade,
|
eventTypeGiftPaidUpgrade,
|
||||||
eventTypeJoin,
|
eventTypeJoin,
|
||||||
|
eventKoFiDonation,
|
||||||
eventTypeOutboundRaid,
|
eventTypeOutboundRaid,
|
||||||
eventTypePart,
|
eventTypePart,
|
||||||
eventTypePermit,
|
eventTypePermit,
|
||||||
|
|
122
internal/apimodules/kofi/kofi.go
Normal file
122
internal/apimodules/kofi/kofi.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
// Package kofi contains a webhook listener to be used in the Ko-fi
|
||||||
|
// API to receive information about (recurring) donations / shop orders
|
||||||
|
package kofi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const actorName = "kofi"
|
||||||
|
|
||||||
|
var (
|
||||||
|
eventCreatorFunc plugins.EventHandlerFunc
|
||||||
|
getModuleConfig plugins.ModuleConfigGetterFunc
|
||||||
|
|
||||||
|
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register provides the plugins.RegisterFunc
|
||||||
|
func Register(args plugins.RegistrationArguments) (err error) {
|
||||||
|
eventCreatorFunc = args.CreateEvent
|
||||||
|
getModuleConfig = args.GetModuleConfigForChannel
|
||||||
|
|
||||||
|
if err = args.RegisterAPIRoute(plugins.HTTPRouteRegistrationArgs{
|
||||||
|
Description: "Endpoint to handle Ko-fi Webhook posts",
|
||||||
|
HandlerFunc: handleKoFiPost,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Module: actorName,
|
||||||
|
Name: "Handle Ko-fi Webhook",
|
||||||
|
Path: "/webhook/{channel}",
|
||||||
|
ResponseType: plugins.HTTPRouteResponseTypeJSON,
|
||||||
|
RouteParams: []plugins.HTTPRouteParamDocumentation{
|
||||||
|
{
|
||||||
|
Description: "Channel to create the event in",
|
||||||
|
Name: "channel",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("registering API route: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleKoFiPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
channel := mux.Vars(r)["channel"]
|
||||||
|
|
||||||
|
channelModuleConf := getModuleConfig(actorName, channel)
|
||||||
|
|
||||||
|
// The data is sent (posted) with a content type of application/x-www-form-urlencoded.
|
||||||
|
// A field named 'data' contains the payment information as a JSON string.
|
||||||
|
jsonData := r.FormValue("data")
|
||||||
|
if jsonData == "" {
|
||||||
|
// Well, no.
|
||||||
|
logrus.WithField("remote_addr", r.RemoteAddr).Warn("received KoFi hook without payload")
|
||||||
|
http.Error(w, "you missed something", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
payload hookPayload
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read the payload
|
||||||
|
if err = json.Unmarshal([]byte(jsonData), &payload); err != nil {
|
||||||
|
logrus.WithError(err).Error("unmarshalling KoFi JSON data")
|
||||||
|
http.Error(w, "that's not valid json, you know", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we know the verification token, validate the payload
|
||||||
|
if validateToken := channelModuleConf.MustString("verification_token", ptrStringEmpty); validateToken != "" && payload.VerificationToken != validateToken {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"expected": fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(validateToken))),
|
||||||
|
"provided": fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(payload.VerificationToken))),
|
||||||
|
}).Error("received Ko-fi payload with invalid verification token")
|
||||||
|
|
||||||
|
http.Error(w, "ehm, who are you?", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := plugins.NewFieldCollection()
|
||||||
|
fields.Set("channel", "#"+strings.TrimLeft(channel, "#"))
|
||||||
|
|
||||||
|
switch payload.Type {
|
||||||
|
case hookTypeDonation, hookTypeSubscription:
|
||||||
|
// Single or Recurring Donation
|
||||||
|
fields.Set("from", payload.FromName)
|
||||||
|
fields.Set("amount", payload.Amount)
|
||||||
|
fields.Set("currency", payload.Currency)
|
||||||
|
fields.Set("isSubscription", payload.IsSubscriptionPayment)
|
||||||
|
fields.Set("isFirstSubPayment", payload.IsFirstSubscriptionPayment)
|
||||||
|
|
||||||
|
if payload.IsPublic {
|
||||||
|
fields.Set("message", payload.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.IsSubscriptionPayment && payload.TierName != nil {
|
||||||
|
fields.Set("tier", *payload.TierName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = eventCreatorFunc("kofi_donation", fields); err != nil {
|
||||||
|
logrus.WithError(err).Error("creating kofi_donation event")
|
||||||
|
http.Error(w, "ehm, that didn't work, I'm sorry", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unsupported, we take that and discard it
|
||||||
|
logrus.WithField("type", payload.Type).Warn("received unhandled hook type")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
51
internal/apimodules/kofi/schema.go
Normal file
51
internal/apimodules/kofi/schema.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package kofi
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type (
|
||||||
|
hookPayload struct {
|
||||||
|
VerificationToken string `json:"verification_token"`
|
||||||
|
MessageID string `json:"message_id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Type hookType `json:"type"`
|
||||||
|
IsPublic bool `json:"is_public"`
|
||||||
|
FromName string `json:"from_name"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
Amount float64 `json:"amount,string"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
IsSubscriptionPayment bool `json:"is_subscription_payment"`
|
||||||
|
IsFirstSubscriptionPayment bool `json:"is_first_subscription_payment"`
|
||||||
|
KofiTransactionID string `json:"kofi_transaction_id"`
|
||||||
|
ShopItems []shopItem `json:"shop_items"`
|
||||||
|
TierName *string `json:"tier_name"`
|
||||||
|
Shipping shippingInfo `json:"shipping"`
|
||||||
|
}
|
||||||
|
|
||||||
|
hookType string
|
||||||
|
|
||||||
|
shippingInfo struct {
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
StreetAddress string `json:"street_address"`
|
||||||
|
City string `json:"city"`
|
||||||
|
StateOrProvince string `json:"state_or_province"`
|
||||||
|
PostalCode string `json:"postal_code"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
Telephone string `json:"telephone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
shopItem struct {
|
||||||
|
DirectLinkCode string `json:"direct_link_code"`
|
||||||
|
VariationName string `json:"variation_name"`
|
||||||
|
Quantity int `json:"quantity"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hookTypeCommission hookType = "Commission"
|
||||||
|
hookTypeDonation hookType = "Donation"
|
||||||
|
hookTypeShopOrder hookType = "Shop Order"
|
||||||
|
hookTypeSubscription hookType = "Subscription"
|
||||||
|
)
|
|
@ -39,6 +39,7 @@ import (
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/vip"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/vip"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/whisper"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/whisper"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/customevent"
|
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/customevent"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/kofi"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/msgformat"
|
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/msgformat"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/overlays"
|
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/overlays"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/raffle"
|
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/raffle"
|
||||||
|
@ -101,6 +102,7 @@ var (
|
||||||
|
|
||||||
// API-only modules
|
// API-only modules
|
||||||
customevent.Register,
|
customevent.Register,
|
||||||
|
kofi.Register,
|
||||||
msgformat.Register,
|
msgformat.Register,
|
||||||
overlays.Register,
|
overlays.Register,
|
||||||
raffle.Register,
|
raffle.Register,
|
||||||
|
|
Loading…
Reference in a new issue