mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-30 02:40:33 +00:00
270 lines
7.4 KiB
Go
270 lines
7.4 KiB
Go
package chat
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
textTemplate "text/template"
|
|
"time"
|
|
|
|
"git.sr.ht/~ashkeel/containers/sync"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/nicklaw5/helix/v2"
|
|
"go.uber.org/zap"
|
|
|
|
"git.sr.ht/~ashkeel/strimertul/database"
|
|
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
|
"git.sr.ht/~ashkeel/strimertul/twitch/template"
|
|
"git.sr.ht/~ashkeel/strimertul/utils"
|
|
)
|
|
|
|
var json = jsoniter.ConfigFastest
|
|
|
|
type Module struct {
|
|
Config Config
|
|
|
|
ctx context.Context
|
|
db *database.LocalDBClient
|
|
api *helix.Client
|
|
user *helix.User
|
|
logger *zap.Logger
|
|
templater template.Engine
|
|
lastMessage *sync.RWSync[time.Time]
|
|
chatHistory *sync.Slice[helix.EventSubChannelChatMessageEvent]
|
|
|
|
commands *sync.Map[string, Command]
|
|
customCommands *sync.Map[string, CustomCommand]
|
|
customTemplates *sync.Map[string, *textTemplate.Template]
|
|
customFunctions textTemplate.FuncMap
|
|
|
|
cancelContext context.CancelFunc
|
|
cancelUpdateSub database.CancelFunc
|
|
cancelWriteRPCSub database.CancelFunc
|
|
cancelChatMessageSub database.CancelFunc
|
|
}
|
|
|
|
func Setup(ctx context.Context, db *database.LocalDBClient, api *helix.Client, user *helix.User, logger *zap.Logger, templater template.Engine) *Module {
|
|
newContext, cancel := context.WithCancel(ctx)
|
|
|
|
mod := &Module{
|
|
ctx: newContext,
|
|
db: db,
|
|
api: api,
|
|
user: user,
|
|
logger: logger,
|
|
templater: templater,
|
|
lastMessage: sync.NewRWSync(time.Now()),
|
|
|
|
commands: sync.NewMap[string, Command](),
|
|
customCommands: sync.NewMap[string, CustomCommand](),
|
|
customTemplates: sync.NewMap[string, *textTemplate.Template](),
|
|
customFunctions: make(textTemplate.FuncMap),
|
|
|
|
cancelContext: cancel,
|
|
}
|
|
|
|
// Get config
|
|
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
|
if errors.Is(err, database.ErrEmptyKey) {
|
|
mod.Config = Config{
|
|
ChatHistory: 0,
|
|
CommandCooldown: 2,
|
|
}
|
|
} else {
|
|
logger.Error("Failed to load chat module config", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
var err error
|
|
mod.cancelChatMessageSub, err = db.SubscribeKey(eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage)
|
|
if err != nil {
|
|
logger.Error("Could not subscribe to chat messages", zap.Error(err))
|
|
}
|
|
|
|
// Load custom commands
|
|
var customCommands map[string]CustomCommand
|
|
err = db.GetJSON(CustomCommandsKey, &customCommands)
|
|
if err != nil {
|
|
if errors.Is(err, database.ErrEmptyKey) {
|
|
customCommands = make(map[string]CustomCommand)
|
|
} else {
|
|
logger.Error("Failed to load custom commands", zap.Error(err))
|
|
}
|
|
}
|
|
mod.customCommands.Set(customCommands)
|
|
|
|
err = mod.updateTemplates()
|
|
if err != nil {
|
|
logger.Error("Failed to parse custom commands", zap.Error(err))
|
|
}
|
|
mod.cancelUpdateSub, err = db.SubscribeKey(CustomCommandsKey, mod.updateCommands)
|
|
if err != nil {
|
|
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
|
|
}
|
|
mod.cancelWriteRPCSub, err = db.SubscribeKey(WriteMessageRPC, mod.handleWriteMessageRPC)
|
|
if err != nil {
|
|
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
|
|
}
|
|
|
|
return mod
|
|
}
|
|
|
|
func (mod *Module) Close() {
|
|
if mod.cancelChatMessageSub != nil {
|
|
mod.cancelChatMessageSub()
|
|
}
|
|
|
|
if mod.cancelUpdateSub != nil {
|
|
mod.cancelUpdateSub()
|
|
}
|
|
|
|
if mod.cancelWriteRPCSub != nil {
|
|
mod.cancelWriteRPCSub()
|
|
}
|
|
|
|
mod.cancelContext()
|
|
}
|
|
|
|
func (mod *Module) onChatMessage(newValue string) {
|
|
var chatMessage helix.EventSubChannelChatMessageEvent
|
|
if err := json.Unmarshal([]byte(newValue), &chatMessage); err != nil {
|
|
mod.logger.Error("Failed to decode incoming chat message", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
// TODO Command cooldown logic here!
|
|
|
|
lowercaseMessage := strings.TrimSpace(strings.ToLower(chatMessage.Message.Text))
|
|
|
|
// Check if it's a command
|
|
if strings.HasPrefix(lowercaseMessage, "!") {
|
|
// Run through supported commands
|
|
for cmd, data := range mod.commands.Copy() {
|
|
if !data.Enabled {
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(lowercaseMessage, cmd) {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(lowercaseMessage, " ", 2)
|
|
if parts[0] != cmd {
|
|
continue
|
|
}
|
|
go data.Handler(chatMessage)
|
|
mod.lastMessage.Set(time.Now())
|
|
}
|
|
}
|
|
|
|
// Run through custom commands
|
|
for cmd, data := range mod.customCommands.Copy() {
|
|
if !data.Enabled {
|
|
continue
|
|
}
|
|
lc := strings.ToLower(cmd)
|
|
if !strings.HasPrefix(lowercaseMessage, lc) {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(lowercaseMessage, " ", 2)
|
|
if parts[0] != lc {
|
|
continue
|
|
}
|
|
go cmdCustom(mod, cmd, data, chatMessage)
|
|
mod.lastMessage.Set(time.Now())
|
|
}
|
|
|
|
err := mod.db.PutJSON(EventKey, chatMessage)
|
|
if err != nil {
|
|
mod.logger.Warn("Could not save chat message to key", zap.Error(err))
|
|
}
|
|
if mod.Config.ChatHistory > 0 {
|
|
history := mod.chatHistory.Get()
|
|
if len(history) >= mod.Config.ChatHistory {
|
|
history = history[len(history)-mod.Config.ChatHistory+1:]
|
|
}
|
|
mod.chatHistory.Set(append(history, chatMessage))
|
|
err = mod.db.PutJSON(HistoryKey, mod.chatHistory.Get())
|
|
if err != nil {
|
|
mod.logger.Warn("Could not save message to chat history", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mod *Module) handleWriteMessageRPC(value string) {
|
|
var request WriteMessageRequest
|
|
if err := json.Unmarshal([]byte(value), &request); err != nil {
|
|
mod.logger.Warn("Failed to decode write message request", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
if request.Announce {
|
|
resp, err := mod.api.SendChatAnnouncement(&helix.SendChatAnnouncementParams{
|
|
BroadcasterID: mod.user.ID,
|
|
ModeratorID: mod.user.ID,
|
|
Message: request.Message,
|
|
})
|
|
if err != nil {
|
|
mod.logger.Error("Failed to send announcement", zap.Error(err))
|
|
}
|
|
if resp.Error != "" {
|
|
mod.logger.Error("Failed to send announcement", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
|
|
}
|
|
return
|
|
}
|
|
|
|
if request.WhisperTo != "" {
|
|
resp, err := mod.api.SendUserWhisper(&helix.SendUserWhisperParams{
|
|
FromUserID: mod.user.ID,
|
|
ToUserID: request.WhisperTo,
|
|
Message: request.Message,
|
|
})
|
|
if err != nil {
|
|
mod.logger.Error("Failed to send whisper", zap.Error(err))
|
|
}
|
|
if resp.Error != "" {
|
|
mod.logger.Error("Failed to send whisper", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
|
|
}
|
|
return
|
|
}
|
|
|
|
resp, err := mod.api.SendChatMessage(&helix.SendChatMessageParams{
|
|
BroadcasterID: mod.user.ID,
|
|
SenderID: mod.user.ID,
|
|
Message: request.Message,
|
|
ReplyParentMessageID: request.ReplyTo,
|
|
})
|
|
if err != nil {
|
|
mod.logger.Error("Failed to send chat message", zap.Error(err))
|
|
}
|
|
if resp.Error != "" {
|
|
mod.logger.Error("Failed to send chat message", zap.String("code", resp.Error), zap.String("message", resp.ErrorMessage))
|
|
}
|
|
}
|
|
|
|
func (mod *Module) updateCommands(value string) {
|
|
err := utils.LoadJSONToWrapped[map[string]CustomCommand](value, mod.customCommands)
|
|
if err != nil {
|
|
mod.logger.Error("Failed to decode new custom commands", zap.Error(err))
|
|
return
|
|
}
|
|
// Recreate templates
|
|
if err := mod.updateTemplates(); err != nil {
|
|
mod.logger.Error("Failed to update custom commands templates", zap.Error(err))
|
|
return
|
|
}
|
|
}
|
|
|
|
func (mod *Module) updateTemplates() error {
|
|
mod.customTemplates.Set(make(map[string]*textTemplate.Template))
|
|
for cmd, tmpl := range mod.customCommands.Copy() {
|
|
tpl, err := mod.templater.MakeTemplate(tmpl.Response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mod.customTemplates.SetKey(cmd, tpl)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (mod *Module) WriteMessage(request WriteMessageRequest) {
|
|
WriteMessage(mod.db, mod.logger, request)
|
|
}
|