mirror of https://git.sr.ht/~ashkeel/strimertul
feat: we can generate our own documentation... wait but why
This commit is contained in:
parent
87c80a81fb
commit
9df69d1b3d
|
@ -7,4 +7,5 @@
|
||||||
.idea
|
.idea
|
||||||
strimertul_*
|
strimertul_*
|
||||||
build/bin
|
build/bin
|
||||||
*.log
|
*.log
|
||||||
|
api.json
|
|
@ -6,6 +6,7 @@ Small broadcasting suite for Twitch, includes:
|
||||||
- Loyalty points system with redeems and community goals
|
- Loyalty points system with redeems and community goals
|
||||||
- Twitch chat integration with custom commands
|
- Twitch chat integration with custom commands
|
||||||
- Support for Twitch alerts and channel point redeems
|
- Support for Twitch alerts and channel point redeems
|
||||||
|
- Built-in runner for simple extensions to add extra features
|
||||||
|
|
||||||
**Note:** some technical/coding experience is currently required to be able to use this effectively, see the technical overview below for more information.
|
**Note:** some technical/coding experience is currently required to be able to use this effectively, see the technical overview below for more information.
|
||||||
|
|
||||||
|
|
6
app.go
6
app.go
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/strimertul/strimertul/docs"
|
||||||
|
|
||||||
"git.sr.ht/~hamcha/containers/sync"
|
"git.sr.ht/~hamcha/containers/sync"
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -137,3 +139,7 @@ func (a *App) GetTwitchLoggedUser() (helix.User, error) {
|
||||||
func (a *App) GetLastLogs() []LogEntry {
|
func (a *App) GetLastLogs() []LogEntry {
|
||||||
return lastLogs.Get()
|
return lastLogs.Get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) GetDocumentation() map[string]docs.KeyObject {
|
||||||
|
return docs.Keys
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/strimertul/strimertul/docs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
enc := jsoniter.ConfigFastest.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
_ = enc.Encode(docs.Keys)
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/strimertul/strimertul/docs/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataObject struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Kind Kind `json:"kind"`
|
||||||
|
Keys []DataObject `json:"keys,omitempty"`
|
||||||
|
Key *DataObject `json:"key,omitempty"`
|
||||||
|
Element *DataObject `json:"element,omitempty"`
|
||||||
|
EnumValues []string `json:"enumValues,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyObject struct {
|
||||||
|
Description string `json:"description"`
|
||||||
|
Schema DataObject `json:"schema"`
|
||||||
|
Tags []interfaces.KeyTag `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Kind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
KindString Kind = "string"
|
||||||
|
KindInt Kind = "int"
|
||||||
|
KindFloat Kind = "float"
|
||||||
|
KindStruct Kind = "object"
|
||||||
|
KindBoolean Kind = "boolean"
|
||||||
|
KindEnum Kind = "enum"
|
||||||
|
KindUnknown Kind = "unknown"
|
||||||
|
KindArray Kind = "array"
|
||||||
|
KindDict Kind = "dictionary"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getKind(typ reflect.Kind) Kind {
|
||||||
|
switch typ {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
|
||||||
|
return KindInt
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return KindFloat
|
||||||
|
case reflect.String:
|
||||||
|
return KindString
|
||||||
|
case reflect.Bool:
|
||||||
|
return KindBoolean
|
||||||
|
case reflect.Struct:
|
||||||
|
return KindStruct
|
||||||
|
case reflect.Map:
|
||||||
|
return KindDict
|
||||||
|
case reflect.Array, reflect.Slice:
|
||||||
|
return KindArray
|
||||||
|
}
|
||||||
|
return KindUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseType(typ reflect.Type) (out DataObject) {
|
||||||
|
out.Name = typ.Name()
|
||||||
|
if enum, ok := Enums[out.Name]; ok {
|
||||||
|
out.Kind = KindEnum
|
||||||
|
for _, it := range enum.Values {
|
||||||
|
out.EnumValues = append(out.EnumValues, fmt.Sprint(it))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out.Kind = getKind(typ.Kind())
|
||||||
|
if out.Kind == KindArray || out.Kind == KindDict {
|
||||||
|
elem := parseType(typ.Elem())
|
||||||
|
out.Element = &elem
|
||||||
|
if out.Kind == KindDict {
|
||||||
|
key := parseType(typ.Key())
|
||||||
|
out.Key = &key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.Kind == KindStruct {
|
||||||
|
for index := 0; index < typ.NumField(); index++ {
|
||||||
|
field := typ.Field(index)
|
||||||
|
obj := parseType(field.Type)
|
||||||
|
if jsonName, ok := field.Tag.Lookup("json"); ok {
|
||||||
|
parts := strings.SplitN(jsonName, ",", 2)
|
||||||
|
obj.Name = parts[0]
|
||||||
|
} else {
|
||||||
|
obj.Name = field.Name
|
||||||
|
}
|
||||||
|
obj.Description = field.Tag.Get("desc")
|
||||||
|
out.Keys = append(out.Keys, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/strimertul/strimertul/docs/interfaces"
|
||||||
|
"github.com/strimertul/strimertul/http"
|
||||||
|
"github.com/strimertul/strimertul/loyalty"
|
||||||
|
"github.com/strimertul/strimertul/twitch"
|
||||||
|
"github.com/strimertul/strimertul/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Enums = interfaces.EnumMap{}
|
||||||
|
Keys = map[string]KeyObject{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func addKeys(keyMap interfaces.KeyMap) {
|
||||||
|
for key, obj := range keyMap {
|
||||||
|
Keys[key] = KeyObject{
|
||||||
|
Description: obj.Description,
|
||||||
|
Tags: obj.Tags,
|
||||||
|
Schema: parseType(obj.Type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Put all enums here
|
||||||
|
utils.MergeMap(Enums, twitch.Enums)
|
||||||
|
|
||||||
|
// Put all keys here
|
||||||
|
addKeys(twitch.Keys)
|
||||||
|
addKeys(loyalty.Keys)
|
||||||
|
addKeys(http.Keys)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
type Enum struct {
|
||||||
|
Values []any
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyDef struct {
|
||||||
|
Description string
|
||||||
|
Type reflect.Type
|
||||||
|
Tags []KeyTag
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
EnumMap map[string]Enum // Go type-system is too prehistorical to support what I need here
|
||||||
|
KeyMap map[string]KeyDef
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyTag string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TagEvent KeyTag = "event"
|
||||||
|
TagRPC KeyTag = "rpc"
|
||||||
|
TagHistory KeyTag = "history"
|
||||||
|
)
|
|
@ -1,88 +0,0 @@
|
||||||
# Twitch integration
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Enable/disable Twitch integration
|
|
||||||
|
|
||||||
The Twitch integration can be enabled/disabled via `stul-meta/modules`, see [modules.md](./modules.md) for more details.
|
|
||||||
|
|
||||||
### Twitch integration configuration
|
|
||||||
|
|
||||||
The Twitch integration can be configured via `twitch/config` using a JSON object like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"enabled": bool, // Enable Twitch module (required)
|
|
||||||
"enable_bot": bool, // Enable IRC bot
|
|
||||||
"api_client_id": string, // Twitch App Client ID
|
|
||||||
"api_client_secret": string // Twitch App Client Secret
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The IRC bot has its own configuration in `twitch/bot-config` as the following JSON object:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"username": string, // Bot username, probably ignored
|
|
||||||
"oauth": string, // OAuth token
|
|
||||||
"channel": string, // Twitch channel to join
|
|
||||||
"chat_keys": bool, // True to enable chatlog keys
|
|
||||||
"chat_history": int // How many messages to save in twitch/chat-history
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If `chat_keys` is enabled, these keys will be updated every time a new message is written in the specified channel:
|
|
||||||
|
|
||||||
- `twitch/ev/chat-message` containing the message that was just written
|
|
||||||
- `twitch/chat-history` containing the updated list of the last N messages (N depends on `chat_history`)
|
|
||||||
|
|
||||||
See [this page](https://github.com/strimertul/strimertul/wiki/Extending-the-bot-with-external-modules) for info on chat message schema.
|
|
||||||
|
|
||||||
## Custom commands
|
|
||||||
|
|
||||||
The bot supports user-defined custom commands for basic things like auto-replies, counters and shoutouts.
|
|
||||||
|
|
||||||
The key `twitch/bot-custom-commands` contains a JSON dictionary of custom commands like the following:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"!command" : {
|
|
||||||
"description": string, // Command description, for UI only
|
|
||||||
"access_level": string, // Minimum required access level, see below
|
|
||||||
"response": string, // Response
|
|
||||||
"enabled": bool // Must be true for the command to work
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Access levels
|
|
||||||
|
|
||||||
The `access_level` property must be a string determining what kind of users can use the command, it can be one of the following:
|
|
||||||
|
|
||||||
| Access level identifier | Minimum access level |
|
|
||||||
| ----------------------- | ------------------------- |
|
|
||||||
| `"streamer"` | Just the broadcaster |
|
|
||||||
| `"moderators"` | Moderators and up |
|
|
||||||
| `"vip"` | VIP and up |
|
|
||||||
| `"subscriber"` | Twitch subscribers and up |
|
|
||||||
| `"everyone"` | Everyone |
|
|
||||||
|
|
||||||
Every level allows people in the upper tiers to use the command as well (eg. a VIP-only command can be used by the broadcaster and moderators).
|
|
||||||
|
|
||||||
### Response templating
|
|
||||||
|
|
||||||
Responses are fully functional golang templates, please refer to [text/template](https://pkg.go.dev/text/template) for a full reference on syntax and functionality.
|
|
||||||
|
|
||||||
The following functions are available:
|
|
||||||
|
|
||||||
- Every function in [sprig](https://masterminds.github.io/sprig/)
|
|
||||||
- [`user .`] retrieves the Twitch username of whoever typed the command
|
|
||||||
- [`param N .`] retrieves the Nth word after the command (ie. "`param 1 .`" on "`!so something awful`" would return "`something`")
|
|
||||||
- [`randomInt MIN MAX`] returns a random integer between MIN and MAX
|
|
||||||
- [`game USERNAME`] returns the current game for Twitch user USERNAME
|
|
||||||
- [`count COUNTER`] increases the counter for key `COUNTER` by 1 and returns it
|
|
||||||
|
|
||||||
## Sending text as the bot
|
|
||||||
|
|
||||||
Writing any string to `twitch/@send-chat-message` will send it as a message in chat from the bot's account
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/strimertul/strimertul/docs/interfaces"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Documentation stuff, keep updated at all times
|
||||||
|
|
||||||
|
var Keys = interfaces.KeyMap{
|
||||||
|
ServerConfigKey: interfaces.KeyDef{
|
||||||
|
Description: "General server configuration",
|
||||||
|
Type: reflect.TypeOf(ServerConfig{}),
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package loyalty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/strimertul/strimertul/docs/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Documentation stuff, keep updated at all times
|
||||||
|
|
||||||
|
var Keys = interfaces.KeyMap{
|
||||||
|
ConfigKey: interfaces.KeyDef{
|
||||||
|
Description: "General configuration for the loyalty subsystem",
|
||||||
|
Type: reflect.TypeOf(Config{}),
|
||||||
|
},
|
||||||
|
RewardsKey: interfaces.KeyDef{
|
||||||
|
Description: "List of available rewards",
|
||||||
|
Type: reflect.TypeOf([]Reward{}),
|
||||||
|
},
|
||||||
|
GoalsKey: interfaces.KeyDef{
|
||||||
|
Description: "List of all goals",
|
||||||
|
Type: reflect.TypeOf([]Goal{}),
|
||||||
|
},
|
||||||
|
PointsPrefix + "<user>": interfaces.KeyDef{
|
||||||
|
Description: "Point entry for a given user",
|
||||||
|
Type: reflect.TypeOf(PointsEntry{}),
|
||||||
|
},
|
||||||
|
QueueKey: interfaces.KeyDef{
|
||||||
|
Description: "All pending redeems",
|
||||||
|
Type: reflect.TypeOf([]Redeem{}),
|
||||||
|
},
|
||||||
|
RedeemEvent: interfaces.KeyDef{
|
||||||
|
Description: "On reward redeemed",
|
||||||
|
Type: reflect.TypeOf(Redeem{}),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagEvent},
|
||||||
|
},
|
||||||
|
CreateRedeemRPC: interfaces.KeyDef{
|
||||||
|
Description: "Create a new pending redeem",
|
||||||
|
Type: reflect.TypeOf(Redeem{}),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagRPC},
|
||||||
|
},
|
||||||
|
RemoveRedeemRPC: interfaces.KeyDef{
|
||||||
|
Description: "Remove a redeem from the queue",
|
||||||
|
Type: reflect.TypeOf(Redeem{}),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagRPC},
|
||||||
|
},
|
||||||
|
}
|
|
@ -20,17 +20,17 @@ const BotAlertsKey = "twitch/bot-modules/alerts/config"
|
||||||
type eventSubNotification struct {
|
type eventSubNotification struct {
|
||||||
Subscription helix.EventSubSubscription `json:"subscription"`
|
Subscription helix.EventSubSubscription `json:"subscription"`
|
||||||
Challenge string `json:"challenge"`
|
Challenge string `json:"challenge"`
|
||||||
Event jsoniter.RawMessage `json:"event"`
|
Event jsoniter.RawMessage `json:"event" desc:"Event payload, as JSON object"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotAlertsConfig struct {
|
type BotAlertsConfig struct {
|
||||||
Follow struct {
|
Follow struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled" desc:"Enable chat message alert on follow"`
|
||||||
Messages []string `json:"messages"`
|
Messages []string `json:"messages" desc:"List of message to write on follow, one at random will be picked"`
|
||||||
} `json:"follow"`
|
} `json:"follow"`
|
||||||
Subscription struct {
|
Subscription struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled" desc:"Enable chat message alert on subscription"`
|
||||||
Messages []string `json:"messages"`
|
Messages []string `json:"messages" desc:"List of message to write on subscription, one at random will be picked"`
|
||||||
Variations []struct {
|
Variations []struct {
|
||||||
MinStreak *int `json:"min_streak,omitempty"`
|
MinStreak *int `json:"min_streak,omitempty"`
|
||||||
IsGifted *bool `json:"is_gifted,omitempty"`
|
IsGifted *bool `json:"is_gifted,omitempty"`
|
||||||
|
@ -38,8 +38,8 @@ type BotAlertsConfig struct {
|
||||||
} `json:"variations"`
|
} `json:"variations"`
|
||||||
} `json:"subscription"`
|
} `json:"subscription"`
|
||||||
GiftSub struct {
|
GiftSub struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled" desc:"Enable chat message alert on gifted subscription"`
|
||||||
Messages []string `json:"messages"`
|
Messages []string `json:"messages" desc:"List of message to write on gifted subscription, one at random will be picked"`
|
||||||
Variations []struct {
|
Variations []struct {
|
||||||
MinCumulative *int `json:"min_cumulative,omitempty"`
|
MinCumulative *int `json:"min_cumulative,omitempty"`
|
||||||
IsAnonymous *bool `json:"is_anonymous,omitempty"`
|
IsAnonymous *bool `json:"is_anonymous,omitempty"`
|
||||||
|
@ -47,16 +47,16 @@ type BotAlertsConfig struct {
|
||||||
} `json:"variations"`
|
} `json:"variations"`
|
||||||
} `json:"gift_sub"`
|
} `json:"gift_sub"`
|
||||||
Raid struct {
|
Raid struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled" desc:"Enable chat message alert on raid"`
|
||||||
Messages []string `json:"messages"`
|
Messages []string `json:"messages" desc:"List of message to write on raid, one at random will be picked"`
|
||||||
Variations []struct {
|
Variations []struct {
|
||||||
MinViewers *int `json:"min_viewers,omitempty"`
|
MinViewers *int `json:"min_viewers,omitempty"`
|
||||||
Messages []string `json:"messages"`
|
Messages []string `json:"messages"`
|
||||||
} `json:"variations"`
|
} `json:"variations"`
|
||||||
} `json:"raid"`
|
} `json:"raid"`
|
||||||
Cheer struct {
|
Cheer struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled" desc:"Enable chat message alert on cheer"`
|
||||||
Messages []string `json:"messages"`
|
Messages []string `json:"messages" desc:"List of message to write on cheer, one at random will be picked"`
|
||||||
Variations []struct {
|
Variations []struct {
|
||||||
MinAmount *int `json:"min_amount,omitempty"`
|
MinAmount *int `json:"min_amount,omitempty"`
|
||||||
Messages []string `json:"messages"`
|
Messages []string `json:"messages"`
|
||||||
|
|
|
@ -14,15 +14,24 @@ import (
|
||||||
const BotTimersKey = "twitch/bot-modules/timers/config"
|
const BotTimersKey = "twitch/bot-modules/timers/config"
|
||||||
|
|
||||||
type BotTimersConfig struct {
|
type BotTimersConfig struct {
|
||||||
Timers map[string]BotTimer `json:"timers"`
|
Timers map[string]BotTimer `json:"timers" desc:"List of timers as a dictionary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotTimer struct {
|
type BotTimer struct {
|
||||||
Enabled bool `json:"enabled"` // Whether the timer is enabled
|
// Whether the timer is enabled
|
||||||
Name string `json:"name"` // Timer name (must be unique)
|
Enabled bool `json:"enabled" desc:"Enable the timer"`
|
||||||
MinimumChatActivity int `json:"minimum_chat_activity"` // Minimum chat messages in the last 5 minutes
|
|
||||||
MinimumDelay int `json:"minimum_delay"` // In seconds
|
// Timer name (must be unique)
|
||||||
Messages []string `json:"messages"` // Messages to write (randomly chosen)
|
Name string `json:"name" desc:"Timer name (must be unique)"`
|
||||||
|
|
||||||
|
// Minimum chat messages in the last 5 minutes for timer to trigger
|
||||||
|
MinimumChatActivity int `json:"minimum_chat_activity" desc:"Minimum chat messages in the last 5 minutes for timer to trigger"`
|
||||||
|
|
||||||
|
// Minimum amount of time (in seconds) that needs to pass before it triggers again
|
||||||
|
MinimumDelay int `json:"minimum_delay" desc:"Minimum amount of time (in seconds) that needs to pass before it triggers again"`
|
||||||
|
|
||||||
|
// Messages to write (randomly chosen)
|
||||||
|
Messages []string `json:"messages" desc:"Messages to write (randomly chosen)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const AverageMessageWindow = 5
|
const AverageMessageWindow = 5
|
||||||
|
|
|
@ -64,18 +64,21 @@ func (c *Client) connectWebsocket(userClient *helix.Client) {
|
||||||
err = json.Unmarshal(wsMessage.Payload, &welcomeData)
|
err = json.Unmarshal(wsMessage.Payload, &welcomeData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("eventsub ws decode error", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
|
c.logger.Error("eventsub ws decode error", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
c.logger.Info("eventsub ws connection established", zap.String("session-id", welcomeData.Session.Id))
|
c.logger.Info("eventsub ws connection established", zap.String("session-id", welcomeData.Session.Id))
|
||||||
// Add subscription to websocket session
|
// Add subscription to websocket session
|
||||||
err = c.addSubscriptionsForSession(userClient, welcomeData.Session.Id)
|
err = c.addSubscriptionsForSession(userClient, welcomeData.Session.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("could not add subscriptions", zap.Error(err))
|
c.logger.Error("could not add subscriptions", zap.Error(err))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case "session_reconnect":
|
case "session_reconnect":
|
||||||
var reconnectData WelcomeMessagePayload
|
var reconnectData WelcomeMessagePayload
|
||||||
err = json.Unmarshal(wsMessage.Payload, &reconnectData)
|
err = json.Unmarshal(wsMessage.Payload, &reconnectData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("eventsub ws decode error", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
|
c.logger.Error("eventsub ws decode error", zap.String("message-type", wsMessage.Metadata.MessageType), zap.Error(err))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
c.logger.Info("eventsub ws connection reset requested", zap.String("session-id", reconnectData.Session.Id), zap.String("reconnect-url", reconnectData.Session.ReconnectUrl))
|
c.logger.Info("eventsub ws connection reset requested", zap.String("session-id", reconnectData.Session.Id), zap.String("reconnect-url", reconnectData.Session.ReconnectUrl))
|
||||||
|
|
||||||
|
@ -83,6 +86,7 @@ func (c *Client) connectWebsocket(userClient *helix.Client) {
|
||||||
newConnection, _, err := websocket.DefaultDialer.Dial(reconnectData.Session.ReconnectUrl, nil)
|
newConnection, _, err := websocket.DefaultDialer.Dial(reconnectData.Session.ReconnectUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error("eventsub ws reconnect error", zap.Error(err))
|
c.logger.Error("eventsub ws reconnect error", zap.Error(err))
|
||||||
|
break
|
||||||
} else {
|
} else {
|
||||||
_ = connection.Close()
|
_ = connection.Close()
|
||||||
connection = newConnection
|
connection = newConnection
|
||||||
|
|
|
@ -4,22 +4,38 @@ const CallbackRoute = "/twitch/callback"
|
||||||
|
|
||||||
const ConfigKey = "twitch/config"
|
const ConfigKey = "twitch/config"
|
||||||
|
|
||||||
|
// Config is the general configuration for the Twitch subsystem
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Enabled bool `json:"enabled"`
|
// Enable subsystem
|
||||||
EnableBot bool `json:"enable_bot"`
|
Enabled bool `json:"enabled" desc:"Enable subsystem"`
|
||||||
APIClientID string `json:"api_client_id"`
|
|
||||||
APIClientSecret string `json:"api_client_secret"`
|
// Enable the chatbot
|
||||||
|
EnableBot bool `json:"enable_bot" desc:"Enable the chatbot"`
|
||||||
|
|
||||||
|
// Twitch API App Client ID
|
||||||
|
APIClientID string `json:"api_client_id" desc:"Twitch API App Client ID"`
|
||||||
|
|
||||||
|
// Twitch API App Client Secret
|
||||||
|
APIClientSecret string `json:"api_client_secret" desc:"Twitch API App Client Secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const StreamInfoKey = "twitch/stream-info"
|
const StreamInfoKey = "twitch/stream-info"
|
||||||
|
|
||||||
const BotConfigKey = "twitch/bot-config"
|
const BotConfigKey = "twitch/bot-config"
|
||||||
|
|
||||||
|
// BotConfig is the general configuration for the Twitch chatbot
|
||||||
type BotConfig struct {
|
type BotConfig struct {
|
||||||
Username string `json:"username"`
|
// Chatbot username (for internal use, ignored by Twitch)
|
||||||
Token string `json:"oauth"`
|
Username string `json:"username" desc:"Chatbot username (for internal use, ignored by Twitch)"`
|
||||||
Channel string `json:"channel"`
|
|
||||||
ChatHistory int `json:"chat_history"`
|
// OAuth key for IRC authentication
|
||||||
|
Token string `json:"oauth" desc:"OAuth key for IRC authentication"`
|
||||||
|
|
||||||
|
// Twitch channel to join and use
|
||||||
|
Channel string `json:"channel" desc:"Twitch channel to join and use"`
|
||||||
|
|
||||||
|
// How many messages to keep in twitch/chat-history
|
||||||
|
ChatHistory int `json:"chat_history" desc:"How many messages to keep in twitch/chat-history"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -28,11 +44,19 @@ const (
|
||||||
ChatActivityKey = "twitch/chat-activity"
|
ChatActivityKey = "twitch/chat-activity"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BotCustomCommand is a definition of a custom command of the chatbot
|
||||||
type BotCustomCommand struct {
|
type BotCustomCommand struct {
|
||||||
Description string `json:"description"`
|
// Command description
|
||||||
AccessLevel AccessLevelType `json:"access_level"`
|
Description string `json:"description" desc:"Command description"`
|
||||||
Response string `json:"response"`
|
|
||||||
Enabled bool `json:"enabled"`
|
// Minimum access level needed to use the command
|
||||||
|
AccessLevel AccessLevelType `json:"access_level" desc:"Minimum access level needed to use the command"`
|
||||||
|
|
||||||
|
// Response template (in Go templating format)
|
||||||
|
Response string `json:"response" desc:"Response template (in Go templating format)"`
|
||||||
|
|
||||||
|
// Is the command enabled?
|
||||||
|
Enabled bool `json:"enabled" desc:"Is the command enabled?"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomCommandsKey = "twitch/bot-custom-commands"
|
const CustomCommandsKey = "twitch/bot-custom-commands"
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package twitch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
irc "github.com/gempir/go-twitch-irc/v4"
|
||||||
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
"github.com/strimertul/strimertul/docs/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Documentation stuff, keep updated at all times
|
||||||
|
|
||||||
|
var Keys = interfaces.KeyMap{
|
||||||
|
ConfigKey: interfaces.KeyDef{
|
||||||
|
Description: "General configuration for the Twitch subsystem",
|
||||||
|
Type: reflect.TypeOf(Config{}),
|
||||||
|
},
|
||||||
|
StreamInfoKey: interfaces.KeyDef{
|
||||||
|
Description: "List of active twitch streams (1 element if live, 0 otherwise)",
|
||||||
|
Type: reflect.TypeOf([]helix.Stream{}),
|
||||||
|
},
|
||||||
|
BotConfigKey: interfaces.KeyDef{
|
||||||
|
Description: "General configuration for the Twitch chatbot",
|
||||||
|
Type: reflect.TypeOf(BotConfig{}),
|
||||||
|
},
|
||||||
|
ChatEventKey: interfaces.KeyDef{
|
||||||
|
Description: "On chat message received",
|
||||||
|
Type: reflect.TypeOf(irc.PrivateMessage{}),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagEvent},
|
||||||
|
},
|
||||||
|
ChatHistoryKey: interfaces.KeyDef{
|
||||||
|
Description: "Last chat messages received",
|
||||||
|
Type: reflect.TypeOf([]irc.PrivateMessage{}),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagHistory},
|
||||||
|
},
|
||||||
|
ChatActivityKey: interfaces.KeyDef{
|
||||||
|
Description: "Number of chat messages in the last minute",
|
||||||
|
Type: reflect.TypeOf(0),
|
||||||
|
},
|
||||||
|
CustomCommandsKey: interfaces.KeyDef{
|
||||||
|
Description: "Chatbot custom commands",
|
||||||
|
Type: reflect.TypeOf(map[string]BotCustomCommand{}),
|
||||||
|
},
|
||||||
|
AuthKey: interfaces.KeyDef{
|
||||||
|
Description: "User access token for the twitch subsystem",
|
||||||
|
Type: reflect.TypeOf(AuthResponse{}),
|
||||||
|
},
|
||||||
|
EventSubEventKey: interfaces.KeyDef{
|
||||||
|
Description: "On Eventsub event received",
|
||||||
|
Type: reflect.TypeOf(NotificationMessagePayload{}),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagEvent},
|
||||||
|
},
|
||||||
|
EventSubHistoryKey: interfaces.KeyDef{
|
||||||
|
Description: "Last eventsub notifications received",
|
||||||
|
Type: reflect.TypeOf([]NotificationMessagePayload{}),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagHistory},
|
||||||
|
},
|
||||||
|
BotAlertsKey: interfaces.KeyDef{
|
||||||
|
Description: "Configuration of chat bot alerts",
|
||||||
|
Type: reflect.TypeOf(BotAlertsConfig{}),
|
||||||
|
},
|
||||||
|
BotTimersKey: interfaces.KeyDef{
|
||||||
|
Description: "Configuration of chat bot timers",
|
||||||
|
Type: reflect.TypeOf(BotTimersConfig{}),
|
||||||
|
},
|
||||||
|
WriteMessageRPC: interfaces.KeyDef{
|
||||||
|
Description: "Send plain text chat message",
|
||||||
|
Type: reflect.TypeOf(""),
|
||||||
|
Tags: []interfaces.KeyTag{interfaces.TagRPC},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var Enums = interfaces.EnumMap{
|
||||||
|
"AccessLevelType": interfaces.Enum{
|
||||||
|
Values: []any{
|
||||||
|
ALTEveryone,
|
||||||
|
ALTSubscribers,
|
||||||
|
ALTVIP,
|
||||||
|
ALTModerators,
|
||||||
|
ALTStreamer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in New Issue