many update
This commit is contained in:
27
.env.example
27
.env.example
@ -1,3 +1,28 @@
|
|||||||
# Tokens
|
# Discord Configuration
|
||||||
DISCORD_TOKEN=""
|
DISCORD_TOKEN=""
|
||||||
|
|
||||||
|
# Container configuration
|
||||||
ROOT_DIR=""
|
ROOT_DIR=""
|
||||||
|
|
||||||
|
# Himbucks System Configuration
|
||||||
|
HIMBUCKS_PER_REWARD=10
|
||||||
|
MESSAGE_COUNT_THRESHOLD=5
|
||||||
|
HIMBUCKS_COOLDOWN_MINUTES=1
|
||||||
|
|
||||||
|
# Markov Chain Configuration
|
||||||
|
MARKOV_DEFAULT_MESSAGES=100
|
||||||
|
MARKOV_MAX_MESSAGES=1000
|
||||||
|
MARKOV_CACHE_SIZE=10
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_MAX_OPEN_CONNS=25
|
||||||
|
DB_MAX_IDLE_CONNS=5
|
||||||
|
DB_CONN_MAX_LIFETIME_MINUTES=5
|
||||||
|
|
||||||
|
# Command Cooldowns (in seconds)
|
||||||
|
PING_COOLDOWN_SECONDS=5
|
||||||
|
HS_COOLDOWN_SECONDS=10
|
||||||
|
MARKOV_COOLDOWN_SECONDS=30
|
||||||
|
HIMBUCKS_COOLDOWN_SECONDS=5
|
||||||
|
HIMBOARD_COOLDOWN_SECONDS=5
|
||||||
|
SENDBUCKS_COOLDOWN_SECONDS=1800
|
@ -1,5 +1,5 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM golang:1.23.2 AS build
|
FROM golang:1.24.3 AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
@ -1,27 +1,60 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"himbot/lib"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"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) {
|
func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string, error) {
|
||||||
channelID := i.ChannelID
|
channelID := i.ChannelID
|
||||||
|
|
||||||
numMessages := 100 // Default value
|
numMessages := lib.AppConfig.MarkovDefaultMessages // Default value from config
|
||||||
if len(i.ApplicationCommandData().Options) > 0 {
|
if len(i.ApplicationCommandData().Options) > 0 {
|
||||||
if i.ApplicationCommandData().Options[0].Name == "messages" {
|
if i.ApplicationCommandData().Options[0].Name == "messages" {
|
||||||
numMessages = int(i.ApplicationCommandData().Options[0].IntValue())
|
numMessages = int(i.ApplicationCommandData().Options[0].IntValue())
|
||||||
if numMessages <= 0 {
|
if numMessages <= 0 {
|
||||||
numMessages = 100
|
numMessages = lib.AppConfig.MarkovDefaultMessages
|
||||||
} else if numMessages > 1000 {
|
} else if numMessages > lib.AppConfig.MarkovMaxMessages {
|
||||||
numMessages = 1000 // Limit to 1000 messages max
|
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
|
// Fetch messages
|
||||||
allMessages, err := fetchMessages(s, channelID, numMessages)
|
allMessages, err := fetchMessages(s, channelID, numMessages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -31,6 +64,9 @@ func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string
|
|||||||
// Build the Markov chain from the fetched messages
|
// Build the Markov chain from the fetched messages
|
||||||
chain := buildMarkovChain(allMessages)
|
chain := buildMarkovChain(allMessages)
|
||||||
|
|
||||||
|
// Cache the chain
|
||||||
|
setCachedChain(cacheKey, chain, allMessages)
|
||||||
|
|
||||||
// Generate a new message using the Markov chain
|
// Generate a new message using the Markov chain
|
||||||
newMessage := generateMessage(chain)
|
newMessage := generateMessage(chain)
|
||||||
|
|
||||||
@ -42,6 +78,49 @@ func MarkovCommand(s *discordgo.Session, i *discordgo.InteractionCreate) (string
|
|||||||
return newMessage, nil
|
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) {
|
func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*discordgo.Message, error) {
|
||||||
var allMessages []*discordgo.Message
|
var allMessages []*discordgo.Message
|
||||||
var lastMessageID string
|
var lastMessageID string
|
||||||
@ -61,7 +140,13 @@ func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*
|
|||||||
break // No more messages to fetch
|
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
|
lastMessageID = batch[len(batch)-1].ID
|
||||||
|
|
||||||
if len(batch) < 100 {
|
if len(batch) < 100 {
|
||||||
@ -72,20 +157,52 @@ func fetchMessages(s *discordgo.Session, channelID string, numMessages int) ([]*
|
|||||||
return allMessages, nil
|
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 {
|
func buildMarkovChain(messages []*discordgo.Message) map[string][]string {
|
||||||
chain := make(map[string][]string)
|
chain := make(map[string][]string)
|
||||||
|
|
||||||
for _, msg := range messages {
|
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
|
// Build the chain by associating each word with the word that follows it
|
||||||
for i := 0; i < len(words)-1; i++ {
|
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
|
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 {
|
func generateMessage(chain map[string][]string) string {
|
||||||
if len(chain) == 0 {
|
if len(chain) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@ -94,22 +211,66 @@ func generateMessage(chain map[string][]string) string {
|
|||||||
words := []string{}
|
words := []string{}
|
||||||
var currentWord string
|
var currentWord string
|
||||||
|
|
||||||
// Start with a random word from the chain
|
// Start with a random word that has good follow-ups
|
||||||
for word := range chain {
|
attempts := 0
|
||||||
|
for word, nextWords := range chain {
|
||||||
|
if len(nextWords) >= 2 && len(word) > 2 { // Prefer words with multiple options
|
||||||
currentWord = word
|
currentWord = word
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
attempts++
|
||||||
// Generate up to 20 words
|
if attempts > 50 { // Fallback to any word
|
||||||
for i := 0; i < 20; i++ {
|
currentWord = word
|
||||||
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 {
|
|
||||||
break
|
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())
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image: your-app-image:latest
|
|
||||||
command: ["/app"]
|
|
||||||
pull_policy: build
|
|
||||||
environment:
|
|
||||||
- DISCORD_TOKEN=$DISCORD_TOKEN
|
|
||||||
- COOLDOWN_ALLOW_LIST=$COOLDOWN_ALLOW_LIST
|
|
@ -4,7 +4,30 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3117:3000"
|
- "3117:3000"
|
||||||
environment:
|
environment:
|
||||||
|
# Discord Configuration
|
||||||
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
||||||
- ROOT_DIR=${ROOT_DIR}
|
|
||||||
|
# Himbucks System Configuration
|
||||||
|
- HIMBUCKS_PER_REWARD=${HIMBUCKS_PER_REWARD:-10}
|
||||||
|
- MESSAGE_COUNT_THRESHOLD=${MESSAGE_COUNT_THRESHOLD:-5}
|
||||||
|
- HIMBUCKS_COOLDOWN_MINUTES=${HIMBUCKS_COOLDOWN_MINUTES:-1}
|
||||||
|
|
||||||
|
# Markov Chain Configuration
|
||||||
|
- MARKOV_DEFAULT_MESSAGES=${MARKOV_DEFAULT_MESSAGES:-100}
|
||||||
|
- MARKOV_MAX_MESSAGES=${MARKOV_MAX_MESSAGES:-1000}
|
||||||
|
- MARKOV_CACHE_SIZE=${MARKOV_CACHE_SIZE:-10}
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
- DB_MAX_OPEN_CONNS=${DB_MAX_OPEN_CONNS:-25}
|
||||||
|
- DB_MAX_IDLE_CONNS=${DB_MAX_IDLE_CONNS:-5}
|
||||||
|
- DB_CONN_MAX_LIFETIME_MINUTES=${DB_CONN_MAX_LIFETIME_MINUTES:-5}
|
||||||
|
|
||||||
|
# Command Cooldowns (in seconds)
|
||||||
|
- PING_COOLDOWN_SECONDS=${PING_COOLDOWN_SECONDS:-5}
|
||||||
|
- HS_COOLDOWN_SECONDS=${HS_COOLDOWN_SECONDS:-10}
|
||||||
|
- MARKOV_COOLDOWN_SECONDS=${MARKOV_COOLDOWN_SECONDS:-30}
|
||||||
|
- HIMBUCKS_COOLDOWN_SECONDS=${HIMBUCKS_COOLDOWN_SECONDS:-5}
|
||||||
|
- HIMBOARD_COOLDOWN_SECONDS=${HIMBOARD_COOLDOWN_SECONDS:-5}
|
||||||
|
- SENDBUCKS_COOLDOWN_SECONDS=${SENDBUCKS_COOLDOWN_SECONDS:-1800}
|
||||||
volumes:
|
volumes:
|
||||||
- ${ROOT_DIR}/himbot_data:/data
|
- ${ROOT_DIR}/himbot_data:/data
|
||||||
|
12
go.mod
12
go.mod
@ -1,18 +1,18 @@
|
|||||||
module himbot
|
module himbot
|
||||||
|
|
||||||
go 1.23
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c // indirect
|
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.28.1
|
github.com/bwmarrin/discordgo v0.29.0
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/tursodatabase/go-libsql v0.0.0-20241011135853-3effbb6dea5c
|
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e
|
||||||
)
|
)
|
||||||
|
24
go.sum
24
go.sum
@ -1,7 +1,7 @@
|
|||||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||||
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
@ -13,19 +13,19 @@ github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c h1:WsJ
|
|||||||
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c/go.mod h1:gIcFddvsvPcRCO6QDmWH9/zcFd5U26QWWRMgZh4ddyo=
|
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240721121621-c0bdc870f11c/go.mod h1:gIcFddvsvPcRCO6QDmWH9/zcFd5U26QWWRMgZh4ddyo=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/tursodatabase/go-libsql v0.0.0-20241011135853-3effbb6dea5c h1:a8TrFzP+zK+uYcMWuLQoNOR78SG/yISSnHwMIcyWa2Q=
|
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e h1:DUEcD8ukLWxIlcRWWJSuAX6IbEQln2bc7t9HOT45FFk=
|
||||||
github.com/tursodatabase/go-libsql v0.0.0-20241011135853-3effbb6dea5c/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
|
github.com/tursodatabase/go-libsql v0.0.0-20250416102726-983f7e9acb0e/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
@ -14,8 +14,15 @@ func HandleCommand(commandName string, cooldownDuration time.Duration, handler C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get user information (handle both guild and DM contexts)
|
||||||
|
user, userErr := GetUser(i)
|
||||||
|
if userErr != nil {
|
||||||
|
RespondWithError(s, i, "Error getting user information: "+userErr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get or create user and guild profile
|
// Get or create user and guild profile
|
||||||
_, createUserError := GetOrCreateUserWithGuild(i.Member.User.ID, i.Member.User.Username, i.GuildID)
|
_, createUserError := GetOrCreateUserWithGuild(user.ID, user.Username, i.GuildID)
|
||||||
|
|
||||||
if createUserError != nil {
|
if createUserError != nil {
|
||||||
RespondWithError(s, i, "Error creating user profile: "+createUserError.Error())
|
RespondWithError(s, i, "Error creating user profile: "+createUserError.Error())
|
||||||
|
90
lib/config.go
Normal file
90
lib/config.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds all configuration values
|
||||||
|
type Config struct {
|
||||||
|
// Discord settings
|
||||||
|
DiscordToken string
|
||||||
|
|
||||||
|
// Himbucks settings
|
||||||
|
HimbucksPerReward int
|
||||||
|
MessageCountThreshold int
|
||||||
|
CooldownPeriod time.Duration
|
||||||
|
|
||||||
|
// Markov settings
|
||||||
|
MarkovDefaultMessages int
|
||||||
|
MarkovMaxMessages int
|
||||||
|
MarkovCacheSize int
|
||||||
|
|
||||||
|
// Database settings
|
||||||
|
MaxOpenConns int
|
||||||
|
MaxIdleConns int
|
||||||
|
ConnMaxLifetime time.Duration
|
||||||
|
|
||||||
|
// Command cooldowns (in seconds)
|
||||||
|
PingCooldown int
|
||||||
|
HsCooldown int
|
||||||
|
MarkovCooldown int
|
||||||
|
HimbucksCooldown int
|
||||||
|
HimboardCooldown int
|
||||||
|
SendbucksCooldown int
|
||||||
|
}
|
||||||
|
|
||||||
|
var AppConfig *Config
|
||||||
|
|
||||||
|
// LoadConfig loads configuration from environment variables
|
||||||
|
func LoadConfig() *Config {
|
||||||
|
config := &Config{
|
||||||
|
// Discord settings
|
||||||
|
DiscordToken: getEnv("DISCORD_TOKEN", ""),
|
||||||
|
|
||||||
|
// Himbucks settings
|
||||||
|
HimbucksPerReward: getEnvInt("HIMBUCKS_PER_REWARD", 10),
|
||||||
|
MessageCountThreshold: getEnvInt("MESSAGE_COUNT_THRESHOLD", 5),
|
||||||
|
CooldownPeriod: time.Duration(getEnvInt("HIMBUCKS_COOLDOWN_MINUTES", 1)) * time.Minute,
|
||||||
|
|
||||||
|
// Markov settings
|
||||||
|
MarkovDefaultMessages: getEnvInt("MARKOV_DEFAULT_MESSAGES", 100),
|
||||||
|
MarkovMaxMessages: getEnvInt("MARKOV_MAX_MESSAGES", 1000),
|
||||||
|
MarkovCacheSize: getEnvInt("MARKOV_CACHE_SIZE", 10),
|
||||||
|
|
||||||
|
// Database settings
|
||||||
|
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 25),
|
||||||
|
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 5),
|
||||||
|
ConnMaxLifetime: time.Duration(getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 5)) * time.Minute,
|
||||||
|
|
||||||
|
// Command cooldowns (in seconds)
|
||||||
|
PingCooldown: getEnvInt("PING_COOLDOWN_SECONDS", 5),
|
||||||
|
HsCooldown: getEnvInt("HS_COOLDOWN_SECONDS", 10),
|
||||||
|
MarkovCooldown: getEnvInt("MARKOV_COOLDOWN_SECONDS", 30),
|
||||||
|
HimbucksCooldown: getEnvInt("HIMBUCKS_COOLDOWN_SECONDS", 5),
|
||||||
|
HimboardCooldown: getEnvInt("HIMBOARD_COOLDOWN_SECONDS", 5),
|
||||||
|
SendbucksCooldown: getEnvInt("SENDBUCKS_COOLDOWN_SECONDS", 1800),
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig = config
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnv gets an environment variable with a default value
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnvInt gets an environment variable as an integer with a default value
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
84
lib/db.go
84
lib/db.go
@ -16,6 +16,15 @@ import (
|
|||||||
var DBClient *sql.DB
|
var DBClient *sql.DB
|
||||||
var DBConnector *libsql.Connector
|
var DBConnector *libsql.Connector
|
||||||
|
|
||||||
|
// Prepared statements
|
||||||
|
var (
|
||||||
|
stmtGetBalance *sql.Stmt
|
||||||
|
stmtUpdateBalance *sql.Stmt
|
||||||
|
stmtGetLeaderboard *sql.Stmt
|
||||||
|
stmtGetUserProfile *sql.Stmt
|
||||||
|
stmtUpdateProfile *sql.Stmt
|
||||||
|
)
|
||||||
|
|
||||||
func InitDB() error {
|
func InitDB() error {
|
||||||
// Determine DB path based on /data directory existence
|
// Determine DB path based on /data directory existence
|
||||||
var dbPath string
|
var dbPath string
|
||||||
@ -31,9 +40,24 @@ func InitDB() error {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure connection pool using config values
|
||||||
|
db.SetMaxOpenConns(AppConfig.MaxOpenConns)
|
||||||
|
db.SetMaxIdleConns(AppConfig.MaxIdleConns)
|
||||||
|
db.SetConnMaxLifetime(AppConfig.ConnMaxLifetime)
|
||||||
|
|
||||||
|
// Test the connection
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
return fmt.Errorf("failed to ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
DBClient = db
|
DBClient = db
|
||||||
|
|
||||||
return runMigrations()
|
if err := runMigrations(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare frequently used statements
|
||||||
|
return prepareStatements()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Migration struct {
|
type Migration struct {
|
||||||
@ -140,3 +164,61 @@ func runMigrations() error {
|
|||||||
log.Println("Database migrations completed successfully")
|
log.Println("Database migrations completed successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareStatements() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Prepare balance query
|
||||||
|
stmtGetBalance, err = DBClient.Prepare(`
|
||||||
|
SELECT gp.currency_balance
|
||||||
|
FROM guild_profiles gp
|
||||||
|
JOIN users u ON gp.user_id = u.id
|
||||||
|
WHERE u.discord_id = ? AND gp.guild_id = ?`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare balance query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare leaderboard query
|
||||||
|
stmtGetLeaderboard, err = DBClient.Prepare(`
|
||||||
|
SELECT u.username, gp.currency_balance, gp.message_count
|
||||||
|
FROM guild_profiles gp
|
||||||
|
JOIN users u ON gp.user_id = u.id
|
||||||
|
WHERE gp.guild_id = ?
|
||||||
|
ORDER BY gp.currency_balance DESC
|
||||||
|
LIMIT ?`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare leaderboard query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare user profile query
|
||||||
|
stmtGetUserProfile, err = DBClient.Prepare(`
|
||||||
|
SELECT message_count, last_reward_at
|
||||||
|
FROM guild_profiles
|
||||||
|
WHERE user_id = ? AND guild_id = ?`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare user profile query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Prepared statements initialized successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupDB closes all prepared statements
|
||||||
|
func CleanupDB() {
|
||||||
|
if stmtGetBalance != nil {
|
||||||
|
stmtGetBalance.Close()
|
||||||
|
}
|
||||||
|
if stmtUpdateBalance != nil {
|
||||||
|
stmtUpdateBalance.Close()
|
||||||
|
}
|
||||||
|
if stmtGetLeaderboard != nil {
|
||||||
|
stmtGetLeaderboard.Close()
|
||||||
|
}
|
||||||
|
if stmtGetUserProfile != nil {
|
||||||
|
stmtGetUserProfile.Close()
|
||||||
|
}
|
||||||
|
if stmtUpdateProfile != nil {
|
||||||
|
stmtUpdateProfile.Close()
|
||||||
|
}
|
||||||
|
log.Println("Database cleanup completed")
|
||||||
|
}
|
||||||
|
@ -8,12 +8,6 @@ import (
|
|||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
HimbucksPerReward = 10
|
|
||||||
MessageCountThreshold = 5
|
|
||||||
CooldownPeriod = time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
type HimbucksEntry struct {
|
type HimbucksEntry struct {
|
||||||
Username string
|
Username string
|
||||||
Balance int
|
Balance int
|
||||||
@ -41,8 +35,8 @@ func ProcessHimbucks(s *discordgo.Session, m *discordgo.MessageCreate, ctx *Proc
|
|||||||
}
|
}
|
||||||
|
|
||||||
messageCount++
|
messageCount++
|
||||||
shouldReward := messageCount >= MessageCountThreshold &&
|
shouldReward := messageCount >= AppConfig.MessageCountThreshold &&
|
||||||
(!lastRewardAt.Valid || time.Since(lastRewardAt.Time) >= CooldownPeriod)
|
(!lastRewardAt.Valid || time.Since(lastRewardAt.Time) >= AppConfig.CooldownPeriod)
|
||||||
|
|
||||||
if shouldReward {
|
if shouldReward {
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
@ -51,7 +45,7 @@ func ProcessHimbucks(s *discordgo.Session, m *discordgo.MessageCreate, ctx *Proc
|
|||||||
message_count = 0,
|
message_count = 0,
|
||||||
last_reward_at = CURRENT_TIMESTAMP
|
last_reward_at = CURRENT_TIMESTAMP
|
||||||
WHERE user_id = ? AND guild_id = ?`,
|
WHERE user_id = ? AND guild_id = ?`,
|
||||||
HimbucksPerReward, ctx.UserID, ctx.GuildID)
|
AppConfig.HimbucksPerReward, ctx.UserID, ctx.GuildID)
|
||||||
} else {
|
} else {
|
||||||
_, err = tx.Exec(`
|
_, err = tx.Exec(`
|
||||||
UPDATE guild_profiles
|
UPDATE guild_profiles
|
||||||
@ -69,12 +63,7 @@ func ProcessHimbucks(s *discordgo.Session, m *discordgo.MessageCreate, ctx *Proc
|
|||||||
|
|
||||||
func GetBalance(discordID, guildID string) (int, error) {
|
func GetBalance(discordID, guildID string) (int, error) {
|
||||||
var balance int
|
var balance int
|
||||||
err := DBClient.QueryRow(`
|
err := stmtGetBalance.QueryRow(discordID, guildID).Scan(&balance)
|
||||||
SELECT gp.currency_balance
|
|
||||||
FROM guild_profiles gp
|
|
||||||
JOIN users u ON gp.user_id = u.id
|
|
||||||
WHERE u.discord_id = ? AND gp.guild_id = ?`,
|
|
||||||
discordID, guildID).Scan(&balance)
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@ -173,14 +162,7 @@ func SendBalance(fromDiscordID, toDiscordID, guildID string, amount int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetLeaderboard(guildID string, limit int) ([]HimbucksEntry, error) {
|
func GetLeaderboard(guildID string, limit int) ([]HimbucksEntry, error) {
|
||||||
rows, err := DBClient.Query(`
|
rows, err := stmtGetLeaderboard.Query(guildID, limit)
|
||||||
SELECT u.username, gp.currency_balance, gp.message_count
|
|
||||||
FROM guild_profiles gp
|
|
||||||
JOIN users u ON gp.user_id = u.id
|
|
||||||
WHERE gp.guild_id = ?
|
|
||||||
ORDER BY gp.currency_balance DESC
|
|
||||||
LIMIT ?`,
|
|
||||||
guildID, limit)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get leaderboard: %w", err)
|
return nil, fmt.Errorf("failed to get leaderboard: %w", err)
|
||||||
}
|
}
|
||||||
|
266
main.go
266
main.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"himbot/command"
|
"himbot/command"
|
||||||
"himbot/lib"
|
"himbot/lib"
|
||||||
"log"
|
"log"
|
||||||
@ -14,6 +15,164 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
commands []*discordgo.ApplicationCommand
|
||||||
|
commandHandlers map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate)
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
godotenv.Load(".env")
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
config := lib.LoadConfig()
|
||||||
|
|
||||||
|
// Initialize commands and handlers with config
|
||||||
|
initCommands(config)
|
||||||
|
initCommandHandlers(config)
|
||||||
|
|
||||||
|
err := lib.InitDB()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DiscordToken == "" {
|
||||||
|
log.Fatalln("No $DISCORD_TOKEN given.")
|
||||||
|
}
|
||||||
|
|
||||||
|
dg, err := discordgo.New("Bot " + config.DiscordToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating Discord session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dg.AddHandler(ready)
|
||||||
|
dg.AddHandler(interactionCreate)
|
||||||
|
|
||||||
|
processorManager := lib.NewMessageProcessorManager()
|
||||||
|
|
||||||
|
// Register processors
|
||||||
|
processorManager.RegisterProcessor(lib.ProcessHimbucks)
|
||||||
|
|
||||||
|
dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
processorManager.ProcessMessage(s, m)
|
||||||
|
})
|
||||||
|
|
||||||
|
dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages
|
||||||
|
|
||||||
|
err = dg.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error opening connection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Bot is now running. Press CTRL-C to exit.")
|
||||||
|
registerCommands(dg)
|
||||||
|
|
||||||
|
sc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||||
|
<-sc
|
||||||
|
|
||||||
|
log.Println("Shutting down gracefully...")
|
||||||
|
|
||||||
|
if lib.DBClient != nil {
|
||||||
|
// Close prepared statements
|
||||||
|
lib.CleanupDB()
|
||||||
|
lib.DBClient.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
dg.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ready(s *discordgo.Session, event *discordgo.Ready) {
|
||||||
|
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func interactionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
|
||||||
|
h(s, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerCommands(s *discordgo.Session) {
|
||||||
|
log.Println("Checking command registration...")
|
||||||
|
|
||||||
|
existingCommands, err := s.ApplicationCommands(s.State.User.ID, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching existing commands: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create maps for easier comparison
|
||||||
|
existingMap := make(map[string]*discordgo.ApplicationCommand)
|
||||||
|
for _, cmd := range existingCommands {
|
||||||
|
existingMap[cmd.Name] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
desiredMap := make(map[string]*discordgo.ApplicationCommand)
|
||||||
|
for _, cmd := range commands {
|
||||||
|
desiredMap[cmd.Name] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete commands that no longer exist
|
||||||
|
for name, existingCmd := range existingMap {
|
||||||
|
if _, exists := desiredMap[name]; !exists {
|
||||||
|
log.Printf("Deleting removed command: %s", name)
|
||||||
|
err := s.ApplicationCommandDelete(s.State.User.ID, "", existingCmd.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error deleting command %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or create commands
|
||||||
|
for _, desiredCmd := range commands {
|
||||||
|
if existingCmd, exists := existingMap[desiredCmd.Name]; exists {
|
||||||
|
// Check if command needs updating (simple comparison)
|
||||||
|
if !commandsEqual(existingCmd, desiredCmd) {
|
||||||
|
log.Printf("Updating command: %s", desiredCmd.Name)
|
||||||
|
_, err := s.ApplicationCommandEdit(s.State.User.ID, "", existingCmd.ID, desiredCmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error updating command %s: %v", desiredCmd.Name, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Command %s is up to date", desiredCmd.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("Creating new command: %s", desiredCmd.Name)
|
||||||
|
_, err := s.ApplicationCommandCreate(s.State.User.ID, "", desiredCmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error creating command %s: %v", desiredCmd.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Command registration completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandsEqual performs a basic comparison between two commands
|
||||||
|
func commandsEqual(existing, desired *discordgo.ApplicationCommand) bool {
|
||||||
|
if existing.Name != desired.Name ||
|
||||||
|
existing.Description != desired.Description ||
|
||||||
|
len(existing.Options) != len(desired.Options) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare options (basic comparison)
|
||||||
|
for i, existingOpt := range existing.Options {
|
||||||
|
if i >= len(desired.Options) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
desiredOpt := desired.Options[i]
|
||||||
|
if existingOpt.Name != desiredOpt.Name ||
|
||||||
|
existingOpt.Description != desiredOpt.Description ||
|
||||||
|
existingOpt.Type != desiredOpt.Type ||
|
||||||
|
existingOpt.Required != desiredOpt.Required {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// initCommands initializes command definitions with configuration
|
||||||
|
func initCommands(config *lib.Config) {
|
||||||
commands = []*discordgo.ApplicationCommand{
|
commands = []*discordgo.ApplicationCommand{
|
||||||
{
|
{
|
||||||
Name: "ping",
|
Name: "ping",
|
||||||
@ -38,7 +197,7 @@ var (
|
|||||||
{
|
{
|
||||||
Type: discordgo.ApplicationCommandOptionInteger,
|
Type: discordgo.ApplicationCommandOptionInteger,
|
||||||
Name: "messages",
|
Name: "messages",
|
||||||
Description: "Number of messages to use (default: 100, max: 1000)",
|
Description: fmt.Sprintf("Number of messages to use (default: %d, max: %d)", config.MarkovDefaultMessages, config.MarkovMaxMessages),
|
||||||
Required: false,
|
Required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -71,103 +230,16 @@ var (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initCommandHandlers initializes command handlers with configuration
|
||||||
|
func initCommandHandlers(config *lib.Config) {
|
||||||
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
|
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
|
||||||
"ping": lib.HandleCommand("ping", 5*time.Second, command.PingCommand),
|
"ping": lib.HandleCommand("ping", time.Duration(config.PingCooldown)*time.Second, command.PingCommand),
|
||||||
"hs": lib.HandleCommand("hs", 10*time.Second, command.HsCommand),
|
"hs": lib.HandleCommand("hs", time.Duration(config.HsCooldown)*time.Second, command.HsCommand),
|
||||||
"markov": lib.HandleCommand("markov", 30*time.Second, command.MarkovCommand),
|
"markov": lib.HandleCommand("markov", time.Duration(config.MarkovCooldown)*time.Second, command.MarkovCommand),
|
||||||
"himbucks": lib.HandleCommand("himbucks", 5*time.Second, command.BalanceGetCommand),
|
"himbucks": lib.HandleCommand("himbucks", time.Duration(config.HimbucksCooldown)*time.Second, command.BalanceGetCommand),
|
||||||
"himboard": lib.HandleCommand("himboard", 5*time.Second, command.LeaderboardCommand),
|
"himboard": lib.HandleCommand("himboard", time.Duration(config.HimboardCooldown)*time.Second, command.LeaderboardCommand),
|
||||||
"sendbucks": lib.HandleCommand("sendbucks", 1800*time.Second, command.BalanceSendCommand),
|
"sendbucks": lib.HandleCommand("sendbucks", time.Duration(config.SendbucksCooldown)*time.Second, command.BalanceSendCommand),
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
godotenv.Load(".env")
|
|
||||||
|
|
||||||
err := lib.InitDB()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to initialize database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
token := os.Getenv("DISCORD_TOKEN")
|
|
||||||
|
|
||||||
if token == "" {
|
|
||||||
log.Fatalln("No $DISCORD_TOKEN given.")
|
|
||||||
}
|
|
||||||
|
|
||||||
dg, err := discordgo.New("Bot " + token)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error creating Discord session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dg.AddHandler(ready)
|
|
||||||
dg.AddHandler(interactionCreate)
|
|
||||||
|
|
||||||
processorManager := lib.NewMessageProcessorManager()
|
|
||||||
|
|
||||||
// Register processors
|
|
||||||
processorManager.RegisterProcessor(lib.ProcessHimbucks)
|
|
||||||
|
|
||||||
dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
||||||
processorManager.ProcessMessage(s, m)
|
|
||||||
})
|
|
||||||
|
|
||||||
dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages
|
|
||||||
|
|
||||||
err = dg.Open()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error opening connection: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Bot is now running. Press CTRL-C to exit.")
|
|
||||||
registerCommands(dg)
|
|
||||||
|
|
||||||
sc := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
|
||||||
<-sc
|
|
||||||
|
|
||||||
if lib.DBClient != nil {
|
|
||||||
lib.DBClient.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
dg.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ready(s *discordgo.Session, event *discordgo.Ready) {
|
|
||||||
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
|
|
||||||
}
|
|
||||||
|
|
||||||
func interactionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
||||||
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
|
|
||||||
h(s, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerCommands(s *discordgo.Session) {
|
|
||||||
// First, delete all existing commands
|
|
||||||
log.Println("Deleting existing commands...")
|
|
||||||
|
|
||||||
existingCommands, err := s.ApplicationCommands(s.State.User.ID, "")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error fetching existing commands: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cmd := range existingCommands {
|
|
||||||
err := s.ApplicationCommandDelete(s.State.User.ID, "", cmd.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error deleting command %s: %v", cmd.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then register the new commands
|
|
||||||
log.Println("Registering new commands...")
|
|
||||||
registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
|
|
||||||
for i, v := range commands {
|
|
||||||
cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", v)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Cannot create '%v' command: %v", v.Name, err)
|
|
||||||
}
|
|
||||||
registeredCommands[i] = cmd
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user