package util import ( "math" "regexp" "strconv" "strings" "unicode" ) // MessageStats holds parsed message metrics. type MessageStats struct { Words int Chars int Links int Images int Questions int Exclamations int Emojis int } var ( linkRe = regexp.MustCompile(`https?://\S+`) imageRe = regexp.MustCompile(`\.(png|jpg|jpeg|gif|webp|svg|bmp)(\?[^\s]*)?$`) emojiRe = regexp.MustCompile(`[\x{1F600}-\x{1F64F}]|[\x{1F300}-\x{1F5FF}]|[\x{1F680}-\x{1F6FF}]|[\x{1F1E0}-\x{1F1FF}]|[\x{2600}-\x{26FF}]|[\x{2700}-\x{27BF}]`) ) // ParseMessage extracts stats from a chat message body. func ParseMessage(body string) MessageStats { words := strings.Fields(body) links := linkRe.FindAllString(body, -1) images := 0 for _, l := range links { if imageRe.MatchString(strings.ToLower(l)) { images++ } } questions := strings.Count(body, "?") exclamations := strings.Count(body, "!") emojis := len(emojiRe.FindAllString(body, -1)) return MessageStats{ Words: len(words), Chars: len([]rune(body)), Links: len(links), Images: images, Questions: questions, Exclamations: exclamations, Emojis: emojis, } } // XPForLevel returns the cumulative XP needed to reach level n. // Uses quadratic curve: level² × 100. func XPForLevel(level int) int { return level * level * 100 } // LevelFromXP returns the current level for a given XP total. // Inverse of level² × 100: level = floor(sqrt(xp / 100)). func LevelFromXP(xp int) int { if xp <= 0 { return 0 } level := 0 for (level+1)*(level+1)*100 <= xp { level++ } return level } // ProgressBar returns a text progress bar like [#####-----] 50%. func ProgressBar(current, max, width int) string { if max <= 0 { return strings.Repeat("-", width) } ratio := float64(current) / float64(max) if ratio > 1 { ratio = 1 } filled := int(math.Round(ratio * float64(width))) if filled > width { filled = width } empty := width - filled pct := int(ratio * 100) return "[" + strings.Repeat("#", filled) + strings.Repeat("-", empty) + "] " + strconv.Itoa(pct) + "%" } // IsCommand checks if body starts with prefix+command (case-insensitive). func IsCommand(body, prefix, command string) bool { cmd := prefix + command lower := strings.ToLower(strings.TrimSpace(body)) if lower == cmd { return true } return strings.HasPrefix(lower, cmd+" ") } // GetArgs returns everything after the command prefix. func GetArgs(body, prefix, command string) string { cmd := prefix + command trimmed := strings.TrimSpace(body) lower := strings.ToLower(trimmed) if !strings.HasPrefix(lower, cmd) { return "" } rest := trimmed[len(cmd):] return strings.TrimSpace(rest) } // HasNonASCII checks if string contains non-ASCII characters. func HasNonASCII(s string) bool { for _, r := range s { if r > unicode.MaxASCII { return true } } return false }