package main import ( "database/sql" "fmt" "log" "os" "os/signal" "strings" "syscall" "github.com/BurntSushi/toml" "github.com/bwmarrin/discordgo" _ "github.com/lib/pq" ) var ( config Config client *discordgo.Session db *sql.DB ) type Config struct { Discord Discord `toml:"discord"` Database Database `toml:"database"` AdminRoles []string `toml:"admin_roles"` } type Discord struct { Token string `toml:"token"` AppID string `toml:"appid"` GuildID string `toml:"guildid"` CategoryID string `toml:"category_id"` } type Database struct { Host string `toml:"host"` Port int `toml:"port"` Name string `toml:"name"` Username string `toml:"username"` Password string `toml:"password"` } func loadConfig(filename string) (Config, error) { var config Config _, err := toml.DecodeFile(filename, &config) return config, err } func connectDb(config Config) (*sql.DB, error) { connectionString := fmt.Sprintf( "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", config.Database.Host, config.Database.Port, config.Database.Username, config.Database.Password, config.Database.Name, ) db, err := sql.Open("postgres", connectionString) if err != nil { return nil, fmt.Errorf("error connecting to the database: %v", err) } err = db.Ping() if err != nil { db.Close() return nil, fmt.Errorf("error pinging the database: %v", err) } log.Println("Successfully connected to the database.") return db, nil } func init() { var err error config, err = loadConfig("config.toml") if err != nil { log.Println("Error occurred whilst trying to load config:", err) return } client, err = discordgo.New("Bot " + config.Discord.Token) if err != nil { log.Println("Error initializing bot:", err) return } db, err = connectDb(config) if err != nil { log.Println("Error initializing db connection:", err) return } } func reset(userNickname, softwareType string) { log.Printf("Resetting %s for user %s\n", softwareType, userNickname) } func getUsernameFromMember(member *discordgo.Member) string { if member == nil { return "UnknownUser" } var userName string if member.Nick != "" { userName = member.Nick } else if member.User != nil && member.User.Username != "" { userName = member.User.Username } else { userName = "UnknownUser" } return userName } var ( commands = []discordgo.ApplicationCommand{ { Name: "setup", Description: "Setup a channel for ticket creation", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionChannel, Name: "channel", Description: "Channel for ticket creation", Required: true, }, }, }, } commandsHandlers = map[string]func(client *discordgo.Session, i *discordgo.InteractionCreate){ "setup": func(client *discordgo.Session, i *discordgo.InteractionCreate) { channelOption := i.ApplicationCommandData().Options[0].ChannelValue(client) _, err := client.ChannelMessageSendComplex(channelOption.ID, &discordgo.MessageSend{ Content: "Click the button below to request a HWID reset:", Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.Button{ Label: "Request Reset", Style: discordgo.PrimaryButton, CustomID: "create_ticket", }, }, }, }, }) if err != nil { log.Println("Error sending message to the specified channel:", err) } err = client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: fmt.Sprintf("Setup completed! Request creation button has been added to %s", channelOption.Mention()), }, }) if err != nil { log.Println("Error sending interaction response:", err) } }, } componentsHandlers = map[string]func(client *discordgo.Session, i *discordgo.InteractionCreate){ "create_ticket": func(client *discordgo.Session, i *discordgo.InteractionCreate) { err := client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "For what would you like a HWID reset:", Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.SelectMenu{ CustomID: "select_software_type", Placeholder: "Choose which software", Options: []discordgo.SelectMenuOption{ { Label: "Vanity", Value: "vanity", Description: "Request a vanity reset", }, { Label: "Mesa", Value: "mesa", Description: "Request a mesa reset", }, }, }, }, }, }, Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) } }, "select_software_type": func(client *discordgo.Session, i *discordgo.InteractionCreate) { selectedOption := i.MessageComponentData().Values[0] var softwareType string if selectedOption == "vanity" { softwareType = "Vanity" } else if selectedOption == "mesa" { softwareType = "Mesa" } categoryID := config.Discord.CategoryID guildID := i.GuildID userID := i.Member.User.ID userName := getUsernameFromMember(i.Member) if !canCreateTicket(userName, selectedOption) { err := client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: fmt.Sprintf("You already have an active %s ticket. Please wait for the administrators to process it.", softwareType), Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) } return } channel, err := client.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{ Name: fmt.Sprintf("reset-%s-%s", softwareType, userName), Type: discordgo.ChannelTypeGuildText, ParentID: categoryID, PermissionOverwrites: []*discordgo.PermissionOverwrite{ { ID: guildID, Type: discordgo.PermissionOverwriteTypeRole, Deny: discordgo.PermissionViewChannel, }, }, }) if err != nil { log.Println("Error creating hwid request channel:", err) return } for _, roleID := range config.AdminRoles { client.ChannelPermissionSet(channel.ID, roleID, discordgo.PermissionOverwriteTypeRole, discordgo.PermissionViewChannel, 0) } err = client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "Request submitted, you'll get a PM when it has been processed", Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) return } _, err = client.ChannelMessageSendComplex(channel.ID, &discordgo.MessageSend{ Content: fmt.Sprintf("Reset request by %s for %s", userName, softwareType), Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.Button{ Label: "Accept", Style: discordgo.PrimaryButton, CustomID: fmt.Sprintf("accept_%s_%s", userID, softwareType), }, discordgo.Button{ Label: "Decline", Style: discordgo.DangerButton, CustomID: fmt.Sprintf("decline_%s_%s", userID, softwareType), }, }, }, }, }) if err != nil { log.Println("Error sending message to the ticket channel:", err) } }, "accept": func(client *discordgo.Session, i *discordgo.InteractionCreate) { data := i.MessageComponentData().CustomID parts := strings.Split(data, "_") if len(parts) != 3 { log.Println("Invalid accept button custom ID") return } userID := parts[1] softwareType := parts[2] member, err := client.GuildMember(i.GuildID, userID) if err != nil { log.Println("Error fetching member info:", err) return } userName := getUsernameFromMember(member) reset(userName, softwareType) err = client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "Request accepted and processed.", Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) } _, err = client.ChannelDelete(i.ChannelID) if err != nil { log.Println("Error deleting channel:", err) } dmChannel, err := client.UserChannelCreate(userID) if err != nil { log.Println("Error creating DM channel:", err) return } _, err = client.ChannelMessageSend(dmChannel.ID, fmt.Sprintf("Your reset request for %s has been accepted and processed.", softwareType)) if err != nil { log.Println("Error sending DM:", err) } }, "decline": func(client *discordgo.Session, i *discordgo.InteractionCreate) { data := i.MessageComponentData().CustomID parts := strings.Split(data, "_") if len(parts) != 3 { log.Println("Invalid decline button custom ID") return } userID := parts[1] err := client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "Request declined.", Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) } _, err = client.ChannelDelete(i.ChannelID) if err != nil { log.Println("Error deleting channel:", err) } dmChannel, err := client.UserChannelCreate(userID) if err != nil { log.Println("Error creating DM channel:", err) return } _, err = client.ChannelMessageSend(dmChannel.ID, "Your reset request has been declined.") if err != nil { log.Println("Error sending DM:", err) } }, } ) func canCreateTicket(userName, softwareType string) bool { guildChannels, err := client.GuildChannels(config.Discord.GuildID) if err != nil { log.Println("Error fetching guild channels:", err) return false } for _, channel := range guildChannels { if channel.Type == discordgo.ChannelTypeGuildText && channel.ParentID == config.Discord.CategoryID { expectedName := fmt.Sprintf("reset-%s-%s", softwareType, userName) if channel.Name == expectedName { return false } } } return true } func main() { if client == nil { log.Println("Bot client is not initialized") return } client.AddHandler(func(client *discordgo.Session, r *discordgo.Ready) { log.Println("Bot is online") cmdIDs := make(map[string]string, len(commands)) for _, cmd := range commands { rcmd, err := client.ApplicationCommandCreate(client.State.User.ID, config.Discord.GuildID, &cmd) if err != nil { log.Fatalf("Cannot create slash command %q: %v", cmd.Name, err) } cmdIDs[rcmd.ID] = rcmd.Name } }) client.AddHandler(func(client *discordgo.Session, i *discordgo.InteractionCreate) { if i.Type == discordgo.InteractionApplicationCommand { if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok { h(client, i) } } else if i.Type == discordgo.InteractionMessageComponent { customID := i.MessageComponentData().CustomID if h, ok := componentsHandlers[customID]; ok { h(client, i) } else if strings.HasPrefix(customID, "accept_") { componentsHandlers["accept"](client, i) } else if strings.HasPrefix(customID, "decline_") { componentsHandlers["decline"](client, i) } } }) err := client.Open() if err != nil { log.Println("Error opening connection:", err) return } stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop log.Println("Gracefully shutting down.") client.Close() if db != nil { db.Close() } }