diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..588d65e --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +DISCORD_TOKEN= +GUILD_ID= +LOG_LEVEL=default +BM_TOKEN= +GITHUB_APP_KEY= +GITHUB_APP_CLIENT_ID= +BEARER= diff --git a/.gitignore b/.gitignore index 0525895..ea9a990 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea -.env \ No newline at end of file +.env +WARP.md \ No newline at end of file diff --git a/commands/registry.go b/commands/registry.go index 9cc3c06..310ec45 100644 --- a/commands/registry.go +++ b/commands/registry.go @@ -10,6 +10,7 @@ func NewRegistry() *Registry { r := &Registry{} r.RegisterCommands( Milpac(), + Warden(), AppsBetaDeploy(), Zulu(), S6ITCheck(), diff --git a/commands/warden.go b/commands/warden.go new file mode 100644 index 0000000..960f876 --- /dev/null +++ b/commands/warden.go @@ -0,0 +1,385 @@ +package commands + +import ( + "fmt" + "strings" + "github.com/7cav/cavbot2/utils" + "github.com/bwmarrin/discordgo" +) + +const ROLE_NAME string = "Warden Verified" + +func Warden() Command { + return Command{ + Definition: &discordgo.ApplicationCommand{ + Name: "warden", + Description: "Warden role management", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "add", + Description: "Add '" + ROLE_NAME + "' role to a user", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "discordname", + Description: "Discord username or nickname to match (partial allowed)", + Required: true, + }, + }, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "remove", + Description: "Remove '" + ROLE_NAME + "' role from a user", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "discordname", + Description: "Discord username or nickname to match (partial allowed)", + Required: true, + }, + }, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "bulkadd", + Description: "Add '" + ROLE_NAME + "' role to a list of users", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "userlist", + Description: "Comma-separated list of Discord usernames or nicknames to match (partial allowed)", + Required: true, + }, + }, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "purge", + Description: "Remove '" + ROLE_NAME + "' role from all users", + Options: []*discordgo.ApplicationCommandOption{}, + }, + }, + }, + Handler: handleWarden, + } +} + +func handleWarden(session *discordgo.Session, interaction *discordgo.InteractionCreate) { + data := interaction.ApplicationCommandData() + if len(data.Options) == 0 { + utils.HandleError(session, interaction, "❌ Invalid warden command") + return + } + + sub := data.Options[0] + + if len(sub.Options) == 0 && sub.Name != "purge" { + utils.HandleError(session, interaction, "❌ Missing discordname argument") + return + } + + guildID := interaction.GuildID + + if guildID == "" { + utils.HandleError(session, interaction, "❌ GUILD_ID not configured") + return + } + + query := "" + if sub.Name != "purge" { + query = sub.Options[0].StringValue() + } + switch sub.Name { + case "add": + handleAddCommand(session, interaction, sub, guildID, query) + case "bulkadd": + handleBulkAddCommand(session, interaction, sub, guildID, query) + case "remove": + handleRemoveCommand(session, interaction, sub, guildID, query) + case "purge": + handlePurgeCommand(session, interaction, guildID) + default: + utils.HandleError(session, interaction, "❌ Unknown subcommand") + } +} + +func handleAddCommand( + session *discordgo.Session, + interaction *discordgo.InteractionCreate, + sub *discordgo.ApplicationCommandInteractionDataOption, + guildID string, + query string, +) { + member := retrieveMemberByName(session, guildID, query) + if member == nil { + return + } + + roleID := findRoleIDByName(session, interaction, guildID, ROLE_NAME) + if roleID == "" { + utils.HandleError(session, interaction, "❌ 'Warden Verified' role not found in guild") + return + } + + addRoleForQuery(session, interaction, guildID, query, roleID) + + err := session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("✅ Added '%s' role to %s#%s", ROLE_NAME,member.User.Username, member.User.Discriminator), + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to send confirmation: %v", err)) + return + } + utils.Info("Warden role assigned", "user", member.User.ID) +} + +func handleRemoveCommand( + session *discordgo.Session, + interaction *discordgo.InteractionCreate, + sub *discordgo.ApplicationCommandInteractionDataOption, + guildID string, + query string, +) { + member := retrieveMemberByName(session, guildID, query) + if member == nil { + return + } + + roleID := findRoleIDByName(session, interaction, guildID, ROLE_NAME) + if roleID == "" { + utils.HandleError(session, interaction, "❌ 'Warden Verified' role not found in guild") + return + } + + removeRoleForQuery(session, interaction, guildID, query, roleID) + + err := session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("✅ Removed '%s' role from %s#%s", ROLE_NAME, member.User.Username, member.User.Discriminator), + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to send confirmation: %v", err)) + return + } + utils.Info("Warden role removed", "user", member.User.ID) +} + +func handleBulkAddCommand( + session *discordgo.Session, + interaction *discordgo.InteractionCreate, + sub *discordgo.ApplicationCommandInteractionDataOption, + guildID string, + query string, +) { + queries := strings.Split(query, ",") + + roleID := findRoleIDByName(session, interaction, guildID, ROLE_NAME) + if roleID == "" { + return + } + + err := session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, + }) + + if err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to acknowledge bulk add: %v", err)) + return + } + + var results []string + for _, q := range queries { + q = strings.TrimSpace(q) + utils.Info("Processing bulk add", "query", q) + if q == "" { + continue + } + results = append(results, addRoleForQuery(session, interaction, guildID, q, roleID)) + } + + content := strings.Join(results, "\n") + _, err = session.InteractionResponseEdit(interaction.Interaction, &discordgo.WebhookEdit{ + Content: &content, + }) + if err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to send bulk add summary: %v", err)) + return + } +} + +func handlePurgeCommand( + session *discordgo.Session, + interaction *discordgo.InteractionCreate, + guildID string, +) { + roleID := findRoleIDByName(session, interaction, guildID, ROLE_NAME) + if roleID == "" { + return + } + + if err := session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, + }); err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to acknowledge purge: %v", err)) + return + } + + var ( + after string + removed int + results []string + ) + + for { + members, err := session.GuildMembers(guildID, after, 1000) + if err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to retrieve guild members: %v", err)) + return + } + if len(members) == 0 { + break + } + + for _, m := range members { + if m.User == nil { + continue + } + + if memberHasRole(m, roleID) { + results = append(results, removeRoleForQuery(session, interaction, guildID, m.User.Username, roleID)) + removed++ + } + } + + after = members[len(members)-1].User.ID + if len(members) < 1000 { + break + } + } + + content := strings.Join(results, "\n") + if content == "" { + content = "✅ Purge complete: no members had the role." + } + + _, err := session.InteractionResponseEdit(interaction.Interaction, &discordgo.WebhookEdit{ + Content: &content, + }) + if err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to send purge summary: %v", err)) + return + } +} + +func memberHasRole(m *discordgo.Member, roleID string) bool { + for _, rid := range m.Roles { + if rid == roleID { + return true + } + } + return false +} + + +func addRoleForQuery( + session *discordgo.Session, + interaction *discordgo.InteractionCreate, + guildID string, + query string, + roleID string, +) string { + member := retrieveMemberByName(session, guildID, query) + if member == nil { + return fmt.Sprintf("❌ No member found matching '%s'", query) + } + + err := session.GuildMemberRoleAdd(guildID, member.User.ID, roleID) + if err != nil { + utils.Error("Failed to add role in bulk", "user", member.User.ID, "error", err) + return fmt.Sprintf("❌ Failed to add role to %s#%s: %v", member.User.Username, member.User.Discriminator, err) + } + + utils.Info("Warden role assigned (bulk)", "user", member.User.ID) + return fmt.Sprintf("✅ Added '%s' role to %s#%s", ROLE_NAME, member.User.Username, member.User.Discriminator) +} + +func removeRoleForQuery( + session *discordgo.Session, + interaction *discordgo.InteractionCreate, + guildID string, + query string, + roleID string, +) string { + member := retrieveMemberByName(session, guildID, query) + if member == nil { + return fmt.Sprintf("❌ No member found matching '%s'", query) + } + + err := session.GuildMemberRoleRemove(guildID, member.User.ID, roleID) + if err != nil { + utils.Error("Failed to remove role in bulk", "user", member.User.ID, "error", err) + return fmt.Sprintf("❌ Failed to remove role from %s#%s: %v", member.User.Username, member.User.Discriminator, err) + } + + utils.Info("Warden role removed ", "user", member.User.ID) + return fmt.Sprintf("✅ Removed '%s' role from %s#%s", ROLE_NAME, member.User.Username, member.User.Discriminator) +} + +func retrieveMemberByName( + session *discordgo.Session, + guildID string, + query string, +) (*discordgo.Member) { + if strings.HasPrefix(query, "<@") && strings.HasSuffix(query, ">") { + userID := strings.TrimSuffix(strings.TrimPrefix(query, "<@"), ">") + member, _ := session.GuildMember(guildID, userID) + + if member != nil { + return member + } + } + + members, err := session.GuildMembersSearch(guildID, query, 10) + + if err != nil { + utils.Error("Failed to search members (silent)", "error", err) + return nil + } + + if len(members) != 1 { + return nil + } + + return members[0] +} + +func findRoleIDByName( + session *discordgo.Session, + interaction *discordgo.InteractionCreate, + guildID string, + roleName string, +) (string) { + roles, err := session.GuildRoles(guildID) + + if err != nil { + utils.HandleError(session, interaction, fmt.Sprintf("❌ Failed to retrieve guild roles: %v", err)) + return "" + } + + for _, role := range roles { + if role.Name == roleName { + return role.ID + } + } + + return "" +}