227 lines
6.3 KiB
Go
227 lines
6.3 KiB
Go
package commands
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"path/filepath"
|
|
"stream-bot/internal/ai"
|
|
"stream-bot/internal/audio"
|
|
"stream-bot/internal/db"
|
|
"stream-bot/internal/logger"
|
|
"stream-bot/internal/parser"
|
|
"stream-bot/internal/twitchapi"
|
|
"stream-bot/internal/userstats"
|
|
"stream-bot/internal/webservices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Processor struct {
|
|
cooldowns sync.Map
|
|
twitchAPI *twitchapi.TwitchAPI
|
|
aiProvider ai.Provider
|
|
webSrvMgr *webservices.Manager
|
|
userStats *userstats.Store
|
|
}
|
|
|
|
func NewProcessor(twitchAPI *twitchapi.TwitchAPI, aiProvider ai.Provider, webSrvMgr *webservices.Manager, userStats *userstats.Store) *Processor {
|
|
return &Processor{
|
|
twitchAPI: twitchAPI,
|
|
aiProvider: aiProvider,
|
|
webSrvMgr: webSrvMgr,
|
|
userStats: userStats,
|
|
}
|
|
}
|
|
|
|
func (p *Processor) ProcessCommand(trigger, username, platform string, isMod, isBroadcaster bool, args string) (response string, soundFiles []string, err error) {
|
|
cmds, err := db.GetCommands()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
var cmd *db.Command
|
|
for _, c := range cmds {
|
|
if c.Trigger == trigger && c.Enabled {
|
|
cmd = &c
|
|
break
|
|
}
|
|
}
|
|
if cmd == nil {
|
|
return "", nil, nil
|
|
}
|
|
|
|
// Проверка прав
|
|
switch cmd.Permission {
|
|
case "broadcaster":
|
|
if !isBroadcaster {
|
|
return "", nil, nil
|
|
}
|
|
case "moderator":
|
|
if !isMod && !isBroadcaster {
|
|
return "", nil, nil
|
|
}
|
|
}
|
|
|
|
// Кулдаун
|
|
if cmd.CooldownSec > 0 {
|
|
last, ok := p.cooldowns.Load(cmd.Trigger)
|
|
if ok && time.Since(last.(time.Time)) < time.Duration(cmd.CooldownSec)*time.Second {
|
|
return "", nil, nil
|
|
}
|
|
p.cooldowns.Store(cmd.Trigger, time.Now())
|
|
}
|
|
|
|
// Обработка тегов <AGE/> и <FOLLOW/>
|
|
template := cmd.Template
|
|
aiResult := ""
|
|
if strings.Contains(template, "<AI/>") && p.aiProvider != nil {
|
|
if args == "" {
|
|
aiResult = "Вы не задали вопрос."
|
|
} else {
|
|
aiResult, err = p.aiProvider.Ask(context.Background(), args)
|
|
if err != nil {
|
|
logger.Error("AI error: %v", err)
|
|
aiResult = "Ошибка при обращении к нейросети."
|
|
}
|
|
}
|
|
}
|
|
if platform == "twitch" && (strings.Contains(template, "<AGE/>") || strings.Contains(template, "<FOLLOW/>")) {
|
|
broadcasterID, err := p.twitchAPI.GetBroadcasterID()
|
|
if err == nil {
|
|
userID, err := p.twitchAPI.GetUserID(username)
|
|
if err == nil {
|
|
if strings.Contains(template, "<AGE/>") {
|
|
createdAt, err := p.twitchAPI.GetUserCreatedAt(userID)
|
|
if err != nil {
|
|
template = strings.ReplaceAll(template, "<AGE/>", "неизвестно")
|
|
} else {
|
|
ageStr := twitchapi.FormatDuration(createdAt)
|
|
template = strings.ReplaceAll(template, "<AGE/>", ageStr)
|
|
}
|
|
}
|
|
if strings.Contains(template, "<FOLLOW/>") {
|
|
followedAt, err := p.twitchAPI.GetFollowCreatedAt(broadcasterID, userID)
|
|
if err != nil {
|
|
template = strings.ReplaceAll(template, "<FOLLOW/>", "не подписан")
|
|
} else {
|
|
followStr := twitchapi.FormatDuration(followedAt)
|
|
template = strings.ReplaceAll(template, "<FOLLOW/>", followStr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Парсинг шаблона (теперь возвращает ещё и timeoutMinutes)
|
|
response, soundFiles, timeoutMinutes, err := parser.ParseTemplate(template, username, args, aiResult, p.getRandomUsername)
|
|
if err != nil {
|
|
logger.Error("Parse error for command %s: %v", cmd.Trigger, err)
|
|
return "", nil, err
|
|
}
|
|
|
|
// Обработка таймаута (если есть тег <timeout/>)
|
|
if timeoutMinutes > 0 && platform == "twitch" {
|
|
if err := p.timeoutUser(username, timeoutMinutes); err != nil {
|
|
logger.Error("Failed to timeout user %s: %v", username, err)
|
|
} else {
|
|
logger.Info("User %s timed out for %d minutes", username, timeoutMinutes)
|
|
}
|
|
}
|
|
|
|
// Воспроизведение звуков
|
|
if len(soundFiles) > 0 && p.webSrvMgr != nil {
|
|
for _, sf := range soundFiles {
|
|
p.sendSoundToAlertServices(sf)
|
|
}
|
|
} else if len(soundFiles) > 0 {
|
|
for _, sf := range soundFiles {
|
|
if err := audio.PlaySound(sf); err != nil {
|
|
logger.Error("Failed to play sound %s: %v", sf, err)
|
|
}
|
|
}
|
|
}
|
|
return response, soundFiles, nil
|
|
}
|
|
|
|
// timeoutUser отправляет пользователя в таймаут через Twitch API
|
|
func (p *Processor) timeoutUser(username string, minutes int) error {
|
|
broadcasterID, err := p.twitchAPI.GetBroadcasterID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
userID, err := p.twitchAPI.GetUserID(username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
moderatorID := broadcasterID // используем ID стримера как модератора
|
|
seconds := minutes * 60
|
|
return p.twitchAPI.TimeoutUser(broadcasterID, moderatorID, userID, seconds)
|
|
}
|
|
|
|
func (p *Processor) UpdateAIProvider(provider ai.Provider) {
|
|
p.aiProvider = provider
|
|
}
|
|
|
|
func (p *Processor) sendSoundToAlertServices(soundFile string) {
|
|
if p.webSrvMgr == nil {
|
|
return
|
|
}
|
|
duplicate, _ := db.GetSetting("duplicate_command_sounds")
|
|
duplicateEnabled := duplicate == "true"
|
|
|
|
services := p.webSrvMgr.GetAllServices()
|
|
for _, srv := range services {
|
|
if srv.GetType() != "alert" {
|
|
continue
|
|
}
|
|
port := srv.GetPort()
|
|
url := fmt.Sprintf("http://localhost:%d/notify", port)
|
|
filename := filepath.Base(soundFile)
|
|
payload := map[string]interface{}{
|
|
"sound": "/sounds/" + filename,
|
|
"duration": 1,
|
|
"title": "",
|
|
"text": "",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
go func() {
|
|
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
logger.Error("Failed to send sound to alert service %d: %v", port, err)
|
|
} else {
|
|
_ = resp.Body.Close()
|
|
}
|
|
}()
|
|
}
|
|
if duplicateEnabled {
|
|
fullPath := filepath.Join("data", "sounds", filepath.Base(soundFile))
|
|
if err := audio.PlaySound(fullPath); err != nil {
|
|
logger.Error("Failed to play duplicated sound %s: %v", fullPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// getRandomUsername возвращает случайное имя из активных пользователей чата
|
|
func (p *Processor) getRandomUsername() string {
|
|
users := p.userStats.GetAll()
|
|
if len(users) == 0 {
|
|
return "кого-то"
|
|
}
|
|
// Отфильтруем пустые имена
|
|
var names []string
|
|
for _, u := range users {
|
|
if u.Username != "" {
|
|
names = append(names, u.Username)
|
|
}
|
|
}
|
|
if len(names) == 0 {
|
|
return "кого-то"
|
|
}
|
|
rand.Seed(time.Now().UnixNano())
|
|
return names[rand.Intn(len(names))]
|
|
}
|