feat: we can generate our own documentation... wait but why

This commit is contained in:
Ash Keel 2023-02-07 22:29:26 +01:00
parent 87c80a81fb
commit 9df69d1b3d
No known key found for this signature in database
GPG Key ID: BAD8D93E7314ED3E
15 changed files with 388 additions and 118 deletions

3
.gitignore vendored
View File

@ -7,4 +7,5 @@
.idea
strimertul_*
build/bin
*.log
*.log
api.json

View File

@ -6,6 +6,7 @@ Small broadcasting suite for Twitch, includes:
- Loyalty points system with redeems and community goals
- Twitch chat integration with custom commands
- 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.

6
app.go
View File

@ -4,6 +4,8 @@ import (
"context"
"strconv"
"github.com/strimertul/strimertul/docs"
"git.sr.ht/~hamcha/containers/sync"
"github.com/nicklaw5/helix/v2"
"github.com/urfave/cli/v2"
@ -137,3 +139,7 @@ func (a *App) GetTwitchLoggedUser() (helix.User, error) {
func (a *App) GetLastLogs() []LogEntry {
return lastLogs.Get()
}
func (a *App) GetDocumentation() map[string]docs.KeyObject {
return docs.Keys
}

14
docs/cmd/docgen/main.go Normal file
View File

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

94
docs/data.go Normal file
View File

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

34
docs/init.go Normal file
View File

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

26
docs/interfaces/common.go Normal file
View File

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

View File

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

15
http/doc.go Normal file
View File

@ -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{}),
},
}

47
loyalty/doc.go Normal file
View File

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

View File

@ -20,17 +20,17 @@ const BotAlertsKey = "twitch/bot-modules/alerts/config"
type eventSubNotification struct {
Subscription helix.EventSubSubscription `json:"subscription"`
Challenge string `json:"challenge"`
Event jsoniter.RawMessage `json:"event"`
Event jsoniter.RawMessage `json:"event" desc:"Event payload, as JSON object"`
}
type BotAlertsConfig struct {
Follow struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Enabled bool `json:"enabled" desc:"Enable chat message alert on follow"`
Messages []string `json:"messages" desc:"List of message to write on follow, one at random will be picked"`
} `json:"follow"`
Subscription struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Enabled bool `json:"enabled" desc:"Enable chat message alert on subscription"`
Messages []string `json:"messages" desc:"List of message to write on subscription, one at random will be picked"`
Variations []struct {
MinStreak *int `json:"min_streak,omitempty"`
IsGifted *bool `json:"is_gifted,omitempty"`
@ -38,8 +38,8 @@ type BotAlertsConfig struct {
} `json:"variations"`
} `json:"subscription"`
GiftSub struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Enabled bool `json:"enabled" desc:"Enable chat message alert on gifted subscription"`
Messages []string `json:"messages" desc:"List of message to write on gifted subscription, one at random will be picked"`
Variations []struct {
MinCumulative *int `json:"min_cumulative,omitempty"`
IsAnonymous *bool `json:"is_anonymous,omitempty"`
@ -47,16 +47,16 @@ type BotAlertsConfig struct {
} `json:"variations"`
} `json:"gift_sub"`
Raid struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Enabled bool `json:"enabled" desc:"Enable chat message alert on raid"`
Messages []string `json:"messages" desc:"List of message to write on raid, one at random will be picked"`
Variations []struct {
MinViewers *int `json:"min_viewers,omitempty"`
Messages []string `json:"messages"`
} `json:"variations"`
} `json:"raid"`
Cheer struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Enabled bool `json:"enabled" desc:"Enable chat message alert on cheer"`
Messages []string `json:"messages" desc:"List of message to write on cheer, one at random will be picked"`
Variations []struct {
MinAmount *int `json:"min_amount,omitempty"`
Messages []string `json:"messages"`

View File

@ -14,15 +14,24 @@ import (
const BotTimersKey = "twitch/bot-modules/timers/config"
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 {
Enabled bool `json:"enabled"` // Whether the timer is enabled
Name string `json:"name"` // Timer name (must be unique)
MinimumChatActivity int `json:"minimum_chat_activity"` // Minimum chat messages in the last 5 minutes
MinimumDelay int `json:"minimum_delay"` // In seconds
Messages []string `json:"messages"` // Messages to write (randomly chosen)
// Whether the timer is enabled
Enabled bool `json:"enabled" desc:"Enable the timer"`
// Timer name (must be unique)
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

View File

@ -64,18 +64,21 @@ func (c *Client) connectWebsocket(userClient *helix.Client) {
err = json.Unmarshal(wsMessage.Payload, &welcomeData)
if err != nil {
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))
// Add subscription to websocket session
err = c.addSubscriptionsForSession(userClient, welcomeData.Session.Id)
if err != nil {
c.logger.Error("could not add subscriptions", zap.Error(err))
break
}
case "session_reconnect":
var reconnectData WelcomeMessagePayload
err = json.Unmarshal(wsMessage.Payload, &reconnectData)
if err != nil {
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))
@ -83,6 +86,7 @@ func (c *Client) connectWebsocket(userClient *helix.Client) {
newConnection, _, err := websocket.DefaultDialer.Dial(reconnectData.Session.ReconnectUrl, nil)
if err != nil {
c.logger.Error("eventsub ws reconnect error", zap.Error(err))
break
} else {
_ = connection.Close()
connection = newConnection

View File

@ -4,22 +4,38 @@ const CallbackRoute = "/twitch/callback"
const ConfigKey = "twitch/config"
// Config is the general configuration for the Twitch subsystem
type Config struct {
Enabled bool `json:"enabled"`
EnableBot bool `json:"enable_bot"`
APIClientID string `json:"api_client_id"`
APIClientSecret string `json:"api_client_secret"`
// Enable subsystem
Enabled bool `json:"enabled" desc:"Enable subsystem"`
// 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 BotConfigKey = "twitch/bot-config"
// BotConfig is the general configuration for the Twitch chatbot
type BotConfig struct {
Username string `json:"username"`
Token string `json:"oauth"`
Channel string `json:"channel"`
ChatHistory int `json:"chat_history"`
// Chatbot username (for internal use, ignored by Twitch)
Username string `json:"username" desc:"Chatbot username (for internal use, ignored by Twitch)"`
// 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 (
@ -28,11 +44,19 @@ const (
ChatActivityKey = "twitch/chat-activity"
)
// BotCustomCommand is a definition of a custom command of the chatbot
type BotCustomCommand struct {
Description string `json:"description"`
AccessLevel AccessLevelType `json:"access_level"`
Response string `json:"response"`
Enabled bool `json:"enabled"`
// Command description
Description string `json:"description" desc:"Command description"`
// 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"

83
twitch/doc.go Normal file
View File

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