package main import ( "bytes" "database/sql" "encoding/json" "fmt" "log" "mime/multipart" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "github.com/BurntSushi/toml" "github.com/bwmarrin/discordgo" "github.com/lib/pq" ) type UserResponse struct { ID string `json:"id"` Username string `json:"username"` Subscription bool `json:"subscription"` Group string `json:"group"` Expiration string `json:"expiration"` Versions []string `json:"versions"` PFP string `json:"pfp"` Message int `json:"message"` } var ( config Config client *discordgo.Session db *sql.DB ) type Config struct { Discord Discord `toml:"discord"` Database Database `toml:"database"` } type Discord struct { Token string `toml:"token"` AppID string `toml:"appid"` GuildID string `toml:"guildid"` CategoryID string `toml:"category_id"` AdminRoles []string `toml:"admin_roles"` } 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.GlobalName != "" { userName = member.User.GlobalName } else { userName = member.User.Username } 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, }, }, }, { Name: "resetvanity", Description: "Reset a user's HWID for Vanity by username or UID", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "identifier", Description: "Usernames or UIDs, separated by commas", Required: true, }, }, }, { Name: "resetmesa", Description: "Reset a user's HWID for Mesa by username or UID", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "identifier", Description: "Usernames or UIDs, separated by commas", Required: true, }, }, }, } commandsHandlers = map[string]func(client *discordgo.Session, i *discordgo.InteractionCreate){ "setup": func(client *discordgo.Session, i *discordgo.InteractionCreate) { hasAdminPermission := false for _, roleID := range config.Discord.AdminRoles { member, err := client.GuildMember(i.GuildID, i.Member.User.ID) if err != nil { log.Println("Error fetching member info:", err) return } for _, role := range member.Roles { if role == roleID { hasAdminPermission = true break } } if hasAdminPermission { break } } if !hasAdminPermission { err := client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "You do not have permission to run this command.", Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) } return } 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) } }, "resetvanity": func(client *discordgo.Session, i *discordgo.InteractionCreate) { resetCommandHandler(client, i, "vanity") }, "resetmesa": func(client *discordgo.Session, i *discordgo.InteractionCreate) { resetCommandHandler(client, i, "mesa") }, } 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 } _, baseUrl := getTableNameAndBaseURL(selectedOption) userUID, _ := fetchUserID(userName, baseUrl) if userUID == 0 { err := client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: fmt.Sprintf("Account not found for %s", 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.Discord.AdminRoles { if roleID != "" { err := client.ChannelPermissionSet(channel.ID, roleID, discordgo.PermissionOverwriteTypeRole, discordgo.PermissionViewChannel|discordgo.PermissionSendMessages, 0) if err != nil { log.Printf("Error setting permissions for role %s: %v", roleID, err) } } } 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", userID, softwareType), Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.Button{ Label: "Accept", Style: discordgo.PrimaryButton, CustomID: fmt.Sprintf("accept_%s_%s_%d", userID, softwareType, userUID), }, discordgo.Button{ Label: "Decline", Style: discordgo.DangerButton, CustomID: fmt.Sprintf("decline_%s_%s_%d", userID, softwareType, userUID), }, }, }, }, }) 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) != 4 { log.Println("Invalid accept button custom ID") return } userID := parts[1] softwareType := parts[2] userUID, _ := strconv.Atoi(parts[3]) member, err := client.GuildMember(i.GuildID, userID) if err != nil { log.Println("Error fetching member info:", err) return } userName := getUsernameFromMember(member) reset(userName, softwareType) tableName, _ := getTableNameAndBaseURL(strings.ToLower(softwareType)) successes, errors := resetAndVerify(tableName, []int{userUID}) if len(errors) > 0 { for _, err := range errors { log.Println("Error resetting hwid:", err) } } else { for _, success := range successes { if success { log.Printf("Reset successful for UID %d", userUID) } else { log.Printf("Reset unsuccessful for UID %d", userUID) } } } log.Printf("Reset the HWID of user %s with UID %d for %s", userName, userUID, 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) != 4 { 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 resetCommandHandler(client *discordgo.Session, i *discordgo.InteractionCreate, softwareType string) { hasAdminPermission := false var err error var tableName string for _, roleID := range config.Discord.AdminRoles { member, err := client.GuildMember(i.GuildID, i.Member.User.ID) if err != nil { log.Println("Error fetching member info:", err) return } for _, role := range member.Roles { if role == roleID { hasAdminPermission = true break } } if hasAdminPermission { break } } if !hasAdminPermission { err := client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "You do not have permission to run this command.", Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) } return } identifier := i.ApplicationCommandData().Options[0].StringValue() identifierList := strings.Split(identifier, ",") var userUIDs []int for _, identifier := range identifierList { identifier = strings.TrimSpace(identifier) var userName string var userUID int var err error if uid, err := strconv.Atoi(identifier); err == nil { userUID = uid } else { userName = identifier } if userName != "" { _, baseURL := getTableNameAndBaseURL(softwareType) userUID, err = fetchUserID(userName, baseURL) if err != nil { log.Println("Error fetching user ID:", err) continue } } if userUID != 0 { userUIDs = append(userUIDs, userUID) } } if len(userUIDs) == 0 { err := client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "No valid users found to reset.", Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", err) } return } tableName, _ = getTableNameAndBaseURL(softwareType) successes, errors := resetAndVerify(tableName, userUIDs) if len(errors) > 0 { for _, err := range errors { log.Println("Error:", err) } } else { for i, success := range successes { if success { log.Printf("Reset successful for UID %d", userUIDs[i]) } else { log.Printf("Reset unsuccessful for UID %d", userUIDs[i]) } } } for _, uid := range userUIDs { log.Printf("Reset the HWID of user with UID %d for %s", uid, softwareType) } err = client.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: fmt.Sprintf("Successfully reset HWID for %d users.", len(userUIDs)), Flags: discordgo.MessageFlagsEphemeral, }, }) if err != nil { log.Println("Error sending interaction response:", 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 getTableNameAndBaseURL(choice string) (string, string) { var tableName, baseURL string switch choice { case "vanity": tableName = "AuthUserData" baseURL = "http://vanitycheats.xyz/UserAuthentication.php" case "mesa": tableName = "AuthUserData-Mesachanger.com" baseURL = "http://mesachanger.com/UserAuthentication.php" default: fmt.Println("Invalid choice. Please choose 'vanity' or 'mesa'.") } return tableName, baseURL } func fetchUserID(username string, baseURL string) (int, error) { requestBody := &bytes.Buffer{} multiPartWriter := multipart.NewWriter(requestBody) err := multiPartWriter.WriteField("username", username) if err != nil { return 0, fmt.Errorf("error adding username field: %w", err) } multiPartWriter.Close() request, err := http.NewRequest("POST", baseURL, requestBody) if err != nil { return 0, fmt.Errorf("error creating request: %w", err) } request.Header.Set("Content-Type", multiPartWriter.FormDataContentType()) client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Do(request) if err != nil { return 0, fmt.Errorf("error sending request: %w", err) } defer resp.Body.Close() var userResp UserResponse err = json.NewDecoder(resp.Body).Decode(&userResp) if err != nil { return 0, fmt.Errorf("error decoding JSON: %w", err) } uid, err := strconv.Atoi(userResp.ID) if err != nil { return 0, fmt.Errorf("error converting user ID: %w", err) } return uid, nil } func resetHWID(tableName string, uids []int) { query := fmt.Sprintf(`UPDATE public.%q SET "StorageIdentifier" = NULL, "BootIdentifier" = NULL WHERE "UID" = ANY($1)`, tableName) _, err := db.Exec(query, pq.Array(uids)) if err != nil { log.Printf("Error resetting HWID: %v", err) return } } func resetAndVerify(tableName string, uids []int) ([]bool, []error) { var successes []bool var errorsSlice []error rows, err := db.Query(fmt.Sprintf(`SELECT "UID", MD5(CONCAT("StorageIdentifier", "BootIdentifier")) FROM public.%q WHERE "UID" = ANY($1)`, tableName), pq.Array(uids)) if err != nil { log.Printf("Error querying database for before hashes: %v", err) return nil, []error{err} } defer rows.Close() beforeHashesMap := make(map[int]string) for rows.Next() { var uid int var beforeHash string err := rows.Scan(&uid, &beforeHash) if err != nil { log.Printf("Error scanning rows: %v", err) errorsSlice = append(errorsSlice, err) continue } beforeHashesMap[uid] = beforeHash } resetHWID(tableName, uids) rows, err = db.Query(fmt.Sprintf(`SELECT "UID", MD5(CONCAT("StorageIdentifier", "BootIdentifier")) FROM public.%q WHERE "UID" = ANY($1)`, tableName), pq.Array(uids)) if err != nil { log.Printf("Error querying database for after hashes: %v", err) return nil, []error{err} } defer rows.Close() afterHashesMap := make(map[int]string) for rows.Next() { var uid int var afterHash string err := rows.Scan(&uid, &afterHash) if err != nil { log.Printf("Error scanning rows: %v", err) errorsSlice = append(errorsSlice, err) continue } afterHashesMap[uid] = afterHash } for _, uid := range uids { afterHash, ok := afterHashesMap[uid] if !ok { errorsSlice = append(errorsSlice, fmt.Errorf("no rows found for UID %d after reset", uid)) successes = append(successes, false) continue } successes = append(successes, beforeHashesMap[uid] != afterHash || afterHash == "d41d8cd98f00b204e9800998ecf8427e") } return successes, errorsSlice } 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() } }