-
-
Notifications
You must be signed in to change notification settings - Fork 0
Add Discord Integration #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a significant new feature: Discord integration. The implementation is comprehensive, covering configuration, commands for linking/unlinking accounts, and the necessary database persistence.
My review focuses on several key areas:
- Critical Blocking Calls: There are several instances of blocking network and I/O calls (
.block()) on the main server thread. These will cause your server to freeze and must be addressed. I've marked these ascritical. - Code Readability: Some methods with asynchronous logic have deeply nested callbacks, which can be refactored for better readability and maintainability.
- Configuration Defaults: The default values in the configuration should be set to be more generic for a public plugin.
- Minor Logic Errors: I found a few minor issues, such as a duplicated condition and incorrect message targets for admin commands.
Overall, this is a great addition. Addressing the blocking calls is the highest priority before this can be safely used on a production server.
src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java
Outdated
Show resolved
Hide resolved
src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java
Outdated
Show resolved
Hide resolved
src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java
Show resolved
Hide resolved
| @Comment("# The Discord server ID.") | ||
| public String serverId = "1179117429301977251"; | ||
|
|
||
| @Comment("# The Discord channel ID for parcel notifications.") | ||
| public String channelId = "1317827115147853834"; | ||
|
|
||
| @Comment("# The Discord role ID for bot administrators.") | ||
| public String botAdminRoleId = "1317589501169893427"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default values for serverId, channelId, and botAdminRoleId are hardcoded with specific IDs. For a distributable plugin, it's better to leave these as empty strings by default. This forces server administrators to configure them explicitly, avoiding confusion and potential errors if they don't notice the pre-filled values.
| @Comment("# The Discord server ID.") | |
| public String serverId = "1179117429301977251"; | |
| @Comment("# The Discord channel ID for parcel notifications.") | |
| public String channelId = "1317827115147853834"; | |
| @Comment("# The Discord role ID for bot administrators.") | |
| public String botAdminRoleId = "1317589501169893427"; | |
| @Comment("# The Discord server ID.") | |
| public String serverId = ""; | |
| @Comment("# The Discord channel ID for parcel notifications.") | |
| public String channelId = ""; | |
| @Comment("# The Discord role ID for bot administrators.") | |
| public String botAdminRoleId = ""; |
src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java
Outdated
Show resolved
Hide resolved
src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java
Outdated
Show resolved
Hide resolved
src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java
Outdated
Show resolved
Hide resolved
...main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java
Show resolved
Hide resolved
...main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java
Outdated
Show resolved
Hide resolved
…r's main thread is not blocked
…d verification processes
…/DiscordLinkRepositoryOrmLite.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…/DiscordLinkRepositoryOrmLite.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds Discord integration to the ParcelLockers plugin, enabling players to link their Minecraft accounts with Discord for notifications. The implementation includes account linking/unlinking commands with verification flow, repository layer for persistence, and Discord client lifecycle management.
Changes:
- Added Discord client manager with async login/logout functionality
- Implemented account linking commands with verification code flow using Paper's Dialog API
- Created repository layer with OrmLite for storing Discord-Minecraft account links
- Extended configuration with Discord settings and related messages
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| build.gradle.kts | Added discord4j-core dependency and invalid JUnit version |
| DiscordLink.java | Simple record for linking Minecraft UUID to Discord ID |
| DiscordNotificationType.java | Unused enum defining notification delivery types |
| DiscordClientManager.java | Manages Discord client lifecycle with async initialization issues |
| DiscordLinkRepository*.java | Repository interface, entity, and OrmLite implementation with query bug |
| DiscordLinkCommand.java | Account linking command with verification flow and security concerns |
| DiscordUnlinkCommand.java | Account unlinking command for self and admin operations |
| PluginConfig.java | Discord configuration settings with hardcoded test values |
| MessageConfig.java | Discord-related user messages with misleading error text |
| ParcelLockers.java | Plugin initialization with race condition and null handling issues |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import org.bukkit.entity.Player; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| @SuppressWarnings("UnstableApiUsage") |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Dialog API (io.papermc.paper.dialog) is marked as @UnstableApiUsage, which is acknowledged in the @SuppressWarnings annotation. However, this API is experimental and may change or be removed in future Paper versions, potentially breaking the verification flow. Consider documenting this dependency and having a fallback mechanism, or using a more stable approach like chat-based verification.
| @SuppressWarnings("UnstableApiUsage") | |
| /** | |
| * Command responsible for linking a Minecraft account with a Discord account. | |
| * <p> | |
| * This implementation relies on Paper's Dialog API ({@code io.papermc.paper.dialog}), | |
| * which is marked as unstable and may change or be removed in future Paper versions. | |
| * If the Dialog API becomes unavailable, this command may need to be updated or | |
| * replaced with a more stable flow (for example, chat-based verification). | |
| */ |
| .build(); | ||
| .missingPermission(new MissingPermissionsHandlerImpl(noticeService)); | ||
|
|
||
| DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DiscordLinkRepository is initialized on line 199 even when Discord integration is disabled (config.discord.enabled = false). This creates an unnecessary database table and wastes resources. The repository should only be initialized when Discord integration is enabled.
| DiscordClient.create(this.token) | ||
| .login() | ||
| .subscribeOn(Schedulers.boundedElastic()) | ||
| .doOnSuccess(client -> { | ||
| this.client = client; | ||
| this.logger.info("Successfully logged in to Discord."); | ||
| }) | ||
| .doOnError(error -> { | ||
| this.logger.severe("Failed to log in to Discord: " + error.getMessage()); | ||
| error.printStackTrace(); | ||
| }) | ||
| .subscribe(); |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When Discord login fails, the error is logged but the plugin continues running with a null client. This will cause NullPointerExceptions when commands try to use the client. Consider throwing an exception or disabling the Discord integration when login fails to prevent the plugin from entering an invalid state.
| DiscordClient.create(this.token) | |
| .login() | |
| .subscribeOn(Schedulers.boundedElastic()) | |
| .doOnSuccess(client -> { | |
| this.client = client; | |
| this.logger.info("Successfully logged in to Discord."); | |
| }) | |
| .doOnError(error -> { | |
| this.logger.severe("Failed to log in to Discord: " + error.getMessage()); | |
| error.printStackTrace(); | |
| }) | |
| .subscribe(); | |
| try { | |
| GatewayDiscordClient loggedInClient = DiscordClient.create(this.token) | |
| .login() | |
| .block(); | |
| if (loggedInClient == null) { | |
| this.logger.severe("Failed to log in to Discord: login returned null client."); | |
| throw new IllegalStateException("Discord login failed: client is null"); | |
| } | |
| this.client = loggedInClient; | |
| this.logger.info("Successfully logged in to Discord."); | |
| } catch (Exception error) { | |
| this.logger.severe("Failed to log in to Discord: " + error.getMessage()); | |
| error.printStackTrace(); | |
| throw new IllegalStateException("Discord login failed", error); | |
| } |
| return; | ||
| } | ||
|
|
||
| if (!verificationData.code().equals(enteredCode)) { |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an invalid verification code is entered, the code is not invalidated from the cache (lines 210-212). This means a user can keep trying to guess the code until they get it right or until the 2-minute expiration occurs, which creates a security vulnerability. The verification should have a limited number of attempts, or the code should be invalidated after a failed attempt.
| if (!verificationData.code().equals(enteredCode)) { | |
| if (!verificationData.code().equals(enteredCode)) { | |
| // Invalidate the verification code after a failed attempt to prevent repeated guessing | |
| this.authCodesCache.invalidate(playerUuid); |
| liteCommandsBuilder.commands( | ||
| new DiscordLinkCommand( | ||
| this.discordClientManager.getClient(), | ||
| discordLinkRepository, | ||
| noticeService, | ||
| miniMessage, | ||
| messageConfig), | ||
| new DiscordUnlinkCommand(discordLinkRepository, noticeService) | ||
| ); |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Discord client is passed to DiscordLinkCommand via getClient() immediately after initialization, which may return null due to the asynchronous login in initialize(). This will cause a NullPointerException when the command is executed and tries to use the client.
| liteCommandsBuilder.commands( | |
| new DiscordLinkCommand( | |
| this.discordClientManager.getClient(), | |
| discordLinkRepository, | |
| noticeService, | |
| miniMessage, | |
| messageConfig), | |
| new DiscordUnlinkCommand(discordLinkRepository, noticeService) | |
| ); | |
| var discordClient = this.discordClientManager.getClient(); | |
| if (discordClient == null) { | |
| this.getLogger().severe("Failed to initialize Discord client. Discord commands will not be available."); | |
| } | |
| else { | |
| liteCommandsBuilder.commands( | |
| new DiscordLinkCommand( | |
| discordClient, | |
| discordLinkRepository, | |
| noticeService, | |
| miniMessage, | |
| messageConfig), | |
| new DiscordUnlinkCommand(discordLinkRepository, noticeService) | |
| ); | |
| } |
|
|
||
| @Comment("# The Discord server ID.") | ||
| public String serverId = "1179117429301977251"; | ||
|
|
||
| @Comment("# The Discord channel ID for parcel notifications.") | ||
| public String channelId = "1317827115147853834"; | ||
|
|
||
| @Comment("# The Discord role ID for bot administrators.") | ||
| public String botAdminRoleId = "1317589501169893427"; |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Discord-related configuration fields (serverId, channelId, botAdminRoleId) are defined but never used in the implementation. These fields suggest incomplete functionality - they appear to be for server notifications or admin role verification, but no code utilizes them. Consider either implementing the intended functionality or removing these unused configuration fields.
| @Comment("# The Discord server ID.") | |
| public String serverId = "1179117429301977251"; | |
| @Comment("# The Discord channel ID for parcel notifications.") | |
| public String channelId = "1317827115147853834"; | |
| @Comment("# The Discord role ID for bot administrators.") | |
| public String botAdminRoleId = "1317589501169893427"; |
| String enteredCode = view.getText("code"); | ||
| this.handleVerification(player, enteredCode); | ||
| }, ClickCallback.Options.builder() | ||
| .uses(1) |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The verification button is configured with only 1 use (line 177), but if the player enters an invalid code, they cannot try again without restarting the entire verification process. This creates a poor user experience as a simple typo forces them to wait for a new code. Consider allowing multiple attempts or reopening the dialog on invalid code entry.
| .uses(1) | |
| .uses(5) |
| var queryBuilder = dao.queryBuilder() | ||
| .where().eq(ID_COLUMN_NAME, discordId); |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query builder chain on lines 45-46 is missing proper method chaining. The queryBuilder() method should be followed by where(), but the current code structure breaks this chain incorrectly, creating an incomplete query that will fail at runtime.
| var queryBuilder = dao.queryBuilder() | |
| .where().eq(ID_COLUMN_NAME, discordId); | |
| var queryBuilder = dao.queryBuilder(); | |
| queryBuilder.where().eq(ID_COLUMN_NAME, discordId); |
| DiscordClient.create(this.token) | ||
| .login() | ||
| .subscribeOn(Schedulers.boundedElastic()) | ||
| .doOnSuccess(client -> { | ||
| this.client = client; | ||
| this.logger.info("Successfully logged in to Discord."); | ||
| }) | ||
| .doOnError(error -> { | ||
| this.logger.severe("Failed to log in to Discord: " + error.getMessage()); | ||
| error.printStackTrace(); | ||
| }) | ||
| .subscribe(); |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Discord client login is performed asynchronously in the initialize() method, but getClient() can be called immediately after, potentially returning null. This creates a race condition where commands or other components trying to use the client might receive null before the login completes. Consider blocking until the client is initialized or implementing a check mechanism to ensure the client is ready before use.
| DiscordClient.create(this.token) | |
| .login() | |
| .subscribeOn(Schedulers.boundedElastic()) | |
| .doOnSuccess(client -> { | |
| this.client = client; | |
| this.logger.info("Successfully logged in to Discord."); | |
| }) | |
| .doOnError(error -> { | |
| this.logger.severe("Failed to log in to Discord: " + error.getMessage()); | |
| error.printStackTrace(); | |
| }) | |
| .subscribe(); | |
| try { | |
| GatewayDiscordClient loggedInClient = DiscordClient.create(this.token) | |
| .login() | |
| .block(); | |
| if (loggedInClient != null) { | |
| this.client = loggedInClient; | |
| this.logger.info("Successfully logged in to Discord."); | |
| } else { | |
| this.logger.severe("Failed to log in to Discord: login returned null client."); | |
| } | |
| } catch (Exception error) { | |
| this.logger.severe("Failed to log in to Discord: " + error.getMessage()); | |
| error.printStackTrace(); | |
| } |
| if (config.discord.botToken.isBlank() || | ||
| config.discord.serverId.isBlank() || | ||
| config.discord.channelId.isBlank() || | ||
| config.discord.botAdminRoleId.isBlank() |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The null check for botToken uses isBlank(), but if the environment variable is not set, botToken will be null, which will cause a NullPointerException. The check should handle null values explicitly before calling isBlank().
| if (config.discord.botToken.isBlank() || | |
| config.discord.serverId.isBlank() || | |
| config.discord.channelId.isBlank() || | |
| config.discord.botAdminRoleId.isBlank() | |
| if (config.discord.botToken == null || config.discord.botToken.isBlank() || | |
| config.discord.serverId == null || config.discord.serverId.isBlank() || | |
| config.discord.channelId == null || config.discord.channelId.isBlank() || | |
| config.discord.botAdminRoleId == null || config.discord.botAdminRoleId.isBlank() |
This pull request introduces Discord integration to the ParcelLockers plugin, enabling users to link their Minecraft accounts with Discord accounts and receive notifications. The changes include configuration options, message customization, and new commands for linking and unlinking accounts. The implementation adds several new classes and updates the plugin's lifecycle to support Discord connectivity and clean shutdown.
Discord Integration Features
PluginConfig, allowing configuration of bot token, server/channel IDs, and admin role ID. [1] [2]DiscordClientManagerto manage Discord client lifecycle (login and logout) and expose the connected client for other components.DiscordLink,DiscordNotificationType, and repositories to support linking Minecraft and Discord accounts. [1] [2]Command and Repository Enhancements
DiscordLinkCommandandDiscordUnlinkCommandfor account management, with supporting repository for persistent links. [1] [2]Configuration and Messaging
MessageConfigto include customizable Discord-related messages for linking, unlinking, and verification flows. [1] [2]Plugin Lifecycle Updates
onEnable) to initialize Discord integration if enabled and validate configuration, disabling the plugin if required properties are missing.onDisable).Minor Refactoring