From 021bea533f561da8f26f44771f357b884c7140f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:52:45 +0000 Subject: [PATCH 1/2] Add whisper/remind slash commands and move listener whisper logic Co-authored-by: jamesrudolph <39609802+jamesrudolph@users.noreply.github.com> --- .../commands/SlashCommandManager.java | 6 + .../uncategorized/RemindMeCommand.java | 46 ++++++ .../commands/uncategorized/WCommand.java | 10 ++ .../uncategorized/WhisperCommand.java | 10 ++ .../uncategorized/WhisperCommandBase.java | 71 +++++++++ .../listeners/AutoCompleteProvider.java | 2 +- .../listeners/MessageListener.java | 125 +-------------- src/main/java/ti4/helpers/Constants.java | 4 + .../java/ti4/service/fow/WhisperService.java | 142 +++++++++++++++++- .../ti4/service/info/CardsInfoService.java | 6 +- 10 files changed, 294 insertions(+), 128 deletions(-) create mode 100644 src/main/java/ti4/discord/interactions/commands/uncategorized/RemindMeCommand.java create mode 100644 src/main/java/ti4/discord/interactions/commands/uncategorized/WCommand.java create mode 100644 src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommand.java create mode 100644 src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java diff --git a/src/main/java/ti4/discord/interactions/commands/SlashCommandManager.java b/src/main/java/ti4/discord/interactions/commands/SlashCommandManager.java index 98dfe719fb..9c8ee245a7 100644 --- a/src/main/java/ti4/discord/interactions/commands/SlashCommandManager.java +++ b/src/main/java/ti4/discord/interactions/commands/SlashCommandManager.java @@ -58,9 +58,12 @@ import ti4.discord.interactions.commands.transaction.TransactionCommand; import ti4.discord.interactions.commands.uncategorized.AllInfoCommand; import ti4.discord.interactions.commands.uncategorized.CardsInfoCommand; +import ti4.discord.interactions.commands.uncategorized.RemindMeCommand; import ti4.discord.interactions.commands.uncategorized.SelectionBoxDemoCommand; import ti4.discord.interactions.commands.uncategorized.ShowDistancesCommand; import ti4.discord.interactions.commands.uncategorized.ShowGameCommand; +import ti4.discord.interactions.commands.uncategorized.WCommand; +import ti4.discord.interactions.commands.uncategorized.WhisperCommand; import ti4.discord.interactions.commands.units.AddUnitDamage; import ti4.discord.interactions.commands.units.AddUnits; import ti4.discord.interactions.commands.units.CaptureCommand; @@ -83,6 +86,9 @@ public class SlashCommandManager { new RemoveAllUnits(), new AllInfoCommand(), new CardsInfoCommand(), + new WhisperCommand(), + new WCommand(), + new RemindMeCommand(), new ShowGameCommand(), new ShowDistancesCommand(), new AddCCCommand(), diff --git a/src/main/java/ti4/discord/interactions/commands/uncategorized/RemindMeCommand.java b/src/main/java/ti4/discord/interactions/commands/uncategorized/RemindMeCommand.java new file mode 100644 index 0000000000..4f2e9998c8 --- /dev/null +++ b/src/main/java/ti4/discord/interactions/commands/uncategorized/RemindMeCommand.java @@ -0,0 +1,46 @@ +package ti4.discord.interactions.commands.uncategorized; + +import java.util.List; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import ti4.discord.interactions.commands.GameStateCommand; +import ti4.game.Player; +import ti4.helpers.Constants; +import ti4.message.MessageHelper; +import ti4.service.fow.WhisperService; + +public class RemindMeCommand extends GameStateCommand { + + public RemindMeCommand() { + super(true, true); + } + + @Override + public String getName() { + return Constants.REMIND_ME; + } + + @Override + public String getDescription() { + return "Send a reminder message to yourself at start of your next turn"; + } + + @Override + public List getOptions() { + return List.of(new OptionData(OptionType.STRING, Constants.MESSAGE, "Message to send").setRequired(true)); + } + + @Override + public void execute(SlashCommandInteractionEvent event) { + Player player = getPlayer(); + if (!player.isRealPlayer()) { + MessageHelper.sendEphemeralMessageToEventChannel(event, "This command only works on real players."); + return; + } + + String message = event.getOption(Constants.MESSAGE, "", OptionMapping::getAsString); + WhisperService.whisperToFutureMe(getGame(), player, message, event.getChannel()); + } +} diff --git a/src/main/java/ti4/discord/interactions/commands/uncategorized/WCommand.java b/src/main/java/ti4/discord/interactions/commands/uncategorized/WCommand.java new file mode 100644 index 0000000000..fbaec4294d --- /dev/null +++ b/src/main/java/ti4/discord/interactions/commands/uncategorized/WCommand.java @@ -0,0 +1,10 @@ +package ti4.discord.interactions.commands.uncategorized; + +import ti4.helpers.Constants; + +public class WCommand extends WhisperCommandBase { + + public WCommand() { + super(Constants.W, "Alias for /whisper"); + } +} diff --git a/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommand.java b/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommand.java new file mode 100644 index 0000000000..3c2dcc1a23 --- /dev/null +++ b/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommand.java @@ -0,0 +1,10 @@ +package ti4.discord.interactions.commands.uncategorized; + +import ti4.helpers.Constants; + +public class WhisperCommand extends WhisperCommandBase { + + public WhisperCommand() { + super(Constants.WHISPER, "Send a private whisper to a player"); + } +} diff --git a/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java b/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java new file mode 100644 index 0000000000..b5a509879d --- /dev/null +++ b/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java @@ -0,0 +1,71 @@ +package ti4.discord.interactions.commands.uncategorized; + +import java.util.List; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import ti4.discord.interactions.commands.GameStateCommand; +import ti4.game.Game; +import ti4.game.Player; +import ti4.helpers.Constants; +import ti4.message.MessageHelper; +import ti4.service.fow.WhisperService; + +abstract class WhisperCommandBase extends GameStateCommand { + + private final String name; + private final String description; + + protected WhisperCommandBase(String name, String description) { + super(true, true); + this.name = name; + this.description = description; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public List getOptions() { + return List.of( + new OptionData(OptionType.STRING, Constants.TO, "Faction or color to whisper to") + .setAutoComplete(true) + .setRequired(true), + new OptionData(OptionType.STRING, Constants.MESSAGE, "Message to send"), + new OptionData(OptionType.BOOLEAN, Constants.FUTURE, "When true, deliver at start of target's turn")); + } + + @Override + public void execute(SlashCommandInteractionEvent event) { + Player sender = getPlayer(); + if (!sender.isRealPlayer()) { + MessageHelper.sendEphemeralMessageToEventChannel(event, "This command only works on real players."); + return; + } + + Game game = getGame(); + String to = event.getOption(Constants.TO, "", OptionMapping::getAsString).toLowerCase(); + Player receiver = WhisperService.getPlayerByColorOrFaction(game, to); + if (receiver == null) { + MessageHelper.sendEphemeralMessageToEventChannel(event, "Player not found: " + to); + return; + } + + String message = event.getOption(Constants.MESSAGE, "", OptionMapping::getAsString); + boolean future = event.getOption(Constants.FUTURE, false, OptionMapping::getAsBoolean); + if (future) { + WhisperService.whisperToFutureColorOrFaction(game, message, sender, receiver, event.getChannel()); + return; + } + + WhisperService.sendWhisper(game, sender, receiver, message, "n", event.getMessageChannel(), event.getGuild()); + } +} diff --git a/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java b/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java index 4ddff2e09a..93e0dd9704 100644 --- a/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java +++ b/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java @@ -925,7 +925,7 @@ private static void handleOptions( .toList(); event.replyChoices(options).queue(Consumers.nop(), BotLogger::catchRestError); } - case Constants.FACTION_COLOR, Constants.TARGET_FACTION_OR_COLOR -> { + case Constants.FACTION_COLOR, Constants.TARGET_FACTION_OR_COLOR, Constants.TO -> { if (!GameManager.isValid(gameName)) return; Game game = GameManager.getManagedGame(gameName).getGame(); String enteredValue = event.getFocusedOption().getValue().toLowerCase(); diff --git a/src/main/java/ti4/discord/interactions/listeners/MessageListener.java b/src/main/java/ti4/discord/interactions/listeners/MessageListener.java index 77ec0fa3c5..322b0a4412 100644 --- a/src/main/java/ti4/discord/interactions/listeners/MessageListener.java +++ b/src/main/java/ti4/discord/interactions/listeners/MessageListener.java @@ -1,8 +1,6 @@ package ti4.discord.interactions.listeners; import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; import javax.annotation.Nonnull; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Role; @@ -19,10 +17,8 @@ import ti4.game.Player; import ti4.game.persistence.GameManager; import ti4.game.persistence.ManagedGame; -import ti4.helpers.AliasHandler; import ti4.helpers.Constants; import ti4.helpers.async.RoundSummaryHelper; -import ti4.image.Mapper; import ti4.logging.BotLogger; import ti4.message.MessageHelper; import ti4.service.emoji.CardEmojis; @@ -37,8 +33,6 @@ class MessageListener extends ListenerAdapter { private static final int EXECUTION_TIME_WARNING_THRESHOLD_SECONDS = 1; - private static final Pattern FUTURE = Pattern.compile("future"); - private static final Pattern PATTERN = Pattern.compile("[^a-zA-Z0-9]+$"); // The mention itself is 23 characters long private static final int BOTHELPER_MENTION_REMINDER_MESSAGE_LENGTH_THRESHOLD = 53; private static final String BOTHELPER_MENTION_REMINDER_TEXT = """ @@ -90,7 +84,7 @@ private static void processMessage(@Nonnull MessageReceivedEvent event, Message reportInterestingMessages(message); if (isValidGameMessage) { - if (handleWhispers(event, message, gameName)) return; + if (WhisperService.handleWhispers(event, message, gameName)) return; if (endOfRoundSummary(event, message, gameName)) return; if (addFactionEmojiReactionsToMessages(event, gameName)) return; } @@ -200,123 +194,6 @@ private static boolean endOfRoundSummary(MessageReceivedEvent event, Message mes return true; } - private static boolean handleWhispers(MessageReceivedEvent event, Message message, String gameName) { - if (message.getContentRaw().contains("used /fow whisper")) { - message.delete().queue(Consumers.nop(), BotLogger::catchRestError); - } - - String messageText = message.getContentRaw(); - if (!messageText.toLowerCase().startsWith("to") || !messageText.contains(" ")) { - return false; - } - - Game game = GameManager.getManagedGame(gameName).getGame(); - if (game == null) { - return true; - } - - // Prevent whispers from fow combat threads - if (game.isFowMode() - && event.getChannel() instanceof ThreadChannel - && event.getChannel().getName().contains("vs") - && event.getChannel().getName().contains("private")) { - return false; - } - - Player sender = getPlayer(event, game); - if (sender == null || !sender.isRealPlayer()) { - return true; - } - - String messageLowerCase = messageText.toLowerCase(); - String receivingColorOrFaction = PATTERN.matcher(StringUtils.substringBetween(messageLowerCase, "to", " ")) - .replaceAll(""); - - if ("futureme".equals(receivingColorOrFaction)) { - whisperToFutureMe(event, game, sender); - GameManager.save( - game, - "Whisper to future by " + sender.getUserName()); // TODO: We should be locking since we're saving - return true; - } - - boolean future = receivingColorOrFaction.startsWith("future"); - receivingColorOrFaction = FUTURE.matcher(receivingColorOrFaction).replaceFirst(""); - if (receivingColorOrFaction.isEmpty()) { - return true; - } - - receivingColorOrFaction = AliasHandler.resolveFaction(receivingColorOrFaction); - if (!Mapper.isValidColor(receivingColorOrFaction) && !Mapper.isValidFaction(receivingColorOrFaction)) { - return true; - } - - if (game.isWhispersDisabled()) { - MessageHelper.sendMessageToChannel( - event.getChannel(), - "Whispers are disabled in this game. To reenable them, use `/game setup whispers_enabled:true`."); - message.delete().queue(Consumers.nop(), BotLogger::catchRestError); - return true; - } - - String messageContent = StringUtils.substringAfter(messageText, " "); - if (messageContent.isEmpty()) { - message.reply("No message content?").queue(Consumers.nop(), BotLogger::catchRestError); - return true; - } - - Player receiver = null; - for (Player player : game.getRealPlayers()) { - if (Objects.equals(receivingColorOrFaction, player.getFaction()) - || Objects.equals(receivingColorOrFaction, player.getColor())) { - receiver = player; - break; - } - } - - if (receiver == null) { - MessageHelper.sendMessageToChannel(event.getChannel(), "Player not found: " + receivingColorOrFaction); - return true; - } - - if (future) { - whisperToFutureColorOrFaction(event, game, messageContent, sender, receiver); - } else { - WhisperService.sendWhisper( - game, sender, receiver, messageContent, "n", event.getChannel(), event.getGuild()); - message.delete().queue(Consumers.nop(), BotLogger::catchRestError); - } - GameManager.save(game, "Whisper"); // TODO: We should be locking since we're saving - return true; - } - - private static void whisperToFutureColorOrFaction( - MessageReceivedEvent event, Game game, String messageContent, Player sender, Player receiver) { - String futureMsgKey = "futureMessageFor_" + receiver.getFaction() + "_" + sender.getFaction(); - game.setStoredValue(futureMsgKey, game.getStoredValue(futureMsgKey) + "\n\n" + messageContent); - MessageHelper.sendMessageToChannel( - event.getChannel(), sender.getFactionEmoji() + " sent someone else a future message"); - MessageHelper.sendMessageToPlayerCardsInfoThread( - sender, - "You sent a future message to " + receiver.getRepresentationNoPing() + ":\n>>> " + messageContent); - event.getMessage().delete().queue(Consumers.nop(), BotLogger::catchRestError); - } - - private static void whisperToFutureMe(MessageReceivedEvent event, Game game, Player player) { - String messageContent = StringUtils.substringAfter(event.getMessage().getContentRaw(), " "); - - String previousThoughts = ""; - if (!game.getStoredValue("futureMessageFor" + player.getFaction()).isEmpty()) { - previousThoughts = game.getStoredValue("futureMessageFor" + player.getFaction()) + "\n\n"; - } - game.setStoredValue("futureMessageFor" + player.getFaction(), previousThoughts + messageContent); - MessageHelper.sendMessageToChannel( - event.getChannel(), player.getFactionEmoji() + " sent themselves a future message"); - MessageHelper.sendMessageToPlayerCardsInfoThread( - player, "You sent yourself a future message:\n>>> " + messageContent); - event.getMessage().delete().queue(Consumers.nop(), BotLogger::catchRestError); - } - private static boolean addFactionEmojiReactionsToMessages(MessageReceivedEvent event, String gameName) { ManagedGame managedGame = GameManager.getManagedGame(gameName); if (managedGame.getGame().isHiddenAgendaMode() diff --git a/src/main/java/ti4/helpers/Constants.java b/src/main/java/ti4/helpers/Constants.java index fce49c296e..538fb4a18a 100644 --- a/src/main/java/ti4/helpers/Constants.java +++ b/src/main/java/ti4/helpers/Constants.java @@ -1046,6 +1046,10 @@ public static String solaxPing() { public static final String PING = "ping"; public static final String PING_ACTIVE_PLAYER = "ping_active_player"; public static final String WHISPER = "whisper"; + public static final String W = "w"; + public static final String TO = "to"; + public static final String FUTURE = "future"; + public static final String REMIND_ME = "remind_me"; public static final String MSG = "msg"; public static final String OVERRIDE_FOW = "override_fow"; public static final String OVERRIDE_EXPLORE_OWNERSHIP_REQ = "override_explore_ownership_req"; diff --git a/src/main/java/ti4/service/fow/WhisperService.java b/src/main/java/ti4/service/fow/WhisperService.java index 46cf32fe3f..cc19a7cb98 100644 --- a/src/main/java/ti4/service/fow/WhisperService.java +++ b/src/main/java/ti4/service/fow/WhisperService.java @@ -1,18 +1,32 @@ package ti4.service.fow; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; import lombok.experimental.UtilityClass; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; -import software.amazon.awssdk.utils.StringUtils; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.function.Consumers; import ti4.game.Game; import ti4.game.Player; +import ti4.game.persistence.GameManager; +import ti4.helpers.AliasHandler; +import ti4.image.Mapper; +import ti4.logging.BotLogger; import ti4.message.MessageHelper; import ti4.service.emoji.ColorEmojis; @UtilityClass public class WhisperService { + private static final Pattern FUTURE = Pattern.compile("future"); + private static final Pattern PATTERN = Pattern.compile("[^a-zA-Z0-9]+$"); + public static void sendWhisper( Game game, Player player, @@ -102,4 +116,130 @@ public static void sendWhisper( MessageHelper.sendPrivateMessageToPlayer(player_, game, feedbackChannel, message, fail, success); } } + + public static boolean handleWhispers(MessageReceivedEvent event, Message message, String gameName) { + if (message.getContentRaw().contains("used /fow whisper")) { + message.delete().queue(Consumers.nop(), BotLogger::catchRestError); + } + + String messageText = message.getContentRaw(); + if (!messageText.toLowerCase().startsWith("to") || !messageText.contains(" ")) { + return false; + } + + Game game = GameManager.getManagedGame(gameName).getGame(); + if (game == null) { + return true; + } + + if (game.isFowMode() + && event.getChannel() instanceof ThreadChannel + && event.getChannel().getName().contains("vs") + && event.getChannel().getName().contains("private")) { + return false; + } + + Player sender = getPlayer(event, game); + if (sender == null || !sender.isRealPlayer()) { + return true; + } + + String messageLowerCase = messageText.toLowerCase(); + String receivingColorOrFaction = + PATTERN.matcher(StringUtils.substringBetween(messageLowerCase, "to", " ")) + .replaceAll(""); + + if ("futureme".equals(receivingColorOrFaction)) { + String messageContent = StringUtils.substringAfter(messageText, " "); + whisperToFutureMe(game, sender, messageContent, event.getChannel()); + event.getMessage().delete().queue(Consumers.nop(), BotLogger::catchRestError); + GameManager.save(game, "Whisper to future by " + sender.getUserName()); + return true; + } + + boolean future = receivingColorOrFaction.startsWith("future"); + receivingColorOrFaction = FUTURE.matcher(receivingColorOrFaction).replaceFirst(""); + if (receivingColorOrFaction.isEmpty()) { + return true; + } + + Player receiver = getPlayerByColorOrFaction(game, receivingColorOrFaction); + if (receiver == null) { + MessageHelper.sendMessageToChannel(event.getChannel(), "Player not found: " + receivingColorOrFaction); + return true; + } + + String messageContent = StringUtils.substringAfter(messageText, " "); + if (messageContent.isEmpty()) { + message.reply("No message content?").queue(Consumers.nop(), BotLogger::catchRestError); + return true; + } + + if (game.isWhispersDisabled()) { + MessageHelper.sendMessageToChannel( + event.getChannel(), + "Whispers are disabled in this game. To reenable them, use `/game setup whispers_enabled:true`."); + message.delete().queue(Consumers.nop(), BotLogger::catchRestError); + return true; + } + + if (future) { + whisperToFutureColorOrFaction(game, messageContent, sender, receiver, event.getChannel()); + } else { + sendWhisper(game, sender, receiver, messageContent, "n", event.getChannel(), event.getGuild()); + message.delete().queue(Consumers.nop(), BotLogger::catchRestError); + } + GameManager.save(game, "Whisper"); + return true; + } + + public static Player getPlayerByColorOrFaction(Game game, String receivingColorOrFaction) { + String resolvedColorOrFaction = AliasHandler.resolveFaction(receivingColorOrFaction); + if (!Mapper.isValidColor(resolvedColorOrFaction) && !Mapper.isValidFaction(resolvedColorOrFaction)) { + return null; + } + for (Player player : game.getRealPlayers()) { + if (Objects.equals(resolvedColorOrFaction, player.getFaction()) + || Objects.equals(resolvedColorOrFaction, player.getColor())) { + return player; + } + } + return null; + } + + public static void whisperToFutureColorOrFaction( + Game game, String messageContent, Player sender, Player receiver, MessageChannel messageChannel) { + String futureMsgKey = "futureMessageFor_" + receiver.getFaction() + "_" + sender.getFaction(); + game.setStoredValue(futureMsgKey, game.getStoredValue(futureMsgKey) + "\n\n" + messageContent); + MessageHelper.sendMessageToChannel(messageChannel, sender.getFactionEmoji() + " sent someone else a future message"); + MessageHelper.sendMessageToPlayerCardsInfoThread( + sender, "You sent a future message to " + receiver.getRepresentationNoPing() + ":\n>>> " + messageContent); + } + + public static void whisperToFutureMe(Game game, Player player, String messageContent, MessageChannel messageChannel) { + String previousThoughts = ""; + if (!game.getStoredValue("futureMessageFor" + player.getFaction()).isEmpty()) { + previousThoughts = game.getStoredValue("futureMessageFor" + player.getFaction()) + "\n\n"; + } + game.setStoredValue("futureMessageFor" + player.getFaction(), previousThoughts + messageContent); + MessageHelper.sendMessageToChannel(messageChannel, player.getFactionEmoji() + " sent themselves a future message"); + MessageHelper.sendMessageToPlayerCardsInfoThread(player, "You sent yourself a future message:\n>>> " + messageContent); + } + + private static Player getPlayer(MessageReceivedEvent event, Game game) { + Player player = game.getPlayer(event.getAuthor().getId()); + if (!game.isCommunityMode()) { + return player; + } + List roles = event.getMember().getRoles(); + for (Player player2 : game.getRealPlayers()) { + if (roles.contains(player2.getRoleForCommunity())) { + return player2; + } + if (player2.getTeamMateIDs().contains(event.getMember().getUser().getId())) { + return player2; + } + } + return player; + } } diff --git a/src/main/java/ti4/service/info/CardsInfoService.java b/src/main/java/ti4/service/info/CardsInfoService.java index ad51befced..ff38522267 100644 --- a/src/main/java/ti4/service/info/CardsInfoService.java +++ b/src/main/java/ti4/service/info/CardsInfoService.java @@ -46,9 +46,11 @@ public static void sendCardsInfo(Game game, Player player) { MessageHelper.sendMessageToPlayerCardsInfoThread(player, """ You may whisper to people from here by starting a message with `to[color]` or `to[faction]`.\ - You may schedule a message to yourself (delivered at start of your next turn) by starting a message with `tofutureme`.\ + You may whisper by slash command with `/whisper` (or `/w`).\ - You may schedule a message to others (delivered at start of their next turn) by starting a message with `tofuture[color]` or `tofuture[faction]`."""); + You may schedule a message to yourself (delivered at start of your next turn) with `/remind_me` (or by starting a message with `tofutureme`).\ + + You may schedule a message to others (delivered at start of their next turn) by starting a message with `tofuture[color]` or `tofuture[faction]`, or by using `/whisper future:true`."""); } public static void sendVariousAdditionalButtons(Game game, Player player) { From 498218144ebb15a896d28a890eb2344d0a74fc2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:54:00 +0000 Subject: [PATCH 2/2] Format whisper service and command changes Co-authored-by: jamesrudolph <39609802+jamesrudolph@users.noreply.github.com> --- .../uncategorized/WhisperCommandBase.java | 3 ++- .../java/ti4/service/fow/WhisperService.java | 22 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java b/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java index b5a509879d..1a746e1b5b 100644 --- a/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java +++ b/src/main/java/ti4/discord/interactions/commands/uncategorized/WhisperCommandBase.java @@ -52,7 +52,8 @@ public void execute(SlashCommandInteractionEvent event) { } Game game = getGame(); - String to = event.getOption(Constants.TO, "", OptionMapping::getAsString).toLowerCase(); + String to = + event.getOption(Constants.TO, "", OptionMapping::getAsString).toLowerCase(); Player receiver = WhisperService.getPlayerByColorOrFaction(game, to); if (receiver == null) { MessageHelper.sendEphemeralMessageToEventChannel(event, "Player not found: " + to); diff --git a/src/main/java/ti4/service/fow/WhisperService.java b/src/main/java/ti4/service/fow/WhisperService.java index cc19a7cb98..05712ac1fc 100644 --- a/src/main/java/ti4/service/fow/WhisperService.java +++ b/src/main/java/ti4/service/fow/WhisperService.java @@ -4,8 +4,8 @@ import java.util.Objects; import java.util.regex.Pattern; import lombok.experimental.UtilityClass; -import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; @@ -145,9 +145,8 @@ public static boolean handleWhispers(MessageReceivedEvent event, Message message } String messageLowerCase = messageText.toLowerCase(); - String receivingColorOrFaction = - PATTERN.matcher(StringUtils.substringBetween(messageLowerCase, "to", " ")) - .replaceAll(""); + String receivingColorOrFaction = PATTERN.matcher(StringUtils.substringBetween(messageLowerCase, "to", " ")) + .replaceAll(""); if ("futureme".equals(receivingColorOrFaction)) { String messageContent = StringUtils.substringAfter(messageText, " "); @@ -211,19 +210,24 @@ public static void whisperToFutureColorOrFaction( Game game, String messageContent, Player sender, Player receiver, MessageChannel messageChannel) { String futureMsgKey = "futureMessageFor_" + receiver.getFaction() + "_" + sender.getFaction(); game.setStoredValue(futureMsgKey, game.getStoredValue(futureMsgKey) + "\n\n" + messageContent); - MessageHelper.sendMessageToChannel(messageChannel, sender.getFactionEmoji() + " sent someone else a future message"); + MessageHelper.sendMessageToChannel( + messageChannel, sender.getFactionEmoji() + " sent someone else a future message"); MessageHelper.sendMessageToPlayerCardsInfoThread( - sender, "You sent a future message to " + receiver.getRepresentationNoPing() + ":\n>>> " + messageContent); + sender, + "You sent a future message to " + receiver.getRepresentationNoPing() + ":\n>>> " + messageContent); } - public static void whisperToFutureMe(Game game, Player player, String messageContent, MessageChannel messageChannel) { + public static void whisperToFutureMe( + Game game, Player player, String messageContent, MessageChannel messageChannel) { String previousThoughts = ""; if (!game.getStoredValue("futureMessageFor" + player.getFaction()).isEmpty()) { previousThoughts = game.getStoredValue("futureMessageFor" + player.getFaction()) + "\n\n"; } game.setStoredValue("futureMessageFor" + player.getFaction(), previousThoughts + messageContent); - MessageHelper.sendMessageToChannel(messageChannel, player.getFactionEmoji() + " sent themselves a future message"); - MessageHelper.sendMessageToPlayerCardsInfoThread(player, "You sent yourself a future message:\n>>> " + messageContent); + MessageHelper.sendMessageToChannel( + messageChannel, player.getFactionEmoji() + " sent themselves a future message"); + MessageHelper.sendMessageToPlayerCardsInfoThread( + player, "You sent yourself a future message:\n>>> " + messageContent); } private static Player getPlayer(MessageReceivedEvent event, Game game) {