package main import ( "fmt" "himbot/command" "himbot/lib" "log" "os" "os/signal" "syscall" "time" "github.com/bwmarrin/discordgo" "github.com/joho/godotenv" ) 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{ { Name: "ping", Description: "ping pong!", }, { Name: "hs", Description: "This command was your nickname in highschool!", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "nickname", Description: "Your nickname in highschool.", Required: true, }, }, }, { Name: "markov", Description: "Why did the Markov chain break up with its partner? Because it couldn't handle the past!", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionInteger, Name: "messages", Description: fmt.Sprintf("Number of messages to use (default: %d, max: %d)", config.MarkovDefaultMessages, config.MarkovMaxMessages), Required: false, }, }, }, { Name: "himbucks", Description: "Check your himbucks balance", }, { Name: "himboard", Description: "View the himbucks leaderboard", }, { Name: "sendbucks", Description: "Send himbucks to another user", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionUser, Name: "user", Description: "The user to send himbucks to", Required: true, }, { Type: discordgo.ApplicationCommandOptionInteger, Name: "amount", Description: "Amount of himbucks to send", Required: true, MinValue: &[]float64{1}[0], }, }, }, } } // initCommandHandlers initializes command handlers with configuration func initCommandHandlers(config *lib.Config) { commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ "ping": lib.HandleCommand("ping", time.Duration(config.PingCooldown)*time.Second, command.PingCommand), "hs": lib.HandleCommand("hs", time.Duration(config.HsCooldown)*time.Second, command.HsCommand), "markov": lib.HandleCommand("markov", time.Duration(config.MarkovCooldown)*time.Second, command.MarkovCommand), "himbucks": lib.HandleCommand("himbucks", time.Duration(config.HimbucksCooldown)*time.Second, command.BalanceGetCommand), "himboard": lib.HandleCommand("himboard", time.Duration(config.HimboardCooldown)*time.Second, command.LeaderboardCommand), "sendbucks": lib.HandleCommand("sendbucks", time.Duration(config.SendbucksCooldown)*time.Second, command.BalanceSendCommand), } }