[kofi] Add kofi_donation event and Ko-fi webhook handler

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2024-02-15 18:25:16 +01:00
parent 6826f507f6
commit 99c366ada0
Signed by: luzifer
SSH key fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
6 changed files with 228 additions and 0 deletions

View file

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

View 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.

View file

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

View 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)
}

View 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"
)

View file

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