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()) } // Обработка тегов и template := cmd.Template aiResult := "" if strings.Contains(template, "") && 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, "") || strings.Contains(template, "")) { broadcasterID, err := p.twitchAPI.GetBroadcasterID() if err == nil { userID, err := p.twitchAPI.GetUserID(username) if err == nil { if strings.Contains(template, "") { createdAt, err := p.twitchAPI.GetUserCreatedAt(userID) if err != nil { template = strings.ReplaceAll(template, "", "неизвестно") } else { ageStr := twitchapi.FormatDuration(createdAt) template = strings.ReplaceAll(template, "", ageStr) } } if strings.Contains(template, "") { followedAt, err := p.twitchAPI.GetFollowCreatedAt(broadcasterID, userID) if err != nil { template = strings.ReplaceAll(template, "", "не подписан") } else { followStr := twitchapi.FormatDuration(followedAt) template = strings.ReplaceAll(template, "", 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 } // Обработка таймаута (если есть тег ) 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))] }