many update

This commit is contained in:
2025-05-26 20:35:50 -06:00
parent 3af77b6bea
commit 0a730be6f7
12 changed files with 608 additions and 177 deletions

View File

@ -1,27 +1,60 @@
package command
import (
"crypto/md5"
"fmt"
"himbot/lib"
"math/rand"
"regexp"
"strings"
"sync"
"time"
"github.com/bwmarrin/discordgo"
)
// Cache for Markov chains to avoid rebuilding for the same channel/message count
type MarkovCache struct {
chains map[string]map[string][]string
hashes map[string]string
mu sync.RWMutex
}
var (
markovCache = &MarkovCache{
chains: make(map[string]map[string][]string),
hashes: make(map[string]string),
}
// Regex for cleaning text
urlRegex = regexp.MustCompile(`https?://[^\s]+`)
mentionRegex = regexp.MustCompile(`<[@#&!][^>]+>`)
emojiRegex = regexp.MustCompile(`<a?:[^:]+:\d+>`)
)
func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
channelID := i.ChannelID
numMessages := 100 // Default value
numMessages := lib.AppConfig.MarkovDefaultMessages // Default value from config
if len(i.ApplicationCommandData().Options) > 0 {
if i.ApplicationCommandData().Options[0].Name == "messages" {
numMessages = int(i.ApplicationCommandData().Options[0].IntValue())
if numMessages <= 0 {
numMessages = 100
} else if numMessages > 1000 {
numMessages = 1000 // Limit to 1000 messages max
numMessages = lib.AppConfig.MarkovDefaultMessages
} else if numMessages > lib.AppConfig.MarkovMaxMessages {
numMessages = lib.AppConfig.MarkovMaxMessages // Limit from config
}
}
}
// Check cache first
cacheKey := fmt.Sprintf("%s:%d", channelID, numMessages)
if chain := getCachedChain(cacheKey); chain != nil {
newMessage := generateMessage(chain)
if newMessage != "" {
return newMessage, nil
}
}
// Fetch messages
allMessages, err := fetchMessages(s, channelID, numMessages)
if err != nil {
@ -31,6 +64,9 @@ func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string
// Build the Markov chain from the fetched messages
chain := buildMarkovChain(allMessages)
// Cache the chain
setCachedChain(cacheKey, chain, allMessages)
// Generate a new message using the Markov chain
newMessage := generateMessage(chain)
@ -42,6 +78,49 @@ func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string
return newMessage, nil
}
func getCachedChain(cacheKey string) map[string][]string {
markovCache.mu.RLock()
defer markovCache.mu.RUnlock()
if chain, exists := markovCache.chains[cacheKey]; exists {
return chain
}
return nil
}
func setCachedChain(cacheKey string, chain map[string][]string, messages []*discordgo.Message) {
// Create a hash of the messages to detect changes
hash := hashMessages(messages)
markovCache.mu.Lock()
defer markovCache.mu.Unlock()
// Only cache if we have a meaningful chain
if len(chain) > 10 {
markovCache.chains[cacheKey] = chain
markovCache.hashes[cacheKey] = hash
// Simple cache cleanup - keep only last N entries from config
if len(markovCache.chains) > lib.AppConfig.MarkovCacheSize {
// Remove oldest entry (simple FIFO)
for k := range markovCache.chains {
delete(markovCache.chains, k)
delete(markovCache.hashes, k)
break
}
}
}
}
func hashMessages(messages []*discordgo.Message) string {
var content strings.Builder
for _, msg := range messages {
content.WriteString(msg.ID)
content.WriteString(msg.Content)
}
return fmt.Sprintf("%x", md5.Sum([]byte(content.String())))
}
func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*discordgo.Message, error) {
var allMessages []*discordgo.Message
var lastMessageID string
@ -61,7 +140,13 @@ func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*
break // No more messages to fetch
}
allMessages = append(allMessages, batch...)
// Filter out bot messages and empty messages during fetch
for _, msg := range batch {
if !msg.Author.Bot && len(strings.TrimSpace(msg.Content)) > 0 {
allMessages = append(allMessages, msg)
}
}
lastMessageID = batch[len(batch)-1].ID
if len(batch) < 100 {
@ -72,20 +157,52 @@ func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*
return allMessages, nil
}
// buildMarkovChain creates a Markov chain from a list of messages
// cleanText removes URLs, mentions, emojis, and normalizes text
func cleanText(text string) string {
// Remove URLs
text = urlRegex.ReplaceAllString(text, "")
// Remove mentions
text = mentionRegex.ReplaceAllString(text, "")
// Remove custom emojis
text = emojiRegex.ReplaceAllString(text, "")
// Normalize whitespace
text = strings.Join(strings.Fields(text), " ")
return strings.TrimSpace(text)
}
// buildMarkovChain creates an improved Markov chain from a list of messages
func buildMarkovChain(messages []*discordgo.Message) map[string][]string {
chain := make(map[string][]string)
for _, msg := range messages {
words := strings.Fields(msg.Content)
cleanedContent := cleanText(msg.Content)
if len(cleanedContent) < 3 { // Skip very short messages
continue
}
words := strings.Fields(cleanedContent)
if len(words) < 2 { // Need at least 2 words for a chain
continue
}
// Build the chain by associating each word with the word that follows it
for i := 0; i < len(words)-1; i++ {
chain[words[i]] = append(chain[words[i]], words[i+1])
currentWord := strings.ToLower(words[i])
nextWord := words[i+1] // Keep original case for next word
// Skip very short words or words with special characters
if len(currentWord) < 2 || strings.ContainsAny(currentWord, "!@#$%^&*()[]{}") {
continue
}
chain[currentWord] = append(chain[currentWord], nextWord)
}
}
return chain
}
// generateMessage creates a new message using the Markov chain
// generateMessage creates a new message using the Markov chain with improved logic
func generateMessage(chain map[string][]string) string {
if len(chain) == 0 {
return ""
@ -94,22 +211,66 @@ func generateMessage(chain map[string][]string) string {
words := []string{}
var currentWord string
// Start with a random word from the chain
for word := range chain {
currentWord = word
break
}
// Generate up to 20 words
for i := 0; i < 20; i++ {
words = append(words, currentWord)
if nextWords, ok := chain[currentWord]; ok && len(nextWords) > 0 {
// Randomly select the next word from the possible follow-ups
currentWord = nextWords[rand.Intn(len(nextWords))]
} else {
// Start with a random word that has good follow-ups
attempts := 0
for word, nextWords := range chain {
if len(nextWords) >= 2 && len(word) > 2 { // Prefer words with multiple options
currentWord = word
break
}
attempts++
if attempts > 50 { // Fallback to any word
currentWord = word
break
}
}
return strings.Join(words, " ")
if currentWord == "" {
return ""
}
// Generate between 5 and 25 words
maxWords := 5 + rand.Intn(20)
for i := 0; i < maxWords; i++ {
// Add current word (capitalize first word)
if i == 0 {
words = append(words, strings.Title(currentWord))
} else {
words = append(words, currentWord)
}
if nextWords, ok := chain[strings.ToLower(currentWord)]; ok && len(nextWords) > 0 {
// Randomly select the next word from the possible follow-ups
currentWord = nextWords[rand.Intn(len(nextWords))]
} else {
// Try to find a new starting point
found := false
for word, nextWords := range chain {
if len(nextWords) > 0 && len(word) > 2 {
currentWord = word
found = true
break
}
}
if !found {
break
}
}
}
result := strings.Join(words, " ")
// Add punctuation if missing
if len(result) > 0 && !strings.ContainsAny(result[len(result)-1:], ".!?") {
// Randomly add punctuation
punctuation := []string{".", "!", "?"}
result += punctuation[rand.Intn(len(punctuation))]
}
return result
}
func init() {
// Seed random number generator
rand.Seed(time.Now().UnixNano())
}