2024-01-05 21:24:54 +00:00
|
|
|
package chatbot
|
|
|
|
|
|
|
|
import (
|
2024-01-30 17:24:07 +00:00
|
|
|
"bufio"
|
2024-02-06 19:57:56 +00:00
|
|
|
"bytes"
|
2024-01-05 21:24:54 +00:00
|
|
|
"context"
|
2024-01-30 17:24:07 +00:00
|
|
|
"crypto/rand"
|
2024-01-05 21:24:54 +00:00
|
|
|
"fmt"
|
2024-02-06 19:57:56 +00:00
|
|
|
"html/template"
|
2024-01-05 21:24:54 +00:00
|
|
|
"log"
|
2024-01-30 17:24:07 +00:00
|
|
|
"math/big"
|
|
|
|
"os"
|
|
|
|
"strings"
|
2024-01-05 21:24:54 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/tylertravisty/rum-goggles/internal/config"
|
|
|
|
rumblelivestreamlib "github.com/tylertravisty/rumble-livestream-lib-go"
|
|
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
|
|
)
|
|
|
|
|
|
|
|
type ChatBot struct {
|
|
|
|
ctx context.Context
|
|
|
|
client *rumblelivestreamlib.Client
|
2024-01-30 17:24:07 +00:00
|
|
|
commands map[string]chan rumblelivestreamlib.ChatView
|
|
|
|
commandsMu sync.Mutex
|
2024-01-05 21:24:54 +00:00
|
|
|
Cfg config.ChatBot
|
|
|
|
logError *log.Logger
|
|
|
|
messages map[string]*message
|
|
|
|
messagesMu sync.Mutex
|
|
|
|
}
|
|
|
|
|
|
|
|
type message struct {
|
2024-02-06 19:57:56 +00:00
|
|
|
cancel context.CancelFunc
|
|
|
|
cancelMu sync.Mutex
|
|
|
|
asChannel bool
|
|
|
|
command string
|
|
|
|
id string
|
|
|
|
interval time.Duration
|
|
|
|
onCommand bool
|
|
|
|
onCommandFollower bool
|
|
|
|
onCommandRantAmount int
|
|
|
|
OnCommandSubscriber bool
|
|
|
|
text string
|
|
|
|
textFromFile []string
|
2024-01-05 21:24:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewChatBot(ctx context.Context, streamUrl string, cfg config.ChatBot, logError *log.Logger) (*ChatBot, error) {
|
2024-01-31 20:29:06 +00:00
|
|
|
client, err := rumblelivestreamlib.NewClient("", validUrl(streamUrl))
|
2024-01-05 21:24:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("chatbot: error creating new client: %v", err)
|
|
|
|
}
|
|
|
|
|
2024-01-30 17:24:07 +00:00
|
|
|
return &ChatBot{ctx: ctx, client: client, Cfg: cfg, commands: map[string]chan rumblelivestreamlib.ChatView{}, logError: logError, messages: map[string]*message{}}, nil
|
2024-01-05 21:24:54 +00:00
|
|
|
}
|
|
|
|
|
2024-01-31 20:29:06 +00:00
|
|
|
func validUrl(url string) string {
|
2024-02-01 02:18:40 +00:00
|
|
|
valid := url
|
2024-01-31 20:29:06 +00:00
|
|
|
if !strings.HasPrefix(valid, "https://") {
|
|
|
|
valid = "https://" + valid
|
|
|
|
}
|
|
|
|
|
|
|
|
return valid
|
|
|
|
}
|
|
|
|
|
2024-01-05 21:24:54 +00:00
|
|
|
func (cb *ChatBot) StartMessage(id string) error {
|
|
|
|
msg, exists := cb.Cfg.Messages[id]
|
|
|
|
if !exists {
|
|
|
|
return fmt.Errorf("chatbot: message does not exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
cb.messagesMu.Lock()
|
|
|
|
defer cb.messagesMu.Unlock()
|
|
|
|
m, exists := cb.messages[id]
|
|
|
|
if exists {
|
|
|
|
m.stop()
|
|
|
|
delete(cb.messages, id)
|
|
|
|
}
|
|
|
|
|
2024-01-30 17:24:07 +00:00
|
|
|
textFromFile := []string{}
|
|
|
|
if msg.TextFile != "" {
|
|
|
|
file, err := os.Open(msg.TextFile)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("chatbot: error opening file with responses: %v", err)
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if line == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
textFromFile = append(textFromFile, line)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-05 21:24:54 +00:00
|
|
|
m = &message{
|
2024-02-06 19:57:56 +00:00
|
|
|
asChannel: msg.AsChannel,
|
|
|
|
command: msg.Command,
|
|
|
|
id: msg.ID,
|
|
|
|
interval: msg.Interval,
|
|
|
|
onCommand: msg.OnCommand,
|
|
|
|
onCommandFollower: msg.OnCommandFollower,
|
|
|
|
onCommandRantAmount: msg.OnCommandRantAmount,
|
|
|
|
OnCommandSubscriber: msg.OnCommandSubscriber,
|
|
|
|
text: msg.Text,
|
|
|
|
textFromFile: textFromFile,
|
2024-01-05 21:24:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
m.cancelMu.Lock()
|
|
|
|
m.cancel = cancel
|
|
|
|
m.cancelMu.Unlock()
|
2024-01-30 17:24:07 +00:00
|
|
|
if msg.OnCommand {
|
|
|
|
go cb.startCommand(ctx, m)
|
|
|
|
} else {
|
|
|
|
go cb.startMessage(ctx, m)
|
|
|
|
}
|
2024-01-05 21:24:54 +00:00
|
|
|
|
|
|
|
cb.messages[id] = m
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-01-30 17:24:07 +00:00
|
|
|
func (cb *ChatBot) startCommand(ctx context.Context, m *message) {
|
|
|
|
cb.commandsMu.Lock()
|
|
|
|
ch := make(chan rumblelivestreamlib.ChatView)
|
|
|
|
cb.commands[m.command] = ch
|
|
|
|
cb.commandsMu.Unlock()
|
|
|
|
|
|
|
|
var prev time.Time
|
|
|
|
for {
|
|
|
|
// TODO: if error, emit error to user, stop loop?
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
2024-02-06 19:57:56 +00:00
|
|
|
case cv := <-ch:
|
|
|
|
if m.onCommandFollower && !cv.IsFollower {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
subscriber := false
|
|
|
|
for _, badge := range cv.Badges {
|
|
|
|
if badge == "recurring_subscription" || badge == "locals_supporter" {
|
|
|
|
subscriber = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if m.OnCommandSubscriber && !subscriber {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// if m.onCommandRantAmount > 0 && cv.Rant < m.onCommandRantAmount * 100 {
|
|
|
|
// break
|
|
|
|
// }
|
|
|
|
|
|
|
|
if cv.Rant < m.onCommandRantAmount*100 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2024-01-30 17:24:07 +00:00
|
|
|
// TODO: parse !command
|
|
|
|
now := time.Now()
|
|
|
|
if now.Sub(prev) < m.interval*time.Second {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2024-02-06 19:57:56 +00:00
|
|
|
err := cb.chatCommand(m, cv)
|
2024-01-30 17:24:07 +00:00
|
|
|
if err != nil {
|
|
|
|
cb.logError.Println("error sending chat:", err)
|
|
|
|
cb.StopMessage(m.id)
|
|
|
|
runtime.EventsEmit(cb.ctx, "ChatBotCommandError-"+m.id, m.id)
|
2024-01-31 20:29:06 +00:00
|
|
|
return
|
2024-01-30 17:24:07 +00:00
|
|
|
} else {
|
|
|
|
prev = now
|
|
|
|
runtime.EventsEmit(cb.ctx, "ChatBotCommandActive-"+m.id, m.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-05 21:24:54 +00:00
|
|
|
func (cb *ChatBot) startMessage(ctx context.Context, m *message) {
|
|
|
|
for {
|
|
|
|
// TODO: if error, emit error to user, stop loop?
|
|
|
|
err := cb.chat(m)
|
|
|
|
if err != nil {
|
|
|
|
cb.logError.Println("error sending chat:", err)
|
|
|
|
cb.StopMessage(m.id)
|
|
|
|
runtime.EventsEmit(cb.ctx, "ChatBotMessageError-"+m.id, m.id)
|
|
|
|
// TODO: stop this loop?
|
|
|
|
} else {
|
|
|
|
runtime.EventsEmit(cb.ctx, "ChatBotMessageActive-"+m.id, m.id)
|
|
|
|
}
|
|
|
|
|
|
|
|
timer := time.NewTimer(m.interval * time.Second)
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
timer.Stop()
|
|
|
|
return
|
|
|
|
case <-timer.C:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-06 19:57:56 +00:00
|
|
|
func (cb *ChatBot) chatCommand(m *message, cv rumblelivestreamlib.ChatView) error {
|
|
|
|
if cb.client == nil {
|
|
|
|
return fmt.Errorf("client is nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
msgText := m.text
|
|
|
|
if len(m.textFromFile) > 0 {
|
|
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(m.textFromFile))))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error generating random number: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
msgText = m.textFromFile[n.Int64()]
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpl, err := template.New("chat").Parse(msgText)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error creating template: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fields := struct {
|
|
|
|
ChannelName string
|
|
|
|
Username string
|
|
|
|
Rant int
|
|
|
|
}{
|
|
|
|
ChannelName: cv.ChannelName,
|
|
|
|
Username: cv.Username,
|
|
|
|
Rant: cv.Rant / 100,
|
|
|
|
}
|
|
|
|
|
|
|
|
var textB bytes.Buffer
|
|
|
|
err = tmpl.Execute(&textB, fields)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error executing template: %v", err)
|
|
|
|
}
|
|
|
|
text := textB.String()
|
|
|
|
|
|
|
|
err = cb.client.Chat(m.asChannel, text)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error sending chat: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-01-05 21:24:54 +00:00
|
|
|
func (cb *ChatBot) chat(m *message) error {
|
|
|
|
if cb.client == nil {
|
|
|
|
return fmt.Errorf("client is nil")
|
|
|
|
}
|
|
|
|
|
2024-01-30 17:24:07 +00:00
|
|
|
text := m.text
|
|
|
|
if len(m.textFromFile) > 0 {
|
|
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(m.textFromFile))))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error generating random number: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
text = m.textFromFile[n.Int64()]
|
|
|
|
}
|
|
|
|
|
|
|
|
err := cb.client.Chat(m.asChannel, text)
|
2024-01-05 21:24:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error sending chat: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) StopAllMessages() error {
|
|
|
|
cb.messagesMu.Lock()
|
|
|
|
defer cb.messagesMu.Unlock()
|
|
|
|
|
|
|
|
for id, m := range cb.messages {
|
|
|
|
m.stop()
|
|
|
|
delete(cb.messages, id)
|
2024-01-30 17:24:07 +00:00
|
|
|
|
|
|
|
if m.command != "" && m.onCommand {
|
|
|
|
cb.commandsMu.Lock()
|
|
|
|
defer cb.commandsMu.Unlock()
|
|
|
|
ch, exists := cb.commands[m.command]
|
|
|
|
if exists {
|
|
|
|
close(ch)
|
|
|
|
delete(cb.commands, m.command)
|
|
|
|
}
|
|
|
|
}
|
2024-01-05 21:24:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) StopMessage(id string) error {
|
|
|
|
cb.messagesMu.Lock()
|
|
|
|
defer cb.messagesMu.Unlock()
|
2024-01-30 17:24:07 +00:00
|
|
|
|
2024-01-05 21:24:54 +00:00
|
|
|
m, exists := cb.messages[id]
|
|
|
|
if exists {
|
|
|
|
m.stop()
|
|
|
|
delete(cb.messages, id)
|
2024-01-30 17:24:07 +00:00
|
|
|
|
|
|
|
if m.command != "" && m.onCommand {
|
|
|
|
cb.commandsMu.Lock()
|
|
|
|
defer cb.commandsMu.Unlock()
|
|
|
|
ch, exists := cb.commands[m.command]
|
|
|
|
if exists {
|
|
|
|
close(ch)
|
|
|
|
delete(cb.commands, m.command)
|
|
|
|
}
|
|
|
|
}
|
2024-01-05 21:24:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *message) stop() {
|
|
|
|
m.cancelMu.Lock()
|
|
|
|
if m.cancel != nil {
|
|
|
|
m.cancel()
|
|
|
|
}
|
|
|
|
m.cancelMu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) Login(username string, password string) error {
|
|
|
|
if cb.client == nil {
|
|
|
|
return fmt.Errorf("chatbot: client is nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
err := cb.client.Login(username, password)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("chatbot: error logging in: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) Logout() error {
|
|
|
|
if cb.client == nil {
|
|
|
|
return fmt.Errorf("chatbot: client is nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
err := cb.client.Logout()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("chatbot: error logging out: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2024-01-30 17:24:07 +00:00
|
|
|
|
|
|
|
func (cb *ChatBot) StartChatStream() error {
|
|
|
|
if cb.client == nil {
|
|
|
|
return fmt.Errorf("chatbot: client is nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
err := cb.client.ChatInfo()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("chatbot: error getting chat info: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = cb.client.StartChatStream(cb.handleChat, cb.handleError)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("chatbot: error starting chat stream: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) StopChatStream() error {
|
|
|
|
if cb.client == nil {
|
|
|
|
return fmt.Errorf("chatbot: client is nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: should a panic be caught here?
|
|
|
|
cb.client.StopChatStream()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) handleChat(cv rumblelivestreamlib.ChatView) {
|
|
|
|
// runtime.EventsEmit(cb.ctx, "ChatMessageReceived", cv)
|
|
|
|
|
|
|
|
if cv.Type != "init" {
|
|
|
|
cb.handleCommand(cv)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) handleCommand(cv rumblelivestreamlib.ChatView) {
|
|
|
|
cb.commandsMu.Lock()
|
|
|
|
defer cb.commandsMu.Unlock()
|
|
|
|
|
|
|
|
words := strings.Split(cv.Text, " ")
|
|
|
|
first := words[0]
|
|
|
|
cmd, exists := cb.commands[first]
|
|
|
|
if !exists {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case cmd <- cv:
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cb *ChatBot) handleError(err error) {
|
|
|
|
cb.logError.Println("error handling chat message:", err)
|
|
|
|
// runtime.EventsEmit(cb.ctx, "ChatError", err)
|
|
|
|
}
|