diff --git a/config.toml.example b/config.toml.example index 4fd6b6e..1b04778 100644 --- a/config.toml.example +++ b/config.toml.example @@ -1,6 +1,8 @@ [discord] -client = "" +token = "" +appid = "" guildid = "" +category_id = "" [database] host = "" @@ -9,3 +11,4 @@ name = "" username = "" password = "" +admin_roles = [""] diff --git a/go.mod b/go.mod index b5f4657..77e66f2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.4 require ( github.com/BurntSushi/toml v1.4.0 github.com/bwmarrin/discordgo v0.28.1 + github.com/lib/pq v1.10.9 ) require ( diff --git a/main.go b/main.go index 102b92f..2110035 100644 --- a/main.go +++ b/main.go @@ -1,25 +1,36 @@ 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 client *discordgo.Session +var ( + config Config + client *discordgo.Session + db *sql.DB +) type Config struct { - Discord Discord `toml:"discord"` - Database Database `toml:"database"` + Discord Discord `toml:"discord"` + Database Database `toml:"database"` + AdminRoles []string `toml:"admin_roles"` } type Discord struct { - Token string `toml:"client"` - GuildID string `toml:"guildid"` + Token string `toml:"token"` + AppID string `toml:"appid"` + GuildID string `toml:"guildid"` + CategoryID string `toml:"category_id"` } type Database struct { @@ -30,42 +41,392 @@ type Database struct { Password string `toml:"password"` } - func loadConfig(filename string) (Config, error) { var config Config _, err := toml.DecodeFile(filename, &config) return config, err } -func init() { - config, err := loadConfig("config.toml") +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 { - fmt.Println("Error occurred whilst trying to load config:", err) + 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 { - fmt.Println("Error initializing bot:", err) + 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 { - fmt.Println("Bot client is not initialized") + log.Println("Bot client is not initialized") return } client.AddHandler(func(client *discordgo.Session, r *discordgo.Ready) { - fmt.Println("Bot is online") + 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 { - fmt.Println("Error opening connection:", err) + log.Println("Error opening connection:", err) return } @@ -73,7 +434,11 @@ func main() { signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop - fmt.Println("Gracefully shutting down.") + log.Println("Gracefully shutting down.") client.Close() + + if db != nil { + db.Close() + } }