diff --git a/config.yaml b/config.yaml index 68d74e8..be69a25 100644 --- a/config.yaml +++ b/config.yaml @@ -65,8 +65,54 @@ event: name: ✅ #id: #animated: true + repeat: + name: 🔁 + #id: + #animated: true + # Emoji for entering the advent calendar giveaway adventcalendar: vote.check + random.coin.heads: + name: 👤 + #id: + #animated: true + random.coin.tails: + name: 🪙 + #id: + #animated: true + random.coin.flip: repeat + random.coin.reflip: repeat + random.dice.1: + #name: 1️⃣ + id: 1322967431106527322 + #animated: true + random.dice.2: + #name: 2️⃣ + id: 1322967432024817748 + #animated: true + random.dice.3: + #name: 3️⃣ + id: 1322967433786691724 + #animated: true + random.dice.4: + #name: 4️⃣ + id: 1322967435170807968 + #animated: true + random.dice.5: + #name: 5️⃣ + id: 1322967436307206145 + #animated: true + random.dice.6: + #name: 6️⃣ + id: 1322967437800378411 + #animated: true + random.dice.rolling: + #name: 🎲 + id: 1322968516311126057 + animated: true + random.dice.reroll: repeat + random.teams.resplit_size: repeat + random.teams.resplit_amount: repeat secretsanta: vote.yes secretsanta.invite.show_match: name: 🎁 diff --git a/data/lang/de.yaml b/data/lang/de.yaml index aeb4d95..6beacdc 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -6,6 +6,7 @@ discord.command: no: Nein msg.self_hidden: Warum ist das unsichtbar? msg.self_hidden.desc: Da du deinen Geburtstag als nicht sichtbar eingetragen hast, kannst diese Nachricht nur du sehen. Du kannst diese Nachricht nun schließen. + msg.page: Seite %d/%d birthday: base: geburtstag @@ -123,6 +124,32 @@ discord.command: msg.winner.details: "__Gewinner: %s__\nTickets: %d/24\nGewinnchance: %.2f%%" msg.winner.congratulation: "Herzlichen Glückwunsch, %s! :heart:\nFrohe Weihnachten an alle!" + random: + base: zufall + base.description: Einige nützliche Zufallsgenerator Befehle + display: Zufallsgenerator + + option.coin: münze + option.coin.description: Wirf eine Münze + option.dice: würfel + option.dice.description: Würfle ein Würfel + option.dice.option.range: bereich + option.dice.option.range.description: "Wie viele Zahlen soll der Würfel haben? (Standard: 6)" + option.teams: gruppen + option.teams.description: Teile Gruppen zu + option.teams.option.members: mitglieder + option.teams.option.members.description: Welche Mitglieder sollen in Gruppen geteilt werden? + option.teams.option.team_size: gruppengröße + option.teams.option.team_size.description: Wie viele Mitglieder sollen in einer Gruppe sein? + option.teams.option.team_amount: gruppenanzahl + option.teams.option.team_amount.description: Wie viele Gruppen sollen erstellt werden? + + msg.dice.roll: You rolled a %d + msg.teams.missing_option: "Fehler: Du musst entweder die Gruppengröße oder die Gruppenanzahl angeben!" + msg.teams.multiple_options: "Fehler: Du kannst nicht gleichzeitig die Gruppengröße und die Gruppenanzahl angeben!" + msg.teams.title: Gruppen + msg.teams.team: Gruppe %d + secretsanta: base: Wichteln display: Wichteln diff --git a/data/lang/en.yaml b/data/lang/en.yaml index 28194dd..d5bda76 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -6,6 +6,7 @@ discord.command: no: No msg.self_hidden: Why is this invisible? msg.self_hidden.desc: Since you've set your birthday to not be visible, this message is also only visible to you. You can close this message now. + msg.page: Page %d/%d birthday: base: birthday @@ -123,6 +124,32 @@ discord.command: msg.winner.details: "__Winner: %s__\nTickets: %d/24\nProbability of winning: %.2f%%" msg.winner.congratulation: "Congratulations, %s! :heart:\nMerry XMas everyone!" + random: + base: random + base.description: Some useful Random Number Generator commands + display: Random Generator + + option.coin: coin + option.coin.description: Flip a coin + option.dice: dice + option.dice.description: Roll a dice + option.dice.option.range: range + option.dice.option.range.description: "How many sides does the dice have? (Default: 6)" + option.teams: teams + option.teams.description: Generate teams + option.teams.option.members: members + option.teams.option.members.description: Which members should be splitted into teams? + option.teams.option.team_size: size + option.teams.option.team_size.description: How many members should be in each team? + option.teams.option.team_amount: amount + option.teams.option.team_amount.description: How many teams should be created? + + msg.dice.roll: You rolled a %d + msg.teams.missing_option: "Error: You need to specify either the team size or the team amount!" + msg.teams.multiple_options: "Error: You can't specify both the team size and the team amount at the same time!" + msg.teams.title: Teams + msg.teams.team: Team %d + secretsanta: base: Secret Santa display: Secret Santa diff --git a/event/command/commandBase.go b/event/command/commandBase.go index ec6ea74..f1df7c1 100644 --- a/event/command/commandBase.go +++ b/event/command/commandBase.go @@ -19,6 +19,7 @@ import ( "cake4everybot/modules/adventcalendar" "cake4everybot/modules/birthday" "cake4everybot/modules/info" + "cake4everybot/modules/random" "cake4everybot/modules/secretsanta" "cake4everybot/util" "fmt" @@ -72,6 +73,7 @@ func Register(s *discordgo.Session, guildID string) error { commandsList = append(commandsList, &birthday.Chat{}) commandsList = append(commandsList, &info.Chat{}) commandsList = append(commandsList, &adventcalendar.Chat{}) + commandsList = append(commandsList, &random.Chat{}) commandsList = append(commandsList, &secretsanta.Chat{}) commandsList = append(commandsList, &secretsanta.MsgCmd{}) // messsage commands diff --git a/event/component/componentBase.go b/event/component/componentBase.go index 36f122a..f49a944 100644 --- a/event/component/componentBase.go +++ b/event/component/componentBase.go @@ -3,6 +3,7 @@ package component import ( "cake4everybot/logger" "cake4everybot/modules/adventcalendar" + "cake4everybot/modules/random" "cake4everybot/modules/secretsanta" "github.com/bwmarrin/discordgo" @@ -34,6 +35,7 @@ func Register() { var componentList []Component componentList = append(componentList, adventcalendar.Component{}) + componentList = append(componentList, random.Component{}) componentList = append(componentList, secretsanta.Component{}) if len(componentList) == 0 { diff --git a/modules/adventcalendar/midnight.go b/modules/adventcalendar/midnight.go index af0a1be..d33eccf 100644 --- a/modules/adventcalendar/midnight.go +++ b/modules/adventcalendar/midnight.go @@ -104,20 +104,7 @@ func splitEntriesToEmbeds(s *discordgo.Session, entries []database.GiveawayEntry for _, e := range entries { totalTickets += e.Weight } - numEmbeds := len(entries)/25 + 1 - embeds := make([]*discordgo.MessageEmbed, 0, numEmbeds) - for i, e := range entries { - if i%25 == 0 { - new := &discordgo.MessageEmbed{} - if numEmbeds > 1 { - new.Description = fmt.Sprintf("Page %d/%d", i/25+1, numEmbeds) - } - util.SetEmbedFooter(s, "module.adventcalendar.embed_footer", new) - embeds = append(embeds, new) - } - - embeds[len(embeds)-1].Fields = append(embeds[len(embeds)-1].Fields, e.ToEmbedField(s, totalTickets)) - } - - return embeds + return util.SplitToEmbedFields(s, entries, 0, "module.adventcalendar.embed_footer", func(e database.GiveawayEntry, _ int) *discordgo.MessageEmbedField { + return e.ToEmbedField(s, totalTickets) + }) } diff --git a/modules/random/chatCommand.go b/modules/random/chatCommand.go new file mode 100644 index 0000000..3cd5b2f --- /dev/null +++ b/modules/random/chatCommand.go @@ -0,0 +1,80 @@ +package random + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + + "github.com/bwmarrin/discordgo" +) + +const ( + // Prefix for translation key, i.e.: + // key := tp+"base" // => random + tp = "discord.command.random." +) + +// The Chat (slash) command of the random package. Has a few sub commands and options to use all +// features through a single chat command. +type Chat struct { + randomBase + ID string +} + +type subcommand interface { + appCmd() *discordgo.ApplicationCommandOption + handle() +} + +// AppCmd (ApplicationCommand) returns the definition of the chat command +func (cmd Chat) AppCmd() *discordgo.ApplicationCommand { + options := []*discordgo.ApplicationCommandOption{ + cmd.subcommandCoin().appCmd(), + cmd.subcommandDice().appCmd(), + cmd.subcommandTeams().appCmd(), + } + + return &discordgo.ApplicationCommand{ + Name: lang.GetDefault(tp + "base"), + NameLocalizations: util.TranslateLocalization(tp + "base"), + Description: lang.GetDefault(tp + "base.description"), + DescriptionLocalizations: util.TranslateLocalization(tp + "base.description"), + Options: options, + } +} + +// Handle handles the functionality of a command +func (cmd Chat) Handle(s *discordgo.Session, i *discordgo.InteractionCreate) { + cmd.InteractionUtil = util.InteractionUtil{Session: s, Interaction: i} + cmd.member = i.Member + cmd.user = i.User + if i.Member != nil { + cmd.user = i.Member.User + } else if i.User != nil { + cmd.member = &discordgo.Member{User: i.User} + } + + subcommandName := i.ApplicationCommandData().Options[0].Name + var sub subcommand + switch subcommandName { + case lang.GetDefault(tp + "option.dice"): + sub = cmd.subcommandDice() + case lang.GetDefault(tp + "option.coin"): + sub = cmd.subcommandCoin() + case lang.GetDefault(tp + "option.teams"): + sub = cmd.subcommandTeams() + default: + return + } + + sub.handle() +} + +// SetID sets the registered command ID for internal uses after uploading to discord +func (cmd *Chat) SetID(id string) { + cmd.ID = id +} + +// GetID gets the registered command ID +func (cmd Chat) GetID() string { + return cmd.ID +} diff --git a/modules/random/component.go b/modules/random/component.go new file mode 100644 index 0000000..d061645 --- /dev/null +++ b/modules/random/component.go @@ -0,0 +1,51 @@ +package random + +import ( + "cake4everybot/util" + "strings" + + "github.com/bwmarrin/discordgo" +) + +// The Component of the random package. +type Component struct { + randomBase + data discordgo.MessageComponentInteractionData +} + +// Handle handles the functionality of a component. +func (c Component) Handle(s *discordgo.Session, i *discordgo.InteractionCreate) { + c.InteractionUtil = util.InteractionUtil{Session: s, Interaction: i} + c.member = i.Member + c.user = i.User + if i.Member != nil { + c.user = i.Member.User + } else if i.User != nil { + c.member = &discordgo.Member{User: i.User} + } + c.data = i.MessageComponentData() + + ids := strings.Split(c.data.CustomID, ".") + // pop the first level identifier + util.ShiftL(ids) + + switch util.ShiftL(ids) { + case "coin": + c.subcommandCoin().handleComponent(ids) + return + case "dice": + c.subcommandDice().handleComponent(ids) + return + case "teams": + c.subcommandTeams().handleComponent(ids) + return + default: + log.Printf("Unknown component interaction ID: %s", c.data.CustomID) + } + +} + +// ID returns the custom ID of the modal to identify the module +func (c Component) ID() string { + return "random" +} diff --git a/modules/random/handleSubcommandCoin.go b/modules/random/handleSubcommandCoin.go new file mode 100644 index 0000000..91df019 --- /dev/null +++ b/modules/random/handleSubcommandCoin.go @@ -0,0 +1,99 @@ +package random + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + "math/rand/v2" + "time" + + "github.com/bwmarrin/discordgo" +) + +// The set subcommand. Used when executing the slash-command "/random coin". +type subcommandCoin struct { + randomBase + *Chat + data *discordgo.ApplicationCommandInteractionDataOption +} + +func (rb randomBase) subcommandCoin() subcommandCoin { + return subcommandCoin{randomBase: rb} +} + +// Constructor for subcommandCoin, the struct for the slash-command "/random coin". +func (cmd *Chat) subcommandCoin() subcommandCoin { + var subcommand *discordgo.ApplicationCommandInteractionDataOption + if cmd.Interaction != nil { + subcommand = cmd.Interaction.ApplicationCommandData().Options[0] + } + return subcommandCoin{ + randomBase: cmd.randomBase, + Chat: cmd, + data: subcommand, + } +} + +func (cmd subcommandCoin) appCmd() *discordgo.ApplicationCommandOption { + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: lang.GetDefault(tp + "option.coin"), + NameLocalizations: *util.TranslateLocalization(tp + "option.coin"), + Description: lang.GetDefault(tp + "option.coin.description"), + DescriptionLocalizations: *util.TranslateLocalization(tp + "option.coin.description"), + } +} + +func (cmd subcommandCoin) handle() { + cmd.ReplyComplex(cmd.flip()) +} + +func (cmd subcommandCoin) handleComponent(ids []string) { + switch id := util.ShiftL(ids); id { + case "reflip": + cmd.ReplyComplexUpdate(cmd.flip()) + return + default: + log.Printf("Unknown component interaction ID in subcommand coin: %s %s", id, ids) + } +} + +func (cmd subcommandCoin) flip() (data *discordgo.InteractionResponseData) { + data = &discordgo.InteractionResponseData{} + + emoji, err := util.GetConfigEmoji(cmd.Session, "random.coin.flip") + if err != nil { + log.Printf("ERROR: could not get emoji: %+v", err) + cmd.ReplyError() + return + } + data.Content = emoji.MessageFormat() + + reflipButton := util.CreateButtonComponent( + "random.coin.reflip", + "", + discordgo.PrimaryButton, + util.GetConfigComponentEmoji("random.coin.reflip")) + reflipButton.Disabled = true + data.Components = []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{reflipButton}}} + + go func() { + time.Sleep(2 * time.Second) + reflipButton.Disabled = false + defer cmd.Session.InteractionResponseEdit(cmd.Interaction.Interaction, util.MessageComplexWebhookEdit(data)) + + side := "heads" + if rand.IntN(2) == 1 { + side = "tails" + } + var emoji *discordgo.Emoji + emoji, err = util.GetConfigEmoji(cmd.Session, "random.coin."+side) + if err != nil { + log.Printf("Warning: could not get emoji: %+v", err) + data.Content = side + return + } + data.Content = emoji.MessageFormat() + }() + + return data +} diff --git a/modules/random/handleSubcommandDice.go b/modules/random/handleSubcommandDice.go new file mode 100644 index 0000000..e557859 --- /dev/null +++ b/modules/random/handleSubcommandDice.go @@ -0,0 +1,141 @@ +package random + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + "fmt" + "math/rand/v2" + "strconv" + "time" + + "github.com/bwmarrin/discordgo" +) + +// The set subcommand. Used when executing the slash-command "/random dice". +type subcommandDice struct { + randomBase + *Chat + data *discordgo.ApplicationCommandInteractionDataOption + + diceRange *discordgo.ApplicationCommandInteractionDataOption // optional +} + +func (rb randomBase) subcommandDice() subcommandDice { + return subcommandDice{randomBase: rb} +} + +// Constructor for subcommandDice, the struct for the slash-command "/random dice". +func (cmd *Chat) subcommandDice() subcommandDice { + var subcommand *discordgo.ApplicationCommandInteractionDataOption + if cmd.Interaction != nil { + subcommand = cmd.Interaction.ApplicationCommandData().Options[0] + } + return subcommandDice{ + randomBase: cmd.randomBase, + Chat: cmd, + data: subcommand, + } +} + +func (cmd subcommandDice) appCmd() *discordgo.ApplicationCommandOption { + options := []*discordgo.ApplicationCommandOption{ + cmd.optionRange(), + } + + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: lang.GetDefault(tp + "option.dice"), + NameLocalizations: *util.TranslateLocalization(tp + "option.dice"), + Description: lang.GetDefault(tp + "option.dice.description"), + DescriptionLocalizations: *util.TranslateLocalization(tp + "option.dice.description"), + Options: options, + } +} + +func (cmd subcommandDice) optionRange() *discordgo.ApplicationCommandOption { + minValueTwo := float64(2) + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionInteger, + Name: lang.GetDefault(tp + "option.dice.option.range"), + NameLocalizations: *util.TranslateLocalization(tp + "option.dice.option.range"), + Description: lang.GetDefault(tp + "option.dice.option.range.description"), + DescriptionLocalizations: *util.TranslateLocalization(tp + "option.dice.option.range.description"), + Required: false, + MinValue: &minValueTwo, + } +} + +func (cmd subcommandDice) handle() { + for _, opt := range cmd.data.Options { + switch opt.Name { + case lang.GetDefault(tp + "option.dice.option.range"): + cmd.diceRange = opt + } + } + diceRange := 6 + if cmd.diceRange != nil { + diceRange = int(cmd.diceRange.IntValue()) + } + cmd.ReplyComplex(cmd.roll(diceRange)) +} + +func (cmd subcommandDice) handleComponent(ids []string) { + switch id := util.ShiftL(ids); id { + case "reroll": + diceRange, _ := strconv.Atoi(util.ShiftL(ids)) + cmd.ReplyComplexUpdate(cmd.roll(diceRange)) + return + default: + log.Printf("Unknown component interaction ID in subcommand dice: %s %s", id, ids) + } +} + +func (cmd subcommandDice) roll(diceRange int) (data *discordgo.InteractionResponseData) { + data = &discordgo.InteractionResponseData{} + var err error + + if diceRange <= 6 { + var emoji *discordgo.Emoji + emoji, err = util.GetConfigEmoji(cmd.Session, "random.dice.rolling") + if err != nil { + log.Printf("ERROR: could not get emoji: %+v", err) + cmd.ReplyError() + return nil + } + data.Content = emoji.MessageFormat() + } else { + data.Embeds = util.SimpleEmbed(0xFF7D00, "...") + } + + rerollButton := util.CreateButtonComponent( + fmt.Sprintf("random.dice.reroll.%d", diceRange), + "", + discordgo.PrimaryButton, + util.GetConfigComponentEmoji("random.dice.reroll"), + ) + rerollButton.Disabled = true + data.Components = []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{rerollButton}}} + + go func() { + time.Sleep(2 * time.Second) + rerollButton.Disabled = false + defer cmd.Session.InteractionResponseEdit(cmd.Interaction.Interaction, util.MessageComplexWebhookEdit(data)) + + diceResult := rand.IntN(diceRange) + 1 + if diceRange > 6 { + data.Embeds = util.SimpleEmbedf(0xFF7D00, lang.GetDefault(tp+"msg.dice.roll"), diceResult) + return + } + + var emoji *discordgo.Emoji + emoji, err = util.GetConfigEmoji(cmd.Session, fmt.Sprintf("random.dice.%d", diceResult)) + if err != nil { + log.Printf("Warning: could not get emoji: %+v", err) + data.Content = fmt.Sprintf(lang.GetDefault(tp+"msg.dice.roll"), diceResult) + return + } + data.Content = emoji.MessageFormat() + }() + + return data +} diff --git a/modules/random/handleSubcommandTeams.go b/modules/random/handleSubcommandTeams.go new file mode 100644 index 0000000..04446f9 --- /dev/null +++ b/modules/random/handleSubcommandTeams.go @@ -0,0 +1,326 @@ +package random + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + "errors" + "fmt" + "math/rand/v2" + "strconv" + "strings" + + "github.com/bwmarrin/discordgo" +) + +// The set subcommand. Used when executing the slash-command "/random teams". +type subcommandTeams struct { + randomBase + *Chat + data *discordgo.ApplicationCommandInteractionDataOption + + members *discordgo.ApplicationCommandInteractionDataOption // required + teamSize *discordgo.ApplicationCommandInteractionDataOption // optional + teamAmount *discordgo.ApplicationCommandInteractionDataOption // optional +} + +func (rb randomBase) subcommandTeams() subcommandTeams { + return subcommandTeams{randomBase: rb} +} + +// Constructor for subcommandTeams, the struct for the slash-command "/random teams". +func (cmd *Chat) subcommandTeams() subcommandTeams { + var subcommand *discordgo.ApplicationCommandInteractionDataOption + if cmd.Interaction != nil { + subcommand = cmd.Interaction.ApplicationCommandData().Options[0] + } + return subcommandTeams{ + randomBase: cmd.randomBase, + Chat: cmd, + data: subcommand, + } +} + +func (cmd subcommandTeams) appCmd() *discordgo.ApplicationCommandOption { + options := []*discordgo.ApplicationCommandOption{ + cmd.optionMembers(), + cmd.optionTeamSize(), + cmd.optionTeamAmount(), + } + + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: lang.GetDefault(tp + "option.teams"), + NameLocalizations: *util.TranslateLocalization(tp + "option.teams"), + Description: lang.GetDefault(tp + "option.teams.description"), + DescriptionLocalizations: *util.TranslateLocalization(tp + "option.teams.description"), + Options: options, + } +} + +func (cmd subcommandTeams) optionMembers() *discordgo.ApplicationCommandOption { + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionRole, + Name: lang.GetDefault(tp + "option.teams.option.members"), + NameLocalizations: *util.TranslateLocalization(tp + "option.teams.option.members"), + Description: lang.GetDefault(tp + "option.teams.option.members.description"), + DescriptionLocalizations: *util.TranslateLocalization(tp + "option.teams.option.members.description"), + Required: true, + } +} + +func (cmd subcommandTeams) optionTeamSize() *discordgo.ApplicationCommandOption { + minValueTwo := float64(2) + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionInteger, + Name: lang.GetDefault(tp + "option.teams.option.team_size"), + NameLocalizations: *util.TranslateLocalization(tp + "option.teams.option.team_size"), + Description: lang.GetDefault(tp + "option.teams.option.team_size.description"), + DescriptionLocalizations: *util.TranslateLocalization(tp + "option.teams.option.team_size.description"), + Required: false, + MinValue: &minValueTwo, + } +} + +func (cmd subcommandTeams) optionTeamAmount() *discordgo.ApplicationCommandOption { + minValueOne := float64(1) + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionInteger, + Name: lang.GetDefault(tp + "option.teams.option.team_amount"), + NameLocalizations: *util.TranslateLocalization(tp + "option.teams.option.team_amount"), + Description: lang.GetDefault(tp + "option.teams.option.team_amount.description"), + DescriptionLocalizations: *util.TranslateLocalization(tp + "option.teams.option.team_amount.description"), + Required: false, + MinValue: &minValueOne, + } +} + +func (cmd subcommandTeams) handle() { + for _, opt := range cmd.data.Options { + switch opt.Name { + case lang.GetDefault(tp + "option.teams.option.members"): + cmd.members = opt + case lang.GetDefault(tp + "option.teams.option.team_size"): + cmd.teamSize = opt + case lang.GetDefault(tp + "option.teams.option.team_amount"): + cmd.teamAmount = opt + } + } + + if cmd.teamSize == nil && cmd.teamAmount == nil { + cmd.ReplyHidden(lang.GetDefault(tp + "msg.teams.missing_option")) + return + } else if cmd.teamSize != nil && cmd.teamAmount != nil { + cmd.ReplyHidden(lang.GetDefault(tp + "msg.teams.multiple_options")) + return + } + + var ( + memberRole = cmd.members.RoleValue(cmd.Session, cmd.Interaction.GuildID) + teamSize int + teamAmount int + ) + if cmd.teamSize != nil { + teamSize = int(cmd.teamSize.IntValue()) + } else { + teamAmount = int(cmd.teamAmount.IntValue()) + } + + members, err := cmd.getMembersWithRole(memberRole.ID) + if err != nil { + log.Printf("ERROR: could not get members with role '%s/%s' (%s): %+v", cmd.Interaction.GuildID, memberRole.ID, memberRole.Name, err) + cmd.ReplyError() + } + + data := &discordgo.InteractionResponseData{} + if cmd.teamSize != nil { + data = cmd.splitTeamsSize(members, teamSize) + } else { + data = cmd.splitTeamsN(members, teamAmount) + } + + cmd.ReplyComplex(data) +} + +func (cmd subcommandTeams) handleComponent(ids []string) { + switch id := util.ShiftL(ids); id { + case "resplit_size": + teamSize, _ := strconv.Atoi(util.ShiftL(ids)) + members, _, err := cmd.parseTeamEmbeds(cmd.Interaction.Message.Embeds) + if err != nil { + log.Printf("ERROR: could not parse team embeds: %+v", err) + cmd.ReplyError() + return + } + + cmd.ReplyComplexUpdate(cmd.splitTeamsSize(members, teamSize)) + return + case "resplit_amount": + members, n, err := cmd.parseTeamEmbeds(cmd.Interaction.Message.Embeds) + if err != nil { + log.Printf("ERROR: could not parse team embeds: %+v", err) + cmd.ReplyError() + return + } + + cmd.ReplyComplexUpdate(cmd.splitTeamsN(members, n)) + return + default: + log.Printf("Unknown component interaction ID in subcommand teams: %s %s", id, ids) + } +} + +// splitTeamsSize splits the members into teams of a maximum size teamSize. +// +// The last team might be smaller. +func (cmd subcommandTeams) splitTeamsSize(members []*discordgo.Member, teamSize int) (data *discordgo.InteractionResponseData) { + data = &discordgo.InteractionResponseData{} + + rand.Shuffle(len(members), func(i, j int) { + members[i], members[j] = members[j], members[i] + }) + + var teams [][]*discordgo.Member + for i := 0; i < len(members); i += teamSize { + end := i + teamSize + if end > len(members) { + end = len(members) + } + teams = append(teams, members[i:end]) + } + data.Embeds = teamsEmbed(cmd.Session, teams) + + resplitButton := util.CreateButtonComponent( + fmt.Sprintf("random.teams.resplit_size.%d", teamSize), + "", + discordgo.PrimaryButton, + util.GetConfigComponentEmoji("random.teams.resplit_size")) + data.Components = []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{resplitButton}}} + + return data +} + +// splitTeamsN splits the members into n teams. +func (cmd subcommandTeams) splitTeamsN(members []*discordgo.Member, n int) (data *discordgo.InteractionResponseData) { + data = &discordgo.InteractionResponseData{} + + rand.Shuffle(len(members), func(i, j int) { + members[i], members[j] = members[j], members[i] + }) + + if n > len(members) { + n = len(members) + } + var teams [][]*discordgo.Member = make([][]*discordgo.Member, n) + for i, member := range members { + teams[i%n] = append(teams[i%n], member) + } + data.Embeds = teamsEmbed(cmd.Session, teams) + + resplitButton := util.CreateButtonComponent( + "random.teams.resplit_amount", + "", + discordgo.PrimaryButton, + util.GetConfigComponentEmoji("random.teams.resplit_amount")) + data.Components = []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{resplitButton}}} + + return data +} + +func (cmd subcommandTeams) getMembersWithRole(roleID string) ([]*discordgo.Member, error) { + var membersWithRole []*discordgo.Member + var after string + + for { + members, err := cmd.Session.GuildMembers(cmd.Interaction.GuildID, after, 1000) + if err != nil { + return nil, err + } + if len(members) == 0 { + break + } + + for _, member := range members { + if util.ContainsString(member.Roles, roleID) { + membersWithRole = append(membersWithRole, member) + } + } + + after = members[len(members)-1].User.ID + } + + return membersWithRole, nil +} + +// teamsEmbed returns one or more embeds listing the given teams. +func teamsEmbed(s *discordgo.Session, teams [][]*discordgo.Member) (embeds []*discordgo.MessageEmbed) { + embeds = util.SplitToEmbedFields(s, teams, 0xFFD700, tp+"display", teamEmbed) + embeds[0].Title = lang.GetDefault(tp + "msg.teams.title") + + if len(embeds[0].Fields) == 1 { + embeds[0].Description = embeds[0].Fields[0].Value + embeds[0].Fields = nil + } + + return embeds +} + +// teamEmbed returns the given team as an embed field. +// +// i is the team number (0-indexed) used for the field name. +func teamEmbed(team []*discordgo.Member, i int) *discordgo.MessageEmbedField { + var value string + for i, member := range team { + value += fmt.Sprintf("%d. %s\n", i, member.Mention()) + } + + return &discordgo.MessageEmbedField{ + Name: fmt.Sprintf(lang.GetDefault(tp+"msg.teams.team"), i+1), + Value: value, + Inline: true, + } +} + +// parseTeamEmbeds parses the members from the given embeds. +// Returns the members and the number of teams. +func (cmd subcommandTeams) parseTeamEmbeds(embeds []*discordgo.MessageEmbed) (members []*discordgo.Member, n int, err error) { + parseMembers := func(text string) error { + for _, line := range strings.Split(text, "\n") { + if line == "" { + continue + } + + memberID := line[5 : len(line)-1] // assuming a format like "1. <@USERID>" or "1. <@!USERID>" + memberID = strings.TrimPrefix(memberID, "!") + + var member *discordgo.Member + member, err = cmd.Session.State.Member(cmd.Interaction.GuildID, memberID) + if errors.Is(err, discordgo.ErrStateNotFound) { + member, err = cmd.Session.GuildMember(cmd.Interaction.GuildID, memberID) + } + if err != nil { + return fmt.Errorf("could not get member %s: %w", memberID, err) + } + members = append(members, member) + } + return nil + } + + // special case for single team + // parse from description instead + if len(embeds) == 1 && len(embeds[0].Fields) == 0 { + err = parseMembers(embeds[0].Description) + return members, 1, err + } + + for _, embed := range embeds { + for _, field := range embed.Fields { + err = parseMembers(field.Value) + if err != nil { + return nil, 0, err + } + n++ + } + } + + return members, n, nil +} diff --git a/modules/random/randombase.go b/modules/random/randombase.go new file mode 100644 index 0000000..b01db2f --- /dev/null +++ b/modules/random/randombase.go @@ -0,0 +1,16 @@ +package random + +import ( + "cake4everybot/logger" + "cake4everybot/util" + + "github.com/bwmarrin/discordgo" +) + +var log = logger.New("Random") + +type randomBase struct { + util.InteractionUtil + member *discordgo.Member + user *discordgo.User +} diff --git a/modules/secretsanta/handlerMessageSetup.go b/modules/secretsanta/handlerMessageSetup.go index 4778995..9c1a225 100644 --- a/modules/secretsanta/handlerMessageSetup.go +++ b/modules/secretsanta/handlerMessageSetup.go @@ -9,7 +9,12 @@ import ( ) func (cmd MsgCmd) handler() { - joinEmoji := util.GetConfigEmoji("secretsanta") + joinEmoji, err := util.GetConfigEmoji(cmd.Session, "secretsanta") + if err != nil { + log.Printf("ERROR: could not get emoji: %+v", err) + cmd.ReplyError() + return + } joinEmojiID := joinEmoji.ID if joinEmojiID == "" { joinEmojiID = joinEmoji.Name diff --git a/util/discord.go b/util/discord.go index 834210b..12b2a58 100644 --- a/util/discord.go +++ b/util/discord.go @@ -16,7 +16,9 @@ package util import ( "database/sql" + "errors" "fmt" + "net/http" "strings" "cake4everybot/data/lang" @@ -67,6 +69,69 @@ func AuthoredEmbed[T *discordgo.User | *discordgo.Member](s *discordgo.Session, return embed } +// SimpleEmbed returns a new embed with the given description and color +// +// For convenience it returns a slice of one and always one embed. +func SimpleEmbed(color int, content string) []*discordgo.MessageEmbed { + return []*discordgo.MessageEmbed{{ + Description: content, + Color: color, + }} +} + +// SimpleEmbedf is like [SimpleEmbed] but formats according to a format specifier +func SimpleEmbedf(color int, format string, a ...any) []*discordgo.MessageEmbed { + return SimpleEmbed(color, fmt.Sprintf(format, a...)) +} + +// SplitToEmbedFields splits a slice of elements into one or more embeds. Each +// embed contains a maximum of 25 elements. +// +// elements: +// The slice to split into embeds +// color: +// The color of the embeds (can be 0 for no color) +// footer: +// The translation key for the footer. See [SetEmbedFooter]. (can be "" for no footer) +// field: +// A function that takes an element and an index and returns a field. If the +// field is nil or has no Name, it will be skipped. Resulting in not adding +// the field to the embed and not incrementing the index for the next element. +func SplitToEmbedFields[S []E, E any](s *discordgo.Session, elements S, color int, footer string, field func(E, int) *discordgo.MessageEmbedField) (embeds []*discordgo.MessageEmbed) { + numEmbeds := (len(elements)-1)/25 + 1 + embeds = make([]*discordgo.MessageEmbed, 0, numEmbeds) + + skipped := 0 + for i, element := range elements { + i -= skipped + field := field(element, i) + + if field == nil || field.Name == "" { + skipped++ + continue + } + + if i%25 == 0 { + new := &discordgo.MessageEmbed{ + Color: color, + } + if footer != "" { + SetEmbedFooter(s, footer, new) + } + embeds = append(embeds, new) + } + embeds[len(embeds)-1].Fields = append(embeds[len(embeds)-1].Fields, field) + } + + if len(embeds) > 1 { + for page, embed := range embeds { + embed.Description = fmt.Sprintf(lang.GetDefault("discord.command.generic.msg.page"), page+1, len(embeds)) + } + } + + return embeds +} + // SetEmbedFooter takes a pointer to an embeds and sets the standard footer with the given name. // // sectionName: @@ -167,22 +232,12 @@ func GetChannelsFromDatabase(s *discordgo.Session, channelName string, guildIDs } // GetConfigComponentEmoji returns a configured [discordgo.ComponentEmoji] for the given name. -func GetConfigComponentEmoji(name string) *discordgo.ComponentEmoji { - e := GetConfigEmoji(name) - return &discordgo.ComponentEmoji{ - Name: e.Name, - ID: e.ID, - Animated: e.Animated, - } -} - -// GetConfigEmoji returns a configured [discordgo.Emoji] for the given name. -func GetConfigEmoji(name string) (e *discordgo.Emoji) { +func GetConfigComponentEmoji(name string) (e *discordgo.ComponentEmoji) { override := viper.GetString("event.emoji." + name) if override != "" && override != name { - return GetConfigEmoji(override) + return GetConfigComponentEmoji(override) } - e = &discordgo.Emoji{ + e = &discordgo.ComponentEmoji{ Name: viper.GetString("event.emoji." + name + ".name"), ID: viper.GetString("event.emoji." + name + ".id"), Animated: viper.GetBool("event.emoji." + name + ".animated"), @@ -193,6 +248,64 @@ func GetConfigEmoji(name string) (e *discordgo.Emoji) { return e } +// GetConfigEmoji returns a configured [discordgo.Emoji] for the given name. +func GetConfigEmoji(s *discordgo.Session, name string) (e *discordgo.Emoji, err error) { + ce := GetConfigComponentEmoji(name) + if ce.ID == "" { + return &discordgo.Emoji{Name: ce.Name}, nil + } + + // try to get cached emoji from cached guilds + for _, guild := range s.State.Guilds { + e, err = s.State.Emoji(guild.ID, ce.ID) + if err == nil { + return e, nil + } else if errors.Is(err, discordgo.ErrStateNotFound) { + continue + } + return nil, fmt.Errorf("emoji '%s' (id: %s) not found in guild '%s': %v", name, ce.ID, guild.ID, err) + } + + // try to get cached emoji from all guilds + after := "" + var allGuilds, guilds []*discordgo.UserGuild + for { + guilds, err = s.UserGuilds(200, "", after, false) + if err != nil { + return nil, fmt.Errorf("get user guilds: %v", err) + } else if len(guilds) == 0 { + //return nil, fmt.Errorf("unknown emoji '%s' (id: %s)", name, ce.ID) + break + } + + for _, guild := range guilds { + e, err = s.State.Emoji(guild.ID, ce.ID) + if err == nil { + return e, nil + } else if errors.Is(err, discordgo.ErrStateNotFound) { + continue + } + return nil, fmt.Errorf("emoji '%s' (id: %s) not found in guild '%s': %v", name, ce.ID, guild.ID, err) + } + after = guilds[len(guilds)-1].ID + allGuilds = append(allGuilds, guilds...) + } + + // try to get emoji from all guilds + for _, guild := range allGuilds { + var restErr *discordgo.RESTError + e, err = s.GuildEmoji(guild.ID, ce.ID) + if err == nil { + return e, nil + } else if errors.As(err, &restErr) && restErr.Response.StatusCode == http.StatusNotFound { + continue + } + return nil, fmt.Errorf("get emoji '%s' (id: %s) from guild '%s': %v", name, ce.ID, guild.ID, err) + } + + return nil, fmt.Errorf("emoji '%s' (id: %s) not found in any guild", name, ce.ID) +} + // CompareEmoji returns true if the two emoji are the same func CompareEmoji[E1, E2 *discordgo.Emoji | *discordgo.ComponentEmoji](e1 E1, e2 E2) bool { return *componentEmoji(e1) == *componentEmoji(e2) @@ -213,30 +326,115 @@ func componentEmoji[E *discordgo.Emoji | *discordgo.ComponentEmoji](e E) *discor panic("Given generic type is not an emoji or component emoji") } -// MessageComplexEdit converts a [discordgo.MessageSend] to a [discordgo.MessageEdit] -func MessageComplexEdit(src *discordgo.MessageSend, channel, id string) *discordgo.MessageEdit { - return &discordgo.MessageEdit{ - Content: &src.Content, - Components: &src.Components, - Embeds: &src.Embeds, - AllowedMentions: src.AllowedMentions, - Flags: src.Flags, - Files: src.Files, +// MessageComplexEdit converts a similar type to a [discordgo.MessageEdit]. +func MessageComplexEdit(src any, channel, id string) *discordgo.MessageEdit { + switch t := src.(type) { + case *discordgo.MessageSend: + return &discordgo.MessageEdit{ + Content: &t.Content, + Components: &t.Components, + Embeds: &t.Embeds, + AllowedMentions: t.AllowedMentions, + Flags: t.Flags, + Files: t.Files, - Channel: channel, - ID: id, + Channel: channel, + ID: id, + } + case *discordgo.WebhookEdit: + return &discordgo.MessageEdit{ + Content: t.Content, + Components: t.Components, + Embeds: t.Embeds, + AllowedMentions: t.AllowedMentions, + Files: t.Files, + Attachments: t.Attachments, + + Channel: channel, + ID: id, + } + case *discordgo.InteractionResponseData: + return &discordgo.MessageEdit{ + Content: &t.Content, + Components: &t.Components, + Embeds: &t.Embeds, + AllowedMentions: t.AllowedMentions, + Files: t.Files, + Attachments: t.Attachments, + + Channel: channel, + ID: id, + } + default: + panic("Given source type is not supported: " + fmt.Sprintf("%T", src)) } } -// MessageComplexSend converts a [discordgo.MessageEdit] to a [discordgo.MessageSend] -func MessageComplexSend(src *discordgo.MessageEdit) *discordgo.MessageSend { - return &discordgo.MessageSend{ - Content: *src.Content, - Components: *src.Components, - Embeds: *src.Embeds, - AllowedMentions: src.AllowedMentions, - Flags: src.Flags, - Files: src.Files, +// MessageComplexSend converts a similar type to a [discordgo.MessageSend]. +func MessageComplexSend(src any) *discordgo.MessageSend { + switch t := src.(type) { + case *discordgo.MessageEdit: + return &discordgo.MessageSend{ + Content: *t.Content, + Embeds: *t.Embeds, + Components: *t.Components, + Files: t.Files, + AllowedMentions: t.AllowedMentions, + Flags: t.Flags, + } + case *discordgo.WebhookEdit: + return &discordgo.MessageSend{ + Content: *t.Content, + Embeds: *t.Embeds, + Components: *t.Components, + Files: t.Files, + AllowedMentions: t.AllowedMentions, + } + case *discordgo.InteractionResponseData: + return &discordgo.MessageSend{ + Content: t.Content, + Embeds: t.Embeds, + TTS: t.TTS, + Components: t.Components, + Files: t.Files, + AllowedMentions: t.AllowedMentions, + Flags: t.Flags, + } + default: + panic("Given source type is not supported: " + fmt.Sprintf("%T", src)) } +} +// MessageComplexWebhookEdit converts a similar type to a [discordgo.WebhookEdit]. +func MessageComplexWebhookEdit(src any) *discordgo.WebhookEdit { + switch t := src.(type) { + case *discordgo.MessageSend: + return &discordgo.WebhookEdit{ + Content: &t.Content, + Components: &t.Components, + Embeds: &t.Embeds, + Files: t.Files, + AllowedMentions: t.AllowedMentions, + } + case *discordgo.MessageEdit: + return &discordgo.WebhookEdit{ + Content: t.Content, + Components: t.Components, + Embeds: t.Embeds, + Files: t.Files, + Attachments: t.Attachments, + AllowedMentions: t.AllowedMentions, + } + case *discordgo.InteractionResponseData: + return &discordgo.WebhookEdit{ + Content: &t.Content, + Components: &t.Components, + Embeds: &t.Embeds, + Files: t.Files, + Attachments: t.Attachments, + AllowedMentions: t.AllowedMentions, + } + default: + panic("Given source type is not supported: " + fmt.Sprintf("%T", src)) + } } diff --git a/util/interaction.go b/util/interaction.go index c83d2d0..bff7301 100644 --- a/util/interaction.go +++ b/util/interaction.go @@ -206,78 +206,42 @@ func (i *InteractionUtil) ReplyHiddenEmbedUpdate(embeds ...*discordgo.MessageEmb i.respond() } -// ReplyComponents sends a message along with the provied message components. -func (i *InteractionUtil) ReplyComponents(components []discordgo.MessageComponent, message string) { - i.respondMessage(false, false) - i.response.Data.Content = message - i.response.Data.Components = components - i.respond() -} - // ReplySimpleEmbed is a shortcut for replying with a simple embed that only contains a single text // and has a color. func (i *InteractionUtil) ReplySimpleEmbed(color int, content string) { - e := &discordgo.MessageEmbed{ - Description: content, - Color: color, - } - i.ReplyEmbed(e) + i.ReplyEmbed(SimpleEmbed(color, content)...) } // ReplySimpleEmbedf formats according to a format specifier and is a shortcut for replying with a // simple embed that only contains a single text and has a color. func (i *InteractionUtil) ReplySimpleEmbedf(color int, format string, a ...any) { - e := &discordgo.MessageEmbed{ - Description: fmt.Sprintf(format, a...), - Color: color, - } - i.ReplyEmbed(e) -} - -// ReplyHiddenSimpleEmbed is like ReplySimpleEmbed but also ephemeral. -func (i *InteractionUtil) ReplyHiddenSimpleEmbed(color int, content string) { - e := &discordgo.MessageEmbed{ - Description: content, - Color: color, - } - i.ReplyHiddenEmbed(e) -} - -// ReplyHiddenSimpleEmbedf is like ReplySimpleEmbedf but also ephemeral. -func (i *InteractionUtil) ReplyHiddenSimpleEmbedf(color int, format string, a ...any) { - e := &discordgo.MessageEmbed{ - Description: fmt.Sprintf(format, a...), - Color: color, - } - i.ReplyHiddenEmbed(e) + i.ReplyEmbed(SimpleEmbedf(color, format, a...)...) } // ReplySimpleEmbedUpdate is like ReplySimpleEmbed but make for an update for components. func (i *InteractionUtil) ReplySimpleEmbedUpdate(color int, content string) { - e := &discordgo.MessageEmbed{ - Description: content, - Color: color, - } - i.ReplyEmbedUpdate(e) + i.ReplyEmbedUpdate(SimpleEmbed(color, content)...) } // ReplySimpleEmbedUpdatef is like ReplySimpleEmbedf but make for an update for components. func (i *InteractionUtil) ReplySimpleEmbedUpdatef(color int, format string, a ...any) { - e := &discordgo.MessageEmbed{ - Description: fmt.Sprintf(format, a...), - Color: color, - } - i.ReplyEmbedUpdate(e) + i.ReplyEmbedUpdate(SimpleEmbedf(color, format, a...)...) +} + +// ReplyHiddenSimpleEmbed is like ReplySimpleEmbed but also ephemeral. +func (i *InteractionUtil) ReplyHiddenSimpleEmbed(color int, content string) { + i.ReplyHiddenEmbed(SimpleEmbed(color, content)...) +} + +// ReplyHiddenSimpleEmbedf is like ReplySimpleEmbedf but also ephemeral. +func (i *InteractionUtil) ReplyHiddenSimpleEmbedf(color int, format string, a ...any) { + i.ReplyHiddenEmbed(SimpleEmbedf(color, format, a...)...) } // ReplyHiddenSimpleEmbedUpdate is like [InteractionUtil.ReplyHiddenSimpleEmbed] but made for an // update for components. func (i *InteractionUtil) ReplyHiddenSimpleEmbedUpdate(color int, content string) { - e := &discordgo.MessageEmbed{ - Description: content, - Color: color, - } - i.ReplyHiddenEmbedUpdate(e) + i.ReplyHiddenEmbedUpdate(SimpleEmbed(color, content)...) } // ReplyHiddenSimpleEmbedUpdatef is like [InteractionUtil.ReplyHiddenSimpleEmbedf] but made for an @@ -286,6 +250,14 @@ func (i *InteractionUtil) ReplyHiddenSimpleEmbedUpdatef(color int, format string i.ReplyHiddenSimpleEmbedUpdate(color, fmt.Sprintf(format, a...)) } +// ReplyComponents sends a message along with the provied message components. +func (i *InteractionUtil) ReplyComponents(components []discordgo.MessageComponent, message string) { + i.respondMessage(false, false) + i.response.Data.Content = message + i.response.Data.Components = components + i.respond() +} + // ReplyComponentsf formats according to a format specifier and sends the result along with the // provied message components. func (i *InteractionUtil) ReplyComponentsf(components []discordgo.MessageComponent, format string, a ...any) { @@ -360,13 +332,20 @@ func (i *InteractionUtil) ReplyComponentsHiddenEmbedUpdate(components []discordg i.respond() } +// ReplyComponentsSimpleEmbed sends an embed message along with the provied message components. +func (i *InteractionUtil) ReplyComponentsSimpleEmbed(components []discordgo.MessageComponent, color int, content string) { + i.ReplyComponentsEmbed(components, SimpleEmbed(color, content)...) +} + +// ReplyComponentsSimpleEmbedf is like [InteractionUtil.ReplyComponentsSimpleEmbed] but formats the +// embed content according to a format specifier. +func (i *InteractionUtil) ReplyComponentsSimpleEmbedf(components []discordgo.MessageComponent, color int, format string, a ...any) { + i.ReplyComponentsSimpleEmbed(components, color, fmt.Sprintf(format, a...)) +} + // ReplyComponentsSimpleEmbedUpdate is like [InteractionUtil.ReplyComponentsSimpleEmbed] but made for an update for components. func (i *InteractionUtil) ReplyComponentsSimpleEmbedUpdate(components []discordgo.MessageComponent, color int, content string) { - e := &discordgo.MessageEmbed{ - Description: content, - Color: color, - } - i.ReplyComponentsEmbedUpdate(components, e) + i.ReplyComponentsEmbedUpdate(components, SimpleEmbed(color, content)...) } // ReplyComponentsSimpleEmbedUpdatef is like [InteractionUtil.ReplyComponentsSimpleEmbedf] but made for an update for components. @@ -377,33 +356,41 @@ func (i *InteractionUtil) ReplyComponentsSimpleEmbedUpdatef(components []discord // ReplyComponentsHiddenSimpleEmbed is like [InteractionUtil.ReplyHiddenSimpleEmbed] but sends the // embed message along with the provied message components. func (i *InteractionUtil) ReplyComponentsHiddenSimpleEmbed(components []discordgo.MessageComponent, color int, content string) { - e := &discordgo.MessageEmbed{ - Description: content, - Color: color, - } - i.ReplyComponentsHiddenEmbed(components, e) -} - -// ReplyComponentsHiddenSimpleEmbedUpdate is like [InteractionUtil.ReplyComponentsHiddenSimpleEmbed] but made for an update for components. -func (i *InteractionUtil) ReplyComponentsHiddenSimpleEmbedUpdate(components []discordgo.MessageComponent, color int, content string) { - e := &discordgo.MessageEmbed{ - Description: content, - Color: color, - } - i.ReplyComponentsHiddenEmbedUpdate(components, e) + i.ReplyComponentsHiddenEmbed(components, SimpleEmbed(color, content)...) } -// ReplyComponentsHiddenSimpleEmbedf is like [InteractionUtil.ReplyComponentsHiddenSimpleEmbed] but -// formats the embed content according to a format specifier. +// ReplyComponentsHiddenSimpleEmbedf is like [InteractionUtil.ReplyHiddenSimpleEmbedf] but sends the +// embed message along with the provied message components. func (i *InteractionUtil) ReplyComponentsHiddenSimpleEmbedf(components []discordgo.MessageComponent, color int, format string, a ...any) { i.ReplyComponentsHiddenSimpleEmbed(components, color, fmt.Sprintf(format, a...)) } +// ReplyComponentsHiddenSimpleEmbedUpdate is like [InteractionUtil.ReplyComponentsHiddenSimpleEmbed] but made for an update for components. +func (i *InteractionUtil) ReplyComponentsHiddenSimpleEmbedUpdate(components []discordgo.MessageComponent, color int, content string) { + i.ReplyComponentsHiddenEmbedUpdate(components, SimpleEmbed(color, content)...) +} + // ReplyComponentsHiddenSimpleEmbedUpdatef is like [InteractionUtil.ReplyComponentsHiddenSimpleEmbedf] but made for an update for components. func (i *InteractionUtil) ReplyComponentsHiddenSimpleEmbedUpdatef(components []discordgo.MessageComponent, color int, format string, a ...any) { i.ReplyComponentsHiddenSimpleEmbedUpdate(components, color, fmt.Sprintf(format, a...)) } +// ReplyComplex sends the given interaction response data to the user. +func (i *InteractionUtil) ReplyComplex(data *discordgo.InteractionResponseData) { + i.respondMessage(false, false) + i.response.Data = data + i.respond() +} + +// ReplyComplexUpdate is like [InteractionUtil.ReplyComplex] but made for an update for components. +func (i *InteractionUtil) ReplyComplexUpdate(data *discordgo.InteractionResponseData) { + if !i.respondMessage(true, false) { + return + } + i.response.Data = data + i.respond() +} + // ReplyAutocomplete returns the given choices to the user. When this is called on an interaction // type outside form an applicationCommandAutocomplete nothing will happen. func (i *InteractionUtil) ReplyAutocomplete(choices []*discordgo.ApplicationCommandOptionChoice) { diff --git a/util/modal.go b/util/modal.go index f5b71e7..f7844f2 100644 --- a/util/modal.go +++ b/util/modal.go @@ -24,8 +24,8 @@ import "github.com/bwmarrin/discordgo" // id // Custom id to identify the button when pressed (automatically prefixed) // style // Style of the button (see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles) // Optional: emoji // An emoji to put in the label, can be empty -func CreateButtonComponent(id, label string, style discordgo.ButtonStyle, emoji *discordgo.ComponentEmoji) discordgo.Button { - return discordgo.Button{ +func CreateButtonComponent(id, label string, style discordgo.ButtonStyle, emoji *discordgo.ComponentEmoji) *discordgo.Button { + return &discordgo.Button{ CustomID: id, Label: label, Style: style,