TTW_Bot_GO/internal/commands/processor.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))]
}