From a7ab8fb7c29ad81bf15aaf70f4c9e4cdd634bb67 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 16 Nov 2024 01:36:25 +0100 Subject: [PATCH 01/17] feat: added secret santa message command --- data/lang/de.yaml | 9 ++++ data/lang/en.yaml | 9 ++++ event/command/commandBase.go | 2 + modules/secretsanta/handlerMessageSetup.go | 46 ++++++++++++++++++++ modules/secretsanta/messageCommandSetup.go | 49 ++++++++++++++++++++++ modules/secretsanta/secretsantabase.go | 22 ++++++++++ 6 files changed, 137 insertions(+) create mode 100644 modules/secretsanta/handlerMessageSetup.go create mode 100644 modules/secretsanta/messageCommandSetup.go create mode 100644 modules/secretsanta/secretsantabase.go diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 3b268b5..b88edd4 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -123,6 +123,15 @@ discord.command: msg.winner.details: "__Gewinner: %s__\nTickets: %d/24\nGewinnchance: %.2f%%" msg.winner.congratulation: "Herzlichen Glückwunsch, %s! :heart:\nFrohe Weihnachten an alle!" + secretsanta: + base: wichteln + display: Wichteln + + title: Wichteln + + msg.setup.no_reactions: Diese Nachricht hat keine Reaktionen. Nur Leute, die mit %s reagiert haben, werden eingeschlossen. + msg.setup.not_enough_reactions: Nicht genug Reaktionen um zu starten. Es werden mindestens %d Reaktionen benötigt. + module: adventcalendar: post.message: Noch %d Mal schlafen bis Heilig Abend! Heute öffnet sich das **Türchen %d**. diff --git a/data/lang/en.yaml b/data/lang/en.yaml index 03b5734..d6e5ea9 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -123,6 +123,15 @@ discord.command: msg.winner.details: "__Winner: %s__\nTickets: %d/24\nProbability of winning: %.2f%%" msg.winner.congratulation: "Congratulations, %s! :heart:\nMerry XMas everyone!" + secretsanta: + setup: Secret Santa + display: Secret Santa + + title: Secret Santa + + msg.setup.no_reactions: This message doesn't have any vote reactions. Only members who reated with %s are included. + msg.setup.not_enough_reactions: Not enough votes to start a game. At least %d votes are required. + module: adventcalendar: post.message: Just sleep %d more times! Its time for **door %d**. diff --git a/event/command/commandBase.go b/event/command/commandBase.go index e89f770..ee709e0 100644 --- a/event/command/commandBase.go +++ b/event/command/commandBase.go @@ -18,6 +18,7 @@ import ( "cake4everybot/modules/adventcalendar" "cake4everybot/modules/birthday" "cake4everybot/modules/info" + "cake4everybot/modules/secretsanta" "cake4everybot/util" "fmt" "log" @@ -69,6 +70,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, &secretsanta.MsgCmd{}) // messsage commands // user commands commandsList = append(commandsList, &birthday.UserShow{}) diff --git a/modules/secretsanta/handlerMessageSetup.go b/modules/secretsanta/handlerMessageSetup.go new file mode 100644 index 0000000..c1822e9 --- /dev/null +++ b/modules/secretsanta/handlerMessageSetup.go @@ -0,0 +1,46 @@ +package secretsanta + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + +) + +func (cmd MsgCmd) handler() { + const emojiName = "👍" + + msg := cmd.data.Resolved.Messages[cmd.data.TargetID] + if len(msg.Reactions) == 0 { + cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.no_reactions"), emojiName) + return + } + var reaction *discordgo.MessageReactions + for _, r := range msg.Reactions { + if r.Emoji.Name != emojiName { + continue + } + reaction = r + break + } + + if reaction == nil { + cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.no_reactions"), emojiName) + return + } + + emojiID := reaction.Emoji.ID + if emojiID == "" { + emojiID = reaction.Emoji.Name + } + users, err := cmd.Session.MessageReactions(msg.ChannelID, msg.ID, emojiID, 100, "", "") + if err != nil { + log.Printf("Error on get users: %v\n", err) + cmd.ReplyError() + return + } + + if len(users) < 2 { + cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.not_enough_reactions"), 2) + return + } +} diff --git a/modules/secretsanta/messageCommandSetup.go b/modules/secretsanta/messageCommandSetup.go new file mode 100644 index 0000000..3f569aa --- /dev/null +++ b/modules/secretsanta/messageCommandSetup.go @@ -0,0 +1,49 @@ +package secretsanta + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + + "github.com/bwmarrin/discordgo" +) + +// MsgCmd represents the mesaage command of the secretsanta package. It adds the ability to start a +// new secret santa game. +type MsgCmd struct { + secretSantaBase + + data discordgo.ApplicationCommandInteractionData + ID string +} + +// AppCmd (ApplicationCommand) returns the definition of the chat command +func (cmd *MsgCmd) AppCmd() *discordgo.ApplicationCommand { + return &discordgo.ApplicationCommand{ + Type: discordgo.MessageApplicationCommand, + Name: lang.GetDefault(tp + "setup"), + NameLocalizations: util.TranslateLocalization(tp + "setup"), + } +} + +// Handle handles the functionality of a command +func (cmd *MsgCmd) 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 + } + + cmd.data = cmd.Interaction.ApplicationCommandData() + cmd.handler() +} + +// SetID sets the registered command ID for internal uses after uploading to discord +func (cmd *MsgCmd) SetID(id string) { + cmd.ID = id +} + +// GetID gets the registered command ID +func (cmd *MsgCmd) GetID() string { + return cmd.ID +} diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go new file mode 100644 index 0000000..9c8e83f --- /dev/null +++ b/modules/secretsanta/secretsantabase.go @@ -0,0 +1,22 @@ +package secretsanta + +import ( + "cake4everybot/util" + logger "log" + + "github.com/bwmarrin/discordgo" +) + +const ( + // Prefix for translation key, i.e.: + // key := tp+"base" // => adventcalendar + tp = "discord.command.secretsanta." +) + +var log = logger.New(logger.Writer(), "[SecretSanta] ", logger.LstdFlags|logger.Lmsgprefix) + +type secretSantaBase struct { + util.InteractionUtil + member *discordgo.Member + user *discordgo.User +} From ec9dc87c0100597b057c778d4c539286ff124397 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 16 Nov 2024 12:14:55 +0100 Subject: [PATCH 02/17] feat: more config and util functions for emojis --- config.yaml | 32 ++++++++++++---- modules/adventcalendar/post.go | 6 +-- modules/secretsanta/handlerMessageSetup.go | 24 ++++++------ util/discord.go | 43 ++++++++++++++++++++++ 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/config.yaml b/config.yaml index 5ae1f5e..428b9d2 100644 --- a/config.yaml +++ b/config.yaml @@ -33,26 +33,42 @@ event: adventcalendar: images: data/images/adventcalendar - # Name: The name of this emoji, e.g. '🎅', '❤️' when a default emoji - # ID: The snowflake ID if when a custom emoji - # Animated: Whether this emoji is animated. Defaults to false - emoji.name: ✅ - #emoji.id: - #emoji.animated: true twitch_giveaway: # The amount of points a single giveaway ticket costs. ticket_cost: 1000 - # Cooldown in minutes before beeing able to by another ticket + # Cooldown in minutes before beeing able to buy another ticket cooldown: 15 # the filepath for of the json giveaway prizes prizes: twitch/prizes.json # the filepath for storing the giveaway cooldown times times: twitch/times.json + emoji: + # Configuration for emojis used by the bot + # Name: The name of this emoji, e.g. '🎅', '❤️' when a default emoji + # ID: The snowflake ID when a custom emoji + # Animated: Whether this emoji is animated. Defaults to false if not set + + # Voting for yes + vote.yes: + name: 👍 + #id: + #animated: true + # Voting for no + vote.no: + name: 👎 + #id: + #animated: true + # Emoji for entering the advent calendar giveaway + adventcalendar: + name: ✅ + #id: + #animated: true + secretsanta: vote.yes + webserver: favicon: webserver/favicon.png - birthday_hour: 8 # Time to trigger daily birthday check (24h format) twitch: name: c4e_bot diff --git a/modules/adventcalendar/post.go b/modules/adventcalendar/post.go index 96e011d..f5aa75b 100644 --- a/modules/adventcalendar/post.go +++ b/modules/adventcalendar/post.go @@ -68,11 +68,7 @@ func postData(t time.Time) *discordgo.MessageSend { fmt.Sprintf("%s.post.%s", Component.ID(Component{}), t.Format("2006.01.02")), lang.GetDefault("module.adventcalendar.post.button"), discordgo.PrimaryButton, - &discordgo.ComponentEmoji{ - Name: viper.GetString("event.adventcalendar.emoji.name"), - ID: viper.GetString("event.adventcalendar.emoji.id"), - Animated: viper.GetBool("event.adventcalendar.emoji.animated"), - }, + util.GetConfigComponentEmoji("adventcalendar"), ), }}, } diff --git a/modules/secretsanta/handlerMessageSetup.go b/modules/secretsanta/handlerMessageSetup.go index c1822e9..9bd6f06 100644 --- a/modules/secretsanta/handlerMessageSetup.go +++ b/modules/secretsanta/handlerMessageSetup.go @@ -7,32 +7,32 @@ import ( ) func (cmd MsgCmd) handler() { - const emojiName = "👍" + joinEmoji := util.GetConfigEmoji("secretsanta") + joinEmojiID := joinEmoji.ID + if joinEmojiID == "" { + joinEmojiID = joinEmoji.Name + } msg := cmd.data.Resolved.Messages[cmd.data.TargetID] if len(msg.Reactions) == 0 { - cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.no_reactions"), emojiName) + cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.no_reactions"), joinEmojiID) return } - var reaction *discordgo.MessageReactions + var hasReaction bool for _, r := range msg.Reactions { - if r.Emoji.Name != emojiName { + if !util.CompareEmoji(r.Emoji, joinEmoji) { continue } - reaction = r + hasReaction = true break } - if reaction == nil { - cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.no_reactions"), emojiName) + if !hasReaction { + cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.no_reactions"), joinEmojiID) return } - emojiID := reaction.Emoji.ID - if emojiID == "" { - emojiID = reaction.Emoji.Name - } - users, err := cmd.Session.MessageReactions(msg.ChannelID, msg.ID, emojiID, 100, "", "") + users, err := cmd.Session.MessageReactions(msg.ChannelID, msg.ID, joinEmojiID, 100, "", "") if err != nil { log.Printf("Error on get users: %v\n", err) cmd.ReplyError() diff --git a/util/discord.go b/util/discord.go index 4724f92..016d7a5 100644 --- a/util/discord.go +++ b/util/discord.go @@ -151,3 +151,46 @@ func GetChannelsFromDatabase(s *discordgo.Session, channelName string) (map[stri return IDMap, nil } + +// 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) *discordgo.Emoji { + override := viper.GetString("event.emoji." + name) + if override != "" && override != name { + return GetConfigEmoji(override) + } + return &discordgo.Emoji{ + Name: viper.GetString("event.emoji." + name + ".name"), + ID: viper.GetString("event.emoji." + name + ".id"), + Animated: viper.GetBool("event.emoji." + name + ".animated"), + } +} + +// 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) +} + +// componentEmoji returns a [discordgo.ComponentEmoji] for the given [discordgo.Emoji] or [discordgo.ComponentEmoji]. +func componentEmoji[E *discordgo.Emoji | *discordgo.ComponentEmoji](e E) *discordgo.ComponentEmoji { + if ee, ok := any(e).(*discordgo.Emoji); ok { + return &discordgo.ComponentEmoji{ + Name: ee.Name, + ID: ee.ID, + Animated: ee.Animated, + } + } + if ce, ok := any(e).(*discordgo.ComponentEmoji); ok { + return ce + } + panic("Given generic type is not an emoji or component emoji") +} From 78cd750bed5be0f337959769880e572d345fcb6c Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 16 Nov 2024 15:49:52 +0100 Subject: [PATCH 03/17] added player lists for secret santa --- .gitignore | 5 + config.yaml | 4 + data/lang/de.yaml | 1 + data/lang/en.yaml | 1 + modules/secretsanta/handlerMessageSetup.go | 38 ++++++ modules/secretsanta/secretsantabase.go | 152 +++++++++++++++++++++ 6 files changed, 201 insertions(+) diff --git a/.gitignore b/.gitignore index 9bc3a8f..90253eb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ cake4everybot # configuration files for tokens, access keys and other connection data *env.yaml + +# runtime config files (as described in config.yml) +modules/secretsanta/players.json +twitch/prizes.json +twitch/times.json diff --git a/config.yaml b/config.yaml index 428b9d2..f0b7cb4 100644 --- a/config.yaml +++ b/config.yaml @@ -34,6 +34,10 @@ event: adventcalendar: images: data/images/adventcalendar + secretsanta: + # the filepath for the players + players: modules/secretsanta/players.json + twitch_giveaway: # The amount of points a single giveaway ticket costs. ticket_cost: 1000 diff --git a/data/lang/de.yaml b/data/lang/de.yaml index b88edd4..6569719 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -131,6 +131,7 @@ discord.command: msg.setup.no_reactions: Diese Nachricht hat keine Reaktionen. Nur Leute, die mit %s reagiert haben, werden eingeschlossen. msg.setup.not_enough_reactions: Nicht genug Reaktionen um zu starten. Es werden mindestens %d Reaktionen benötigt. + msg.setup.users: Teilnehmer module: adventcalendar: diff --git a/data/lang/en.yaml b/data/lang/en.yaml index d6e5ea9..5d1eea2 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -131,6 +131,7 @@ discord.command: msg.setup.no_reactions: This message doesn't have any vote reactions. Only members who reated with %s are included. msg.setup.not_enough_reactions: Not enough votes to start a game. At least %d votes are required. + msg.setup.users: Members module: adventcalendar: diff --git a/modules/secretsanta/handlerMessageSetup.go b/modules/secretsanta/handlerMessageSetup.go index 9bd6f06..16e8acf 100644 --- a/modules/secretsanta/handlerMessageSetup.go +++ b/modules/secretsanta/handlerMessageSetup.go @@ -3,7 +3,9 @@ package secretsanta import ( "cake4everybot/data/lang" "cake4everybot/util" + "fmt" + "github.com/bwmarrin/discordgo" ) func (cmd MsgCmd) handler() { @@ -43,4 +45,40 @@ func (cmd MsgCmd) handler() { cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.not_enough_reactions"), 2) return } + + e := &discordgo.MessageEmbed{ + Title: lang.GetDefault(tp + "title"), + Color: 0x690042, + } + + var ( + names string + players []*player = make([]*player, 0, len(users)) + ) + for _, u := range users { + member, err := cmd.Session.GuildMember(cmd.Interaction.GuildID, u.ID) + if member == nil { + log.Printf("WARN: Could not get member '%s' from guild '%s': %v", u.ID, cmd.Interaction.GuildID, err) + continue + } + players = append(players, &player{Member: member}) + names += fmt.Sprintf("%s\n", member.Mention()) + } + if len(players) < 2 { + cmd.ReplyHiddenf(lang.GetDefault(tp+"msg.setup.not_enough_reactions"), 2) + return + } + util.AddEmbedField(e, lang.GetDefault(tp+"msg.setup.users"), names, false) + + players = derangementMatch(players) + + err = cmd.setPlayers(players) + if err != nil { + log.Printf("Error on set players: %v\n", err) + cmd.ReplyError() + return + } + + util.SetEmbedFooter(cmd.Session, tp+"display", e) + cmd.ReplyHiddenEmbed(e) } diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index 9c8e83f..f9451a2 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -2,9 +2,14 @@ package secretsanta import ( "cake4everybot/util" + "encoding/json" + "fmt" logger "log" + "os" "github.com/bwmarrin/discordgo" + "github.com/spf13/viper" + "golang.org/x/exp/rand" ) const ( @@ -20,3 +25,150 @@ type secretSantaBase struct { member *discordgo.Member user *discordgo.User } + +// getPlayers returns the list of players for the current guild. If it is the first time, it loads +// the players from the file or creates an empty file. +func (ssb secretSantaBase) getPlayers() ([]*player, error) { + if allPlayers != nil { + return allPlayers[ssb.Interaction.GuildID], nil + } + + log.Println("First time getting players. Loading from file...") + playersPath := viper.GetString("event.secretsanta.players") + playersData, err := os.ReadFile(playersPath) + if err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("read players file: %v", err) + } + allPlayers = make(AllPlayers) + playersData, err = json.Marshal(allPlayers) + if err != nil { + return nil, fmt.Errorf("marshal players file: %v", err) + } + err = os.WriteFile(playersPath, playersData, 0644) + if err != nil { + return nil, fmt.Errorf("write players file: %v", err) + } + log.Printf("Created players file: %s\n", playersPath) + return []*player{}, nil + } + allPlayersUnresolved := AllPlayersUnresolved{} + err = json.Unmarshal(playersData, &allPlayersUnresolved) + if err != nil { + return nil, fmt.Errorf("unmarshal players file: %v", err) + } + err = allPlayersUnresolved.Resolve(ssb.Session) + if err != nil { + return nil, fmt.Errorf("resolve players file: %v", err) + } + log.Printf("Got %d guilds from file", len(allPlayers)) + + return allPlayers[ssb.Interaction.GuildID], nil +} + +// setPlayers sets the players for the current guild. +func (ssb secretSantaBase) setPlayers(players []*player) (err error) { + if _, err = ssb.getPlayers(); err != nil { + return err + } + + allPlayers[ssb.Interaction.GuildID] = players + playersData, err := json.Marshal(allPlayers) + if err != nil { + return fmt.Errorf("marshal players file: %v", err) + } + err = os.WriteFile(viper.GetString("event.secretsanta.players"), playersData, 0644) + if err != nil { + return fmt.Errorf("write players file: %v", err) + } + return nil +} + +// player is a player in the secret santa game +type player struct { + *discordgo.Member + + // Match is the matched player + Match *player + // Address is the address of the player + Address string +} + +type playerUnresolved struct { + ID string `json:"id"` + MatchID string `json:"match"` + Address string `json:"address"` +} + +// AllPlayers is a map from guild ID to a list of players +type AllPlayers map[string][]*player + +// allPlayers is the current state of all players. +// See [AllPlayers] +var allPlayers AllPlayers + +// MarshalJSON implements json.Marshaler +func (allPlayers AllPlayers) MarshalJSON() ([]byte, error) { + m := make(AllPlayersUnresolved) + for guildID, players := range allPlayers { + for _, player := range players { + m[guildID] = append(m[guildID], &playerUnresolved{ + ID: player.User.ID, + MatchID: player.Match.User.ID, + Address: player.Address, + }) + } + + } + return json.Marshal(m) +} + +// AllPlayersUnresolved is a map from guild ID to a list of unresolved players. +// Unresolved players have no member but only an ID +type AllPlayersUnresolved map[string][]*playerUnresolved + +// Resolve resolves allPlayersUnresolved into allPlayers +func (allPlayersUnresolved AllPlayersUnresolved) Resolve(s *discordgo.Session) (err error) { + allPlayers = make(AllPlayers) + for guildID, unresolvedPlayers := range allPlayersUnresolved { + resolvedPlayers := map[string]*player{} + for _, up := range unresolvedPlayers { + member, err := s.GuildMember(guildID, up.ID) + if err != nil { + return fmt.Errorf("failed to get guild member %s/%s: %v", guildID, up.ID, err) + } + resolvedPlayers[up.ID] = &player{ + Member: member, + Match: resolvedPlayers[up.MatchID], + Address: up.Address, + } + } + for _, rp := range resolvedPlayers { + if rp.Match != nil { + continue + } + rp.Match = resolvedPlayers[rp.User.ID] + + allPlayers[guildID] = append(allPlayers[guildID], rp) + } + } + return nil +} + +// derangementMatch matches the players in a way that no one gets matched to themselves. +func derangementMatch(players []*player) []*player { + n := len(players) + players2 := make([]*player, n) + copy(players2, players) + + for i := 0; i < n-1; i++ { + j := i + rand.Intn(n-i-1) + 1 + players2[i], players2[j] = players2[j], players2[i] + } + + for i, item := range players { + item.Match = players2[i] + } + + return players +} From 42ec2842c675e2cfaad8393037f6b1b3a645afd4 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 16 Nov 2024 18:55:52 +0100 Subject: [PATCH 04/17] added secret santa invitation message --- config.yaml | 12 +++ data/lang/de.yaml | 8 ++ data/lang/en.yaml | 8 ++ event/component/componentBase.go | 2 + go.mod | 2 +- modules/secretsanta/component.go | 47 +++++++++++ modules/secretsanta/handleComponentInvite.go | 64 +++++++++++++++ modules/secretsanta/handleComponentSetup.go | 84 ++++++++++++++++++++ modules/secretsanta/handlerMessageSetup.go | 8 +- 9 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 modules/secretsanta/component.go create mode 100644 modules/secretsanta/handleComponentInvite.go create mode 100644 modules/secretsanta/handleComponentSetup.go diff --git a/config.yaml b/config.yaml index f0b7cb4..dd9c05e 100644 --- a/config.yaml +++ b/config.yaml @@ -70,6 +70,18 @@ event: #id: #animated: true secretsanta: vote.yes + secretsanta.invite.show_match: + name: 🎁 + #id: + #animated: true + secretsanta.invite.set_address: + name: 🏠 + #id: + #animated: true + secretsanta.invite.show_address: + name: 🏠 + #id: + #animated: true webserver: favicon: webserver/favicon.png diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 6569719..2f67ffb 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -132,6 +132,14 @@ discord.command: msg.setup.no_reactions: Diese Nachricht hat keine Reaktionen. Nur Leute, die mit %s reagiert haben, werden eingeschlossen. msg.setup.not_enough_reactions: Nicht genug Reaktionen um zu starten. Es werden mindestens %d Reaktionen benötigt. msg.setup.users: Teilnehmer + msg.setup.invite: Einladen + msg.setup.error: "%d Einladungen konnten nicht verschickt werden." + msg.setup.success: Einladungen wurden verschickt! + + msg.invite.title: Einladung zum Wichteln. + msg.invite.button.show_match: Partner anzeigen + msg.invite.button.set_address: Deine Adresse eintragen + msg.invite.button.show_address: Adresse deines Partners anzeigen module: adventcalendar: diff --git a/data/lang/en.yaml b/data/lang/en.yaml index 5d1eea2..edb4512 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -132,6 +132,14 @@ discord.command: msg.setup.no_reactions: This message doesn't have any vote reactions. Only members who reated with %s are included. msg.setup.not_enough_reactions: Not enough votes to start a game. At least %d votes are required. msg.setup.users: Members + msg.setup.invite: Invite + msg.setup.error: Failed to send %d invites. + msg.setup.success: Invites sent! + + msg.invite.title: Invite for secret santa. + msg.invite.button.show_match: Show match + msg.invite.button.set_address: Set your address + msg.invite.button.show_address: Show your partners address module: adventcalendar: diff --git a/event/component/componentBase.go b/event/component/componentBase.go index dd54eb2..f4d03ec 100644 --- a/event/component/componentBase.go +++ b/event/component/componentBase.go @@ -2,6 +2,7 @@ package component import ( "cake4everybot/modules/adventcalendar" + "cake4everybot/modules/secretsanta" "log" "github.com/bwmarrin/discordgo" @@ -31,6 +32,7 @@ func Register() { var componentList []Component componentList = append(componentList, adventcalendar.Component{}) + componentList = append(componentList, secretsanta.Component{}) if len(componentList) == 0 { return diff --git a/go.mod b/go.mod index 47c08fe..e29cd61 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/kesuaheli/twitchgo v0.2.7 github.com/spf13/viper v1.19.0 + golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f ) require ( @@ -27,7 +28,6 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.29.0 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/modules/secretsanta/component.go b/modules/secretsanta/component.go new file mode 100644 index 0000000..fa5b009 --- /dev/null +++ b/modules/secretsanta/component.go @@ -0,0 +1,47 @@ +package secretsanta + +import ( + "cake4everybot/util" + "strings" + + "github.com/bwmarrin/discordgo" +) + +// The Component of the secret santa package. +type Component struct { + secretSantaBase + 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 "setup": + c.handleSetup(s, ids) + return + case "invite": + c.handleInvite(s, ids) + 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 "secretsanta" +} diff --git a/modules/secretsanta/handleComponentInvite.go b/modules/secretsanta/handleComponentInvite.go new file mode 100644 index 0000000..a30e0d7 --- /dev/null +++ b/modules/secretsanta/handleComponentInvite.go @@ -0,0 +1,64 @@ +package secretsanta + +import ( + "cake4everybot/util" + + "github.com/bwmarrin/discordgo" +) + +func (c Component) handleInvite(s *discordgo.Session, ids []string) { + switch util.ShiftL(ids) { + case "show_match": + c.handleInviteShowMatch(s, ids) + return + case "set_address": + c.handleInviteSetAddress(s, ids) + return + case "show_address": + c.handleInviteShowAddress(s, ids) + return + default: + log.Printf("Unknown component interaction ID: %s", c.data.CustomID) + } +} + +func (c Component) handleInviteShowMatch(s *discordgo.Session, ids []string) { + guildID := util.ShiftL(ids) + _, err := c.getPlayers() + if err != nil { + log.Printf("ERROR: could not get players: %+v", err) + c.ReplyError() + return + } + players := allPlayers[guildID] + if len(players) == 0 { + log.Printf("ERROR: no players in guild %s", guildID) + c.ReplyError() + return + } + var player *player + for _, p := range players { + if p.User.ID == c.Interaction.User.ID { + player = p + } + } + + if player == nil { + log.Printf("ERROR: could not find player %s in guild %s: %+v", c.Interaction.User.ID, guildID, c.Interaction.User.ID) + c.ReplyError() + return + } + + e := util.AuthoredEmbed(s, player.Match.Member, tp+"display") + + util.SetEmbedFooter(s, tp+"display", e) + c.ReplyHiddenEmbed(e) +} + +func (c Component) handleInviteSetAddress(s *discordgo.Session, ids []string) { + +} + +func (c Component) handleInviteShowAddress(s *discordgo.Session, ids []string) { + +} diff --git a/modules/secretsanta/handleComponentSetup.go b/modules/secretsanta/handleComponentSetup.go new file mode 100644 index 0000000..8be1dff --- /dev/null +++ b/modules/secretsanta/handleComponentSetup.go @@ -0,0 +1,84 @@ +package secretsanta + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + "fmt" + + "github.com/bwmarrin/discordgo" +) + +func (c Component) handleSetup(s *discordgo.Session, ids []string) { + switch util.ShiftL(ids) { + case "invite": + c.handleSetupInvite(s) + return + default: + log.Printf("Unknown component interaction ID: %s", c.data.CustomID) + } +} + +func (c Component) handleSetupInvite(s *discordgo.Session) { + players, err := c.getPlayers() + if err != nil { + log.Printf("ERROR: could not get players: %+v", err) + c.ReplyError() + return + } + c.ReplyDeferedHidden() + + inviteMessage := &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{{ + Title: lang.GetDefault(tp + "msg.invite.title"), + Fields: []*discordgo.MessageEmbedField{}, + }}, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + util.CreateButtonComponent( + fmt.Sprintf("secretsanta.invite.show_match.%s", c.Interaction.GuildID), + lang.GetDefault(tp+"msg.invite.button.show_match"), + discordgo.PrimaryButton, + util.GetConfigComponentEmoji("secretsanta.invite.show_match"), + ), + util.CreateButtonComponent( + fmt.Sprintf("secretsanta.invite.set_address.%s", c.Interaction.GuildID), + lang.GetDefault(tp+"msg.invite.button.set_address"), + discordgo.SecondaryButton, + util.GetConfigComponentEmoji("secretsanta.invite.set_address"), + ), + util.CreateButtonComponent( + fmt.Sprintf("secretsanta.invite.show_address.%s", c.Interaction.GuildID), + lang.GetDefault(tp+"msg.invite.button.show_address"), + discordgo.SecondaryButton, + util.GetConfigComponentEmoji("secretsanta.invite.show_address"), + ), + }}, + }, + } + + var errCount int + for _, player := range players { + var DMChannel *discordgo.Channel + DMChannel, err = s.UserChannelCreate(player.User.ID) + if err != nil { + log.Printf("ERROR: could not create DM channel for user %s: %+v", player.User.ID, err) + errCount++ + continue + } + + _, err = s.ChannelMessageSendComplex(DMChannel.ID, inviteMessage) + if err != nil { + log.Printf("ERROR: could not send invite: %+v", err) + errCount++ + continue + } + log.Printf("Sent invite to user %s in channel %s", player.User.ID, DMChannel.ID) + } + + if errCount > 0 { + c.ReplyHiddenf("Failed to send %d invites!", errCount) + return + } + + c.ReplyHidden(lang.GetDefault(tp + "msg.setup.success")) +} diff --git a/modules/secretsanta/handlerMessageSetup.go b/modules/secretsanta/handlerMessageSetup.go index 16e8acf..64570fd 100644 --- a/modules/secretsanta/handlerMessageSetup.go +++ b/modules/secretsanta/handlerMessageSetup.go @@ -79,6 +79,12 @@ func (cmd MsgCmd) handler() { return } + components := []discordgo.MessageComponent{ + discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + util.CreateButtonComponent("secretsanta.setup.invite", "Invite", discordgo.SuccessButton, nil), + }}, + } + util.SetEmbedFooter(cmd.Session, tp+"display", e) - cmd.ReplyHiddenEmbed(e) + cmd.ReplyComponentsHiddenEmbed(components, e) } From 671085816a9f7fd2adcfa6ef7b33c9297fe08f08 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sun, 17 Nov 2024 18:13:59 +0100 Subject: [PATCH 05/17] refactored player storing for secret santa --- modules/secretsanta/component.go | 4 +- modules/secretsanta/handleComponentInvite.go | 37 ++++++-------- modules/secretsanta/handleComponentSetup.go | 10 ++-- modules/secretsanta/handlerMessageSetup.go | 4 +- modules/secretsanta/secretsantabase.go | 52 +++++++++----------- 5 files changed, 47 insertions(+), 60 deletions(-) diff --git a/modules/secretsanta/component.go b/modules/secretsanta/component.go index fa5b009..a15fcad 100644 --- a/modules/secretsanta/component.go +++ b/modules/secretsanta/component.go @@ -31,10 +31,10 @@ func (c Component) Handle(s *discordgo.Session, i *discordgo.InteractionCreate) switch util.ShiftL(ids) { case "setup": - c.handleSetup(s, ids) + c.handleSetup(ids) return case "invite": - c.handleInvite(s, ids) + c.handleInvite(ids) default: log.Printf("Unknown component interaction ID: %s", c.data.CustomID) } diff --git a/modules/secretsanta/handleComponentInvite.go b/modules/secretsanta/handleComponentInvite.go index a30e0d7..cfb8f3f 100644 --- a/modules/secretsanta/handleComponentInvite.go +++ b/modules/secretsanta/handleComponentInvite.go @@ -6,59 +6,52 @@ import ( "github.com/bwmarrin/discordgo" ) -func (c Component) handleInvite(s *discordgo.Session, ids []string) { +func (c Component) handleInvite(ids []string) { switch util.ShiftL(ids) { case "show_match": - c.handleInviteShowMatch(s, ids) + c.handleInviteShowMatch(ids) return case "set_address": - c.handleInviteSetAddress(s, ids) + c.handleInviteSetAddress(ids) return case "show_address": - c.handleInviteShowAddress(s, ids) + c.handleInviteShowAddress(ids) return default: log.Printf("Unknown component interaction ID: %s", c.data.CustomID) } } -func (c Component) handleInviteShowMatch(s *discordgo.Session, ids []string) { - guildID := util.ShiftL(ids) - _, err := c.getPlayers() +func (c Component) handleInviteShowMatch(ids []string) { + c.Interaction.GuildID = util.ShiftL(ids) + players, err := c.getPlayers() if err != nil { log.Printf("ERROR: could not get players: %+v", err) c.ReplyError() return } - players := allPlayers[guildID] if len(players) == 0 { - log.Printf("ERROR: no players in guild %s", guildID) + log.Printf("ERROR: no players in guild %s", c.Interaction.GuildID) c.ReplyError() return } - var player *player - for _, p := range players { - if p.User.ID == c.Interaction.User.ID { - player = p - } - } - - if player == nil { - log.Printf("ERROR: could not find player %s in guild %s: %+v", c.Interaction.User.ID, guildID, c.Interaction.User.ID) + player, ok := players[c.Interaction.User.ID] + if !ok { + log.Printf("ERROR: could not find player %s in guild %s: %+v", c.Interaction.User.ID, c.Interaction.GuildID, c.Interaction.User.ID) c.ReplyError() return } - e := util.AuthoredEmbed(s, player.Match.Member, tp+"display") + e := util.AuthoredEmbed(c.Session, player.Match.Member, tp+"display") - util.SetEmbedFooter(s, tp+"display", e) + util.SetEmbedFooter(c.Session, tp+"display", e) c.ReplyHiddenEmbed(e) } -func (c Component) handleInviteSetAddress(s *discordgo.Session, ids []string) { +func (c Component) handleInviteSetAddress(ids []string) { } -func (c Component) handleInviteShowAddress(s *discordgo.Session, ids []string) { +func (c Component) handleInviteShowAddress(ids []string) { } diff --git a/modules/secretsanta/handleComponentSetup.go b/modules/secretsanta/handleComponentSetup.go index 8be1dff..03f5bca 100644 --- a/modules/secretsanta/handleComponentSetup.go +++ b/modules/secretsanta/handleComponentSetup.go @@ -8,17 +8,17 @@ import ( "github.com/bwmarrin/discordgo" ) -func (c Component) handleSetup(s *discordgo.Session, ids []string) { +func (c Component) handleSetup(ids []string) { switch util.ShiftL(ids) { case "invite": - c.handleSetupInvite(s) + c.handleSetupInvite() return default: log.Printf("Unknown component interaction ID: %s", c.data.CustomID) } } -func (c Component) handleSetupInvite(s *discordgo.Session) { +func (c Component) handleSetupInvite() { players, err := c.getPlayers() if err != nil { log.Printf("ERROR: could not get players: %+v", err) @@ -59,14 +59,14 @@ func (c Component) handleSetupInvite(s *discordgo.Session) { var errCount int for _, player := range players { var DMChannel *discordgo.Channel - DMChannel, err = s.UserChannelCreate(player.User.ID) + DMChannel, err = c.Session.UserChannelCreate(player.User.ID) if err != nil { log.Printf("ERROR: could not create DM channel for user %s: %+v", player.User.ID, err) errCount++ continue } - _, err = s.ChannelMessageSendComplex(DMChannel.ID, inviteMessage) + _, err = c.Session.ChannelMessageSendComplex(DMChannel.ID, inviteMessage) if err != nil { log.Printf("ERROR: could not send invite: %+v", err) errCount++ diff --git a/modules/secretsanta/handlerMessageSetup.go b/modules/secretsanta/handlerMessageSetup.go index 64570fd..deca2e5 100644 --- a/modules/secretsanta/handlerMessageSetup.go +++ b/modules/secretsanta/handlerMessageSetup.go @@ -53,7 +53,7 @@ func (cmd MsgCmd) handler() { var ( names string - players []*player = make([]*player, 0, len(users)) + players = map[string]*player{} ) for _, u := range users { member, err := cmd.Session.GuildMember(cmd.Interaction.GuildID, u.ID) @@ -61,7 +61,7 @@ func (cmd MsgCmd) handler() { log.Printf("WARN: Could not get member '%s' from guild '%s': %v", u.ID, cmd.Interaction.GuildID, err) continue } - players = append(players, &player{Member: member}) + players[u.ID] = &player{Member: member} names += fmt.Sprintf("%s\n", member.Mention()) } if len(players) < 2 { diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index f9451a2..be0b672 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -28,7 +28,7 @@ type secretSantaBase struct { // getPlayers returns the list of players for the current guild. If it is the first time, it loads // the players from the file or creates an empty file. -func (ssb secretSantaBase) getPlayers() ([]*player, error) { +func (ssb secretSantaBase) getPlayers() (map[string]*player, error) { if allPlayers != nil { return allPlayers[ssb.Interaction.GuildID], nil } @@ -50,7 +50,7 @@ func (ssb secretSantaBase) getPlayers() ([]*player, error) { return nil, fmt.Errorf("write players file: %v", err) } log.Printf("Created players file: %s\n", playersPath) - return []*player{}, nil + return map[string]*player{}, nil } allPlayersUnresolved := AllPlayersUnresolved{} err = json.Unmarshal(playersData, &allPlayersUnresolved) @@ -67,7 +67,7 @@ func (ssb secretSantaBase) getPlayers() ([]*player, error) { } // setPlayers sets the players for the current guild. -func (ssb secretSantaBase) setPlayers(players []*player) (err error) { +func (ssb secretSantaBase) setPlayers(players map[string]*player) (err error) { if _, err = ssb.getPlayers(); err != nil { return err } @@ -95,13 +95,12 @@ type player struct { } type playerUnresolved struct { - ID string `json:"id"` MatchID string `json:"match"` Address string `json:"address"` } // AllPlayers is a map from guild ID to a list of players -type AllPlayers map[string][]*player +type AllPlayers map[string]map[string]*player // allPlayers is the current state of all players. // See [AllPlayers] @@ -111,63 +110,58 @@ var allPlayers AllPlayers func (allPlayers AllPlayers) MarshalJSON() ([]byte, error) { m := make(AllPlayersUnresolved) for guildID, players := range allPlayers { - for _, player := range players { - m[guildID] = append(m[guildID], &playerUnresolved{ - ID: player.User.ID, + for userID, player := range players { + m[guildID][userID] = &playerUnresolved{ MatchID: player.Match.User.ID, Address: player.Address, - }) + } } - } return json.Marshal(m) } // AllPlayersUnresolved is a map from guild ID to a list of unresolved players. // Unresolved players have no member but only an ID -type AllPlayersUnresolved map[string][]*playerUnresolved +type AllPlayersUnresolved map[string]map[string]*playerUnresolved // Resolve resolves allPlayersUnresolved into allPlayers func (allPlayersUnresolved AllPlayersUnresolved) Resolve(s *discordgo.Session) (err error) { allPlayers = make(AllPlayers) for guildID, unresolvedPlayers := range allPlayersUnresolved { - resolvedPlayers := map[string]*player{} - for _, up := range unresolvedPlayers { - member, err := s.GuildMember(guildID, up.ID) + allPlayers[guildID] = make(map[string]*player) + for userID, up := range unresolvedPlayers { + member, err := s.GuildMember(guildID, userID) if err != nil { - return fmt.Errorf("failed to get guild member %s/%s: %v", guildID, up.ID, err) + return fmt.Errorf("failed to get guild member %s/%s: %v", guildID, userID, err) } - resolvedPlayers[up.ID] = &player{ + allPlayers[guildID][userID] = &player{ Member: member, - Match: resolvedPlayers[up.MatchID], + Match: allPlayers[guildID][up.MatchID], Address: up.Address, } } - for _, rp := range resolvedPlayers { + for userID, rp := range allPlayers[guildID] { if rp.Match != nil { continue } - rp.Match = resolvedPlayers[rp.User.ID] - - allPlayers[guildID] = append(allPlayers[guildID], rp) + rp.Match = allPlayers[guildID][unresolvedPlayers[userID].MatchID] } } return nil } // derangementMatch matches the players in a way that no one gets matched to themselves. -func derangementMatch(players []*player) []*player { +func derangementMatch(players map[string]*player) map[string]*player { n := len(players) - players2 := make([]*player, n) - copy(players2, players) + playersSlice := make([]*player, 0, n) + for _, p := range players { + p.Match = p + playersSlice = append(playersSlice, p) + } for i := 0; i < n-1; i++ { j := i + rand.Intn(n-i-1) + 1 - players2[i], players2[j] = players2[j], players2[i] - } - - for i, item := range players { - item.Match = players2[i] + playersSlice[i].Match, playersSlice[j].Match = playersSlice[j].Match, playersSlice[i].Match } return players From a381177723b98feae6db54e174434decba8d081d Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sun, 17 Nov 2024 18:31:30 +0100 Subject: [PATCH 06/17] added address modal --- data/lang/de.yaml | 7 ++++ data/lang/en.yaml | 9 ++++- modules/secretsanta/handleComponentInvite.go | 35 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 2f67ffb..7696201 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -137,10 +137,17 @@ discord.command: msg.setup.success: Einladungen wurden verschickt! msg.invite.title: Einladung zum Wichteln. + msg.invite.description: Du nimmst am Cake4Everyone Wichteln teil. Klicke die Knöpfe unten, um deinen Partner zu sehen und deine Adresse einzutragen. + msg.invite.set_address.match: Dein Partner hat eine Adresse eingetragen + msg.invite.set_address: Deine Adresse wurde eingetragen msg.invite.button.show_match: Partner anzeigen msg.invite.button.set_address: Deine Adresse eintragen msg.invite.button.show_address: Adresse deines Partners anzeigen + msg.invite.modal.set_address.title: Deine Adresse eintragen + msg.invite.modal.set_address.label: Deine Adresse wird deinem Wichtel angezeigt + msg.invite.modal.set_address.placeholder: "Max Mustermann\nMusterstraße 1\n12345 Musterstadt" + module: adventcalendar: post.message: Noch %d Mal schlafen bis Heilig Abend! Heute öffnet sich das **Türchen %d**. diff --git a/data/lang/en.yaml b/data/lang/en.yaml index edb4512..15f7178 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -137,10 +137,17 @@ discord.command: msg.setup.success: Invites sent! msg.invite.title: Invite for secret santa. - msg.invite.button.show_match: Show match + msg.invite.description: You are participating in Cake4Everyone Secret Santa. Click the buttons below to see your partner and set your address. + msg.invite.set_address.match: Your partner has set an address + msg.invite.set_address: Your address is set + msg.invite.button.show_match: Show partner msg.invite.button.set_address: Set your address msg.invite.button.show_address: Show your partners address + msg.invite.modal.set_address.title: Set your address + msg.invite.modal.set_address.label: Your address will be shown your secret santa + msg.invite.modal.set_address.placeholder: "Mr. John Doe\n123 Main Street\nAnytown, USA 12345" + module: adventcalendar: post.message: Just sleep %d more times! Its time for **door %d**. diff --git a/modules/secretsanta/handleComponentInvite.go b/modules/secretsanta/handleComponentInvite.go index cfb8f3f..f4010b3 100644 --- a/modules/secretsanta/handleComponentInvite.go +++ b/modules/secretsanta/handleComponentInvite.go @@ -1,6 +1,7 @@ package secretsanta import ( + "cake4everybot/data/lang" "cake4everybot/util" "github.com/bwmarrin/discordgo" @@ -49,7 +50,41 @@ func (c Component) handleInviteShowMatch(ids []string) { } func (c Component) handleInviteSetAddress(ids []string) { + c.Interaction.GuildID = util.ShiftL(ids) + players, err := c.getPlayers() + if err != nil { + log.Printf("ERROR: could not get players: %+v", err) + c.ReplyError() + return + } + if len(players) == 0 { + log.Printf("ERROR: no players in guild %s", c.Interaction.GuildID) + c.ReplyError() + return + } + + var player *player + for _, p := range players { + if p.User.ID == c.Interaction.User.ID { + player = p + } + } + if player == nil { + log.Printf("ERROR: could not find player %s in guild %s: %+v", c.Interaction.User.ID, c.Interaction.GuildID, c.Interaction.User.ID) + c.ReplyError() + return + } + c.ReplyModal("secretsanta.invite.set_address_modal."+c.Interaction.GuildID, lang.GetDefault(tp+"msg.invite.modal.set_address.title"), discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + discordgo.TextInput{ + CustomID: "address", + Label: lang.GetDefault(tp + "msg.invite.modal.set_address.label"), + Style: discordgo.TextInputParagraph, + Placeholder: lang.GetDefault(tp + "msg.invite.modal.set_address.placeholder"), + Value: player.Address, + Required: true, + }, + }}) } func (c Component) handleInviteShowAddress(ids []string) { From d519f1042cf82748b9df7b5342b2773dfc9a09cf Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Mon, 18 Nov 2024 19:16:39 +0100 Subject: [PATCH 07/17] added modal handler --- event/commands.go | 7 ++++ event/event.go | 2 + event/modal/componentBase.go | 42 ++++++++++++++++++++ modules/secretsanta/component.go | 27 ++++++++++++- modules/secretsanta/handleComponentInvite.go | 2 +- 5 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 event/modal/componentBase.go diff --git a/event/commands.go b/event/commands.go index 78f199b..ccf97d2 100644 --- a/event/commands.go +++ b/event/commands.go @@ -17,6 +17,7 @@ package event import ( "cake4everybot/event/command" "cake4everybot/event/component" + "cake4everybot/event/modal" "strings" "github.com/bwmarrin/discordgo" @@ -42,5 +43,11 @@ func handleInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreat } else { log.Printf("got component interaction from unknown module '%s' (full id '%s')", strings.Split(data.CustomID, ".")[0], data.CustomID) } + + case discordgo.InteractionModalSubmit: + data := i.ModalSubmitData() + if m, ok := modal.ModalMap[strings.Split(data.CustomID, ".")[0]]; ok { + m.HandleModal(s, i) + } } } diff --git a/event/event.go b/event/event.go index 18a55c1..f12f2e9 100644 --- a/event/event.go +++ b/event/event.go @@ -17,6 +17,7 @@ package event import ( "cake4everybot/event/command" "cake4everybot/event/component" + "cake4everybot/event/modal" "cake4everybot/event/twitch" logger "log" @@ -33,6 +34,7 @@ func PostRegister(dc *discordgo.Session, t *twitchgo.Twitch, guildID string) err return err } component.Register() + modal.Register() twitch.Register(t) diff --git a/event/modal/componentBase.go b/event/modal/componentBase.go new file mode 100644 index 0000000..24ebf37 --- /dev/null +++ b/event/modal/componentBase.go @@ -0,0 +1,42 @@ +package modal + +import ( + "cake4everybot/modules/secretsanta" + "log" + + "github.com/bwmarrin/discordgo" +) + +// Modal is an interface wrapper for all message components. +type Modal interface { + // Function of a component. + // All things that should happen after submitting a modal. + HandleModal(*discordgo.Session, *discordgo.InteractionCreate) + + // Custom ID of the modal to identify the module + ID() string +} + +// ModalMap holds all active modals. It maps them from a unique string identifier to the +// corresponding [Modal]. +var ModalMap = make(map[string]Modal) + +// Register registers modals +func Register() { + // This is the list of modals to use. Add a modal via + // simply appending the struct (which must implement the + // [Modal] interface) to the list, e.g.: + // + // ModalList = append(ModalList, mymodule.MyComponent{}) + var ModalList []Modal + + ModalList = append(ModalList, secretsanta.Component{}) + + if len(ModalList) == 0 { + return + } + for _, c := range ModalList { + ModalMap[c.ID()] = c + } + log.Printf("Added %d modal handler(s)!", len(ModalMap)) +} diff --git a/modules/secretsanta/component.go b/modules/secretsanta/component.go index a15fcad..8757e73 100644 --- a/modules/secretsanta/component.go +++ b/modules/secretsanta/component.go @@ -10,7 +10,8 @@ import ( // The Component of the secret santa package. type Component struct { secretSantaBase - data discordgo.MessageComponentInteractionData + data discordgo.MessageComponentInteractionData + modal discordgo.ModalSubmitInteractionData } // Handle handles the functionality of a component. @@ -35,12 +36,36 @@ func (c Component) Handle(s *discordgo.Session, i *discordgo.InteractionCreate) return case "invite": c.handleInvite(ids) + return default: log.Printf("Unknown component interaction ID: %s", c.data.CustomID) } } +// HandleModal handles the functionality of a modal. +func (c Component) HandleModal(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} + } + //lint:ignore SA4005 assignment to c.modal is intentional + c.modal = i.ModalSubmitData() + + ids := strings.Split(c.modal.CustomID, ".") + // pop the first level identifier + util.ShiftL(ids) + + switch util.ShiftL(ids) { + default: + log.Printf("Unknown modal submit ID: %s", c.modal.CustomID) + } +} + // ID returns the custom ID of the modal to identify the module func (c Component) ID() string { return "secretsanta" diff --git a/modules/secretsanta/handleComponentInvite.go b/modules/secretsanta/handleComponentInvite.go index f4010b3..a99779c 100644 --- a/modules/secretsanta/handleComponentInvite.go +++ b/modules/secretsanta/handleComponentInvite.go @@ -75,7 +75,7 @@ func (c Component) handleInviteSetAddress(ids []string) { return } - c.ReplyModal("secretsanta.invite.set_address_modal."+c.Interaction.GuildID, lang.GetDefault(tp+"msg.invite.modal.set_address.title"), discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + c.ReplyModal("secretsanta.set_address."+c.Interaction.GuildID, lang.GetDefault(tp+"msg.invite.modal.set_address.title"), discordgo.ActionsRow{Components: []discordgo.MessageComponent{ discordgo.TextInput{ CustomID: "address", Label: lang.GetDefault(tp + "msg.invite.modal.set_address.label"), From 61a5a1a48b882354cffe0f096914846a9bdca634 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Mon, 18 Nov 2024 21:31:41 +0100 Subject: [PATCH 08/17] feat: added set address modal handler --- data/lang/de.yaml | 2 ++ data/lang/en.yaml | 2 ++ modules/secretsanta/component.go | 4 ++- modules/secretsanta/handleModal.go | 55 ++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 modules/secretsanta/handleModal.go diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 7696201..3ba4342 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -140,6 +140,8 @@ discord.command: msg.invite.description: Du nimmst am Cake4Everyone Wichteln teil. Klicke die Knöpfe unten, um deinen Partner zu sehen und deine Adresse einzutragen. msg.invite.set_address.match: Dein Partner hat eine Adresse eingetragen msg.invite.set_address: Deine Adresse wurde eingetragen + msg.invite.set_address.changed: Deine Adresse wurde aktualisiert auf + msg.invite.set_address.not_changed: Deine Adresse wurde nicht aktualisiert, weil es die gleiche ist wie vorher. msg.invite.button.show_match: Partner anzeigen msg.invite.button.set_address: Deine Adresse eintragen msg.invite.button.show_address: Adresse deines Partners anzeigen diff --git a/data/lang/en.yaml b/data/lang/en.yaml index 15f7178..8cadb1b 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -140,6 +140,8 @@ discord.command: msg.invite.description: You are participating in Cake4Everyone Secret Santa. Click the buttons below to see your partner and set your address. msg.invite.set_address.match: Your partner has set an address msg.invite.set_address: Your address is set + msg.invite.set_address.changed: Your address was updated to + msg.invite.set_address.not_changed: Your address was not updated, because it is the same as before. msg.invite.button.show_match: Show partner msg.invite.button.set_address: Set your address msg.invite.button.show_address: Show your partners address diff --git a/modules/secretsanta/component.go b/modules/secretsanta/component.go index 8757e73..8bfc761 100644 --- a/modules/secretsanta/component.go +++ b/modules/secretsanta/component.go @@ -53,7 +53,6 @@ func (c Component) HandleModal(s *discordgo.Session, i *discordgo.InteractionCre } else if i.User != nil { c.member = &discordgo.Member{User: i.User} } - //lint:ignore SA4005 assignment to c.modal is intentional c.modal = i.ModalSubmitData() ids := strings.Split(c.modal.CustomID, ".") @@ -61,6 +60,9 @@ func (c Component) HandleModal(s *discordgo.Session, i *discordgo.InteractionCre util.ShiftL(ids) switch util.ShiftL(ids) { + case "set_address": + c.handleModalSetAddress(ids) + return default: log.Printf("Unknown modal submit ID: %s", c.modal.CustomID) } diff --git a/modules/secretsanta/handleModal.go b/modules/secretsanta/handleModal.go new file mode 100644 index 0000000..827f79d --- /dev/null +++ b/modules/secretsanta/handleModal.go @@ -0,0 +1,55 @@ +package secretsanta + +import ( + "cake4everybot/data/lang" + "cake4everybot/util" + "fmt" + + "github.com/bwmarrin/discordgo" +) + +func (c Component) handleModalSetAddress(ids []string) { + c.Interaction.GuildID = util.ShiftL(ids) + players, err := c.getPlayers() + if err != nil { + log.Printf("ERROR: could not get players: %+v", err) + c.ReplyError() + return + } + if len(players) == 0 { + log.Printf("ERROR: no players in guild %s", c.Interaction.GuildID) + c.ReplyError() + return + } + player, ok := players[c.Interaction.User.ID] + if !ok { + log.Printf("ERROR: could not find player %s in guild %s: %+v", c.Interaction.User.ID, c.Interaction.GuildID, c.Interaction.User.ID) + c.ReplyError() + return + } + + addressFiled := c.modal.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput) + if addressFiled.Value == player.Address { + c.ReplyHidden(lang.GetDefault(tp + "msg.invite.set_address.not_changed")) + return + } + + player.Address = addressFiled.Value + err = c.setPlayers(players) + if err != nil { + log.Printf("ERROR: could not set players: %+v", err) + c.ReplyError() + return + } + + e := &discordgo.MessageEmbed{ + Color: 0x00FF00, + Fields: []*discordgo.MessageEmbedField{{ + Name: lang.GetDefault(tp + "msg.invite.set_address.changed"), + Value: fmt.Sprintf("```\n%s\n```", player.Address), + }}, + } + + util.SetEmbedFooter(c.Session, tp+"display", e) + c.ReplyHiddenEmbed(e) +} From c7808cce4d5d89c0332e4039c25713845e983317 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Mon, 18 Nov 2024 21:41:34 +0100 Subject: [PATCH 09/17] fix: entry to nil map --- modules/secretsanta/secretsantabase.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index be0b672..02ee0f2 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -110,6 +110,7 @@ var allPlayers AllPlayers func (allPlayers AllPlayers) MarshalJSON() ([]byte, error) { m := make(AllPlayersUnresolved) for guildID, players := range allPlayers { + m[guildID] = make(map[string]*playerUnresolved) for userID, player := range players { m[guildID][userID] = &playerUnresolved{ MatchID: player.Match.User.ID, From c43264eb5c8c8b9599660494a946f1f48d2bd6e5 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Mon, 18 Nov 2024 23:24:03 +0100 Subject: [PATCH 10/17] feat: added show address to show match --- config.yaml | 4 ---- data/lang/de.yaml | 9 ++++++--- data/lang/en.yaml | 9 ++++++--- modules/secretsanta/handleComponentInvite.go | 18 +++++++++++------- modules/secretsanta/handleComponentSetup.go | 6 ------ util/discord.go | 2 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/config.yaml b/config.yaml index dd9c05e..c18e76c 100644 --- a/config.yaml +++ b/config.yaml @@ -78,10 +78,6 @@ event: name: 🏠 #id: #animated: true - secretsanta.invite.show_address: - name: 🏠 - #id: - #animated: true webserver: favicon: webserver/favicon.png diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 3ba4342..d804ea1 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -139,12 +139,15 @@ discord.command: msg.invite.title: Einladung zum Wichteln. msg.invite.description: Du nimmst am Cake4Everyone Wichteln teil. Klicke die Knöpfe unten, um deinen Partner zu sehen und deine Adresse einzutragen. msg.invite.set_address.match: Dein Partner hat eine Adresse eingetragen + msg.invite.button.show_match: Partner anzeigen + msg.invite.show_match.title: Dein Partner ist %s + msg.invite.show_match.description: Bitte breite ein Wichtelgeschenk vor und schicke es ihm/ihr. Halte dich dabei an unsere vereinbarten Regeln. + msg.invite.show_match.address: Adresse + msg.invite.show_match.address_not_set: Dein Partner hat noch keine Adresse eingetragen msg.invite.set_address: Deine Adresse wurde eingetragen + msg.invite.button.set_address: Deine Adresse eintragen msg.invite.set_address.changed: Deine Adresse wurde aktualisiert auf msg.invite.set_address.not_changed: Deine Adresse wurde nicht aktualisiert, weil es die gleiche ist wie vorher. - msg.invite.button.show_match: Partner anzeigen - msg.invite.button.set_address: Deine Adresse eintragen - msg.invite.button.show_address: Adresse deines Partners anzeigen msg.invite.modal.set_address.title: Deine Adresse eintragen msg.invite.modal.set_address.label: Deine Adresse wird deinem Wichtel angezeigt diff --git a/data/lang/en.yaml b/data/lang/en.yaml index 8cadb1b..aa80312 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -139,12 +139,15 @@ discord.command: msg.invite.title: Invite for secret santa. msg.invite.description: You are participating in Cake4Everyone Secret Santa. Click the buttons below to see your partner and set your address. msg.invite.set_address.match: Your partner has set an address + msg.invite.button.show_match: Show partner + msg.invite.show_match.title: Your partner is %s + msg.invite.show_match.description: Please prepare and send them a secret gift according to our agreed rules. + msg.invite.show_match.address: Address + msg.invite.show_match.address_not_set: Your partner has not set an address yet msg.invite.set_address: Your address is set + msg.invite.button.set_address: Set your address msg.invite.set_address.changed: Your address was updated to msg.invite.set_address.not_changed: Your address was not updated, because it is the same as before. - msg.invite.button.show_match: Show partner - msg.invite.button.set_address: Set your address - msg.invite.button.show_address: Show your partners address msg.invite.modal.set_address.title: Set your address msg.invite.modal.set_address.label: Your address will be shown your secret santa diff --git a/modules/secretsanta/handleComponentInvite.go b/modules/secretsanta/handleComponentInvite.go index a99779c..3bc4bd4 100644 --- a/modules/secretsanta/handleComponentInvite.go +++ b/modules/secretsanta/handleComponentInvite.go @@ -3,6 +3,7 @@ package secretsanta import ( "cake4everybot/data/lang" "cake4everybot/util" + "fmt" "github.com/bwmarrin/discordgo" ) @@ -15,9 +16,6 @@ func (c Component) handleInvite(ids []string) { case "set_address": c.handleInviteSetAddress(ids) return - case "show_address": - c.handleInviteShowAddress(ids) - return default: log.Printf("Unknown component interaction ID: %s", c.data.CustomID) } @@ -44,6 +42,16 @@ func (c Component) handleInviteShowMatch(ids []string) { } e := util.AuthoredEmbed(c.Session, player.Match.Member, tp+"display") + e.Title = fmt.Sprintf(lang.GetDefault(tp+"msg.invite.show_match.title"), player.Match.Member.DisplayName()) + e.Description = lang.GetDefault(tp + "msg.invite.show_match.description") + e.Fields = append(e.Fields, &discordgo.MessageEmbedField{ + Name: lang.GetDefault(tp + "msg.invite.show_match.address"), + Value: fmt.Sprintf("```\n%s\n```", player.Match.Address), + }) + if player.Match.Address == "" { + log.Printf("%s has no address set: %+v", player.Match.Member.DisplayName(), player.Match) + e.Fields[0].Value = lang.GetDefault(tp + "msg.invite.show_match.address_not_set") + } util.SetEmbedFooter(c.Session, tp+"display", e) c.ReplyHiddenEmbed(e) @@ -86,7 +94,3 @@ func (c Component) handleInviteSetAddress(ids []string) { }, }}) } - -func (c Component) handleInviteShowAddress(ids []string) { - -} diff --git a/modules/secretsanta/handleComponentSetup.go b/modules/secretsanta/handleComponentSetup.go index 03f5bca..49f308f 100644 --- a/modules/secretsanta/handleComponentSetup.go +++ b/modules/secretsanta/handleComponentSetup.go @@ -46,12 +46,6 @@ func (c Component) handleSetupInvite() { discordgo.SecondaryButton, util.GetConfigComponentEmoji("secretsanta.invite.set_address"), ), - util.CreateButtonComponent( - fmt.Sprintf("secretsanta.invite.show_address.%s", c.Interaction.GuildID), - lang.GetDefault(tp+"msg.invite.button.show_address"), - discordgo.SecondaryButton, - util.GetConfigComponentEmoji("secretsanta.invite.show_address"), - ), }}, }, } diff --git a/util/discord.go b/util/discord.go index 016d7a5..3fce1f3 100644 --- a/util/discord.go +++ b/util/discord.go @@ -48,7 +48,7 @@ func AuthoredEmbed[T *discordgo.User | *discordgo.Member](s *discordgo.Session, panic("Given generic type is not an discord user or member") } user = member.User - username = member.Nick + username = member.DisplayName() } if username == "" { From 6010a172375f79f72a868e565bec87059f98dcda Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Fri, 22 Nov 2024 19:43:25 +0100 Subject: [PATCH 11/17] fix: base translation key for secret santa --- data/lang/de.yaml | 2 +- data/lang/en.yaml | 2 +- modules/secretsanta/messageCommandSetup.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/lang/de.yaml b/data/lang/de.yaml index d804ea1..15a7cef 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -124,7 +124,7 @@ discord.command: msg.winner.congratulation: "Herzlichen Glückwunsch, %s! :heart:\nFrohe Weihnachten an alle!" secretsanta: - base: wichteln + base: Wichteln display: Wichteln title: Wichteln diff --git a/data/lang/en.yaml b/data/lang/en.yaml index aa80312..0d9e5c9 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -124,7 +124,7 @@ discord.command: msg.winner.congratulation: "Congratulations, %s! :heart:\nMerry XMas everyone!" secretsanta: - setup: Secret Santa + base: Secret Santa display: Secret Santa title: Secret Santa diff --git a/modules/secretsanta/messageCommandSetup.go b/modules/secretsanta/messageCommandSetup.go index 3f569aa..c9f7afd 100644 --- a/modules/secretsanta/messageCommandSetup.go +++ b/modules/secretsanta/messageCommandSetup.go @@ -20,8 +20,8 @@ type MsgCmd struct { func (cmd *MsgCmd) AppCmd() *discordgo.ApplicationCommand { return &discordgo.ApplicationCommand{ Type: discordgo.MessageApplicationCommand, - Name: lang.GetDefault(tp + "setup"), - NameLocalizations: util.TranslateLocalization(tp + "setup"), + Name: lang.GetDefault(tp + "base"), + NameLocalizations: util.TranslateLocalization(tp + "base"), } } From e41ab233ca81c66dc3f3412aa17de0723fe283f9 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Fri, 22 Nov 2024 23:05:23 +0100 Subject: [PATCH 12/17] added secret santa feedback messages --- config.yaml | 4 ++ data/lang/de.yaml | 2 + data/lang/en.yaml | 2 + modules/secretsanta/handleComponentInvite.go | 8 ++- modules/secretsanta/handleComponentSetup.go | 28 +++++--- modules/secretsanta/handleModal.go | 38 +++++++++++ modules/secretsanta/handlerMessageSetup.go | 2 - modules/secretsanta/secretsantabase.go | 67 ++++++++++++++++++-- 8 files changed, 131 insertions(+), 20 deletions(-) diff --git a/config.yaml b/config.yaml index c18e76c..a387eac 100644 --- a/config.yaml +++ b/config.yaml @@ -78,6 +78,10 @@ event: name: 🏠 #id: #animated: true + secretsanta.invite.delete: + name: 🗑️ + #id: + #animated: true webserver: favicon: webserver/favicon.png diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 15a7cef..6f48b83 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -148,6 +148,8 @@ discord.command: msg.invite.button.set_address: Deine Adresse eintragen msg.invite.set_address.changed: Deine Adresse wurde aktualisiert auf msg.invite.set_address.not_changed: Deine Adresse wurde nicht aktualisiert, weil es die gleiche ist wie vorher. + msg.invite.set_address.match_updated: Dein Partner hat gerade seine/ihre Adresse aktualisiert. Das ist nur um dich zu informieren - Boop Boop 🤖 + msg.invite.button.delete: Schließen msg.invite.modal.set_address.title: Deine Adresse eintragen msg.invite.modal.set_address.label: Deine Adresse wird deinem Wichtel angezeigt diff --git a/data/lang/en.yaml b/data/lang/en.yaml index 0d9e5c9..07adc80 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -148,6 +148,8 @@ discord.command: msg.invite.button.set_address: Set your address msg.invite.set_address.changed: Your address was updated to msg.invite.set_address.not_changed: Your address was not updated, because it is the same as before. + msg.invite.set_address.match_updated: Your partner just updated their address. This is just to inform you - Beep Boop 🤖 + msg.invite.button.delete: Close msg.invite.modal.set_address.title: Set your address msg.invite.modal.set_address.label: Your address will be shown your secret santa diff --git a/modules/secretsanta/handleComponentInvite.go b/modules/secretsanta/handleComponentInvite.go index 3bc4bd4..3d5d90f 100644 --- a/modules/secretsanta/handleComponentInvite.go +++ b/modules/secretsanta/handleComponentInvite.go @@ -16,6 +16,13 @@ func (c Component) handleInvite(ids []string) { case "set_address": c.handleInviteSetAddress(ids) return + case "delete": + err := c.Session.ChannelMessageDelete(c.Interaction.ChannelID, c.Interaction.Message.ID) + if err != nil { + log.Printf("ERROR: could not delete message %s/%s: %+v", c.Interaction.ChannelID, c.Interaction.Message.ID, err) + c.ReplyError() + } + return default: log.Printf("Unknown component interaction ID: %s", c.data.CustomID) } @@ -49,7 +56,6 @@ func (c Component) handleInviteShowMatch(ids []string) { Value: fmt.Sprintf("```\n%s\n```", player.Match.Address), }) if player.Match.Address == "" { - log.Printf("%s has no address set: %+v", player.Match.Member.DisplayName(), player.Match) e.Fields[0].Value = lang.GetDefault(tp + "msg.invite.show_match.address_not_set") } diff --git a/modules/secretsanta/handleComponentSetup.go b/modules/secretsanta/handleComponentSetup.go index 49f308f..b2d8b79 100644 --- a/modules/secretsanta/handleComponentSetup.go +++ b/modules/secretsanta/handleComponentSetup.go @@ -26,12 +26,10 @@ func (c Component) handleSetupInvite() { return } c.ReplyDeferedHidden() + players = derangementMatch(players) inviteMessage := &discordgo.MessageSend{ - Embeds: []*discordgo.MessageEmbed{{ - Title: lang.GetDefault(tp + "msg.invite.title"), - Fields: []*discordgo.MessageEmbedField{}, - }}, + Embeds: make([]*discordgo.MessageEmbed, 1), Components: []discordgo.MessageComponent{ discordgo.ActionsRow{Components: []discordgo.MessageComponent{ util.CreateButtonComponent( @@ -50,27 +48,37 @@ func (c Component) handleSetupInvite() { }, } - var errCount int + var failedToSend string for _, player := range players { var DMChannel *discordgo.Channel DMChannel, err = c.Session.UserChannelCreate(player.User.ID) if err != nil { log.Printf("ERROR: could not create DM channel for user %s: %+v", player.User.ID, err) - errCount++ + failedToSend += "\n- " + player.Mention() continue } - _, err = c.Session.ChannelMessageSendComplex(DMChannel.ID, inviteMessage) + inviteMessage.Embeds[0] = player.InviteEmbed(c.Session) + var msg *discordgo.Message + msg, err = c.Session.ChannelMessageSendComplex(DMChannel.ID, inviteMessage) if err != nil { log.Printf("ERROR: could not send invite: %+v", err) - errCount++ + failedToSend += "\n- " + player.Mention() continue } + player.MessageID = msg.ID log.Printf("Sent invite to user %s in channel %s", player.User.ID, DMChannel.ID) } - if errCount > 0 { - c.ReplyHiddenf("Failed to send %d invites!", errCount) + if failedToSend != "" { + c.ReplyHiddenf("Failed to send invites to:%s", failedToSend) + return + } + + err = c.setPlayers(players) + if err != nil { + log.Printf("ERROR: could not save players to file: %+v", err) + c.ReplyError() return } diff --git a/modules/secretsanta/handleModal.go b/modules/secretsanta/handleModal.go index 827f79d..8c18147 100644 --- a/modules/secretsanta/handleModal.go +++ b/modules/secretsanta/handleModal.go @@ -42,6 +42,44 @@ func (c Component) handleModalSetAddress(ids []string) { return } + _, err = c.Session.ChannelMessageEditEmbed(c.Interaction.ChannelID, player.MessageID, player.InviteEmbed(c.Session)) + if err != nil { + log.Printf("ERROR: could not update bot message for %s '%s/%s': %+v", player.DisplayName(), c.Interaction.ChannelID, player.MessageID, err) + c.ReplyError() + return + } + + santaPlayer := c.getSantaForPlayer(player.User.ID) + santaChannel, err := c.Session.UserChannelCreate(santaPlayer.User.ID) + if err != nil { + log.Printf("ERROR: could not get user channel for %s: %+v", santaPlayer.DisplayName(), err) + c.ReplyError() + return + } + _, err = c.Session.ChannelMessageEditEmbed(santaChannel.ID, santaPlayer.MessageID, santaPlayer.InviteEmbed(c.Session)) + if err != nil { + log.Printf("ERROR: could not update bot message for %s '%s/%s': %+v", santaPlayer.DisplayName(), santaChannel.ID, santaPlayer.MessageID, err) + c.ReplyError() + return + } + _, err = c.Session.ChannelMessageSendComplex(santaChannel.ID, &discordgo.MessageSend{ + Content: lang.GetDefault(tp + "msg.invite.set_address.match_updated"), + Reference: &discordgo.MessageReference{MessageID: santaPlayer.MessageID}, + Components: []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + util.CreateButtonComponent( + "secretsanta.invite.delete", + lang.GetDefault(tp+"msg.invite.button.delete"), + discordgo.DangerButton, + util.GetConfigComponentEmoji("secretsanta.invite.delete"), + ), + }}}, + }) + if err != nil { + log.Printf("ERROR: could not send address update message for %s '%s/%s': %+v", santaPlayer.DisplayName(), santaChannel.ID, santaPlayer.MessageID, err) + c.ReplyError() + return + } + e := &discordgo.MessageEmbed{ Color: 0x00FF00, Fields: []*discordgo.MessageEmbedField{{ diff --git a/modules/secretsanta/handlerMessageSetup.go b/modules/secretsanta/handlerMessageSetup.go index deca2e5..4778995 100644 --- a/modules/secretsanta/handlerMessageSetup.go +++ b/modules/secretsanta/handlerMessageSetup.go @@ -70,8 +70,6 @@ func (cmd MsgCmd) handler() { } util.AddEmbedField(e, lang.GetDefault(tp+"msg.setup.users"), names, false) - players = derangementMatch(players) - err = cmd.setPlayers(players) if err != nil { log.Printf("Error on set players: %v\n", err) diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index 02ee0f2..badcaf8 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -1,6 +1,7 @@ package secretsanta import ( + "cake4everybot/data/lang" "cake4everybot/util" "encoding/json" "fmt" @@ -66,6 +67,19 @@ func (ssb secretSantaBase) getPlayers() (map[string]*player, error) { return allPlayers[ssb.Interaction.GuildID], nil } +// getSantaForPlayer returns the santa player of the given player for the current guild i.e. the +// player whose match is the given player. +// +// It panics if the player doesn't exist. +func (ssb secretSantaBase) getSantaForPlayer(playerID string) *player { + for _, player := range allPlayers[ssb.Interaction.GuildID] { + if player.Match.User.ID == playerID { + return player + } + } + panic(fmt.Sprintf("tried to get santa for player that doesn't exist: '%s' in guild '%s'", playerID, ssb.Interaction.GuildID)) +} + // setPlayers sets the players for the current guild. func (ssb secretSantaBase) setPlayers(players map[string]*player) (err error) { if _, err = ssb.getPlayers(); err != nil { @@ -92,11 +106,44 @@ type player struct { Match *player // Address is the address of the player Address string + // MessageID is the message the bot sent to the player + MessageID string +} + +// InviteEmbed returns an embed for the player to be sent by the bot. +func (player *player) InviteEmbed(s *discordgo.Session) (e *discordgo.MessageEmbed) { + var matchValue, addressValue = "❌", "❌" + if player != nil && player.Match.Address != "" { + matchValue = "✅" + } + if player != nil && player.Address != "" { + addressValue = "✅" + } + + e = &discordgo.MessageEmbed{ + Title: lang.GetDefault(tp + "msg.invite.title"), + Description: lang.GetDefault(tp + "msg.invite.description"), + Fields: []*discordgo.MessageEmbedField{ + { + Name: lang.GetDefault(tp + "msg.invite.set_address.match"), + Value: matchValue, + Inline: true, + }, + { + Name: lang.GetDefault(tp + "msg.invite.set_address"), + Value: addressValue, + Inline: true, + }, + }, + } + util.SetEmbedFooter(s, tp+"display", e) + return e } type playerUnresolved struct { - MatchID string `json:"match"` - Address string `json:"address"` + MatchID string `json:"match"` + Address string `json:"address"` + MessageID string `json:"message"` } // AllPlayers is a map from guild ID to a list of players @@ -112,9 +159,14 @@ func (allPlayers AllPlayers) MarshalJSON() ([]byte, error) { for guildID, players := range allPlayers { m[guildID] = make(map[string]*playerUnresolved) for userID, player := range players { + var matchID string + if player.Match != nil { + matchID = player.Match.User.ID + } m[guildID][userID] = &playerUnresolved{ - MatchID: player.Match.User.ID, - Address: player.Address, + MatchID: matchID, + Address: player.Address, + MessageID: player.MessageID, } } } @@ -136,9 +188,10 @@ func (allPlayersUnresolved AllPlayersUnresolved) Resolve(s *discordgo.Session) ( return fmt.Errorf("failed to get guild member %s/%s: %v", guildID, userID, err) } allPlayers[guildID][userID] = &player{ - Member: member, - Match: allPlayers[guildID][up.MatchID], - Address: up.Address, + Member: member, + Match: allPlayers[guildID][up.MatchID], + Address: up.Address, + MessageID: up.MessageID, } } for userID, rp := range allPlayers[guildID] { From c97867630ccbf61defb6742cad3fbf727d043819 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 23 Nov 2024 01:24:31 +0100 Subject: [PATCH 13/17] fix: followup messages without plain text context --- util/interaction.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/util/interaction.go b/util/interaction.go index 3d5d3a8..389a693 100644 --- a/util/interaction.go +++ b/util/interaction.go @@ -42,9 +42,19 @@ func (i *InteractionUtil) respond() { } if i.acknowledged { - _, err := i.Session.FollowupMessageCreate(i.Interaction.Interaction, true, &discordgo.WebhookParams{ - Content: i.response.Data.Content, - }) + data := &discordgo.WebhookParams{ + AllowedMentions: i.response.Data.AllowedMentions, + Components: i.response.Data.Components, + Content: i.response.Data.Content, + Embeds: i.response.Data.Embeds, + Files: i.response.Data.Files, + Flags: i.response.Data.Flags, + TTS: i.response.Data.TTS, + } + if i.response.Data.Attachments != nil { + data.Attachments = *i.response.Data.Attachments + } + _, err := i.Session.FollowupMessageCreate(i.Interaction.Interaction, true, data) if err != nil { log.Printf("ERROR: could not send follow up message: %+v\n%s", err, debug.Stack()) } From 458d4210319315f21eb6b96e10948bf2c40c62ee Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 23 Nov 2024 01:25:33 +0100 Subject: [PATCH 14/17] feat: added blacklist for secret santa matches --- .gitignore | 1 + config.yaml | 3 +++ data/lang/de.yaml | 2 ++ data/lang/en.yaml | 2 ++ modules/secretsanta/handleComponentSetup.go | 12 ++++++--- modules/secretsanta/secretsantabase.go | 29 +++++++++++++++++++-- 6 files changed, 43 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 90253eb..8c74cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ cake4everybot *env.yaml # runtime config files (as described in config.yml) +modules/secretsanta/backlist.json modules/secretsanta/players.json twitch/prizes.json twitch/times.json diff --git a/config.yaml b/config.yaml index a387eac..20748be 100644 --- a/config.yaml +++ b/config.yaml @@ -37,6 +37,9 @@ event: secretsanta: # the filepath for the players players: modules/secretsanta/players.json + # the filepath for the blacklist + # the blacklist maps a user ID to a list of user IDs that should not be matched + blacklist: modules/secretsanta/backlist.json twitch_giveaway: # The amount of points a single giveaway ticket costs. diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 6f48b83..9304531 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -132,7 +132,9 @@ discord.command: msg.setup.no_reactions: Diese Nachricht hat keine Reaktionen. Nur Leute, die mit %s reagiert haben, werden eingeschlossen. msg.setup.not_enough_reactions: Nicht genug Reaktionen um zu starten. Es werden mindestens %d Reaktionen benötigt. msg.setup.users: Teilnehmer + msg.setup.match_error: Fehler beim Auslosen msg.setup.invite: Einladen + msg.setup.invite.error: "Einladung(en) konnte(n) nicht verschickt werden an:%s" msg.setup.error: "%d Einladungen konnten nicht verschickt werden." msg.setup.success: Einladungen wurden verschickt! diff --git a/data/lang/en.yaml b/data/lang/en.yaml index 07adc80..e2e4540 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -132,7 +132,9 @@ discord.command: msg.setup.no_reactions: This message doesn't have any vote reactions. Only members who reated with %s are included. msg.setup.not_enough_reactions: Not enough votes to start a game. At least %d votes are required. msg.setup.users: Members + msg.setup.match_error: Failed to match msg.setup.invite: Invite + msg.setup.invite.error: "Failed to send invite(s) to:%s" msg.setup.error: Failed to send %d invites. msg.setup.success: Invites sent! diff --git a/modules/secretsanta/handleComponentSetup.go b/modules/secretsanta/handleComponentSetup.go index b2d8b79..b037c81 100644 --- a/modules/secretsanta/handleComponentSetup.go +++ b/modules/secretsanta/handleComponentSetup.go @@ -26,7 +26,12 @@ func (c Component) handleSetupInvite() { return } c.ReplyDeferedHidden() - players = derangementMatch(players) + players, err = derangementMatch(players) + if err != nil { + log.Printf("ERROR: could not match players: %+v", err) + c.ReplySimpleEmbed(0xFF0000, lang.GetDefault(tp+"msg.setup.match_error")) + return + } inviteMessage := &discordgo.MessageSend{ Embeds: make([]*discordgo.MessageEmbed, 1), @@ -67,11 +72,10 @@ func (c Component) handleSetupInvite() { continue } player.MessageID = msg.ID - log.Printf("Sent invite to user %s in channel %s", player.User.ID, DMChannel.ID) } if failedToSend != "" { - c.ReplyHiddenf("Failed to send invites to:%s", failedToSend) + c.ReplyHiddenSimpleEmbedf(0xFF0000, lang.GetDefault(tp+"msg.setup.invite.error"), failedToSend) return } @@ -82,5 +86,5 @@ func (c Component) handleSetupInvite() { return } - c.ReplyHidden(lang.GetDefault(tp + "msg.setup.success")) + c.ReplyHiddenSimpleEmbed(0x690042, lang.GetDefault(tp+"msg.setup.success")) } diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index badcaf8..1687a3b 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -123,6 +123,7 @@ func (player *player) InviteEmbed(s *discordgo.Session) (e *discordgo.MessageEmb e = &discordgo.MessageEmbed{ Title: lang.GetDefault(tp + "msg.invite.title"), Description: lang.GetDefault(tp + "msg.invite.description"), + Color: 0x690042, Fields: []*discordgo.MessageEmbedField{ { Name: lang.GetDefault(tp + "msg.invite.set_address.match"), @@ -205,7 +206,7 @@ func (allPlayersUnresolved AllPlayersUnresolved) Resolve(s *discordgo.Session) ( } // derangementMatch matches the players in a way that no one gets matched to themselves. -func derangementMatch(players map[string]*player) map[string]*player { +func derangementMatch(players map[string]*player) (map[string]*player, error) { n := len(players) playersSlice := make([]*player, 0, n) for _, p := range players { @@ -218,5 +219,29 @@ func derangementMatch(players map[string]*player) map[string]*player { playersSlice[i].Match, playersSlice[j].Match = playersSlice[j].Match, playersSlice[i].Match } - return players + blacklistPath := viper.GetString("event.secretsanta.blacklist") + blacklistData, err := os.ReadFile(blacklistPath) + if err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("read blacklist file: %w", err) + } + err = os.WriteFile(blacklistPath, []byte("{}"), 0644) + if err != nil { + return nil, fmt.Errorf("write blacklist file: %w", err) + } + return players, nil + } + + blacklist := make(map[string][]string) + err = json.Unmarshal(blacklistData, &blacklist) + if err != nil { + return nil, fmt.Errorf("parse blacklist file: %w", err) + } + for id, blacklisted := range blacklist { + if player, ok := players[id]; ok && util.ContainsString(blacklisted, player.Match.User.ID) { + return nil, fmt.Errorf("%s has a blacklisted match", player.DisplayName()) + } + } + + return players, nil } From 1080b4b033987c10ae1aad857544526eff19f247 Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 23 Nov 2024 18:29:26 +0100 Subject: [PATCH 15/17] refactor: loading blacklist in own function Also made blacklist a global var --- modules/secretsanta/secretsantabase.go | 44 +++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index 1687a3b..18371b1 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -205,8 +205,32 @@ func (allPlayersUnresolved AllPlayersUnresolved) Resolve(s *discordgo.Session) ( return nil } +var blacklist map[string][]string + +// loadBlacklist loads the blacklist from the configured file path. +func loadBlacklist() (err error) { + blacklistPath := viper.GetString("event.secretsanta.blacklist") + blacklistData, err := os.ReadFile(blacklistPath) + if err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("read blacklist file: %w", err) + } + if err = os.WriteFile(blacklistPath, []byte("{}"), 0644); err != nil { + return fmt.Errorf("write blacklist file: %w", err) + } + return nil + } + + blacklist = make(map[string][]string) + if err = json.Unmarshal(blacklistData, &blacklist); err != nil { + return fmt.Errorf("unmarshal blacklist file: %w", err) + } + return nil +} + // derangementMatch matches the players in a way that no one gets matched to themselves. func derangementMatch(players map[string]*player) (map[string]*player, error) { + loadBlacklist() n := len(players) playersSlice := make([]*player, 0, n) for _, p := range players { @@ -219,27 +243,9 @@ func derangementMatch(players map[string]*player) (map[string]*player, error) { playersSlice[i].Match, playersSlice[j].Match = playersSlice[j].Match, playersSlice[i].Match } - blacklistPath := viper.GetString("event.secretsanta.blacklist") - blacklistData, err := os.ReadFile(blacklistPath) - if err != nil { - if !os.IsNotExist(err) { - return nil, fmt.Errorf("read blacklist file: %w", err) - } - err = os.WriteFile(blacklistPath, []byte("{}"), 0644) - if err != nil { - return nil, fmt.Errorf("write blacklist file: %w", err) - } - return players, nil - } - - blacklist := make(map[string][]string) - err = json.Unmarshal(blacklistData, &blacklist) - if err != nil { - return nil, fmt.Errorf("parse blacklist file: %w", err) - } for id, blacklisted := range blacklist { if player, ok := players[id]; ok && util.ContainsString(blacklisted, player.Match.User.ID) { - return nil, fmt.Errorf("%s has a blacklisted match", player.DisplayName()) + return nil, fmt.Errorf("'%s' has a blacklisted match: '%s'", player.DisplayName(), player.Match.DisplayName()) } } From 08418056d6c37711c7878f19243288a8b89ccd6c Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sat, 23 Nov 2024 18:44:01 +0100 Subject: [PATCH 16/17] fix(attempt): made it more unlikely to get an blacklisted match --- modules/secretsanta/secretsantabase.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index 18371b1..f13ed48 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -240,6 +240,13 @@ func derangementMatch(players map[string]*player) (map[string]*player, error) { for i := 0; i < n-1; i++ { j := i + rand.Intn(n-i-1) + 1 + if !util.ContainsString(blacklist[playersSlice[i].User.ID], playersSlice[j].Match.User.ID) { + log.Printf("%s would match blacklisted %s. Attempt to generate new match.", playersSlice[i].DisplayName(), playersSlice[j].Match.DisplayName()) + // HACK: very simple attempt to avoid blacklisted matches + // just in case a match is blacklisted, generate a new match. If it is still + // blacklisted, then it will be caught by the blacklist check below. + j = i + rand.Intn(n-i-1) + 1 + } playersSlice[i].Match, playersSlice[j].Match = playersSlice[j].Match, playersSlice[i].Match } From 9358940f150d8f668e9c8f709c033b51f5be7d1f Mon Sep 17 00:00:00 2001 From: Kesuaheli Date: Sun, 24 Nov 2024 00:00:07 +0100 Subject: [PATCH 17/17] feat(secret santa): added nudging match --- config.yaml | 4 + data/lang/de.yaml | 12 ++- data/lang/en.yaml | 12 ++- modules/secretsanta/handleComponentInvite.go | 106 +++++++++++++++++-- modules/secretsanta/handleModal.go | 1 + modules/secretsanta/secretsantabase.go | 15 ++- util/interaction.go | 76 +++++++++++++ 7 files changed, 213 insertions(+), 13 deletions(-) diff --git a/config.yaml b/config.yaml index 20748be..06107ea 100644 --- a/config.yaml +++ b/config.yaml @@ -85,6 +85,10 @@ event: name: 🗑️ #id: #animated: true + secretsanta.invite.nudge_match: + name: 👉 + #id: + #animated: true webserver: favicon: webserver/favicon.png diff --git a/data/lang/de.yaml b/data/lang/de.yaml index 9304531..5733840 100644 --- a/data/lang/de.yaml +++ b/data/lang/de.yaml @@ -145,13 +145,23 @@ discord.command: msg.invite.show_match.title: Dein Partner ist %s msg.invite.show_match.description: Bitte breite ein Wichtelgeschenk vor und schicke es ihm/ihr. Halte dich dabei an unsere vereinbarten Regeln. msg.invite.show_match.address: Adresse - msg.invite.show_match.address_not_set: Dein Partner hat noch keine Adresse eingetragen + msg.invite.show_match.address_not_set: Dein Partner hat noch keine Adresse eingetragen. + + Falls schon einige Tage vergangen sind, kannst du den "Anstupsen" Knopf drücken. Dein Partner bekommt daraufhin einen Hinweis die Adresse einzutragen. + msg.invite.show_match.nudge_description: Wenn du vermutest, dass die eingetragene Adresse einen Fehler enthält, kannst du unten den den "Anstupsen" Knopf drücken. Dein Partner bekommt daraufhin einen Hinweis die Adresse zu aktualisieren. msg.invite.set_address: Deine Adresse wurde eingetragen msg.invite.button.set_address: Deine Adresse eintragen msg.invite.set_address.changed: Deine Adresse wurde aktualisiert auf msg.invite.set_address.not_changed: Deine Adresse wurde nicht aktualisiert, weil es die gleiche ist wie vorher. msg.invite.set_address.match_updated: Dein Partner hat gerade seine/ihre Adresse aktualisiert. Das ist nur um dich zu informieren - Boop Boop 🤖 msg.invite.button.delete: Schließen + msg.invite.button.nudge_match: Anstupsen + msg.invite.nudge_match.confirm: Bist du sicher, dass du deinen Partner anstupsen möchtest? + + Bestätige, dass du deinen Partner anstupsen möchtest. Er/Sie bekommt dann einen Hinweis die Adresse zu aktualisieren. + msg.invite.nudge_match.success: Dein Partner wurde erfolgreich angestupst + msg.invite.nudge_match.pending: Du hast deinen Partner angestupst. Ich melde mich, sobald er/sie die Adresse aktualisiert hat. + msg.invite.nudge_received: Dein Wichtel hat dich anstupst. Bitte trage eine Adresse ein falls du es noch nicht gemacht hast oder überprüfe deine bereits eingetragene Adresse ob sie eventuell einen Tippfehler oder ähnliches enthält. msg.invite.modal.set_address.title: Deine Adresse eintragen msg.invite.modal.set_address.label: Deine Adresse wird deinem Wichtel angezeigt diff --git a/data/lang/en.yaml b/data/lang/en.yaml index e2e4540..3e0e369 100644 --- a/data/lang/en.yaml +++ b/data/lang/en.yaml @@ -145,13 +145,23 @@ discord.command: msg.invite.show_match.title: Your partner is %s msg.invite.show_match.description: Please prepare and send them a secret gift according to our agreed rules. msg.invite.show_match.address: Address - msg.invite.show_match.address_not_set: Your partner has not set an address yet + msg.invite.show_match.address_not_set: Your partner has not set an address yet. + + If some days passed, you can click the "Nudge" button below. Your partner will receive a notification saying that they enter their address. + msg.invite.show_match.nudge_description: If you think the entered address is wrong, you can click the "Nudge" button below. Your partner will receive a notification saying that they should update their address. msg.invite.set_address: Your address is set msg.invite.button.set_address: Set your address msg.invite.set_address.changed: Your address was updated to msg.invite.set_address.not_changed: Your address was not updated, because it is the same as before. msg.invite.set_address.match_updated: Your partner just updated their address. This is just to inform you - Beep Boop 🤖 msg.invite.button.delete: Close + msg.invite.button.nudge_match: Nudge + msg.invite.nudge_match.confirm: Are you sure you want to nudge your partner? + + Please confirm that you want to nudge your partner. They will receive a notification that they should update their address. + msg.invite.nudge_match.success: Your partner was nudged! + msg.invite.nudge_match.pending: You nudged your partner. I will notify you, when they updated their address. + msg.invite.nudge_received: Your santa nudged you. Please enter an address if you haven't done it yet or check your already entered address for any typos. msg.invite.modal.set_address.title: Set your address msg.invite.modal.set_address.label: Your address will be shown your secret santa diff --git a/modules/secretsanta/handleComponentInvite.go b/modules/secretsanta/handleComponentInvite.go index 3d5d90f..a182105 100644 --- a/modules/secretsanta/handleComponentInvite.go +++ b/modules/secretsanta/handleComponentInvite.go @@ -4,6 +4,7 @@ import ( "cake4everybot/data/lang" "cake4everybot/util" "fmt" + "strings" "github.com/bwmarrin/discordgo" ) @@ -16,6 +17,11 @@ func (c Component) handleInvite(ids []string) { case "set_address": c.handleInviteSetAddress(ids) return + case "nudge_match": + c.handleInviteNudgeMatch(ids) + case "confirm_nudge": + c.handleInviteConfirmNudge(ids) + return case "delete": err := c.Session.ChannelMessageDelete(c.Interaction.ChannelID, c.Interaction.Message.ID) if err != nil { @@ -51,16 +57,26 @@ func (c Component) handleInviteShowMatch(ids []string) { e := util.AuthoredEmbed(c.Session, player.Match.Member, tp+"display") e.Title = fmt.Sprintf(lang.GetDefault(tp+"msg.invite.show_match.title"), player.Match.Member.DisplayName()) e.Description = lang.GetDefault(tp + "msg.invite.show_match.description") + e.Color = 0x690042 e.Fields = append(e.Fields, &discordgo.MessageEmbedField{ Name: lang.GetDefault(tp + "msg.invite.show_match.address"), - Value: fmt.Sprintf("```\n%s\n```", player.Match.Address), + Value: fmt.Sprintf("```\n%s\n```\n%s", player.Match.Address, lang.GetDefault(tp+"msg.invite.show_match.nudge_description")), }) if player.Match.Address == "" { e.Fields[0].Value = lang.GetDefault(tp + "msg.invite.show_match.address_not_set") } util.SetEmbedFooter(c.Session, tp+"display", e) - c.ReplyHiddenEmbed(e) + c.ReplyComponentsHiddenEmbed( + []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + util.CreateButtonComponent( + fmt.Sprintf("secretsanta.invite.nudge_match.%s", c.Interaction.GuildID), + lang.GetDefault(tp+"msg.invite.button.nudge_match"), + discordgo.SecondaryButton, + util.GetConfigComponentEmoji("secretsanta.invite.nudge_match"), + ), + }}}, + e) } func (c Component) handleInviteSetAddress(ids []string) { @@ -77,13 +93,8 @@ func (c Component) handleInviteSetAddress(ids []string) { return } - var player *player - for _, p := range players { - if p.User.ID == c.Interaction.User.ID { - player = p - } - } - if player == nil { + player, ok := players[c.Interaction.User.ID] + if !ok { log.Printf("ERROR: could not find player %s in guild %s: %+v", c.Interaction.User.ID, c.Interaction.GuildID, c.Interaction.User.ID) c.ReplyError() return @@ -100,3 +111,80 @@ func (c Component) handleInviteSetAddress(ids []string) { }, }}) } + +func (c Component) handleInviteNudgeMatch(ids []string) { + c.ReplyComponentsHiddenSimpleEmbedUpdate( + []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + util.CreateButtonComponent( + "secretsanta.invite.confirm_nudge."+strings.Join(ids, "."), + lang.GetDefault(tp+"msg.invite.button.nudge_match"), + discordgo.PrimaryButton, + util.GetConfigComponentEmoji("secretsanta.invite.nudge_match"), + ), + }}}, + 0x690042, + lang.GetDefault(tp+"msg.invite.nudge_match.confirm")) +} + +func (c Component) handleInviteConfirmNudge(ids []string) { + c.Interaction.GuildID = util.ShiftL(ids) + players, err := c.getPlayers() + if err != nil { + log.Printf("ERROR: could not get players: %+v", err) + c.ReplyError() + return + } + if len(players) == 0 { + log.Printf("ERROR: no players in guild %s", c.Interaction.GuildID) + c.ReplyError() + return + } + + player, ok := players[c.Interaction.User.ID] + if !ok { + log.Printf("ERROR: could not find player %s in guild %s: %+v", c.Interaction.User.ID, c.Interaction.GuildID, c.Interaction.User.ID) + c.ReplyError() + return + } + player.Match.PendingNudge = true + + matchChannel, err := c.Session.UserChannelCreate(player.Match.User.ID) + if err != nil { + log.Printf("ERROR: could not create DM channel with user %s: %+v", player.Match.User.ID, err) + c.ReplyError() + return + } + _, err = c.Session.ChannelMessageEditEmbed(matchChannel.ID, player.Match.MessageID, player.Match.InviteEmbed(c.Session)) + if err != nil { + log.Printf("ERROR: could not edit match message embed: %+v", err) + c.ReplyError() + return + } + + data := &discordgo.MessageSend{ + Content: lang.GetDefault(tp + "msg.invite.nudge_received"), + Reference: &discordgo.MessageReference{MessageID: player.Match.MessageID}, + Components: []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{ + util.CreateButtonComponent( + "secretsanta.invite.delete", + lang.GetDefault(tp+"msg.invite.button.delete"), + discordgo.DangerButton, + util.GetConfigComponentEmoji("secretsanta.invite.delete"), + ), + }}}, + } + _, err = c.Session.ChannelMessageSendComplex(matchChannel.ID, data) + if err != nil { + log.Printf("ERROR: could not send nudge message: %+v", err) + c.ReplyError() + return + } + + _, err = c.Session.ChannelMessageEditEmbed(c.Interaction.ChannelID, player.MessageID, player.InviteEmbed(c.Session)) + if err != nil { + log.Printf("ERROR: could not edit invite message embed: %+v", err) + c.ReplyError() + return + } + c.ReplyHiddenSimpleEmbedUpdate(0x690042, lang.GetDefault(tp+"msg.invite.nudge_match.success")) +} diff --git a/modules/secretsanta/handleModal.go b/modules/secretsanta/handleModal.go index 8c18147..be4c7dd 100644 --- a/modules/secretsanta/handleModal.go +++ b/modules/secretsanta/handleModal.go @@ -35,6 +35,7 @@ func (c Component) handleModalSetAddress(ids []string) { } player.Address = addressFiled.Value + player.PendingNudge = false err = c.setPlayers(players) if err != nil { log.Printf("ERROR: could not set players: %+v", err) diff --git a/modules/secretsanta/secretsantabase.go b/modules/secretsanta/secretsantabase.go index f13ed48..5f1a25c 100644 --- a/modules/secretsanta/secretsantabase.go +++ b/modules/secretsanta/secretsantabase.go @@ -108,16 +108,27 @@ type player struct { Address string // MessageID is the message the bot sent to the player MessageID string + // PendingNudge is true if the player has received a nugde from their santa and they haven't changed their + // address yet i.e. the nudge is still pending. + PendingNudge bool } // InviteEmbed returns an embed for the player to be sent by the bot. func (player *player) InviteEmbed(s *discordgo.Session) (e *discordgo.MessageEmbed) { var matchValue, addressValue = "❌", "❌" if player != nil && player.Match.Address != "" { - matchValue = "✅" + if player.Match.PendingNudge { + matchValue = fmt.Sprintf("%s %s", "⌛", lang.GetDefault(tp+"msg.invite.nudge_match.pending")) + } else { + matchValue = "✅" + } } if player != nil && player.Address != "" { - addressValue = "✅" + if player.PendingNudge { + addressValue = fmt.Sprintf("%s %s", "⚠️", lang.GetDefault(tp+"msg.invite.nudge_received")) + } else { + addressValue = "✅" + } } e = &discordgo.MessageEmbed{ diff --git a/util/interaction.go b/util/interaction.go index 389a693..d4e2897 100644 --- a/util/interaction.go +++ b/util/interaction.go @@ -195,6 +195,17 @@ func (i *InteractionUtil) ReplyHiddenEmbed(embeds ...*discordgo.MessageEmbed) { i.respond() } +// ReplyHiddenEmbedUpdate is like [InteractionUtil.ReplyHiddenEmbed] but made for an update for +// components. +func (i *InteractionUtil) ReplyHiddenEmbedUpdate(embeds ...*discordgo.MessageEmbed) { + if !i.respondMessage(true, false) { + return + } + i.response.Data.Embeds = embeds + i.response.Data.Flags = discordgo.MessageFlagsEphemeral + 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) @@ -259,6 +270,22 @@ func (i *InteractionUtil) ReplySimpleEmbedUpdatef(color int, format string, a .. i.ReplyEmbedUpdate(e) } +// 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) +} + +// ReplyHiddenSimpleEmbedUpdatef is like [InteractionUtil.ReplyHiddenSimpleEmbedf] but made for an +// update for components. +func (i *InteractionUtil) ReplyHiddenSimpleEmbedUpdatef(color int, format string, a ...any) { + i.ReplyHiddenSimpleEmbedUpdate(color, fmt.Sprintf(format, a...)) +} + // 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) { @@ -302,6 +329,16 @@ func (i *InteractionUtil) ReplyComponentsEmbed(components []discordgo.MessageCom i.respond() } +// ReplyComponentsEmbedUpdate is like [InteractionUtil.ReplyComponentsEmbed] but made for an update for components. +func (i *InteractionUtil) ReplyComponentsEmbedUpdate(components []discordgo.MessageComponent, embeds ...*discordgo.MessageEmbed) { + if !i.respondMessage(true, false) { + return + } + i.response.Data.Embeds = embeds + i.response.Data.Components = components + i.respond() +} + // ReplyComponentsHiddenEmbed sends the given embeds as ephemeral reply along with the provided message // components. func (i *InteractionUtil) ReplyComponentsHiddenEmbed(components []discordgo.MessageComponent, embeds ...*discordgo.MessageEmbed) { @@ -312,6 +349,45 @@ func (i *InteractionUtil) ReplyComponentsHiddenEmbed(components []discordgo.Mess i.respond() } +// ReplyComponentsHiddenEmbedUpdate is like [InteractionUtil.ReplyComponentsHiddenEmbed] but made for an update for components. +func (i *InteractionUtil) ReplyComponentsHiddenEmbedUpdate(components []discordgo.MessageComponent, embeds ...*discordgo.MessageEmbed) { + if !i.respondMessage(true, false) { + return + } + i.response.Data.Embeds = embeds + i.response.Data.Components = components + i.response.Data.Flags = discordgo.MessageFlagsEphemeral + i.respond() +} + +// 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) +} + +// ReplyComponentsSimpleEmbedUpdatef is like [InteractionUtil.ReplyComponentsSimpleEmbedf] but made for an update for components. +func (i *InteractionUtil) ReplyComponentsSimpleEmbedUpdatef(components []discordgo.MessageComponent, color int, format string, a ...any) { + i.ReplyComponentsSimpleEmbedUpdate(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) { + e := &discordgo.MessageEmbed{ + Description: content, + Color: color, + } + i.ReplyComponentsHiddenEmbedUpdate(components, e) +} + +// 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...)) +} + // 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) {