TTW_Bot_GO/internal/parser/parser.go

233 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package parser
import (
"fmt"
"math/rand"
"regexp"
"strconv"
"strings"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// ParseTemplate возвращает текст, список звуков и количество минут для таймаута (0 если нет)
func ParseTemplate(template string, username string, args string, aiResult string, getRandomUsername func() string) (result string, soundFiles []string, timeoutMinutes int, err error) {
// Замена простых переменных
result = strings.ReplaceAll(template, "<USERNAME/>", "@"+username)
result = strings.ReplaceAll(result, "<RANDOMUSER/>", getRandomUsername())
result = strings.ReplaceAll(result, "<ARG/>", args)
result = strings.ReplaceAll(result, "<AI/>", aiResult)
// Обработка <random>, <song>, <timeout>
result, soundFiles, timeoutMinutes = processTags(result)
// Рекурсивная обработка всех <group>
result, err = processGroups(result)
if err != nil {
return "", nil, 0, err
}
result = strings.ReplaceAll(result, "\n", " ")
result = strings.ReplaceAll(result, "\r", " ")
result = regexp.MustCompile(`\s+`).ReplaceAllString(result, " ")
result = strings.TrimSpace(result)
return result, soundFiles, timeoutMinutes, nil
}
func getRandomViewer() string {
names := []string{"зрителя", "кого-то", "случайного пользователя", "незнакомца"}
return names[rand.Intn(len(names))]
}
// processTags обрабатывает теги <random>, <song>, <timeout>
func processTags(text string) (string, []string, int) {
// <random>
randomRe := regexp.MustCompile(`<random\s+s=([-0-9]+)\s+e=([-0-9]+)\s*/>`)
text = randomRe.ReplaceAllStringFunc(text, func(m string) string {
sub := randomRe.FindStringSubmatch(m)
s, _ := strconv.Atoi(sub[1])
e, _ := strconv.Atoi(sub[2])
if s > e {
s, e = e, s
}
val := rand.Intn(e-s+1) + s
return strconv.Itoa(val)
})
// <song>
songRe := regexp.MustCompile(`<song\s+([^>]+)\s*/>`)
soundFiles := make([]string, 0)
text = songRe.ReplaceAllStringFunc(text, func(m string) string {
fRe := regexp.MustCompile(`f="([^"]+)"`)
matches := fRe.FindStringSubmatch(m)
if len(matches) > 1 {
soundFiles = append(soundFiles, matches[1])
}
return ""
})
// <timeout minutes="X"/>
timeoutMinutes := 0
timeoutRe := regexp.MustCompile(`<timeout\s+minutes="?([0-9]+)"?\s*/>`)
text = timeoutRe.ReplaceAllStringFunc(text, func(m string) string {
sub := timeoutRe.FindStringSubmatch(m)
if len(sub) > 1 {
minutes, _ := strconv.Atoi(sub[1])
if minutes > 0 {
timeoutMinutes = minutes
}
}
return ""
})
return text, soundFiles, timeoutMinutes
}
func processRandomAndSong(text string) (string, []string) {
// Обработка <random>
randomRe := regexp.MustCompile(`<random\s+s=([-0-9]+)\s+e=([-0-9]+)\s*/>`)
text = randomRe.ReplaceAllStringFunc(text, func(m string) string {
sub := randomRe.FindStringSubmatch(m)
s, _ := strconv.Atoi(sub[1])
e, _ := strconv.Atoi(sub[2])
if s > e {
s, e = e, s
}
val := rand.Intn(e-s+1) + s
return strconv.Itoa(val)
})
// Обработка <song> с любыми атрибутами
songRe := regexp.MustCompile(`<song\s+([^>]+)\s*/>`)
soundFiles := make([]string, 0)
text = songRe.ReplaceAllStringFunc(text, func(m string) string {
// Извлекаем значение атрибута f="..."
fRe := regexp.MustCompile(`f="([^"]+)"`)
matches := fRe.FindStringSubmatch(m)
if len(matches) > 1 {
soundFiles = append(soundFiles, matches[1])
}
return "" // заменяем тег на пустую строку
})
return text, soundFiles
}
// processGroups и остальные функции без изменений ...
func processGroups(text string) (string, error) {
var err error
for {
text, err = processOneGroup(text)
if err != nil {
return "", err
}
if !strings.Contains(text, "<group>") {
break
}
}
return text, nil
}
func processOneGroup(text string) (string, error) {
start := strings.Index(text, "<group>")
if start == -1 {
return text, nil
}
end := findMatchingClosingTag(text, start)
if end == -1 {
return "", fmt.Errorf("unclosed <group> at position %d", start)
}
inner := text[start+7 : end]
sections := extractGSections(inner)
if len(sections) == 0 {
return "", fmt.Errorf("no <g> sections inside <group> at position %d", start)
}
chosen := sections[rand.Intn(len(sections))]
newText := text[:start] + chosen + text[end+8:]
return newText, nil
}
// findMatchingClosingTag ищет позицию закрывающего </group> с учётом вложенности
func findMatchingClosingTag(text string, start int) int {
depth := 1
i := start + 7
for i < len(text) {
if strings.HasPrefix(text[i:], "<group>") {
depth++
i += 7
} else if strings.HasPrefix(text[i:], "</group>") {
depth--
if depth == 0 {
return i
}
i += 8
} else {
i++
}
}
return -1
}
// extractGSections извлекает содержимое всех <g>...</g> на верхнем уровне (не внутри вложенных групп)
func extractGSections(s string) []string {
var result []string
i := 0
for i < len(s) {
if strings.HasPrefix(s[i:], "<g>") {
startContent := i + 3
j := startContent
depth := 0
for j < len(s) {
if strings.HasPrefix(s[j:], "<group>") {
depth++
j += 7
} else if strings.HasPrefix(s[j:], "</group>") {
if depth > 0 {
depth--
}
j += 8
} else if strings.HasPrefix(s[j:], "</g>") && depth == 0 {
content := s[startContent:j]
result = append(result, content)
i = j + 4
break
} else {
j++
}
}
if j >= len(s) {
break
}
} else if strings.HasPrefix(s[i:], "<group>") {
// Пропускаем вложенную группу целиком
groupEnd := findMatchingClosingTag(s, i)
if groupEnd == -1 {
break
}
i = groupEnd + 8
} else {
i++
}
}
return result
}
// ValidateTemplate проверяет баланс тегов
func ValidateTemplate(template string) error {
balance := 0
for i := 0; i < len(template); i++ {
if strings.HasPrefix(template[i:], "<group>") {
balance++
i += 6
} else if strings.HasPrefix(template[i:], "</group>") {
balance--
i += 7
}
}
if balance != 0 {
return fmt.Errorf("unbalanced <group> tags")
}
return nil
}