Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions src/main/java/ti4/discord/JdaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.Category;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.requests.ErrorResponse;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction;
import net.dv8tion.jda.api.utils.ChunkingFilter;
Expand Down Expand Up @@ -109,6 +111,8 @@ public class JdaService {
public static final List<Guild> serversToCreateNewGamesOn = new ArrayList<>();
public static final List<Guild> fowServers = new ArrayList<>();

private static final int MEMBER_CACHE_MAX_SIZE = 20_000;

private static final ExecutorService EVENT_EXECUTOR = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
Thread.ofPlatform().name("ti4-jda-event-", 0).factory());
Expand All @@ -117,18 +121,16 @@ public static void startJdaAndRegisterListeners(String[] args) {
BotLogger.info("STARTING JDA");
jda = JDABuilder.createDefault(args[0])
.setEventPool(EVENT_EXECUTOR)
// Needed to listen for joins/leaves.
// This is required to cache all members of a guild (including chunking)
// Needed to listen for joins/leaves and perform targeted member fetches.
.enableIntents(GatewayIntent.GUILD_MEMBERS)
// Needed to parse raw user messages.
.enableIntents(GatewayIntent.MESSAGE_CONTENT)
// Needed for emoji searches and validation
.enableIntents(GatewayIntent.GUILD_EXPRESSIONS)
// It *appears* we need to pull all members or else the bot has trouble pinging players
// but that may be a misunderstanding, in case we want to try to use an LRU cache in the future
// and avoid loading every user at startup
.setMemberCachePolicy(MemberCachePolicy.ALL)
.setChunkingFilter(ChunkingFilter.ALL)
// Cache up to MEMBER_CACHE_MAX_SIZE most-recently-used members, evicting older
// entries automatically. ChunkingFilter.NONE prevents the startup bulk load.
.setMemberCachePolicy(MemberCachePolicy.lru(MEMBER_CACHE_MAX_SIZE))
.setChunkingFilter(ChunkingFilter.NONE)
// This allows us to use our own ShutdownHook, created below
.setEnableShutdownHook(false)
.build();
Expand Down Expand Up @@ -561,13 +563,53 @@ public static boolean isValidGuild(String guildId) {

@Nullable
public static String getUsername(String userId) {
Member member = guildPrimary.getMemberById(userId);
Member member = getMemberById(guildPrimary, userId);
if (member != null) return member.getEffectiveName();
User user = jda.getUserById(userId);
User user = getUserById(userId);
if (user != null) return user.getEffectiveName();
return null;
}

/**
* Resolves a guild member by user ID. Uses JDA's cache-aware {@code retrieveMemberById}, which
* returns the cached member immediately if available, otherwise makes a blocking REST call.
* Should only be called from background threads (e.g. ExecutorServiceManager tasks), not from
* JDA event-dispatch threads.
*/
@Nullable
public static Member getMemberById(@Nullable Guild guild, @Nullable String userId) {
if (guild == null || userId == null) return null;
try {
return guild.retrieveMemberById(userId).complete();
} catch (ErrorResponseException e) {
if (e.getErrorResponse() != ErrorResponse.UNKNOWN_MEMBER
&& e.getErrorResponse() != ErrorResponse.UNKNOWN_USER) {
BotLogger.warning(
"Unable to retrieve Discord member `" + userId + "` from guild `" + guild.getId() + "`.", e);
}
return null;
}
}

/**
* Resolves a Discord user by user ID. Uses JDA's cache-aware {@code retrieveUserById}, which
* returns the cached user immediately if available, otherwise makes a blocking REST call.
* Should only be called from background threads (e.g. ExecutorServiceManager tasks), not from
* JDA event-dispatch threads.
*/
@Nullable
public static User getUserById(@Nullable String userId) {
if (jda == null || userId == null) return null;
try {
return jda.retrieveUserById(userId).complete();
} catch (ErrorResponseException e) {
if (e.getErrorResponse() != ErrorResponse.UNKNOWN_USER) {
BotLogger.warning("Unable to retrieve Discord user `" + userId + "`.", e);
}
return null;
}
}

public static void shutdown() {
try {
AsyncTI4DiscordBot.markShuttingDown();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ private static List<Member> fetchMembersFromMessage(ButtonInteractionEvent event
for (int i = 0; i < StringUtils.countMatches(buttonMsg, "<@"); i++) {
String user = buttonMsg.split("@")[i + 1];
user = StringUtils.substringBefore(user, ">");
Member member = event.getGuild().getMemberById(user);
Member member = JdaService.getMemberById(event.getGuild(), user);
if (member != null) {
members.add(member);
}
Expand All @@ -170,7 +170,7 @@ private static List<Member> fetchMembersFromMessage(ModalInteractionEvent event)
for (int i = 0; i < StringUtils.countMatches(buttonMsg, "<@"); i++) {
String user = buttonMsg.split("@")[i + 1];
user = StringUtils.substringBefore(user, ">");
Member member = event.getGuild().getMemberById(user);
Member member = JdaService.getMemberById(event.getGuild(), user);
if (member != null) {
members.add(member);
}
Expand Down Expand Up @@ -400,7 +400,7 @@ private static List<Member> parseMembersFromButtonMessage(ButtonInteractionEvent

for (int i = 3; i < parts.length; i++) {
String userId = StringUtils.substringBefore(parts[i], ".");
Member member = event.getGuild().getMemberById(userId);
Member member = JdaService.getMemberById(event.getGuild(), userId);
if (member != null) {
members.add(member);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package ti4.discord.interactions.commands.bothelper;

import ti4.discord.JdaService;
import java.util.List;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.ISnowflake;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
Expand Down Expand Up @@ -53,12 +53,10 @@ public void execute(SlashCommandInteractionEvent event) {
}

// CHECK IF GUILD HAS ALL PLAYERS LISTED
List<String> guildMemberIDs =
guild.getMembers().stream().map(ISnowflake::getId).toList();
boolean sendInviteLink = false;
int count = 0;
for (Member member : members) {
if (!guildMemberIDs.contains(member.getId())) {
if (JdaService.getMemberById(guild, member.getId()) == null) {
MessageHelper.sendMessageToEventChannel(
event,
member.getAsMention() + " is not a member of the server **" + guild.getName()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ti4.discord.interactions.commands.event;

import ti4.discord.JdaService;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
Expand Down Expand Up @@ -44,7 +45,7 @@ public void execute(SlashCommandInteractionEvent event) {
sb.append("-----------\n");

Player player = getPlayer();
User userById = event.getJDA().getUserById(player.getUserID());
User userById = JdaService.getUserById(player.getUserID());
if (userById != null) {
if (game.isCommunityMode() && player.getPrivateChannel() != null) {
MessageHelper.sendMessageToChannel(player.getPrivateChannel(), sb.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ti4.discord.interactions.commands.event;

import ti4.discord.JdaService;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
Expand Down Expand Up @@ -45,7 +46,7 @@ public void execute(SlashCommandInteractionEvent event) {
sb.append("-----------\n");

Player player = getPlayer();
User userById = event.getJDA().getUserById(player.getUserID());
User userById = JdaService.getUserById(player.getUserID());
if (userById != null) {
if (game.isCommunityMode() && player.getPrivateChannel() != null) {
MessageHelper.sendMessageToChannel(player.getPrivateChannel(), sb.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ti4.discord.interactions.commands.game;

import ti4.discord.JdaService;
import java.util.ArrayList;
import java.util.List;
import net.dv8tion.jda.api.Permission;
Expand Down Expand Up @@ -52,10 +53,10 @@ public void execute(SlashCommandInteractionEvent event) {

ManagedGame game = GameManager.getManagedGame(gameName);
Guild guild = game.getGuild();
Member member = guild.getMemberById(user.getId());
Member member = JdaService.getMemberById(guild, user.getId());

if (member == null && event.getGuild() != null) {
member = event.getGuild().getMemberById(user.getId());
member = JdaService.getMemberById(event.getGuild(), user.getId());
}

// INVITE TO GAME SERVER IF MISSING
Expand Down Expand Up @@ -101,7 +102,7 @@ private void addObserver(
SlashCommandInteractionEvent event, String userID, GuildChannel channel, boolean skipMessage) {
if (channel == null) return;
Guild guild = channel.getGuild();
Member user = guild.getMemberById(userID);
Member user = JdaService.getMemberById(guild, userID);
channel.getPermissionContainer()
.upsertPermissionOverride(user)
.grant(Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND)
Expand All @@ -121,7 +122,7 @@ private void removeObserver(
// This resets the member's perms to the default value,
// -> -> -> SO IF THE USER IS IN THE GAME, THEY DON'T GET REMOVED
Guild guild = channel.getGuild();
Member user = guild.getMemberById(userID);
Member user = JdaService.getMemberById(guild, userID);
channel.getPermissionContainer()
.upsertPermissionOverride(user)
.clear(Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ public void execute(SlashCommandInteractionEvent event) {
if (guild == null) {
guild = event.getGuild();
}
Member newMember = guild.getMemberById(replacementUser.getId());
Member newMember = JdaService.getMemberById(guild, replacementUser.getId());
if (newMember == null) {
MessageHelper.replyToMessage(event, "Added player must be on the game's server.");
return;
}

// REMOVE ROLE
Member oldMember = guild.getMemberById(replacedPlayer.getUserID());
Member oldMember = JdaService.getMemberById(guild, replacedPlayer.getUserID());
List<Role> roles = guild.getRolesByName(game.getName(), true);
if (oldMember != null && roles.size() == 1) {
guild.removeRoleFromMember(oldMember, roles.getFirst()).queue(Consumers.nop(), BotLogger::catchRestError);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public void execute(SlashCommandInteractionEvent event) {
}

player.getCardsInfoThread()
.removeThreadMember(JdaService.jda.getUserById(targetPlayer.getUserID()))
.removeThreadMember(JdaService.getUserById(targetPlayer.getUserID()))
.queue(Consumers.nop(), BotLogger::catchRestError);
targetPlayer
.getCardsInfoThread()
.removeThreadMember(JdaService.jda.getUserById(player.getUserID()))
.removeThreadMember(JdaService.getUserById(player.getUserID()))
.queue(Consumers.nop(), BotLogger::catchRestError);

MessageHelper.sendMessageToEventChannel(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ti4.discord.interactions.commands.player;

import ti4.discord.JdaService;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -343,7 +344,7 @@ public void execute(SlashCommandInteractionEvent event) {
UserSettingsManager.save(userSettings);

Guild guild = event.getGuild();
Member removedMember = guild.getMemberById(player.getUserID());
Member removedMember = JdaService.getMemberById(guild, player.getUserID());
List<Role> roles = guild.getRolesByName(game.getName(), true);
if (removedMember != null && roles.size() == 1) {
guild.removeRoleFromMember(removedMember, roles.getFirst())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,7 @@ private static void handleOptions(
DraftManager draftManager = game.getDraftManager();
String enteredValue = event.getFocusedOption().getValue().toLowerCase();
Predicate<String> isUnknownUserId =
id -> game.getPlayer(id) == null || event.getGuild().getMemberById(id) == null;
id -> game.getPlayer(id) == null || JdaService.getMemberById(event.getGuild(), id) == null;
List<String> userIDs = draftManager.getPlayerUserIds().stream()
.filter(isUnknownUserId)
.toList();
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/ti4/game/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -3650,7 +3650,7 @@ public String getPing() {
}
StringBuilder sb = new StringBuilder(getName()).append(' ');
for (String playerID : getPlayerIDs()) {
User user = JdaService.jda.getUserById(playerID);
User user = JdaService.getUserById(playerID);
if (user != null) sb.append(user.getAsMention()).append(' ');
}
return sb.toString();
Expand Down Expand Up @@ -3771,7 +3771,7 @@ public List<Player> getPlayersWithGMRole() {
Role gmRole = roles.isEmpty() ? null : roles.getFirst();
return players.values().stream()
.filter(player -> {
Member user = getGuild().getMemberById(player.getUserID());
Member user = JdaService.getMemberById(getGuild(), player.getUserID());
return user != null && user.getRoles().contains(gmRole);
})
.toList();
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/ti4/game/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -1512,7 +1512,7 @@ public Member getMember() {
if (game == null) return null;
Guild guild = game.getGuild();
if (guild == null) return null;
return guild.getMemberById(getUserID());
return JdaService.getMemberById(guild, getUserID());
}

public User getUser() {
Expand All @@ -1521,7 +1521,7 @@ public User getUser() {

private User getUser(String userId) {
// TODO: This is to handle JDA being null during tests. We should think of a cleaner solution.
return JdaService.jda == null ? null : JdaService.jda.getUserById(userId);
return JdaService.jda == null ? null : JdaService.getUserById(userId);
}

@Override
Expand All @@ -1538,7 +1538,7 @@ public String getUserName() {
User userById = getUser();
if (userById == null) return super.getUserName();

Member member = JdaService.guildPrimary.getMemberById(getUserID());
Member member = JdaService.getMemberById(JdaService.guildPrimary, getUserID());
if (member == null) {
setUserName(userById.getName());
} else {
Expand Down Expand Up @@ -1605,7 +1605,7 @@ public String getRepresentation(boolean overrideFow, boolean ping, boolean noCol
if (roleForCommunity == null && !getTeamMateIDs().isEmpty()) {
StringBuilder sb = new StringBuilder((noFactionIcon ? "" : getFactionEmoji()));
for (String userID : getTeamMateIDs()) {
User userById = JdaService.jda.getUserById(userID);
User userById = JdaService.getUserById(userID);
if (userById == null) {
continue;
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/ti4/helpers/ButtonHelper.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ti4.helpers;

import ti4.discord.JdaService;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.substringAfter;
Expand Down Expand Up @@ -930,7 +931,7 @@ public static void removeUser(
}

Guild guild = event.getGuild();
Member removedMember = guild.getMemberById(player.getUserID());
Member removedMember = JdaService.getMemberById(guild, player.getUserID());
List<Role> roles = guild.getRolesByName(game.getName(), true);
if (removedMember != null && roles.size() == 1) {
guild.removeRoleFromMember(removedMember, roles.getFirst())
Expand Down Expand Up @@ -8391,7 +8392,7 @@ public static void sendMessageToRightStratThread(
String threadName = game.getName() + "-round-" + game.getRound() + "-" + stratName.toLowerCase();
for (ThreadChannel threadChannel_ : threadChannels) {
if ("pbd1000".equalsIgnoreCase(game.getName()) || "pbd100two".equalsIgnoreCase(game.getName())) {
if (!threadChannel_.getMembers().contains(game.getGuild().getMemberById(player.getUserID()))) {
if (!threadChannel_.getMembers().contains(JdaService.getMemberById(game.getGuild(), player.getUserID()))) {
continue;
}
}
Expand Down
6 changes: 1 addition & 5 deletions src/main/java/ti4/helpers/GameLaunchThreadHelper.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package ti4.helpers;

import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.experimental.UtilityClass;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.ISnowflake;
import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel;
import ti4.discord.JdaService;
import ti4.game.Game;
Expand All @@ -26,10 +24,8 @@ public static void checkIfCanCloseGameLaunchThread(Game game, boolean notify) {
if (threadChannel == null) {
return;
}
List<String> guildMemberIDs =
guild.getMembers().stream().map(ISnowflake::getId).toList();
for (String playerIds : game.getPlayerIDs()) {
if (!guildMemberIDs.contains(playerIds)) {
if (JdaService.getMemberById(guild, playerIds) == null) {
return;
}
}
Expand Down
Loading