From c5a6d1eda5bd99c500a3f468377b1327988c8dcf Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:22:45 +0100 Subject: [PATCH 01/36] Begin working on Discord integration --- build.gradle.kts | 3 + .../parcellockers/ParcelLockers.java | 14 ++++ .../implementation/PluginConfig.java | 21 ++++++ .../discord/DiscordClientManager.java | 40 ++++++++++++ .../parcellockers/discord/DiscordLink.java | 6 ++ .../discord/DiscordNotificationType.java | 7 ++ .../discord/command/DiscordLinkCommand.java | 25 ++++++++ .../discord/repository/DiscordLinkEntity.java | 38 +++++++++++ .../repository/DiscordLinkRepository.java | 17 +++++ .../DiscordLinkRepositoryOrmLite.java | 64 +++++++++++++++++++ 10 files changed, 235 insertions(+) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java diff --git a/build.gradle.kts b/build.gradle.kts index 32fc9c0b..dee77d25 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,6 +78,9 @@ dependencies { // vault compileOnly("com.github.MilkBowl:VaultAPI:1.7.1") + // discord integration library + paperLibrary("com.discord4j:discord4j-core:3.3.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.2") testImplementation("org.junit.jupiter:junit-jupiter-params:6.0.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.2") diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index cf4c7a5c..4ad80986 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -16,6 +16,7 @@ import com.eternalcode.parcellockers.database.DatabaseManager; import com.eternalcode.parcellockers.delivery.DeliveryManager; import com.eternalcode.parcellockers.delivery.repository.DeliveryRepositoryOrmLite; +import com.eternalcode.parcellockers.discord.DiscordClientManager; import com.eternalcode.parcellockers.gui.GuiManager; import com.eternalcode.parcellockers.gui.implementation.locker.LockerGui; import com.eternalcode.parcellockers.gui.implementation.remote.MainGui; @@ -198,6 +199,19 @@ public void onEnable() { new LoadUserController(userManager, server) ).forEach(controller -> server.getPluginManager().registerEvents(controller, this)); + DiscordClientManager discordClientManager; + + if (config.discord.enabled) { + discordClientManager = new DiscordClientManager( + config.discord.botToken, + config.discord.serverId, + config.discord.channelId, + config.discord.botAdminRoleId, + this.getLogger() + ); + discordClientManager.initialize(); + } + new Metrics(this, 17677); new UpdaterService(this.getPluginMeta().getVersion()); diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java index 2132a377..8ec773ad 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java @@ -24,6 +24,9 @@ public class PluginConfig extends OkaeriConfig { @Comment({ "", "# The plugin GUI settings." }) public GuiSettings guiSettings = new GuiSettings(); + @Comment({ "", "# The plugin Discord integration settings." }) + public DiscordSettings discord = new DiscordSettings(); + public static class Settings extends OkaeriConfig { @Comment("# Whether the player after entering the server should receive information about the new version of the plugin?") @@ -357,4 +360,22 @@ public static class GuiSettings extends OkaeriConfig { @Comment({ "", "# The lore line showing when the parcel has arrived. Placeholders: {DATE} - arrival date" }) public String parcelArrivedLine = "&aArrived on: &2{DATE}"; } + + public static class DiscordSettings extends OkaeriConfig { + + @Comment("# Whether Discord integration is enabled.") + public boolean enabled = false; + + @Comment("# The Discord bot token.") + public String botToken = ""; + + @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 = ""; + } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java new file mode 100644 index 00000000..fa2c3dcd --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java @@ -0,0 +1,40 @@ +package com.eternalcode.parcellockers.discord; + +import discord4j.common.util.Snowflake; +import discord4j.core.DiscordClient; +import discord4j.core.GatewayDiscordClient; +import java.util.logging.Logger; + +public class DiscordClientManager { + + private final String token; + private final Snowflake serverId; + private final Snowflake channelId; + private final Snowflake botAdminRole; + private final Logger logger; + + private GatewayDiscordClient client; + + public DiscordClientManager(String token, String serverId, String channelId, String botAdminRole, Logger logger) { + this.token = token; + this.serverId = Snowflake.of(serverId); + this.channelId = Snowflake.of(channelId); + this.botAdminRole = Snowflake.of(botAdminRole); + this.logger = logger; + } + + public void initialize() { + this.logger.info("Discord integration is enabled. Logging in to Discord..."); + this.client = DiscordClient.create(this.token) + .login() + .block(); + this.logger.info("Successfully logged in to Discord."); + } + + public void shutdown() { + this.logger.info("Shutting down Discord client..."); + if (this.client != null) { + this.client.logout().block(); + } + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java new file mode 100644 index 00000000..023817b1 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java @@ -0,0 +1,6 @@ +package com.eternalcode.parcellockers.discord; + +import java.util.UUID; + +public record DiscordLink(UUID minecraftUuid, String discordId) { +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java new file mode 100644 index 00000000..de1880f4 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java @@ -0,0 +1,7 @@ +package com.eternalcode.parcellockers.discord; + +public enum DiscordNotificationType { + SERVER, + DM, + BOTH +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java new file mode 100644 index 00000000..6684d822 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -0,0 +1,25 @@ +package com.eternalcode.parcellockers.discord.command; + +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.permission.Permission; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@Command(name = "parcel discordlink") +public class DiscordLinkCommand { + + @Execute + void execute(@Context Player player, @Arg long discordId) { + // Implementation for linking Discord ID to Minecraft player goes here + } + + @Execute + @Permission("parcellockers.admin") + void admin(@Context CommandSender sender, @Arg Player player, @Arg long discordId) { + + } + +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java new file mode 100644 index 00000000..3d6d3916 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java @@ -0,0 +1,38 @@ +package com.eternalcode.parcellockers.discord.repository; + +import com.eternalcode.parcellockers.discord.DiscordLink; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import java.util.UUID; + +@DatabaseTable(tableName = "discord_links") +class DiscordLinkEntity { + + @DatabaseField(id = true, columnName = "minecraft_uuid") + private String minecraftUuid; + + @DatabaseField(index = true, columnName = "discord_id") + private String discordId; + + DiscordLinkEntity() {} + + DiscordLinkEntity(String minecraftUuid, String discordId) { + this.minecraftUuid = minecraftUuid; + this.discordId = discordId; + } + + public static DiscordLinkEntity fromDomain(DiscordLink link) { + return new DiscordLinkEntity( + link.minecraftUuid().toString(), + link.discordId() + ); + } + + public DiscordLink toDomain() { + return new DiscordLink( + UUID.fromString(this.minecraftUuid), + this.discordId + ); + } + +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java new file mode 100644 index 00000000..22872ff5 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java @@ -0,0 +1,17 @@ +package com.eternalcode.parcellockers.discord.repository; + +import com.eternalcode.parcellockers.discord.DiscordLink; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface DiscordLinkRepository { + + CompletableFuture save(DiscordLink link); + + CompletableFuture> findByPlayerUuid(UUID playerUuid); + CompletableFuture> findByDiscordId(String discordId); + + CompletableFuture deleteByPlayerUuid(UUID playerUuid); + CompletableFuture deleteByDiscordId(String discordId); +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java new file mode 100644 index 00000000..8b1d105d --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java @@ -0,0 +1,64 @@ +package com.eternalcode.parcellockers.discord.repository; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.parcellockers.database.DatabaseManager; +import com.eternalcode.parcellockers.database.wrapper.AbstractRepositoryOrmLite; +import com.eternalcode.parcellockers.discord.DiscordLink; +import com.eternalcode.parcellockers.shared.exception.DatabaseException; +import com.j256.ormlite.dao.Dao.CreateOrUpdateStatus; +import com.j256.ormlite.table.TableUtils; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class DiscordLinkRepositoryOrmLite extends AbstractRepositoryOrmLite implements DiscordLinkRepository { + + public static final String ID_COLUMN_NAME = "discord_id"; + + public DiscordLinkRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) { + super(databaseManager, scheduler); + + try { + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), DiscordLink.class); + } catch (SQLException ex) { + throw new DatabaseException("Failed to initialize DiscordLink table", ex); + } + } + + @Override + public CompletableFuture save(DiscordLink link) { + return this.save(DiscordLinkEntity.class, DiscordLinkEntity.fromDomain(link)) + .thenApply(CreateOrUpdateStatus::isCreated); + } + + @Override + public CompletableFuture> findByPlayerUuid(UUID playerUuid) { + return this.selectSafe(DiscordLinkEntity.class, playerUuid.toString()) + .thenApply(optionalEntity -> optionalEntity.map(DiscordLinkEntity::toDomain)); + } + + @Override + public CompletableFuture> findByDiscordId(String discordId) { + return this.action(DiscordLinkEntity.class, dao -> { + var queryBuilder = dao.queryBuilder() + .where().eq(ID_COLUMN_NAME, discordId); + return dao.queryForFirst(queryBuilder.prepare()); + }).thenApply(optionalEntity -> optionalEntity != null ? Optional.of(optionalEntity.toDomain()) : Optional.empty()); + } + + @Override + public CompletableFuture deleteByPlayerUuid(UUID playerUuid) { + return this.deleteById(DiscordLinkEntity.class, playerUuid.toString()) + .thenApply(deletedRows -> deletedRows > 0); + } + + @Override + public CompletableFuture deleteByDiscordId(String discordId) { + return this.action(DiscordLinkEntity.class, dao -> { + var deleteBuilder = dao.deleteBuilder(); + deleteBuilder.where().eq(ID_COLUMN_NAME, discordId); + return deleteBuilder.delete(); + }).thenApply(deletedRows -> deletedRows > 0); + } +} From 6307b0e8a2884408d266852c27a688780573e32e Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:51:14 +0100 Subject: [PATCH 02/36] Finish implementing Discord integration --- .../parcellockers/ParcelLockers.java | 68 ++++-- .../implementation/MessageConfig.java | 79 ++++++ .../implementation/PluginConfig.java | 10 +- .../discord/DiscordClientManager.java | 13 +- .../discord/command/DiscordLinkCommand.java | 224 +++++++++++++++++- .../discord/command/DiscordUnlinkCommand.java | 90 +++++++ .../DiscordLinkRepositoryOrmLite.java | 5 +- 7 files changed, 451 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 4ad80986..038ebe39 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -17,6 +17,10 @@ import com.eternalcode.parcellockers.delivery.DeliveryManager; import com.eternalcode.parcellockers.delivery.repository.DeliveryRepositoryOrmLite; import com.eternalcode.parcellockers.discord.DiscordClientManager; +import com.eternalcode.parcellockers.discord.command.DiscordLinkCommand; +import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; +import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; +import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; import com.eternalcode.parcellockers.gui.GuiManager; import com.eternalcode.parcellockers.gui.implementation.locker.LockerGui; import com.eternalcode.parcellockers.gui.implementation.remote.MainGui; @@ -73,6 +77,7 @@ public final class ParcelLockers extends JavaPlugin { private SkullAPI skullAPI; private DatabaseManager databaseManager; private Economy economy; + private DiscordClientManager discordClientManager; @Override public void onEnable() { @@ -177,19 +182,53 @@ public void onEnable() { this.skullAPI ); - this.liteCommands = LiteBukkitFactory.builder(this.getName(), this) + var liteCommandsBuilder = LiteBukkitFactory.builder(this.getName(), this) .extension(new LiteAdventureExtension<>()) .message(LiteBukkitMessages.PLAYER_ONLY, messageConfig.playerOnlyCommand) .message(LiteBukkitMessages.PLAYER_NOT_FOUND, messageConfig.playerNotFound) .commands(LiteCommandsAnnotations.of( new ParcelCommand(mainGUI), new ParcelLockersCommand(configService, config, noticeService), - new DebugCommand(parcelService, lockerManager, itemStorageManager, parcelContentManager, + new DebugCommand( + parcelService, lockerManager, itemStorageManager, parcelContentManager, noticeService, deliveryManager) )) .invalidUsage(new InvalidUsageHandlerImpl(noticeService)) - .missingPermission(new MissingPermissionsHandlerImpl(noticeService)) - .build(); + .missingPermission(new MissingPermissionsHandlerImpl(noticeService)); + + DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); + + if (config.discord.enabled) { + if (config.discord.botToken.isBlank() || + config.discord.botAdminRoleId.isBlank() || + config.discord.serverId.isBlank() || + config.discord.channelId.isBlank() || + config.discord.botAdminRoleId.isBlank() + ) { + this.getLogger().severe("Discord integration is enabled but some of the properties are not set! Disabling..."); + server.getPluginManager().disablePlugin(this); + return; + } + + this.discordClientManager = new DiscordClientManager( + config.discord.botToken, + this.getLogger() + ); + this.discordClientManager.initialize(); + + liteCommandsBuilder.commands( + new DiscordLinkCommand( + this.discordClientManager.getClient(), + discordLinkRepository, + noticeService, + miniMessage, + scheduler, + messageConfig), + new DiscordUnlinkCommand(discordLinkRepository, noticeService) + ); + } + + this.liteCommands = liteCommandsBuilder.build(); Stream.of( new LockerInteractionController(lockerManager, lockerGUI, scheduler), @@ -199,21 +238,8 @@ public void onEnable() { new LoadUserController(userManager, server) ).forEach(controller -> server.getPluginManager().registerEvents(controller, this)); - DiscordClientManager discordClientManager; - - if (config.discord.enabled) { - discordClientManager = new DiscordClientManager( - config.discord.botToken, - config.discord.serverId, - config.discord.channelId, - config.discord.botAdminRoleId, - this.getLogger() - ); - discordClientManager.initialize(); - } - - new Metrics(this, 17677); - new UpdaterService(this.getPluginMeta().getVersion()); + Metrics metrics = new Metrics(this, 17677); + UpdaterService updaterService = new UpdaterService(this.getPluginMeta().getVersion()); parcelRepository.findAll().thenAccept(optionalParcels -> optionalParcels .stream() @@ -239,6 +265,10 @@ public void onDisable() { if (this.skullAPI != null) { this.skullAPI.shutdown(); } + + if (this.discordClientManager != null) { + this.discordClientManager.shutdown(); + } } private boolean setupEconomy() { diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index 531bd961..1dfab877 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -37,6 +37,10 @@ public class MessageConfig extends OkaeriConfig { @Comment("# These messages are used for administrative actions such as deleting all lockers or parcels.") public AdminMessages admin = new AdminMessages(); + @Comment({"", "# Messages related to Discord integration can be configured here." }) + @Comment("# These messages are used for linking Discord accounts with Minecraft accounts.") + public DiscordMessages discord = new DiscordMessages(); + public static class ParcelMessages extends OkaeriConfig { public Notice sent = Notice.builder() .chat("&2✔ &aParcel sent successfully.") @@ -178,4 +182,79 @@ public static class AdminMessages extends OkaeriConfig { public Notice deletedContents = Notice.chat("&4⚠ &cAll ({COUNT}) parcel contents have been deleted!"); public Notice deletedDeliveries = Notice.chat("&4⚠ &cAll ({COUNT}) deliveries have been deleted!"); } + + public static class DiscordMessages extends OkaeriConfig { + public Notice verificationAlreadyPending = Notice.builder() + .chat("&4✘ &cYou already have a pending verification. Please complete it or wait for it to expire.") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice alreadyLinked = Notice.builder() + .chat("&4✘ &cYour Minecraft account is already linked to a Discord account!") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice discordAlreadyLinked = Notice.builder() + .chat("&4✘ &cThis Discord account is already linked to another Minecraft account!") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice userNotFound = Notice.builder() + .chat("&4✘ &cCould not find a Discord user with that ID!") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice verificationCodeSent = Notice.builder() + .chat("&2✔ &aA verification code has been sent to your Discord DM. Please check your messages.") + .sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP) + .build(); + public Notice cannotSendDm = Notice.builder() + .chat("&4✘ &cCould not send a DM to your Discord account. Please make sure your DMs are open.") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice verificationExpired = Notice.builder() + .chat("&4✘ &cYour verification code has expired. Please run the command again.") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice invalidCode = Notice.builder() + .chat("&4✘ &cInvalid verification code. Please try again in 2 minutes.") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice linkSuccess = Notice.builder() + .chat("&2✔ &aYour Discord account has been successfully linked!") + .sound(SoundEventKeys.ENTITY_PLAYER_LEVELUP) + .build(); + public Notice linkFailed = Notice.builder() + .chat("&4✘ &cFailed to link your Discord account. Please try again later.") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice verificationCancelled = Notice.builder() + .chat("&6⚠ &eVerification cancelled.") + .sound(SoundEventKeys.BLOCK_NOTE_BLOCK_BASS) + .build(); + public Notice playerAlreadyLinked = Notice.chat("&4✘ &cThis player already has a linked Discord account!"); + public Notice adminLinkSuccess = Notice.chat("&2✔ &aSuccessfully linked the Discord account to the player."); + + @Comment({"", "# Unlink messages" }) + public Notice notLinked = Notice.builder() + .chat("&4✘ &cYour Minecraft account is not linked to any Discord account!") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice unlinkSuccess = Notice.builder() + .chat("&2✔ &aYour Discord account has been successfully unlinked!") + .sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP) + .build(); + public Notice unlinkFailed = Notice.builder() + .chat("&4✘ &cFailed to unlink the Discord account. Please try again later.") + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) + .build(); + public Notice playerNotLinked = Notice.chat("&4✘ &cThis player does not have a linked Discord account!"); + public Notice adminUnlinkSuccess = Notice.chat("&2✔ &aSuccessfully unlinked the Discord account from the player."); + public Notice discordNotLinked = Notice.chat("&4✘ &cNo Minecraft account is linked to this Discord ID!"); + public Notice adminUnlinkByDiscordSuccess = Notice.chat("&2✔ &aSuccessfully unlinked the Minecraft account from the Discord ID."); + + @Comment({"", "# Dialog configuration for verification" }) + public String verificationDialogTitle = "&6Enter your Discord verification code:"; + public String verificationDialogPlaceholder = "&7Enter 4-digit code"; + + @Comment({"", "# The message sent to the Discord user via DM" }) + @Comment("# Placeholders: {CODE} - the verification code, {PLAYER} - the Minecraft player name") + public String discordDmVerificationMessage = "**📦 ParcelLockers Verification**\n\nPlayer **{PLAYER}** is trying to link their Minecraft account to your Discord account.\n\nYour verification code is: **{CODE}**\n\nThis code will expire in 2 minutes."; + } } diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java index 8ec773ad..28a1f757 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java @@ -364,18 +364,18 @@ public static class GuiSettings extends OkaeriConfig { public static class DiscordSettings extends OkaeriConfig { @Comment("# Whether Discord integration is enabled.") - public boolean enabled = false; + public boolean enabled = true; @Comment("# The Discord bot token.") - public String botToken = ""; + public String botToken = System.getenv("DISCORD_BOT_TOKEN"); @Comment("# The Discord server ID.") - public String serverId = ""; + public String serverId = "1179117429301977251"; @Comment("# The Discord channel ID for parcel notifications.") - public String channelId = ""; + public String channelId = "1317827115147853834"; @Comment("# The Discord role ID for bot administrators.") - public String botAdminRoleId = ""; + public String botAdminRoleId = "1317589501169893427"; } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java index fa2c3dcd..16b1cb47 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java @@ -1,6 +1,5 @@ package com.eternalcode.parcellockers.discord; -import discord4j.common.util.Snowflake; import discord4j.core.DiscordClient; import discord4j.core.GatewayDiscordClient; import java.util.logging.Logger; @@ -8,18 +7,12 @@ public class DiscordClientManager { private final String token; - private final Snowflake serverId; - private final Snowflake channelId; - private final Snowflake botAdminRole; private final Logger logger; private GatewayDiscordClient client; - public DiscordClientManager(String token, String serverId, String channelId, String botAdminRole, Logger logger) { + public DiscordClientManager(String token, Logger logger) { this.token = token; - this.serverId = Snowflake.of(serverId); - this.channelId = Snowflake.of(channelId); - this.botAdminRole = Snowflake.of(botAdminRole); this.logger = logger; } @@ -37,4 +30,8 @@ public void shutdown() { this.client.logout().block(); } } + + public GatewayDiscordClient getClient() { + return this.client; + } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 6684d822..77c3b902 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -1,25 +1,241 @@ package com.eternalcode.parcellockers.discord.command; +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; +import com.eternalcode.parcellockers.discord.DiscordLink; +import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; +import com.eternalcode.parcellockers.notification.NoticeService; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.permission.Permission; +import discord4j.common.util.Snowflake; +import discord4j.core.GatewayDiscordClient; +import discord4j.core.object.entity.User; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.dialog.DialogResponseView; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.input.DialogInput; +import io.papermc.paper.registry.data.dialog.type.DialogType; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -@Command(name = "parcel discordlink") +@SuppressWarnings("UnstableApiUsage") +@Command(name = "parcel linkdiscord") public class DiscordLinkCommand { + private static final Random RANDOM = new Random(); + + private final GatewayDiscordClient client; + private final DiscordLinkRepository discordLinkRepository; + private final NoticeService noticeService; + private final MiniMessage miniMessage; + private final Scheduler scheduler; + private final MessageConfig messageConfig; + + private final Cache authCodesCache = Caffeine.newBuilder() + .expireAfterWrite(2, TimeUnit.MINUTES) + .build(); + + public DiscordLinkCommand( + GatewayDiscordClient client, + DiscordLinkRepository discordLinkRepository, + NoticeService noticeService, + MiniMessage miniMessage, + Scheduler scheduler, + MessageConfig messageConfig + ) { + this.client = client; + this.discordLinkRepository = discordLinkRepository; + this.noticeService = noticeService; + this.miniMessage = miniMessage; + this.scheduler = scheduler; + this.messageConfig = messageConfig; + } + @Execute - void execute(@Context Player player, @Arg long discordId) { - // Implementation for linking Discord ID to Minecraft player goes here + void linkSelf(@Context Player player, @Arg long discordId) { + UUID playerUuid = player.getUniqueId(); + String discordIdString = String.valueOf(discordId); + + // Check if the player already has a pending verification + if (this.authCodesCache.getIfPresent(playerUuid) != null) { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); + return; + } + + // Check if the player already has a linked Discord account + this.discordLinkRepository.findByPlayerUuid(playerUuid).thenAccept(existingLink -> { + if (existingLink.isPresent()) { + this.noticeService.player(playerUuid, messages -> messages.discord.alreadyLinked); + return; + } + + // Check if the Discord account is already linked to another Minecraft account + this.discordLinkRepository.findByDiscordId(discordIdString).thenAccept(existingDiscordLink -> { + if (existingDiscordLink.isPresent()) { + this.noticeService.player(playerUuid, messages -> messages.discord.discordAlreadyLinked); + return; + } + + // Try to get the Discord user + User discordUser; + try { + discordUser = this.client.getUserById(Snowflake.of(discordId)).block(); + } catch (Exception ex) { + this.noticeService.player(playerUuid, messages -> messages.discord.userNotFound); + return; + } + + if (discordUser == null) { + this.noticeService.player(playerUuid, messages -> messages.discord.userNotFound); + return; + } + + // Generate a 4-digit verification code + String verificationCode = this.generateVerificationCode(); + + // Store the verification data in cache + this.authCodesCache.put(playerUuid, new VerificationData(discordIdString, verificationCode)); + + // Send the verification code to the Discord user via DM + discordUser.getPrivateChannel() + .flatMap(channel -> channel.createMessage( + this.messageConfig.discord.discordDmVerificationMessage + .replace("{CODE}", verificationCode) + .replace("{PLAYER}", player.getName()) + )) + .doOnSuccess(message -> { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationCodeSent); + this.scheduler.run(() -> this.showVerificationDialog(player)); + }) + .doOnError(error -> { + this.authCodesCache.invalidate(playerUuid); + this.noticeService.player(playerUuid, messages -> messages.discord.cannotSendDm); + }) + .subscribe(); + }); + }); } @Execute @Permission("parcellockers.admin") - void admin(@Context CommandSender sender, @Arg Player player, @Arg long discordId) { + void linkOther(@Context CommandSender sender, @Arg Player player, @Arg long discordId) { + UUID playerUuid = player.getUniqueId(); + String discordIdString = String.valueOf(discordId); + + // Admin bypass - directly link without verification + this.discordLinkRepository.findByPlayerUuid(playerUuid).thenAccept(existingLink -> { + if (existingLink.isPresent()) { + this.noticeService.console(messages -> messages.discord.playerAlreadyLinked); + return; + } + + this.discordLinkRepository.findByDiscordId(discordIdString).thenAccept(existingDiscordLink -> { + if (existingDiscordLink.isPresent()) { + this.noticeService.console(messages -> messages.discord.discordAlreadyLinked); + return; + } + + DiscordLink link = new DiscordLink(playerUuid, discordIdString); + this.discordLinkRepository.save(link).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); + this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + } + }); + }); + }); + } + + private void showVerificationDialog(Player player) { + Dialog verificationDialog = Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogTitle)) + .canCloseWithEscape(false) + .inputs(List.of( + DialogInput.text("code", this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogPlaceholder)) + .build() + )) + .build() + ) + .type(DialogType.confirmation( + ActionButton.create( + this.miniMessage.deserialize("Verify"), + this.miniMessage.deserialize("Click to verify your Discord account"), + 200, + DialogAction.customClick((DialogResponseView view, Audience audience) -> { + String enteredCode = view.getText("code"); + this.handleVerification(player, enteredCode); + }, ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ), + ActionButton.create( + this.miniMessage.deserialize("Cancel"), + this.miniMessage.deserialize("Click to cancel verification"), + 200, + DialogAction.customClick( + (DialogResponseView view, Audience audience) -> { + this.authCodesCache.invalidate(player.getUniqueId()); + this.noticeService.player(player.getUniqueId(), messages -> messages.discord.verificationCancelled); + }, + ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ) + )) + ); + + player.showDialog(verificationDialog); + } + + private void handleVerification(Player player, String enteredCode) { + UUID playerUuid = player.getUniqueId(); + VerificationData verificationData = this.authCodesCache.getIfPresent(playerUuid); + + if (verificationData == null) { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationExpired); + return; + } + + if (!verificationData.code().equals(enteredCode)) { + this.noticeService.player(playerUuid, messages -> messages.discord.invalidCode); + return; + } + + // Code matches - remove from cache and create the link + this.authCodesCache.invalidate(playerUuid); + + DiscordLink link = new DiscordLink(playerUuid, verificationData.discordId()); + this.discordLinkRepository.save(link).thenAccept(success -> { + if (success) { + this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); + } else { + this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); + } + }); + } + private String generateVerificationCode() { + int code = 1000 + RANDOM.nextInt(9000); // generates 1000-9999 + return String.valueOf(code); } + private record VerificationData(String discordId, String code) {} } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java new file mode 100644 index 00000000..c7edf7f1 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java @@ -0,0 +1,90 @@ +package com.eternalcode.parcellockers.discord.command; + +import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; +import com.eternalcode.parcellockers.notification.NoticeService; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.permission.Permission; +import java.util.UUID; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@Command(name = "parcel unlinkdiscord") +public class DiscordUnlinkCommand { + + private final DiscordLinkRepository discordLinkRepository; + private final NoticeService noticeService; + + public DiscordUnlinkCommand( + DiscordLinkRepository discordLinkRepository, + NoticeService noticeService + ) { + this.discordLinkRepository = discordLinkRepository; + this.noticeService = noticeService; + } + + @Execute + void unlinkSelf(@Context Player player) { + UUID playerUuid = player.getUniqueId(); + + this.discordLinkRepository.findByPlayerUuid(playerUuid).thenAccept(existingLink -> { + if (existingLink.isEmpty()) { + this.noticeService.player(playerUuid, messages -> messages.discord.notLinked); + return; + } + + this.discordLinkRepository.deleteByPlayerUuid(playerUuid).thenAccept(success -> { + if (success) { + this.noticeService.player(playerUuid, messages -> messages.discord.unlinkSuccess); + } else { + this.noticeService.player(playerUuid, messages -> messages.discord.unlinkFailed); + } + }); + }); + } + + @Execute + @Permission("parcellockers.admin") + void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { + UUID targetUuid = targetPlayer.getUniqueId(); + + this.discordLinkRepository.findByPlayerUuid(targetUuid).thenAccept(existingLink -> { + if (existingLink.isEmpty()) { + this.noticeService.viewer(sender, messages -> messages.discord.playerNotLinked); + return; + } + + this.discordLinkRepository.deleteByPlayerUuid(targetUuid).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkSuccess); + this.noticeService.player(targetUuid, messages -> messages.discord.unlinkSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + } + }); + }); + } + + @Execute + @Permission("parcellockers.admin") + void unlinkByDiscordId(@Context CommandSender sender, @Arg long discordId) { + String discordIdString = String.valueOf(discordId); + + this.discordLinkRepository.findByDiscordId(discordIdString).thenAccept(existingLink -> { + if (existingLink.isEmpty()) { + this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); + return; + } + + this.discordLinkRepository.deleteByDiscordId(discordIdString).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkByDiscordSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + } + }); + }); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java index 8b1d105d..32f63a19 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java @@ -6,6 +6,7 @@ import com.eternalcode.parcellockers.discord.DiscordLink; import com.eternalcode.parcellockers.shared.exception.DatabaseException; import com.j256.ormlite.dao.Dao.CreateOrUpdateStatus; +import com.j256.ormlite.stmt.DeleteBuilder; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; import java.util.Optional; @@ -20,7 +21,7 @@ public DiscordLinkRepositoryOrmLite(DatabaseManager databaseManager, Scheduler s super(databaseManager, scheduler); try { - TableUtils.createTableIfNotExists(databaseManager.connectionSource(), DiscordLink.class); + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), DiscordLinkEntity.class); } catch (SQLException ex) { throw new DatabaseException("Failed to initialize DiscordLink table", ex); } @@ -56,7 +57,7 @@ public CompletableFuture deleteByPlayerUuid(UUID playerUuid) { @Override public CompletableFuture deleteByDiscordId(String discordId) { return this.action(DiscordLinkEntity.class, dao -> { - var deleteBuilder = dao.deleteBuilder(); + DeleteBuilder deleteBuilder = dao.deleteBuilder(); deleteBuilder.where().eq(ID_COLUMN_NAME, discordId); return deleteBuilder.delete(); }).thenApply(deletedRows -> deletedRows > 0); From 9e4a99876379108cda653dbf26cbdfc961fb3b25 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:07:52 +0100 Subject: [PATCH 03/36] Add `@Async` annotation to command executors to ensure that the server's main thread is not blocked --- .../parcellockers/discord/command/DiscordLinkCommand.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 77c3b902..e9b3bef5 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -8,6 +8,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.async.Async; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; @@ -65,6 +66,7 @@ public DiscordLinkCommand( this.messageConfig = messageConfig; } + @Async @Execute void linkSelf(@Context Player player, @Arg long discordId) { UUID playerUuid = player.getUniqueId(); @@ -130,6 +132,7 @@ void linkSelf(@Context Player player, @Arg long discordId) { }); } + @Async @Execute @Permission("parcellockers.admin") void linkOther(@Context CommandSender sender, @Arg Player player, @Arg long discordId) { From 1109f1190777440eeec384c078563694d24c595e Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:24:20 +0100 Subject: [PATCH 04/36] Remove `@Async` annotations --- .../parcellockers/discord/command/DiscordLinkCommand.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index e9b3bef5..77c3b902 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -8,7 +8,6 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import dev.rollczi.litecommands.annotations.argument.Arg; -import dev.rollczi.litecommands.annotations.async.Async; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; @@ -66,7 +65,6 @@ public DiscordLinkCommand( this.messageConfig = messageConfig; } - @Async @Execute void linkSelf(@Context Player player, @Arg long discordId) { UUID playerUuid = player.getUniqueId(); @@ -132,7 +130,6 @@ void linkSelf(@Context Player player, @Arg long discordId) { }); } - @Async @Execute @Permission("parcellockers.admin") void linkOther(@Context CommandSender sender, @Arg Player player, @Arg long discordId) { From 67ac9a29fc4b73ebacda881b9e72ba45deb54366 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:05:59 +0100 Subject: [PATCH 05/36] Refactor Discord integration to use reactive programming for login and verification processes --- .../parcellockers/ParcelLockers.java | 1 - .../discord/DiscordClientManager.java | 15 +- .../discord/command/DiscordLinkCommand.java | 209 +++++++++++------- 3 files changed, 144 insertions(+), 81 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 038ebe39..617553e5 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -222,7 +222,6 @@ public void onEnable() { discordLinkRepository, noticeService, miniMessage, - scheduler, messageConfig), new DiscordUnlinkCommand(discordLinkRepository, noticeService) ); diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java index 16b1cb47..b638b681 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java @@ -3,6 +3,7 @@ import discord4j.core.DiscordClient; import discord4j.core.GatewayDiscordClient; import java.util.logging.Logger; +import reactor.core.scheduler.Schedulers; public class DiscordClientManager { @@ -18,10 +19,18 @@ public DiscordClientManager(String token, Logger logger) { public void initialize() { this.logger.info("Discord integration is enabled. Logging in to Discord..."); - this.client = DiscordClient.create(this.token) + DiscordClient.create(this.token) .login() - .block(); - this.logger.info("Successfully logged in to Discord."); + .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(); } public void shutdown() { diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 77c3b902..d3f95b09 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -1,6 +1,6 @@ package com.eternalcode.parcellockers.discord.command; -import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.multification.notice.provider.NoticeProvider; import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import com.eternalcode.parcellockers.discord.DiscordLink; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; @@ -25,12 +25,16 @@ import java.util.List; import java.util.Random; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.event.ClickCallback; import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; @SuppressWarnings("UnstableApiUsage") @Command(name = "parcel linkdiscord") @@ -42,7 +46,6 @@ public class DiscordLinkCommand { private final DiscordLinkRepository discordLinkRepository; private final NoticeService noticeService; private final MiniMessage miniMessage; - private final Scheduler scheduler; private final MessageConfig messageConfig; private final Cache authCodesCache = Caffeine.newBuilder() @@ -54,14 +57,12 @@ public DiscordLinkCommand( DiscordLinkRepository discordLinkRepository, NoticeService noticeService, MiniMessage miniMessage, - Scheduler scheduler, MessageConfig messageConfig ) { this.client = client; this.discordLinkRepository = discordLinkRepository; this.noticeService = noticeService; this.miniMessage = miniMessage; - this.scheduler = scheduler; this.messageConfig = messageConfig; } @@ -70,96 +71,88 @@ void linkSelf(@Context Player player, @Arg long discordId) { UUID playerUuid = player.getUniqueId(); String discordIdString = String.valueOf(discordId); - // Check if the player already has a pending verification if (this.authCodesCache.getIfPresent(playerUuid) != null) { this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); return; } - // Check if the player already has a linked Discord account - this.discordLinkRepository.findByPlayerUuid(playerUuid).thenAccept(existingLink -> { - if (existingLink.isPresent()) { - this.noticeService.player(playerUuid, messages -> messages.discord.alreadyLinked); - return; - } - - // Check if the Discord account is already linked to another Minecraft account - this.discordLinkRepository.findByDiscordId(discordIdString).thenAccept(existingDiscordLink -> { - if (existingDiscordLink.isPresent()) { - this.noticeService.player(playerUuid, messages -> messages.discord.discordAlreadyLinked); - return; - } - - // Try to get the Discord user - User discordUser; - try { - discordUser = this.client.getUserById(Snowflake.of(discordId)).block(); - } catch (Exception ex) { - this.noticeService.player(playerUuid, messages -> messages.discord.userNotFound); - return; - } - - if (discordUser == null) { - this.noticeService.player(playerUuid, messages -> messages.discord.userNotFound); - return; + this.validateAndLink(playerUuid, discordIdString) + .thenCompose(validationResult -> { + if (!validationResult.isValid()) { + this.noticeService.player(playerUuid, validationResult.errorMessage()); + return CompletableFuture.completedFuture(null); } - // Generate a 4-digit verification code - String verificationCode = this.generateVerificationCode(); - - // Store the verification data in cache - this.authCodesCache.put(playerUuid, new VerificationData(discordIdString, verificationCode)); - - // Send the verification code to the Discord user via DM - discordUser.getPrivateChannel() - .flatMap(channel -> channel.createMessage( - this.messageConfig.discord.discordDmVerificationMessage - .replace("{CODE}", verificationCode) - .replace("{PLAYER}", player.getName()) - )) - .doOnSuccess(message -> { - this.noticeService.player(playerUuid, messages -> messages.discord.verificationCodeSent); - this.scheduler.run(() -> this.showVerificationDialog(player)); - }) - .doOnError(error -> { - this.authCodesCache.invalidate(playerUuid); - this.noticeService.player(playerUuid, messages -> messages.discord.cannotSendDm); - }) - .subscribe(); + return this.sendVerification(playerUuid, discordIdString, player, validationResult.discordUser()) + .toFuture(); + }) + .exceptionally(error -> { + this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); + return null; }); - }); } @Execute @Permission("parcellockers.admin") - void linkOther(@Context CommandSender sender, @Arg Player player, @Arg long discordId) { - UUID playerUuid = player.getUniqueId(); + void linkOther(@Context CommandSender sender, @Arg String playerName, @Arg long discordId) { String discordIdString = String.valueOf(discordId); - // Admin bypass - directly link without verification - this.discordLinkRepository.findByPlayerUuid(playerUuid).thenAccept(existingLink -> { - if (existingLink.isPresent()) { - this.noticeService.console(messages -> messages.discord.playerAlreadyLinked); - return; - } - - this.discordLinkRepository.findByDiscordId(discordIdString).thenAccept(existingDiscordLink -> { - if (existingDiscordLink.isPresent()) { - this.noticeService.console(messages -> messages.discord.discordAlreadyLinked); - return; + this.resolvePlayerUuid(playerName) + .thenCompose(playerUuid -> { + if (playerUuid == null) { + this.noticeService.viewer(sender, messages -> messages.discord.userNotFound); + return CompletableFuture.completedFuture(null); } - DiscordLink link = new DiscordLink(playerUuid, discordIdString); - this.discordLinkRepository.save(link).thenAccept(success -> { - if (success) { - this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); - this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); - } - }); + return this.validateAndLink(playerUuid, discordIdString) + .thenCompose(validationResult -> { + if (!validationResult.isValid()) { + this.noticeService.viewer(sender, validationResult.errorMessage()); + return CompletableFuture.completedFuture(null); + } + + DiscordLink link = new DiscordLink(playerUuid, discordIdString); + return this.discordLinkRepository.save(link) + .thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); + this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + } + }); + }); + }) + .exceptionally(error -> { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + return null; }); - }); + } + + private Mono sendVerification(UUID playerUuid, String discordId, Player player, User discordUser) { + String code = this.generateVerificationCode(); + + VerificationData data = new VerificationData(discordId, code); + if (this.authCodesCache.asMap().putIfAbsent(playerUuid, data) != null) { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); + return Mono.empty(); + } + + return discordUser.getPrivateChannel() + .flatMap(channel -> channel.createMessage( + this.messageConfig.discord.discordDmVerificationMessage + .replace("{CODE}", code) + .replace("{PLAYER}", player.getName()) + )) + .doOnSuccess(msg -> { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationCodeSent); + this.showVerificationDialog(player); + }) + .doOnError(error -> { + this.authCodesCache.invalidate(playerUuid); + this.noticeService.player(playerUuid, messages -> messages.discord.cannotSendDm); + }) + .then(); } private void showVerificationDialog(Player player) { @@ -232,10 +225,72 @@ private void handleVerification(Player player, String enteredCode) { }); } + private CompletableFuture validateAndLink(UUID playerUuid, String discordIdString) { + return this.discordLinkRepository.findByPlayerUuid(playerUuid) + .thenCompose(existingPlayerLink -> { + if (existingPlayerLink.isPresent()) { + return CompletableFuture.completedFuture( + ValidationResult.error(messages -> messages.discord.alreadyLinked) + ); + } + + return this.discordLinkRepository.findByDiscordId(discordIdString) + .thenCompose(existingDiscordLink -> { + if (existingDiscordLink.isPresent()) { + return CompletableFuture.completedFuture( + ValidationResult.error(messages -> messages.discord.discordAlreadyLinked) + ); + } + + return this.client.getUserById(Snowflake.of(Long.parseLong(discordIdString))) + .map(ValidationResult::success) + .onErrorResume(error -> Mono.just( + ValidationResult.error(messages -> messages.discord.userNotFound) + )) + .toFuture(); + }); + }); + } + private String generateVerificationCode() { int code = 1000 + RANDOM.nextInt(9000); // generates 1000-9999 return String.valueOf(code); } + private CompletableFuture resolvePlayerUuid(String playerName) { + return CompletableFuture.supplyAsync(() -> { + Player online = Bukkit.getPlayerExact(playerName); + if (online != null) { + return online.getUniqueId(); + } + + OfflinePlayer offline = Bukkit.getOfflinePlayer(playerName); + return offline.hasPlayedBefore() ? offline.getUniqueId() : null; + }); + } + private record VerificationData(String discordId, String code) {} + + private record ValidationResult( + boolean valid, + User discordUser, + NoticeProvider errorMessageGetter + ) { + static ValidationResult success(User user) { + return new ValidationResult(true, user, null); + } + + static ValidationResult error(NoticeProvider messageGetter) { + return new ValidationResult(false, null, messageGetter); + } + + boolean isValid() { + return this.valid; + } + + NoticeProvider errorMessage() { + return this.errorMessageGetter; + } + } } + From e98e26342e0a0b4478908f81b15d1cbb4cdde94b Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:07:23 +0100 Subject: [PATCH 06/36] Remove redundant check for bot admin role ID in Discord configuration validation --- src/main/java/com/eternalcode/parcellockers/ParcelLockers.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 617553e5..4dfd37d6 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -200,7 +200,6 @@ public void onEnable() { if (config.discord.enabled) { if (config.discord.botToken.isBlank() || - config.discord.botAdminRoleId.isBlank() || config.discord.serverId.isBlank() || config.discord.channelId.isBlank() || config.discord.botAdminRoleId.isBlank() From 875103046046791ee4a4fea4a00f5494f7214258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:08:19 +0100 Subject: [PATCH 07/36] Update src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../discord/repository/DiscordLinkRepositoryOrmLite.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java index 32f63a19..1e8547aa 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java @@ -30,7 +30,7 @@ public DiscordLinkRepositoryOrmLite(DatabaseManager databaseManager, Scheduler s @Override public CompletableFuture save(DiscordLink link) { return this.save(DiscordLinkEntity.class, DiscordLinkEntity.fromDomain(link)) - .thenApply(CreateOrUpdateStatus::isCreated); + .thenApply(status -> status.isCreated() || status.isUpdated()); } @Override From 031d4669bd6151be32396c92afa974ad82ad6c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:08:39 +0100 Subject: [PATCH 08/36] Update src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../discord/repository/DiscordLinkRepositoryOrmLite.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java index 1e8547aa..485e0bb0 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java @@ -45,7 +45,7 @@ public CompletableFuture> findByDiscordId(String discordId var queryBuilder = dao.queryBuilder() .where().eq(ID_COLUMN_NAME, discordId); return dao.queryForFirst(queryBuilder.prepare()); - }).thenApply(optionalEntity -> optionalEntity != null ? Optional.of(optionalEntity.toDomain()) : Optional.empty()); + }).thenApply(entity -> Optional.ofNullable(entity).map(DiscordLinkEntity::toDomain)); } @Override From ed98f109cdc9b07196383972db202937710101a3 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:40:13 +0100 Subject: [PATCH 09/36] Add service layer --- .../parcellockers/ParcelLockers.java | 7 +++- .../discord/DiscordLinkService.java | 18 ++++++++ .../discord/DiscordLinkServiceImpl.java | 41 +++++++++++++++++++ .../discord/command/DiscordLinkCommand.java | 19 ++++----- .../discord/command/DiscordUnlinkCommand.java | 20 ++++----- 5 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkServiceImpl.java diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 4dfd37d6..f3e616e4 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -17,6 +17,8 @@ import com.eternalcode.parcellockers.delivery.DeliveryManager; import com.eternalcode.parcellockers.delivery.repository.DeliveryRepositoryOrmLite; import com.eternalcode.parcellockers.discord.DiscordClientManager; +import com.eternalcode.parcellockers.discord.DiscordLinkService; +import com.eternalcode.parcellockers.discord.DiscordLinkServiceImpl; import com.eternalcode.parcellockers.discord.command.DiscordLinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; @@ -197,6 +199,7 @@ public void onEnable() { .missingPermission(new MissingPermissionsHandlerImpl(noticeService)); DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); + DiscordLinkService discordLinkService = new DiscordLinkServiceImpl(discordLinkRepository); if (config.discord.enabled) { if (config.discord.botToken.isBlank() || @@ -218,11 +221,11 @@ public void onEnable() { liteCommandsBuilder.commands( new DiscordLinkCommand( this.discordClientManager.getClient(), - discordLinkRepository, + discordLinkService, noticeService, miniMessage, messageConfig), - new DiscordUnlinkCommand(discordLinkRepository, noticeService) + new DiscordUnlinkCommand(discordLinkService, noticeService) ); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java new file mode 100644 index 00000000..11c36592 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java @@ -0,0 +1,18 @@ +package com.eternalcode.parcellockers.discord; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public interface DiscordLinkService { + + CompletableFuture unlinkDiscordId(String discordId); + + CompletableFuture unlinkPlayer(UUID playerUuid); + + CompletableFuture createLink(UUID playerUuid, String discordId); + + CompletableFuture> findLinkByDiscordId(String discordId); + + CompletableFuture> findLinkByPlayer(UUID playerUuid); +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkServiceImpl.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkServiceImpl.java new file mode 100644 index 00000000..d5c67220 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkServiceImpl.java @@ -0,0 +1,41 @@ +package com.eternalcode.parcellockers.discord; + +import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class DiscordLinkServiceImpl implements DiscordLinkService { + + private final DiscordLinkRepository repository; + + public DiscordLinkServiceImpl(DiscordLinkRepository repository) { + this.repository = repository; + } + + @Override + public CompletableFuture> findLinkByPlayer(UUID playerUuid) { + return this.repository.findByPlayerUuid(playerUuid); + } + + @Override + public CompletableFuture> findLinkByDiscordId(String discordId) { + return this.repository.findByDiscordId(discordId); + } + + @Override + public CompletableFuture createLink(UUID playerUuid, String discordId) { + DiscordLink link = new DiscordLink(playerUuid, discordId); + return this.repository.save(link); + } + + @Override + public CompletableFuture unlinkPlayer(UUID playerUuid) { + return this.repository.deleteByPlayerUuid(playerUuid); + } + + @Override + public CompletableFuture unlinkDiscordId(String discordId) { + return this.repository.deleteByDiscordId(discordId); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index d3f95b09..41b3d536 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -2,8 +2,7 @@ import com.eternalcode.multification.notice.provider.NoticeProvider; import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; -import com.eternalcode.parcellockers.discord.DiscordLink; -import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; +import com.eternalcode.parcellockers.discord.DiscordLinkService; import com.eternalcode.parcellockers.notification.NoticeService; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; @@ -43,7 +42,7 @@ public class DiscordLinkCommand { private static final Random RANDOM = new Random(); private final GatewayDiscordClient client; - private final DiscordLinkRepository discordLinkRepository; + private final DiscordLinkService discordLinkService; private final NoticeService noticeService; private final MiniMessage miniMessage; private final MessageConfig messageConfig; @@ -54,13 +53,13 @@ public class DiscordLinkCommand { public DiscordLinkCommand( GatewayDiscordClient client, - DiscordLinkRepository discordLinkRepository, + DiscordLinkService discordLinkService, NoticeService noticeService, MiniMessage miniMessage, MessageConfig messageConfig ) { this.client = client; - this.discordLinkRepository = discordLinkRepository; + this.discordLinkService = discordLinkService; this.noticeService = noticeService; this.miniMessage = miniMessage; this.messageConfig = messageConfig; @@ -111,8 +110,7 @@ void linkOther(@Context CommandSender sender, @Arg String playerName, @Arg long return CompletableFuture.completedFuture(null); } - DiscordLink link = new DiscordLink(playerUuid, discordIdString); - return this.discordLinkRepository.save(link) + return this.discordLinkService.createLink(playerUuid, discordIdString) .thenAccept(success -> { if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); @@ -215,8 +213,7 @@ private void handleVerification(Player player, String enteredCode) { // Code matches - remove from cache and create the link this.authCodesCache.invalidate(playerUuid); - DiscordLink link = new DiscordLink(playerUuid, verificationData.discordId()); - this.discordLinkRepository.save(link).thenAccept(success -> { + this.discordLinkService.createLink(playerUuid, verificationData.discordId()).thenAccept(success -> { if (success) { this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); } else { @@ -226,7 +223,7 @@ private void handleVerification(Player player, String enteredCode) { } private CompletableFuture validateAndLink(UUID playerUuid, String discordIdString) { - return this.discordLinkRepository.findByPlayerUuid(playerUuid) + return this.discordLinkService.findLinkByPlayer(playerUuid) .thenCompose(existingPlayerLink -> { if (existingPlayerLink.isPresent()) { return CompletableFuture.completedFuture( @@ -234,7 +231,7 @@ private CompletableFuture validateAndLink(UUID playerUuid, Str ); } - return this.discordLinkRepository.findByDiscordId(discordIdString) + return this.discordLinkService.findLinkByDiscordId(discordIdString) .thenCompose(existingDiscordLink -> { if (existingDiscordLink.isPresent()) { return CompletableFuture.completedFuture( diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java index c7edf7f1..fe48e791 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java @@ -1,6 +1,6 @@ package com.eternalcode.parcellockers.discord.command; -import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; +import com.eternalcode.parcellockers.discord.DiscordLinkService; import com.eternalcode.parcellockers.notification.NoticeService; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.command.Command; @@ -14,14 +14,14 @@ @Command(name = "parcel unlinkdiscord") public class DiscordUnlinkCommand { - private final DiscordLinkRepository discordLinkRepository; + private final DiscordLinkService discordLinkService; private final NoticeService noticeService; public DiscordUnlinkCommand( - DiscordLinkRepository discordLinkRepository, + DiscordLinkService discordLinkService, NoticeService noticeService ) { - this.discordLinkRepository = discordLinkRepository; + this.discordLinkService = discordLinkService; this.noticeService = noticeService; } @@ -29,13 +29,13 @@ public DiscordUnlinkCommand( void unlinkSelf(@Context Player player) { UUID playerUuid = player.getUniqueId(); - this.discordLinkRepository.findByPlayerUuid(playerUuid).thenAccept(existingLink -> { + this.discordLinkService.findLinkByPlayer(playerUuid).thenAccept(existingLink -> { if (existingLink.isEmpty()) { this.noticeService.player(playerUuid, messages -> messages.discord.notLinked); return; } - this.discordLinkRepository.deleteByPlayerUuid(playerUuid).thenAccept(success -> { + this.discordLinkService.unlinkPlayer(playerUuid).thenAccept(success -> { if (success) { this.noticeService.player(playerUuid, messages -> messages.discord.unlinkSuccess); } else { @@ -50,13 +50,13 @@ void unlinkSelf(@Context Player player) { void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { UUID targetUuid = targetPlayer.getUniqueId(); - this.discordLinkRepository.findByPlayerUuid(targetUuid).thenAccept(existingLink -> { + this.discordLinkService.findLinkByPlayer(targetUuid).thenAccept(existingLink -> { if (existingLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.playerNotLinked); return; } - this.discordLinkRepository.deleteByPlayerUuid(targetUuid).thenAccept(success -> { + this.discordLinkService.unlinkPlayer(targetUuid).thenAccept(success -> { if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkSuccess); this.noticeService.player(targetUuid, messages -> messages.discord.unlinkSuccess); @@ -72,13 +72,13 @@ void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { void unlinkByDiscordId(@Context CommandSender sender, @Arg long discordId) { String discordIdString = String.valueOf(discordId); - this.discordLinkRepository.findByDiscordId(discordIdString).thenAccept(existingLink -> { + this.discordLinkService.findLinkByDiscordId(discordIdString).thenAccept(existingLink -> { if (existingLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); return; } - this.discordLinkRepository.deleteByDiscordId(discordIdString).thenAccept(success -> { + this.discordLinkService.unlinkDiscordId(discordIdString).thenAccept(success -> { if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkByDiscordSuccess); } else { From 14ae1982413d1847d90c695956fc5d7b30da7b24 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:52:12 +0100 Subject: [PATCH 10/36] Add DiscordSRV hook integration --- build.gradle.kts | 8 ++ .../parcellockers/ParcelLockers.java | 17 ++- .../implementation/MessageConfig.java | 17 +++ .../discord/DiscordSrvLinkService.java | 114 ++++++++++++++++++ .../command/DiscordSrvLinkCommand.java | 54 +++++++++ .../command/DiscordSrvUnlinkCommand.java | 90 ++++++++++++++ 6 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java diff --git a/build.gradle.kts b/build.gradle.kts index bdb37dbb..2c1809e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ repositories { maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.eternalcode.pl/releases") maven("https://storehouse.okaeri.eu/repository/maven-public/") + maven("https://nexus.scarsz.me/content/groups/public/") // DiscordSRV } dependencies { @@ -81,6 +82,9 @@ dependencies { // discord integration library paperLibrary("com.discord4j:discord4j-core:3.3.0") + // discordsrv (optional integration) + compileOnly("com.discordsrv:discordsrv:1.29.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.2") testImplementation("org.junit.jupiter:junit-jupiter-params:6.0.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.2") @@ -111,6 +115,10 @@ paper { required = true load = PaperPluginDescription.RelativeLoadOrder.BEFORE } + register("DiscordSRV") { + required = false + load = PaperPluginDescription.RelativeLoadOrder.BEFORE + } } } diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index f3e616e4..76a203da 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -19,7 +19,10 @@ import com.eternalcode.parcellockers.discord.DiscordClientManager; import com.eternalcode.parcellockers.discord.DiscordLinkService; import com.eternalcode.parcellockers.discord.DiscordLinkServiceImpl; +import com.eternalcode.parcellockers.discord.DiscordSrvLinkService; import com.eternalcode.parcellockers.discord.command.DiscordLinkCommand; +import com.eternalcode.parcellockers.discord.command.DiscordSrvLinkCommand; +import com.eternalcode.parcellockers.discord.command.DiscordSrvUnlinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; @@ -201,7 +204,19 @@ public void onEnable() { DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); DiscordLinkService discordLinkService = new DiscordLinkServiceImpl(discordLinkRepository); - if (config.discord.enabled) { + // Check if DiscordSRV is installed and available + boolean discordSrvAvailable = server.getPluginManager().isPluginEnabled("DiscordSRV") + && DiscordSrvLinkService.isAvailable(); + + if (discordSrvAvailable) { + this.getLogger().info("DiscordSRV detected! Using DiscordSRV for account linking."); + DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(); + + liteCommandsBuilder.commands( + new DiscordSrvLinkCommand(discordSrvLinkService, noticeService), + new DiscordSrvUnlinkCommand(discordSrvLinkService, noticeService) + ); + } else if (config.discord.enabled) { if (config.discord.botToken.isBlank() || config.discord.serverId.isBlank() || config.discord.channelId.isBlank() || diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index 1dfab877..7ef87fac 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -256,5 +256,22 @@ public static class DiscordMessages extends OkaeriConfig { @Comment({"", "# The message sent to the Discord user via DM" }) @Comment("# Placeholders: {CODE} - the verification code, {PLAYER} - the Minecraft player name") public String discordDmVerificationMessage = "**📦 ParcelLockers Verification**\n\nPlayer **{PLAYER}** is trying to link their Minecraft account to your Discord account.\n\nYour verification code is: **{CODE}**\n\nThis code will expire in 2 minutes."; + + @Comment({"", "# DiscordSRV integration messages" }) + @Comment("# These messages are shown when DiscordSRV is installed and handles account linking") + public Notice discordSrvLinkRedirect = Notice.builder() + .chat("&6⚠ &eTo link your Discord account, use the DiscordSRV linking system.") + .chat("&6⚠ &eYour linking code is: &a{CODE}") + .chat("&6⚠ &eSend this code to the Discord bot in a private message.") + .sound(SoundEventKeys.BLOCK_NOTE_BLOCK_CHIME) + .build(); + public Notice discordSrvAlreadyLinked = Notice.builder() + .chat("&2✔ &aYour account is already linked via DiscordSRV!") + .sound(SoundEventKeys.ENTITY_VILLAGER_YES) + .build(); + public Notice discordSrvUnlinkRedirect = Notice.builder() + .chat("&6⚠ &eTo unlink your Discord account, please use the DiscordSRV unlinking system.") + .sound(SoundEventKeys.BLOCK_NOTE_BLOCK_CHIME) + .build(); } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java new file mode 100644 index 00000000..ee026502 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java @@ -0,0 +1,114 @@ +package com.eternalcode.parcellockers.discord; + +import github.scarsz.discordsrv.DiscordSRV; +import github.scarsz.discordsrv.dependencies.jda.api.entities.User; +import github.scarsz.discordsrv.util.DiscordUtil; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * DiscordSRV-based implementation of DiscordLinkService. + * Delegates account linking functionality to DiscordSRV plugin. + */ +public class DiscordSrvLinkService implements DiscordLinkService { + + /** + * Checks if DiscordSRV is properly initialized and ready. + */ + public static boolean isAvailable() { + try { + return DiscordSRV.getPlugin() != null + && DiscordSRV.getPlugin().getAccountLinkManager() != null; + } catch (Exception | NoClassDefFoundError e) { + return false; + } + } + + @Override + public CompletableFuture> findLinkByPlayer(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> { + String discordId = DiscordSRV.getPlugin().getAccountLinkManager().getDiscordId(playerUuid); + if (discordId == null) { + return Optional.empty(); + } + return Optional.of(new DiscordLink(playerUuid, discordId)); + }); + } + + @Override + public CompletableFuture> findLinkByDiscordId(String discordId) { + return CompletableFuture.supplyAsync(() -> { + UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(discordId); + if (playerUuid == null) { + return Optional.empty(); + } + return Optional.of(new DiscordLink(playerUuid, discordId)); + }); + } + + @Override + public CompletableFuture createLink(UUID playerUuid, String discordId) { + return CompletableFuture.supplyAsync(() -> { + try { + DiscordSRV.getPlugin().getAccountLinkManager().link(discordId, playerUuid); + return true; + } catch (Exception e) { + return false; + } + }); + } + + @Override + public CompletableFuture unlinkPlayer(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> { + try { + DiscordSRV.getPlugin().getAccountLinkManager().unlink(playerUuid); + return true; + } catch (Exception e) { + return false; + } + }); + } + + @Override + public CompletableFuture unlinkDiscordId(String discordId) { + return CompletableFuture.supplyAsync(() -> { + try { + UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(discordId); + if (playerUuid == null) { + return false; + } + DiscordSRV.getPlugin().getAccountLinkManager().unlink(playerUuid); + return true; + } catch (Exception e) { + return false; + } + }); + } + + /** + * Gets the Discord user by their ID using DiscordSRV's JDA instance. + */ + public Optional getDiscordUser(String discordId) { + try { + return Optional.ofNullable(DiscordUtil.getUserById(discordId)); + } catch (Exception e) { + return Optional.empty(); + } + } + + /** + * Gets the linking code for a player to use in Discord. + * Returns empty if the player is already linked. + */ + public Optional getLinkingCode(UUID playerUuid) { + String existingDiscordId = DiscordSRV.getPlugin().getAccountLinkManager().getDiscordId(playerUuid); + if (existingDiscordId != null) { + return Optional.empty(); // Already linked + } + + String code = DiscordSRV.getPlugin().getAccountLinkManager().generateCode(playerUuid); + return Optional.ofNullable(code); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java new file mode 100644 index 00000000..0aafa947 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java @@ -0,0 +1,54 @@ +package com.eternalcode.parcellockers.discord.command; + +import com.eternalcode.parcellockers.discord.DiscordSrvLinkService; +import com.eternalcode.parcellockers.notification.NoticeService; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.annotations.execute.Execute; +import java.util.Optional; +import java.util.UUID; +import org.bukkit.entity.Player; + +/** + * Command for linking Discord accounts when DiscordSRV is installed. + * Redirects users to use DiscordSRV's linking system. + */ +@Command(name = "parcel linkdiscord") +public class DiscordSrvLinkCommand { + + private final DiscordSrvLinkService discordSrvLinkService; + private final NoticeService noticeService; + + public DiscordSrvLinkCommand( + DiscordSrvLinkService discordSrvLinkService, + NoticeService noticeService + ) { + this.discordSrvLinkService = discordSrvLinkService; + this.noticeService = noticeService; + } + + @Execute + void linkSelf(@Context Player player) { + UUID playerUuid = player.getUniqueId(); + + this.discordSrvLinkService.findLinkByPlayer(playerUuid).thenAccept(existingLink -> { + if (existingLink.isPresent()) { + this.noticeService.player(playerUuid, messages -> messages.discord.discordSrvAlreadyLinked); + return; + } + + Optional linkingCode = this.discordSrvLinkService.getLinkingCode(playerUuid); + if (linkingCode.isEmpty()) { + // This shouldn't happen if the player is not linked, but handle it gracefully + this.noticeService.player(playerUuid, messages -> messages.discord.discordSrvAlreadyLinked); + return; + } + + this.noticeService.create() + .notice(messages -> messages.discord.discordSrvLinkRedirect) + .placeholder("{CODE}", linkingCode.get()) + .player(playerUuid) + .send(); + }); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java new file mode 100644 index 00000000..57b820c8 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java @@ -0,0 +1,90 @@ +package com.eternalcode.parcellockers.discord.command; + +import com.eternalcode.parcellockers.discord.DiscordSrvLinkService; +import com.eternalcode.parcellockers.notification.NoticeService; +import dev.rollczi.litecommands.annotations.argument.Arg; +import dev.rollczi.litecommands.annotations.command.Command; +import dev.rollczi.litecommands.annotations.context.Context; +import dev.rollczi.litecommands.annotations.execute.Execute; +import dev.rollczi.litecommands.annotations.permission.Permission; +import java.util.UUID; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +/** + * Command for unlinking Discord accounts when DiscordSRV is installed. + * Regular players are redirected to use DiscordSRV's unlinking system, + * while admins can still forcefully unlink accounts. + */ +@Command(name = "parcel unlinkdiscord") +public class DiscordSrvUnlinkCommand { + + private final DiscordSrvLinkService discordSrvLinkService; + private final NoticeService noticeService; + + public DiscordSrvUnlinkCommand( + DiscordSrvLinkService discordSrvLinkService, + NoticeService noticeService + ) { + this.discordSrvLinkService = discordSrvLinkService; + this.noticeService = noticeService; + } + + @Execute + void unlinkSelf(@Context Player player) { + UUID playerUuid = player.getUniqueId(); + + this.discordSrvLinkService.findLinkByPlayer(playerUuid).thenAccept(existingLink -> { + if (existingLink.isEmpty()) { + this.noticeService.player(playerUuid, messages -> messages.discord.notLinked); + return; + } + + // Redirect to DiscordSRV's unlinking system + this.noticeService.player(playerUuid, messages -> messages.discord.discordSrvUnlinkRedirect); + }); + } + + @Execute + @Permission("parcellockers.admin") + void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { + UUID targetUuid = targetPlayer.getUniqueId(); + + this.discordSrvLinkService.findLinkByPlayer(targetUuid).thenAccept(existingLink -> { + if (existingLink.isEmpty()) { + this.noticeService.viewer(sender, messages -> messages.discord.playerNotLinked); + return; + } + + this.discordSrvLinkService.unlinkPlayer(targetUuid).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkSuccess); + this.noticeService.player(targetUuid, messages -> messages.discord.unlinkSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + } + }); + }); + } + + @Execute + @Permission("parcellockers.admin") + void unlinkByDiscordId(@Context CommandSender sender, @Arg long discordId) { + String discordIdString = String.valueOf(discordId); + + this.discordSrvLinkService.findLinkByDiscordId(discordIdString).thenAccept(existingLink -> { + if (existingLink.isEmpty()) { + this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); + return; + } + + this.discordSrvLinkService.unlinkDiscordId(discordIdString).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkByDiscordSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + } + }); + }); + } +} From 7de214f6381bd0688c9480fcb719ab204481bc3a Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:20:39 +0100 Subject: [PATCH 11/36] feat: Update DiscordSRV integration and add parcel delivery notifications --- build.gradle.kts | 3 +- .../parcellockers/ParcelLockers.java | 78 +++++++++-------- .../implementation/MessageConfig.java | 4 + .../discord/DiscordSrvLinkService.java | 12 --- .../ParcelDeliverNotificationController.java | 84 +++++++++++++++++++ 5 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java diff --git a/build.gradle.kts b/build.gradle.kts index 2c1809e6..2d0e02e5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ dependencies { paperLibrary("com.discord4j:discord4j-core:3.3.0") // discordsrv (optional integration) - compileOnly("com.discordsrv:discordsrv:1.29.0") + compileOnly("com.discordsrv:discordsrv:1.30.4") testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.2") testImplementation("org.junit.jupiter:junit-jupiter-params:6.0.2") @@ -135,6 +135,7 @@ tasks { downloadPlugins.modrinth("luckperms", "v5.5.17-bukkit") downloadPlugins.modrinth("vaultunlocked", "2.17.0") downloadPlugins.modrinth("essentialsx", "2.21.2") + downloadPlugins.modrinth("discordsrv", "1.30.4") } test { diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 76a203da..feb7412a 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -24,6 +24,7 @@ import com.eternalcode.parcellockers.discord.command.DiscordSrvLinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordSrvUnlinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; +import com.eternalcode.parcellockers.discord.controller.ParcelDeliverNotificationController; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; import com.eternalcode.parcellockers.gui.GuiManager; @@ -204,43 +205,52 @@ public void onEnable() { DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); DiscordLinkService discordLinkService = new DiscordLinkServiceImpl(discordLinkRepository); - // Check if DiscordSRV is installed and available - boolean discordSrvAvailable = server.getPluginManager().isPluginEnabled("DiscordSRV") - && DiscordSrvLinkService.isAvailable(); - - if (discordSrvAvailable) { - this.getLogger().info("DiscordSRV detected! Using DiscordSRV for account linking."); - DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(); - - liteCommandsBuilder.commands( - new DiscordSrvLinkCommand(discordSrvLinkService, noticeService), - new DiscordSrvUnlinkCommand(discordSrvLinkService, noticeService) - ); - } else if (config.discord.enabled) { - if (config.discord.botToken.isBlank() || - config.discord.serverId.isBlank() || - config.discord.channelId.isBlank() || - config.discord.botAdminRoleId.isBlank() - ) { - this.getLogger().severe("Discord integration is enabled but some of the properties are not set! Disabling..."); - server.getPluginManager().disablePlugin(this); - return; + if (config.discord.enabled) { + if (server.getPluginManager().isPluginEnabled("DiscordSRV")) { + this.getLogger().info("DiscordSRV detected! Using DiscordSRV for account linking."); + DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(); + + liteCommandsBuilder.commands( + new DiscordSrvLinkCommand(discordSrvLinkService, noticeService), + new DiscordSrvUnlinkCommand(discordSrvLinkService, noticeService) + ); + } else if (config.discord.enabled) { + if (config.discord.botToken.isBlank() || + config.discord.serverId.isBlank() || + config.discord.channelId.isBlank() || + config.discord.botAdminRoleId.isBlank() + ) { + this.getLogger() + .severe("Discord integration is enabled but some of the properties are not set! Disabling..."); + server.getPluginManager().disablePlugin(this); + return; + } + + this.discordClientManager = new DiscordClientManager( + config.discord.botToken, + this.getLogger() + ); + this.discordClientManager.initialize(); + + liteCommandsBuilder.commands( + new DiscordLinkCommand( + this.discordClientManager.getClient(), + discordLinkService, + noticeService, + miniMessage, + messageConfig), + new DiscordUnlinkCommand(discordLinkService, noticeService) + ); } - - this.discordClientManager = new DiscordClientManager( - config.discord.botToken, - this.getLogger() - ); - this.discordClientManager.initialize(); - - liteCommandsBuilder.commands( - new DiscordLinkCommand( + server.getPluginManager().registerEvents( + new ParcelDeliverNotificationController( this.discordClientManager.getClient(), discordLinkService, - noticeService, - miniMessage, - messageConfig), - new DiscordUnlinkCommand(discordLinkService, noticeService) + userManager, + messageConfig, + this.getLogger() + ), + this ); } diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index 7ef87fac..febcfd07 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -257,6 +257,10 @@ public static class DiscordMessages extends OkaeriConfig { @Comment("# Placeholders: {CODE} - the verification code, {PLAYER} - the Minecraft player name") public String discordDmVerificationMessage = "**📦 ParcelLockers Verification**\n\nPlayer **{PLAYER}** is trying to link their Minecraft account to your Discord account.\n\nYour verification code is: **{CODE}**\n\nThis code will expire in 2 minutes."; + @Comment({"", "# The message sent to the Discord user when a parcel is delivered" }) + @Comment("# Placeholders: {PARCEL_NAME}, {SENDER}, {RECEIVER}, {DESCRIPTION}, {SIZE}, {PRIORITY}") + public String parcelDeliveryNotification = "**📦 Parcel Delivered!**\n\nYour parcel **{PARCEL_NAME}** has been delivered!\n\n**From:** {SENDER}\n**Size:** {SIZE}\n**Priority:** {PRIORITY}\n**Description:** {DESCRIPTION}"; + @Comment({"", "# DiscordSRV integration messages" }) @Comment("# These messages are shown when DiscordSRV is installed and handles account linking") public Notice discordSrvLinkRedirect = Notice.builder() diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java index ee026502..36b59cc1 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java @@ -13,18 +13,6 @@ */ public class DiscordSrvLinkService implements DiscordLinkService { - /** - * Checks if DiscordSRV is properly initialized and ready. - */ - public static boolean isAvailable() { - try { - return DiscordSRV.getPlugin() != null - && DiscordSRV.getPlugin().getAccountLinkManager() != null; - } catch (Exception | NoClassDefFoundError e) { - return false; - } - } - @Override public CompletableFuture> findLinkByPlayer(UUID playerUuid) { return CompletableFuture.supplyAsync(() -> { diff --git a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java new file mode 100644 index 00000000..2d631e4a --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java @@ -0,0 +1,84 @@ +package com.eternalcode.parcellockers.discord.controller; + +import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; +import com.eternalcode.parcellockers.discord.DiscordLinkService; +import com.eternalcode.parcellockers.parcel.Parcel; +import com.eternalcode.parcellockers.parcel.event.ParcelDeliverEvent; +import com.eternalcode.parcellockers.user.User; +import com.eternalcode.parcellockers.user.UserManager; +import discord4j.common.util.Snowflake; +import discord4j.core.GatewayDiscordClient; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import reactor.core.scheduler.Schedulers; + +public class ParcelDeliverNotificationController implements Listener { + + private final GatewayDiscordClient client; + private final DiscordLinkService discordLinkService; + private final UserManager userManager; + private final MessageConfig messageConfig; + private final Logger logger; + + public ParcelDeliverNotificationController( + GatewayDiscordClient client, + DiscordLinkService discordLinkService, + UserManager userManager, + MessageConfig messageConfig, + Logger logger + ) { + this.client = client; + this.discordLinkService = discordLinkService; + this.userManager = userManager; + this.messageConfig = messageConfig; + this.logger = logger; + } + + @EventHandler + void onParcelDeliver(ParcelDeliverEvent event) { + if (event.isCancelled()) { + return; + } + + Parcel parcel = event.getParcel(); + UUID receiverUuid = parcel.receiver(); + + this.discordLinkService.findLinkByPlayer(receiverUuid) + .thenAccept(optionalLink -> optionalLink.ifPresent(link -> { + String discordId = link.discordId(); + this.sendDeliveryNotification(parcel, discordId); + })); + } + + private void sendDeliveryNotification(Parcel parcel, String discordId) { + CompletableFuture senderNameFuture = this.userManager.get(parcel.sender()) + .thenApply(optionalUser -> optionalUser.map(User::name).orElse("Unknown")); + + CompletableFuture receiverNameFuture = this.userManager.get(parcel.receiver()) + .thenApply(optionalUser -> optionalUser.map(User::name).orElse("Unknown")); + + senderNameFuture.thenCombine(receiverNameFuture, (senderName, receiverName) -> { + String message = this.messageConfig.discord.parcelDeliveryNotification + .replace("{PARCEL_NAME}", parcel.name()) + .replace("{SENDER}", senderName) + .replace("{RECEIVER}", receiverName) + .replace("{DESCRIPTION}", parcel.description() != null ? parcel.description() : "") + .replace("{SIZE}", parcel.size().name()) + .replace("{PRIORITY}", parcel.priority() ? "Yes" : "No"); + + this.client.getUserById(Snowflake.of(Long.parseLong(discordId))) + .flatMap(user -> user.getPrivateChannel()) + .flatMap(channel -> channel.createMessage(message)) + .subscribeOn(Schedulers.boundedElastic()) + .doOnError(error -> this.logger.warning( + "Failed to send parcel delivery notification to Discord user " + discordId + ": " + error.getMessage() + )) + .subscribe(); + + return null; + }); + } +} From 143d8a103bf137f0793a31a1c403f3cb8f3d0970 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:41:13 +0100 Subject: [PATCH 12/36] feat: Implement abstract DiscordNotificationService with Discord4J and DiscordSRV support --- .../parcellockers/ParcelLockers.java | 44 ++++++++++++------- ...l.java => DiscordFallbackLinkService.java} | 4 +- .../discord/DiscordNotificationType.java | 7 --- .../ParcelDeliverNotificationController.java | 25 +++-------- .../Discord4JNotificationService.java | 40 +++++++++++++++++ .../DiscordNotificationService.java | 19 ++++++++ .../DiscordSrvNotificationService.java | 44 +++++++++++++++++++ 7 files changed, 140 insertions(+), 43 deletions(-) rename src/main/java/com/eternalcode/parcellockers/discord/{DiscordLinkServiceImpl.java => DiscordFallbackLinkService.java} (89%) delete mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index feb7412a..2d7200eb 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -10,6 +10,7 @@ import com.eternalcode.parcellockers.configuration.ConfigService; import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import com.eternalcode.parcellockers.configuration.implementation.PluginConfig; +import com.eternalcode.parcellockers.configuration.implementation.PluginConfig.DiscordSettings; import com.eternalcode.parcellockers.content.ParcelContentManager; import com.eternalcode.parcellockers.content.repository.ParcelContentRepository; import com.eternalcode.parcellockers.content.repository.ParcelContentRepositoryOrmLite; @@ -17,14 +18,17 @@ import com.eternalcode.parcellockers.delivery.DeliveryManager; import com.eternalcode.parcellockers.delivery.repository.DeliveryRepositoryOrmLite; import com.eternalcode.parcellockers.discord.DiscordClientManager; +import com.eternalcode.parcellockers.discord.DiscordFallbackLinkService; import com.eternalcode.parcellockers.discord.DiscordLinkService; -import com.eternalcode.parcellockers.discord.DiscordLinkServiceImpl; import com.eternalcode.parcellockers.discord.DiscordSrvLinkService; import com.eternalcode.parcellockers.discord.command.DiscordLinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordSrvLinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordSrvUnlinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; import com.eternalcode.parcellockers.discord.controller.ParcelDeliverNotificationController; +import com.eternalcode.parcellockers.discord.notification.Discord4JNotificationService; +import com.eternalcode.parcellockers.discord.notification.DiscordNotificationService; +import com.eternalcode.parcellockers.discord.notification.DiscordSrvNotificationService; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; import com.eternalcode.parcellockers.gui.GuiManager; @@ -203,22 +207,26 @@ public void onEnable() { .missingPermission(new MissingPermissionsHandlerImpl(noticeService)); DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); - DiscordLinkService discordLinkService = new DiscordLinkServiceImpl(discordLinkRepository); + DiscordSettings discordSettings = config.discord; + if (discordSettings.enabled) { + DiscordNotificationService notificationService; + DiscordLinkService activeLinkService; - if (config.discord.enabled) { if (server.getPluginManager().isPluginEnabled("DiscordSRV")) { this.getLogger().info("DiscordSRV detected! Using DiscordSRV for account linking."); DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(); + activeLinkService = discordSrvLinkService; + notificationService = new DiscordSrvNotificationService(this.getLogger()); liteCommandsBuilder.commands( new DiscordSrvLinkCommand(discordSrvLinkService, noticeService), new DiscordSrvUnlinkCommand(discordSrvLinkService, noticeService) ); - } else if (config.discord.enabled) { - if (config.discord.botToken.isBlank() || - config.discord.serverId.isBlank() || - config.discord.channelId.isBlank() || - config.discord.botAdminRoleId.isBlank() + } else { + if ((discordSettings.botToken == null || discordSettings.botToken.isBlank()) || + discordSettings.serverId.isBlank() || + discordSettings.channelId.isBlank() || + discordSettings.botAdminRoleId.isBlank() ) { this.getLogger() .severe("Discord integration is enabled but some of the properties are not set! Disabling..."); @@ -227,28 +235,34 @@ public void onEnable() { } this.discordClientManager = new DiscordClientManager( - config.discord.botToken, + discordSettings.botToken, this.getLogger() ); this.discordClientManager.initialize(); + activeLinkService = new DiscordFallbackLinkService(discordLinkRepository); + notificationService = new Discord4JNotificationService( + this.discordClientManager.getClient(), + this.getLogger() + ); + liteCommandsBuilder.commands( new DiscordLinkCommand( this.discordClientManager.getClient(), - discordLinkService, + activeLinkService, noticeService, miniMessage, messageConfig), - new DiscordUnlinkCommand(discordLinkService, noticeService) + new DiscordUnlinkCommand(activeLinkService, noticeService) ); } + server.getPluginManager().registerEvents( new ParcelDeliverNotificationController( - this.discordClientManager.getClient(), - discordLinkService, + notificationService, + activeLinkService, userManager, - messageConfig, - this.getLogger() + messageConfig ), this ); diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkServiceImpl.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java similarity index 89% rename from src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkServiceImpl.java rename to src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java index d5c67220..132e9bde 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkServiceImpl.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java @@ -5,11 +5,11 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; -public class DiscordLinkServiceImpl implements DiscordLinkService { +public class DiscordFallbackLinkService implements DiscordLinkService { private final DiscordLinkRepository repository; - public DiscordLinkServiceImpl(DiscordLinkRepository repository) { + public DiscordFallbackLinkService(DiscordLinkRepository repository) { this.repository = repository; } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java deleted file mode 100644 index de1880f4..00000000 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordNotificationType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.eternalcode.parcellockers.discord; - -public enum DiscordNotificationType { - SERVER, - DM, - BOTH -} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java index 2d631e4a..3482be0b 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java @@ -2,39 +2,33 @@ import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import com.eternalcode.parcellockers.discord.DiscordLinkService; +import com.eternalcode.parcellockers.discord.notification.DiscordNotificationService; import com.eternalcode.parcellockers.parcel.Parcel; import com.eternalcode.parcellockers.parcel.event.ParcelDeliverEvent; import com.eternalcode.parcellockers.user.User; import com.eternalcode.parcellockers.user.UserManager; -import discord4j.common.util.Snowflake; -import discord4j.core.GatewayDiscordClient; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.logging.Logger; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import reactor.core.scheduler.Schedulers; public class ParcelDeliverNotificationController implements Listener { - private final GatewayDiscordClient client; + private final DiscordNotificationService notificationService; private final DiscordLinkService discordLinkService; private final UserManager userManager; private final MessageConfig messageConfig; - private final Logger logger; public ParcelDeliverNotificationController( - GatewayDiscordClient client, + DiscordNotificationService notificationService, DiscordLinkService discordLinkService, UserManager userManager, - MessageConfig messageConfig, - Logger logger + MessageConfig messageConfig ) { - this.client = client; + this.notificationService = notificationService; this.discordLinkService = discordLinkService; this.userManager = userManager; this.messageConfig = messageConfig; - this.logger = logger; } @EventHandler @@ -69,14 +63,7 @@ private void sendDeliveryNotification(Parcel parcel, String discordId) { .replace("{SIZE}", parcel.size().name()) .replace("{PRIORITY}", parcel.priority() ? "Yes" : "No"); - this.client.getUserById(Snowflake.of(Long.parseLong(discordId))) - .flatMap(user -> user.getPrivateChannel()) - .flatMap(channel -> channel.createMessage(message)) - .subscribeOn(Schedulers.boundedElastic()) - .doOnError(error -> this.logger.warning( - "Failed to send parcel delivery notification to Discord user " + discordId + ": " + error.getMessage() - )) - .subscribe(); + this.notificationService.sendPrivateMessage(discordId, message); return null; }); diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java new file mode 100644 index 00000000..498d5435 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java @@ -0,0 +1,40 @@ +package com.eternalcode.parcellockers.discord.notification; + +import discord4j.common.util.Snowflake; +import discord4j.core.GatewayDiscordClient; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; +import reactor.core.scheduler.Schedulers; + +/** + * Discord4J-based implementation of DiscordNotificationService. + * Uses the Discord4J library to send notifications. + */ +public class Discord4JNotificationService implements DiscordNotificationService { + + private final GatewayDiscordClient client; + private final Logger logger; + + public Discord4JNotificationService(GatewayDiscordClient client, Logger logger) { + this.client = client; + this.logger = logger; + } + + @Override + public CompletableFuture sendPrivateMessage(String discordId, String message) { + CompletableFuture future = new CompletableFuture<>(); + + this.client.getUserById(Snowflake.of(Long.parseLong(discordId))) + .flatMap(user -> user.getPrivateChannel()) + .flatMap(channel -> channel.createMessage(message)) + .subscribeOn(Schedulers.boundedElastic()) + .doOnSuccess(msg -> future.complete(true)) + .doOnError(error -> { + this.logger.warning("Failed to send private message to Discord user " + discordId + ": " + error.getMessage()); + future.complete(false); + }) + .subscribe(); + + return future; + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java new file mode 100644 index 00000000..91e6ab2b --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java @@ -0,0 +1,19 @@ +package com.eternalcode.parcellockers.discord.notification; + +import java.util.concurrent.CompletableFuture; + +/** + * Service interface for sending Discord notifications. + * Implementations can use different Discord libraries (Discord4J, DiscordSRV/JDA, etc.) + */ +public interface DiscordNotificationService { + + /** + * Sends a private message to a Discord user. + * + * @param discordId the Discord user ID to send the message to + * @param message the message content to send + * @return a CompletableFuture that completes when the message is sent, returning true on success + */ + CompletableFuture sendPrivateMessage(String discordId, String message); +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java new file mode 100644 index 00000000..aeb4fc19 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java @@ -0,0 +1,44 @@ +package com.eternalcode.parcellockers.discord.notification; + +import github.scarsz.discordsrv.dependencies.jda.api.entities.User; +import github.scarsz.discordsrv.util.DiscordUtil; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; + +/** + * DiscordSRV-based implementation of DiscordNotificationService. + * Uses DiscordSRV's JDA instance to send notifications. + */ +public class DiscordSrvNotificationService implements DiscordNotificationService { + + private final Logger logger; + + public DiscordSrvNotificationService(Logger logger) { + this.logger = logger; + } + + @Override + public CompletableFuture sendPrivateMessage(String discordId, String message) { + return CompletableFuture.supplyAsync(() -> { + try { + User user = DiscordUtil.getUserById(discordId); + if (user == null) { + this.logger.warning("Could not find Discord user with ID: " + discordId); + return false; + } + + user.openPrivateChannel() + .flatMap(channel -> channel.sendMessage(message)) + .queue( + success -> {}, + error -> this.logger.warning("Failed to send private message to Discord user " + discordId + ": " + error.getMessage()) + ); + + return true; + } catch (Exception e) { + this.logger.warning("Failed to send private message to Discord user " + discordId + ": " + e.getMessage()); + return false; + } + }); + } +} From 161cec0e5a9ee7bcafa2016bdbd319d91415b7b0 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:43:54 +0100 Subject: [PATCH 13/36] fix: declare appropriate events as async --- .../itemstorage/event/ItemStorageUpdateEvent.java | 1 + .../parcellockers/locker/event/LockerCreateEvent.java | 1 + .../parcellockers/locker/event/LockerDeleteEvent.java | 2 ++ .../parcellockers/parcel/event/ParcelDeliverEvent.java | 1 + .../parcellockers/parcel/event/ParcelSendEvent.java | 4 +++- 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/itemstorage/event/ItemStorageUpdateEvent.java b/src/main/java/com/eternalcode/parcellockers/itemstorage/event/ItemStorageUpdateEvent.java index f1653887..bebb56fc 100644 --- a/src/main/java/com/eternalcode/parcellockers/itemstorage/event/ItemStorageUpdateEvent.java +++ b/src/main/java/com/eternalcode/parcellockers/itemstorage/event/ItemStorageUpdateEvent.java @@ -16,6 +16,7 @@ public class ItemStorageUpdateEvent extends Event implements Cancellable { private boolean cancelled; public ItemStorageUpdateEvent(ItemStorage oldItemStorage, ItemStorage updatedItemStorage) { + super(true); this.oldItemStorage = oldItemStorage; this.updatedItemStorage = updatedItemStorage; } diff --git a/src/main/java/com/eternalcode/parcellockers/locker/event/LockerCreateEvent.java b/src/main/java/com/eternalcode/parcellockers/locker/event/LockerCreateEvent.java index 932ecc83..cc5f5e82 100644 --- a/src/main/java/com/eternalcode/parcellockers/locker/event/LockerCreateEvent.java +++ b/src/main/java/com/eternalcode/parcellockers/locker/event/LockerCreateEvent.java @@ -16,6 +16,7 @@ public class LockerCreateEvent extends Event implements Cancellable { private boolean cancelled; public LockerCreateEvent(Locker locker, UUID player) { + super(true); this.locker = locker; this.player = player; } diff --git a/src/main/java/com/eternalcode/parcellockers/locker/event/LockerDeleteEvent.java b/src/main/java/com/eternalcode/parcellockers/locker/event/LockerDeleteEvent.java index b7d629ee..232dfdbd 100644 --- a/src/main/java/com/eternalcode/parcellockers/locker/event/LockerDeleteEvent.java +++ b/src/main/java/com/eternalcode/parcellockers/locker/event/LockerDeleteEvent.java @@ -7,6 +7,8 @@ import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; +// Called when a locker is deleted +// Warning: this event is not called when all lockers are deleted through "/parcel debug delete lockers" command public class LockerDeleteEvent extends Event implements Cancellable { private static final HandlerList HANDLER_LIST = new HandlerList(); diff --git a/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelDeliverEvent.java b/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelDeliverEvent.java index d844df18..47ac5347 100644 --- a/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelDeliverEvent.java +++ b/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelDeliverEvent.java @@ -14,6 +14,7 @@ public class ParcelDeliverEvent extends Event implements Cancellable { private boolean cancelled; public ParcelDeliverEvent(Parcel parcel) { + super(true); this.parcel = parcel; } diff --git a/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelSendEvent.java b/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelSendEvent.java index 41571232..c1c83d69 100644 --- a/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelSendEvent.java +++ b/src/main/java/com/eternalcode/parcellockers/parcel/event/ParcelSendEvent.java @@ -13,7 +13,9 @@ public class ParcelSendEvent extends Event implements Cancellable { private final Parcel parcel; private boolean cancelled; - public ParcelSendEvent(Parcel parcel) {this.parcel = parcel; + public ParcelSendEvent(Parcel parcel) { + super(true); + this.parcel = parcel; } public static HandlerList getHandlerList() { From f2b470a5e2ade37fe1dbf22f61e7935ea01a4499 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:51:06 +0100 Subject: [PATCH 14/36] fix: avoid DiscordClientManager#getClient race conditions by using blocking discord login --- .../discord/DiscordClientManager.java | 25 ++++++++++--------- .../DiscordLinkRepositoryOrmLite.java | 4 +-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java index b638b681..c6c1f7f0 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java @@ -3,7 +3,6 @@ import discord4j.core.DiscordClient; import discord4j.core.GatewayDiscordClient; import java.util.logging.Logger; -import reactor.core.scheduler.Schedulers; public class DiscordClientManager { @@ -19,18 +18,20 @@ public DiscordClientManager(String token, Logger logger) { public void initialize() { this.logger.info("Discord integration is enabled. Logging in to Discord..."); - DiscordClient.create(this.token) - .login() - .subscribeOn(Schedulers.boundedElastic()) - .doOnSuccess(client -> { - this.client = client; + try { + GatewayDiscordClient discordClient = DiscordClient.create(this.token) + .login() + .block(); + if (discordClient != null) { + this.client = discordClient; this.logger.info("Successfully logged in to Discord."); - }) - .doOnError(error -> { - this.logger.severe("Failed to log in to Discord: " + error.getMessage()); - error.printStackTrace(); - }) - .subscribe(); + } else { + this.logger.severe("Failed to log in to Discord: login returned null client."); + } + } catch (Exception exception) { + this.logger.severe("Failed to log in to Discord: " + exception.getMessage()); + exception.printStackTrace(); + } } public void shutdown() { diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java index 485e0bb0..0b96abf7 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java @@ -5,7 +5,6 @@ import com.eternalcode.parcellockers.database.wrapper.AbstractRepositoryOrmLite; import com.eternalcode.parcellockers.discord.DiscordLink; import com.eternalcode.parcellockers.shared.exception.DatabaseException; -import com.j256.ormlite.dao.Dao.CreateOrUpdateStatus; import com.j256.ormlite.stmt.DeleteBuilder; import com.j256.ormlite.table.TableUtils; import java.sql.SQLException; @@ -43,7 +42,8 @@ public CompletableFuture> findByPlayerUuid(UUID playerUuid public CompletableFuture> findByDiscordId(String discordId) { return this.action(DiscordLinkEntity.class, dao -> { var queryBuilder = dao.queryBuilder() - .where().eq(ID_COLUMN_NAME, discordId); + .where() + .eq(ID_COLUMN_NAME, discordId); return dao.queryForFirst(queryBuilder.prepare()); }).thenApply(entity -> Optional.ofNullable(entity).map(DiscordLinkEntity::toDomain)); } From 9342e82c36850bd7368436e55c7ee76ede87489c Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:54:44 +0100 Subject: [PATCH 15/36] Remove redundant discord properties --- .../com/eternalcode/parcellockers/ParcelLockers.java | 6 +----- .../configuration/implementation/PluginConfig.java | 9 --------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 2d7200eb..4f52d497 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -223,11 +223,7 @@ public void onEnable() { new DiscordSrvUnlinkCommand(discordSrvLinkService, noticeService) ); } else { - if ((discordSettings.botToken == null || discordSettings.botToken.isBlank()) || - discordSettings.serverId.isBlank() || - discordSettings.channelId.isBlank() || - discordSettings.botAdminRoleId.isBlank() - ) { + if (config.discord.botToken == null || config.discord.botToken.isBlank()) { this.getLogger() .severe("Discord integration is enabled but some of the properties are not set! Disabling..."); server.getPluginManager().disablePlugin(this); diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java index 28a1f757..f7e788c9 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java @@ -368,14 +368,5 @@ public static class DiscordSettings extends OkaeriConfig { @Comment("# The Discord bot token.") public String botToken = System.getenv("DISCORD_BOT_TOKEN"); - - @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"; } } From a271e9fa5ce4976e4ff7263ac169639cd6feb5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:56:08 +0100 Subject: [PATCH 16/36] Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../discord/command/DiscordLinkCommand.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 41b3d536..b8805113 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -35,7 +35,14 @@ import org.bukkit.entity.Player; import reactor.core.publisher.Mono; -@SuppressWarnings("UnstableApiUsage") +/** + * Command responsible for linking a Minecraft account with a Discord account. + *

+ * 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). + */ @Command(name = "parcel linkdiscord") public class DiscordLinkCommand { From 88098089850b351d25fc3934dd2e617c94cfb1c8 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:57:39 +0100 Subject: [PATCH 17/36] fix: initialize DiscordLinkRepository only when fallback Discord integration is enabled --- src/main/java/com/eternalcode/parcellockers/ParcelLockers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 4f52d497..e8d397e4 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -206,7 +206,6 @@ public void onEnable() { .invalidUsage(new InvalidUsageHandlerImpl(noticeService)) .missingPermission(new MissingPermissionsHandlerImpl(noticeService)); - DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); DiscordSettings discordSettings = config.discord; if (discordSettings.enabled) { DiscordNotificationService notificationService; @@ -236,6 +235,7 @@ public void onEnable() { ); this.discordClientManager.initialize(); + DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); activeLinkService = new DiscordFallbackLinkService(discordLinkRepository); notificationService = new Discord4JNotificationService( this.discordClientManager.getClient(), From 457490b6b73ea07beb8a840359a58bb917173921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:59:36 +0100 Subject: [PATCH 18/36] Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../discord/command/DiscordLinkCommand.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index b8805113..c302f75d 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -246,7 +246,16 @@ private CompletableFuture validateAndLink(UUID playerUuid, Str ); } - return this.client.getUserById(Snowflake.of(Long.parseLong(discordIdString))) + final long discordIdLong; + try { + discordIdLong = Long.parseLong(discordIdString); + } catch (NumberFormatException e) { + return CompletableFuture.completedFuture( + ValidationResult.error(messages -> messages.discord.userNotFound) + ); + } + + return this.client.getUserById(Snowflake.of(discordIdLong)) .map(ValidationResult::success) .onErrorResume(error -> Mono.just( ValidationResult.error(messages -> messages.discord.userNotFound) From dee0d6c044b8a8b3500f36d48841df321a2bf127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:00:44 +0100 Subject: [PATCH 19/36] Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../parcellockers/discord/command/DiscordLinkCommand.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index c302f75d..6fb5b28d 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -213,6 +213,8 @@ private void handleVerification(Player player, String enteredCode) { } if (!verificationData.code().equals(enteredCode)) { + // Invalidate the verification code after a failed attempt to prevent repeated guessing + this.authCodesCache.invalidate(playerUuid); this.noticeService.player(playerUuid, messages -> messages.discord.invalidCode); return; } From 73477f6541bb24afdc2c7ff6a11153d90fd90c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:01:07 +0100 Subject: [PATCH 20/36] Update src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../configuration/implementation/MessageConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index febcfd07..4ca65530 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -213,7 +213,7 @@ public static class DiscordMessages extends OkaeriConfig { .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice invalidCode = Notice.builder() - .chat("&4✘ &cInvalid verification code. Please try again in 2 minutes.") + .chat("&4✘ &cInvalid verification code. Please run the command again to restart the verification process.") .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice linkSuccess = Notice.builder() From b8d54bf8d38f0c009f711fe06b377a024e949f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:14:03 +0100 Subject: [PATCH 21/36] Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../parcellockers/discord/command/DiscordLinkCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 6fb5b28d..1b0cd430 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -268,7 +268,7 @@ private CompletableFuture validateAndLink(UUID playerUuid, Str } private String generateVerificationCode() { - int code = 1000 + RANDOM.nextInt(9000); // generates 1000-9999 + int code = ThreadLocalRandom.current().nextInt(1000, 10000); return String.valueOf(code); } From a7f2a35266a99fa138b149395567affaa95d0eef Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:26:24 +0100 Subject: [PATCH 22/36] fix: add logging to DiscordSrvLinkService for better error handling --- .../eternalcode/parcellockers/ParcelLockers.java | 2 +- .../discord/DiscordSrvLinkService.java | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index e8d397e4..1a7bcc17 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -213,7 +213,7 @@ public void onEnable() { if (server.getPluginManager().isPluginEnabled("DiscordSRV")) { this.getLogger().info("DiscordSRV detected! Using DiscordSRV for account linking."); - DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(); + DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(this.getLogger()); activeLinkService = discordSrvLinkService; notificationService = new DiscordSrvNotificationService(this.getLogger()); diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java index 36b59cc1..a1f254b6 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java @@ -6,6 +6,8 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; /** * DiscordSRV-based implementation of DiscordLinkService. @@ -13,6 +15,12 @@ */ public class DiscordSrvLinkService implements DiscordLinkService { + private final Logger logger; + + public DiscordSrvLinkService(Logger logger) { + this.logger = logger; + } + @Override public CompletableFuture> findLinkByPlayer(UUID playerUuid) { return CompletableFuture.supplyAsync(() -> { @@ -42,6 +50,7 @@ public CompletableFuture createLink(UUID playerUuid, String discordId) DiscordSRV.getPlugin().getAccountLinkManager().link(discordId, playerUuid); return true; } catch (Exception e) { + this.logger.log(Level.WARNING, "Failed to create DiscordSRV link", e); return false; } }); @@ -53,7 +62,8 @@ public CompletableFuture unlinkPlayer(UUID playerUuid) { try { DiscordSRV.getPlugin().getAccountLinkManager().unlink(playerUuid); return true; - } catch (Exception e) { + } catch (Exception exception) { + this.logger.log(Level.WARNING, "Failed to unlink DiscordSRV player", exception); return false; } }); @@ -69,7 +79,8 @@ public CompletableFuture unlinkDiscordId(String discordId) { } DiscordSRV.getPlugin().getAccountLinkManager().unlink(playerUuid); return true; - } catch (Exception e) { + } catch (Exception exception) { + this.logger.log(Level.WARNING, "Failed to unlink DiscordSRV user by Discord ID", exception); return false; } }); From 6d1c84eb5bfe79062a812fb1135efc6b5eedf303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:35:28 +0100 Subject: [PATCH 23/36] Update PluginConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../configuration/implementation/PluginConfig.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java index f7e788c9..f632052d 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java @@ -366,7 +366,11 @@ public static class DiscordSettings extends OkaeriConfig { @Comment("# Whether Discord integration is enabled.") public boolean enabled = true; - @Comment("# The Discord bot token.") - public String botToken = System.getenv("DISCORD_BOT_TOKEN"); + @Comment({ + "# The Discord bot token used by the bot to connect.", + "# It is recommended to set this value here, or via the DISCORD_BOT_TOKEN environment variable.", + "# If left empty, make sure the DISCORD_BOT_TOKEN environment variable is set before starting the server." + }) + public String botToken = ""; } } From 5cbffe73e81a0f49b3bd556fd2fcadd1e35b6b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:36:09 +0100 Subject: [PATCH 24/36] Update ParcelDeliverNotificationController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../discord/controller/ParcelDeliverNotificationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java index 3482be0b..9633a897 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java @@ -59,7 +59,7 @@ private void sendDeliveryNotification(Parcel parcel, String discordId) { .replace("{PARCEL_NAME}", parcel.name()) .replace("{SENDER}", senderName) .replace("{RECEIVER}", receiverName) - .replace("{DESCRIPTION}", parcel.description() != null ? parcel.description() : "") + .replace("{DESCRIPTION}", parcel.description() != null ? parcel.description() : "No description") .replace("{SIZE}", parcel.size().name()) .replace("{PRIORITY}", parcel.priority() ? "Yes" : "No"); From 77b19aa8000b4c0722b2af4eb2a6b50244260d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20K=C4=99dziora?= <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:41:55 +0100 Subject: [PATCH 25/36] Update ParcelDeliverNotificationController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../discord/controller/ParcelDeliverNotificationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java index 9633a897..32ca7d9f 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java @@ -61,7 +61,7 @@ private void sendDeliveryNotification(Parcel parcel, String discordId) { .replace("{RECEIVER}", receiverName) .replace("{DESCRIPTION}", parcel.description() != null ? parcel.description() : "No description") .replace("{SIZE}", parcel.size().name()) - .replace("{PRIORITY}", parcel.priority() ? "Yes" : "No"); + .replace("{PRIORITY}", parcel.priority() ? "🔴 High Priority" : "⚪ Normal Priority"); this.notificationService.sendPrivateMessage(discordId, message); From d0ed324a0bd69b50bbeda01fd2b425931d3d0a94 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:44:33 +0100 Subject: [PATCH 26/36] Add missing import --- .../parcellockers/discord/command/DiscordLinkCommand.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 1b0cd430..868b5239 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -22,9 +22,9 @@ import io.papermc.paper.registry.data.dialog.input.DialogInput; import io.papermc.paper.registry.data.dialog.type.DialogType; import java.util.List; -import java.util.Random; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.event.ClickCallback; @@ -46,8 +46,6 @@ @Command(name = "parcel linkdiscord") public class DiscordLinkCommand { - private static final Random RANDOM = new Random(); - private final GatewayDiscordClient client; private final DiscordLinkService discordLinkService; private final NoticeService noticeService; From 35556514fc2849e0a003c6dcedfa628f0238d093 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:46:05 +0100 Subject: [PATCH 27/36] WIP - Adjust to DMK suggestions --- .../parcellockers/ParcelLockers.java | 7 +++- .../discord/argument/SnowflakeArgument.java | 38 +++++++++++++++++++ .../discord/command/DiscordLinkCommand.java | 27 +++++-------- .../command/DiscordSrvLinkCommand.java | 4 +- .../command/DiscordSrvUnlinkCommand.java | 17 +++++---- .../discord/command/DiscordUnlinkCommand.java | 17 +++++---- ...DiscordDeliverNotificationController.java} | 4 +- .../discord/repository/DiscordLinkEntity.java | 8 ++-- 8 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java rename src/main/java/com/eternalcode/parcellockers/discord/controller/{ParcelDeliverNotificationController.java => DiscordDeliverNotificationController.java} (96%) diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 1a7bcc17..829ba2e6 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -21,11 +21,12 @@ import com.eternalcode.parcellockers.discord.DiscordFallbackLinkService; import com.eternalcode.parcellockers.discord.DiscordLinkService; import com.eternalcode.parcellockers.discord.DiscordSrvLinkService; +import com.eternalcode.parcellockers.discord.argument.SnowflakeArgument; import com.eternalcode.parcellockers.discord.command.DiscordLinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordSrvLinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordSrvUnlinkCommand; import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; -import com.eternalcode.parcellockers.discord.controller.ParcelDeliverNotificationController; +import com.eternalcode.parcellockers.discord.controller.DiscordDeliverNotificationController; import com.eternalcode.parcellockers.discord.notification.Discord4JNotificationService; import com.eternalcode.parcellockers.discord.notification.DiscordNotificationService; import com.eternalcode.parcellockers.discord.notification.DiscordSrvNotificationService; @@ -69,6 +70,7 @@ import dev.rollczi.liteskullapi.LiteSkullFactory; import dev.rollczi.liteskullapi.SkullAPI; import dev.triumphteam.gui.TriumphGui; +import discord4j.common.util.Snowflake; import java.io.File; import java.sql.SQLException; import java.time.Duration; @@ -194,6 +196,7 @@ public void onEnable() { var liteCommandsBuilder = LiteBukkitFactory.builder(this.getName(), this) .extension(new LiteAdventureExtension<>()) + .argument(Snowflake.class, new SnowflakeArgument()) .message(LiteBukkitMessages.PLAYER_ONLY, messageConfig.playerOnlyCommand) .message(LiteBukkitMessages.PLAYER_NOT_FOUND, messageConfig.playerNotFound) .commands(LiteCommandsAnnotations.of( @@ -254,7 +257,7 @@ public void onEnable() { } server.getPluginManager().registerEvents( - new ParcelDeliverNotificationController( + new DiscordDeliverNotificationController( notificationService, activeLinkService, userManager, diff --git a/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java b/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java new file mode 100644 index 00000000..9775fce9 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java @@ -0,0 +1,38 @@ +package com.eternalcode.parcellockers.discord.argument; + +import dev.rollczi.litecommands.argument.Argument; +import dev.rollczi.litecommands.argument.parser.ParseResult; +import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; +import dev.rollczi.litecommands.invocation.Invocation; +import dev.rollczi.litecommands.suggestion.SuggestionContext; +import dev.rollczi.litecommands.suggestion.SuggestionResult; +import discord4j.common.util.Snowflake; +import org.bukkit.command.CommandSender; + +public class SnowflakeArgument extends ArgumentResolver { + + @Override + protected ParseResult parse( + Invocation invocation, + Argument context, + String argument) { + try { + // Try to parse the string as a Snowflake + Snowflake snowflake = Snowflake.of(argument); + return ParseResult.success(snowflake); + } + catch (NumberFormatException exception) { + // If parsing fails, return an error + return ParseResult.failure("&4✘ &cInvalid Discord ID format! Please provide a valid Discord ID."); + } + } + + @Override + public SuggestionResult suggest( + Invocation invocation, + Argument argument, + SuggestionContext context) { + // No suggestions for Discord IDs (they are unique numeric values) + return SuggestionResult.empty(); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 868b5239..f0961fda 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -71,9 +71,9 @@ public DiscordLinkCommand( } @Execute - void linkSelf(@Context Player player, @Arg long discordId) { + void linkSelf(@Context Player player, @Arg Snowflake discordId) { UUID playerUuid = player.getUniqueId(); - String discordIdString = String.valueOf(discordId); + String discordIdString = discordId.asString(); if (this.authCodesCache.getIfPresent(playerUuid) != null) { this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); @@ -98,8 +98,8 @@ void linkSelf(@Context Player player, @Arg long discordId) { @Execute @Permission("parcellockers.admin") - void linkOther(@Context CommandSender sender, @Arg String playerName, @Arg long discordId) { - String discordIdString = String.valueOf(discordId); + void linkOther(@Context CommandSender sender, @Arg String playerName, @Arg Snowflake discordId) { + String discordIdString = discordId.asString(); this.resolvePlayerUuid(playerName) .thenCompose(playerUuid -> { @@ -231,31 +231,22 @@ private void handleVerification(Player player, String enteredCode) { private CompletableFuture validateAndLink(UUID playerUuid, String discordIdString) { return this.discordLinkService.findLinkByPlayer(playerUuid) - .thenCompose(existingPlayerLink -> { - if (existingPlayerLink.isPresent()) { + .thenCompose(optionalLink -> { + if (optionalLink.isPresent()) { return CompletableFuture.completedFuture( ValidationResult.error(messages -> messages.discord.alreadyLinked) ); } return this.discordLinkService.findLinkByDiscordId(discordIdString) - .thenCompose(existingDiscordLink -> { - if (existingDiscordLink.isPresent()) { + .thenCompose(optionalDiscordLink -> { + if (optionalDiscordLink.isPresent()) { return CompletableFuture.completedFuture( ValidationResult.error(messages -> messages.discord.discordAlreadyLinked) ); } - final long discordIdLong; - try { - discordIdLong = Long.parseLong(discordIdString); - } catch (NumberFormatException e) { - return CompletableFuture.completedFuture( - ValidationResult.error(messages -> messages.discord.userNotFound) - ); - } - - return this.client.getUserById(Snowflake.of(discordIdLong)) + return this.client.getUserById(Snowflake.of(discordIdString)) .map(ValidationResult::success) .onErrorResume(error -> Mono.just( ValidationResult.error(messages -> messages.discord.userNotFound) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java index 0aafa947..a4408950 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java @@ -31,8 +31,8 @@ public DiscordSrvLinkCommand( void linkSelf(@Context Player player) { UUID playerUuid = player.getUniqueId(); - this.discordSrvLinkService.findLinkByPlayer(playerUuid).thenAccept(existingLink -> { - if (existingLink.isPresent()) { + this.discordSrvLinkService.findLinkByPlayer(playerUuid).thenAccept(optionalLink -> { + if (optionalLink.isPresent()) { this.noticeService.player(playerUuid, messages -> messages.discord.discordSrvAlreadyLinked); return; } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java index 57b820c8..4e6aeae5 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java @@ -7,6 +7,7 @@ import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.permission.Permission; +import discord4j.common.util.Snowflake; import java.util.UUID; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -34,8 +35,8 @@ public DiscordSrvUnlinkCommand( void unlinkSelf(@Context Player player) { UUID playerUuid = player.getUniqueId(); - this.discordSrvLinkService.findLinkByPlayer(playerUuid).thenAccept(existingLink -> { - if (existingLink.isEmpty()) { + this.discordSrvLinkService.findLinkByPlayer(playerUuid).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { this.noticeService.player(playerUuid, messages -> messages.discord.notLinked); return; } @@ -50,8 +51,8 @@ void unlinkSelf(@Context Player player) { void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { UUID targetUuid = targetPlayer.getUniqueId(); - this.discordSrvLinkService.findLinkByPlayer(targetUuid).thenAccept(existingLink -> { - if (existingLink.isEmpty()) { + this.discordSrvLinkService.findLinkByPlayer(targetUuid).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.playerNotLinked); return; } @@ -69,11 +70,11 @@ void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { @Execute @Permission("parcellockers.admin") - void unlinkByDiscordId(@Context CommandSender sender, @Arg long discordId) { - String discordIdString = String.valueOf(discordId); + void unlinkByDiscordId(@Context CommandSender sender, @Arg Snowflake discordId) { + String discordIdString = discordId.asString(); - this.discordSrvLinkService.findLinkByDiscordId(discordIdString).thenAccept(existingLink -> { - if (existingLink.isEmpty()) { + this.discordSrvLinkService.findLinkByDiscordId(discordIdString).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); return; } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java index fe48e791..c8b8a82d 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java @@ -7,6 +7,7 @@ import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.permission.Permission; +import discord4j.common.util.Snowflake; import java.util.UUID; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -29,8 +30,8 @@ public DiscordUnlinkCommand( void unlinkSelf(@Context Player player) { UUID playerUuid = player.getUniqueId(); - this.discordLinkService.findLinkByPlayer(playerUuid).thenAccept(existingLink -> { - if (existingLink.isEmpty()) { + this.discordLinkService.findLinkByPlayer(playerUuid).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { this.noticeService.player(playerUuid, messages -> messages.discord.notLinked); return; } @@ -50,8 +51,8 @@ void unlinkSelf(@Context Player player) { void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { UUID targetUuid = targetPlayer.getUniqueId(); - this.discordLinkService.findLinkByPlayer(targetUuid).thenAccept(existingLink -> { - if (existingLink.isEmpty()) { + this.discordLinkService.findLinkByPlayer(targetUuid).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.playerNotLinked); return; } @@ -69,11 +70,11 @@ void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { @Execute @Permission("parcellockers.admin") - void unlinkByDiscordId(@Context CommandSender sender, @Arg long discordId) { - String discordIdString = String.valueOf(discordId); + void unlinkByDiscordId(@Context CommandSender sender, @Arg Snowflake discordId) { + String discordIdString = discordId.asString(); - this.discordLinkService.findLinkByDiscordId(discordIdString).thenAccept(existingLink -> { - if (existingLink.isEmpty()) { + this.discordLinkService.findLinkByDiscordId(discordIdString).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); return; } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java b/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java similarity index 96% rename from src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java rename to src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java index 32ca7d9f..1c3ce0df 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/controller/ParcelDeliverNotificationController.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java @@ -12,14 +12,14 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -public class ParcelDeliverNotificationController implements Listener { +public class DiscordDeliverNotificationController implements Listener { private final DiscordNotificationService notificationService; private final DiscordLinkService discordLinkService; private final UserManager userManager; private final MessageConfig messageConfig; - public ParcelDeliverNotificationController( + public DiscordDeliverNotificationController( DiscordNotificationService notificationService, DiscordLinkService discordLinkService, UserManager userManager, diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java index 3d6d3916..8b1ec077 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java @@ -9,28 +9,28 @@ class DiscordLinkEntity { @DatabaseField(id = true, columnName = "minecraft_uuid") - private String minecraftUuid; + private UUID minecraftUuid; @DatabaseField(index = true, columnName = "discord_id") private String discordId; DiscordLinkEntity() {} - DiscordLinkEntity(String minecraftUuid, String discordId) { + DiscordLinkEntity(UUID minecraftUuid, String discordId) { this.minecraftUuid = minecraftUuid; this.discordId = discordId; } public static DiscordLinkEntity fromDomain(DiscordLink link) { return new DiscordLinkEntity( - link.minecraftUuid().toString(), + link.minecraftUuid(), link.discordId() ); } public DiscordLink toDomain() { return new DiscordLink( - UUID.fromString(this.minecraftUuid), + this.minecraftUuid, this.discordId ); } From 8df0ed9cda2454db28b0d8191c35bb76f1b2ba41 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:50:36 +0100 Subject: [PATCH 28/36] Use OfflinePlayer instead of String --- .../discord/command/DiscordLinkCommand.java | 58 ++++++------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index f0961fda..ffda3c31 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -29,7 +29,6 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.event.ClickCallback; import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -98,38 +97,28 @@ void linkSelf(@Context Player player, @Arg Snowflake discordId) { @Execute @Permission("parcellockers.admin") - void linkOther(@Context CommandSender sender, @Arg String playerName, @Arg Snowflake discordId) { + void linkOther(@Context CommandSender sender, @Arg OfflinePlayer player, @Arg Snowflake discordId) { String discordIdString = discordId.asString(); + UUID playerUuid = player.getUniqueId(); - this.resolvePlayerUuid(playerName) - .thenCompose(playerUuid -> { - if (playerUuid == null) { - this.noticeService.viewer(sender, messages -> messages.discord.userNotFound); - return CompletableFuture.completedFuture(null); - } - - return this.validateAndLink(playerUuid, discordIdString) - .thenCompose(validationResult -> { - if (!validationResult.isValid()) { - this.noticeService.viewer(sender, validationResult.errorMessage()); - return CompletableFuture.completedFuture(null); - } + this.validateAndLink(playerUuid, discordIdString).thenCompose(validationResult -> { + if (!validationResult.isValid()) { + this.noticeService.viewer(sender, validationResult.errorMessage()); + return CompletableFuture.completedFuture(null); + } - return this.discordLinkService.createLink(playerUuid, discordIdString) - .thenAccept(success -> { - if (success) { - this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); - this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); - } - }); - }); - }) - .exceptionally(error -> { - this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); - return null; + return this.discordLinkService.createLink(playerUuid, discordIdString).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); + this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + } }); + }).exceptionally(error -> { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + return null; + }); } private Mono sendVerification(UUID playerUuid, String discordId, Player player, User discordUser) { @@ -261,17 +250,6 @@ private String generateVerificationCode() { return String.valueOf(code); } - private CompletableFuture resolvePlayerUuid(String playerName) { - return CompletableFuture.supplyAsync(() -> { - Player online = Bukkit.getPlayerExact(playerName); - if (online != null) { - return online.getUniqueId(); - } - - OfflinePlayer offline = Bukkit.getOfflinePlayer(playerName); - return offline.hasPlayedBefore() ? offline.getUniqueId() : null; - }); - } private record VerificationData(String discordId, String code) {} From a31f0c2585c86568b5b77d9d36c103d89198cf86 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:51:17 +0100 Subject: [PATCH 29/36] Use OfflinePlayer in unlinkPlayer executor too --- .../parcellockers/discord/command/DiscordUnlinkCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java index c8b8a82d..51e5b9b6 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java @@ -9,6 +9,7 @@ import dev.rollczi.litecommands.annotations.permission.Permission; import discord4j.common.util.Snowflake; import java.util.UUID; +import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -48,7 +49,7 @@ void unlinkSelf(@Context Player player) { @Execute @Permission("parcellockers.admin") - void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { + void unlinkPlayer(@Context CommandSender sender, @Arg OfflinePlayer targetPlayer) { UUID targetUuid = targetPlayer.getUniqueId(); this.discordLinkService.findLinkByPlayer(targetUuid).thenAccept(optionalLink -> { From e4070c3268c59ad117e0fa588a205c5c90b56879 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:15:17 +0100 Subject: [PATCH 30/36] Align with single-responsibility-principle --- .../parcellockers/ParcelLockers.java | 33 ++- .../discord/command/DiscordLinkCommand.java | 252 ++++-------------- .../DiscordLinkValidationService.java | 64 +++++ .../DiscordVerificationDialogFactory.java | 85 ++++++ .../DiscordVerificationService.java | 124 +++++++++ .../verification/VerificationCache.java | 59 ++++ .../VerificationCodeGenerator.java | 22 ++ .../verification/VerificationData.java | 9 + 8 files changed, 438 insertions(+), 210 deletions(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 829ba2e6..29382dbb 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -32,6 +32,11 @@ import com.eternalcode.parcellockers.discord.notification.DiscordSrvNotificationService; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; +import com.eternalcode.parcellockers.discord.verification.DiscordLinkValidationService; +import com.eternalcode.parcellockers.discord.verification.DiscordVerificationDialogFactory; +import com.eternalcode.parcellockers.discord.verification.DiscordVerificationService; +import com.eternalcode.parcellockers.discord.verification.VerificationCache; +import com.eternalcode.parcellockers.discord.verification.VerificationCodeGenerator; import com.eternalcode.parcellockers.gui.GuiManager; import com.eternalcode.parcellockers.gui.implementation.locker.LockerGui; import com.eternalcode.parcellockers.gui.implementation.remote.MainGui; @@ -245,13 +250,33 @@ public void onEnable() { this.getLogger() ); + DiscordLinkValidationService validationService = new DiscordLinkValidationService( + activeLinkService, + this.discordClientManager.getClient() + ); + + VerificationCache verificationCache = new VerificationCache(); + VerificationCodeGenerator codeGenerator = new VerificationCodeGenerator(); + DiscordVerificationDialogFactory dialogFactory = new DiscordVerificationDialogFactory( + miniMessage, + messageConfig + ); + + DiscordVerificationService verificationService = new DiscordVerificationService( + verificationCache, + codeGenerator, + dialogFactory, + activeLinkService, + noticeService, + messageConfig + ); + liteCommandsBuilder.commands( new DiscordLinkCommand( - this.discordClientManager.getClient(), activeLinkService, - noticeService, - miniMessage, - messageConfig), + validationService, + verificationService, + noticeService), new DiscordUnlinkCommand(activeLinkService, noticeService) ); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index ffda3c31..d861903e 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -1,72 +1,43 @@ package com.eternalcode.parcellockers.discord.command; -import com.eternalcode.multification.notice.provider.NoticeProvider; -import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import com.eternalcode.parcellockers.discord.DiscordLinkService; +import com.eternalcode.parcellockers.discord.verification.DiscordLinkValidationService; +import com.eternalcode.parcellockers.discord.verification.DiscordVerificationService; import com.eternalcode.parcellockers.notification.NoticeService; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; +import com.eternalcode.parcellockers.shared.validation.ValidationResult; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Context; import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.permission.Permission; import discord4j.common.util.Snowflake; -import discord4j.core.GatewayDiscordClient; -import discord4j.core.object.entity.User; -import io.papermc.paper.dialog.Dialog; -import io.papermc.paper.dialog.DialogResponseView; -import io.papermc.paper.registry.data.dialog.ActionButton; -import io.papermc.paper.registry.data.dialog.DialogBase; -import io.papermc.paper.registry.data.dialog.action.DialogAction; -import io.papermc.paper.registry.data.dialog.input.DialogInput; -import io.papermc.paper.registry.data.dialog.type.DialogType; -import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; -import net.kyori.adventure.audience.Audience; -import net.kyori.adventure.text.event.ClickCallback; -import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import reactor.core.publisher.Mono; /** * Command responsible for linking a Minecraft account with a Discord account. - *

- * 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). */ @Command(name = "parcel linkdiscord") public class DiscordLinkCommand { - private final GatewayDiscordClient client; private final DiscordLinkService discordLinkService; + private final DiscordLinkValidationService validationService; + private final DiscordVerificationService verificationService; private final NoticeService noticeService; - private final MiniMessage miniMessage; - private final MessageConfig messageConfig; - - private final Cache authCodesCache = Caffeine.newBuilder() - .expireAfterWrite(2, TimeUnit.MINUTES) - .build(); public DiscordLinkCommand( - GatewayDiscordClient client, DiscordLinkService discordLinkService, - NoticeService noticeService, - MiniMessage miniMessage, - MessageConfig messageConfig + DiscordLinkValidationService validationService, + DiscordVerificationService verificationService, + NoticeService noticeService ) { - this.client = client; this.discordLinkService = discordLinkService; + this.validationService = validationService; + this.verificationService = verificationService; this.noticeService = noticeService; - this.miniMessage = miniMessage; - this.messageConfig = messageConfig; } @Execute @@ -74,20 +45,22 @@ void linkSelf(@Context Player player, @Arg Snowflake discordId) { UUID playerUuid = player.getUniqueId(); String discordIdString = discordId.asString(); - if (this.authCodesCache.getIfPresent(playerUuid) != null) { + if (this.verificationService.hasPendingVerification(playerUuid)) { this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); return; } - this.validateAndLink(playerUuid, discordIdString) + this.validationService.validate(playerUuid, discordIdString) .thenCompose(validationResult -> { if (!validationResult.isValid()) { - this.noticeService.player(playerUuid, validationResult.errorMessage()); + this.sendValidationError(playerUuid, validationResult); return CompletableFuture.completedFuture(null); } - return this.sendVerification(playerUuid, discordIdString, player, validationResult.discordUser()) - .toFuture(); + return this.validationService.getDiscordUser(discordIdString) + .thenCompose(discordUser -> + this.verificationService.startVerification(player, discordIdString, discordUser).toFuture() + ); }) .exceptionally(error -> { this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); @@ -101,177 +74,44 @@ void linkOther(@Context CommandSender sender, @Arg OfflinePlayer player, @Arg Sn String discordIdString = discordId.asString(); UUID playerUuid = player.getUniqueId(); - this.validateAndLink(playerUuid, discordIdString).thenCompose(validationResult -> { - if (!validationResult.isValid()) { - this.noticeService.viewer(sender, validationResult.errorMessage()); - return CompletableFuture.completedFuture(null); - } - - return this.discordLinkService.createLink(playerUuid, discordIdString).thenAccept(success -> { - if (success) { - this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); - this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); - } - }); - }).exceptionally(error -> { - this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); - return null; - }); - } - - private Mono sendVerification(UUID playerUuid, String discordId, Player player, User discordUser) { - String code = this.generateVerificationCode(); - - VerificationData data = new VerificationData(discordId, code); - if (this.authCodesCache.asMap().putIfAbsent(playerUuid, data) != null) { - this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); - return Mono.empty(); - } - - return discordUser.getPrivateChannel() - .flatMap(channel -> channel.createMessage( - this.messageConfig.discord.discordDmVerificationMessage - .replace("{CODE}", code) - .replace("{PLAYER}", player.getName()) - )) - .doOnSuccess(msg -> { - this.noticeService.player(playerUuid, messages -> messages.discord.verificationCodeSent); - this.showVerificationDialog(player); - }) - .doOnError(error -> { - this.authCodesCache.invalidate(playerUuid); - this.noticeService.player(playerUuid, messages -> messages.discord.cannotSendDm); - }) - .then(); - } - - private void showVerificationDialog(Player player) { - Dialog verificationDialog = Dialog.create(builder -> builder.empty() - .base(DialogBase.builder(this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogTitle)) - .canCloseWithEscape(false) - .inputs(List.of( - DialogInput.text("code", this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogPlaceholder)) - .build() - )) - .build() - ) - .type(DialogType.confirmation( - ActionButton.create( - this.miniMessage.deserialize("Verify"), - this.miniMessage.deserialize("Click to verify your Discord account"), - 200, - DialogAction.customClick((DialogResponseView view, Audience audience) -> { - String enteredCode = view.getText("code"); - this.handleVerification(player, enteredCode); - }, ClickCallback.Options.builder() - .uses(1) - .lifetime(ClickCallback.DEFAULT_LIFETIME) - .build()) - ), - ActionButton.create( - this.miniMessage.deserialize("Cancel"), - this.miniMessage.deserialize("Click to cancel verification"), - 200, - DialogAction.customClick( - (DialogResponseView view, Audience audience) -> { - this.authCodesCache.invalidate(player.getUniqueId()); - this.noticeService.player(player.getUniqueId(), messages -> messages.discord.verificationCancelled); - }, - ClickCallback.Options.builder() - .uses(1) - .lifetime(ClickCallback.DEFAULT_LIFETIME) - .build()) - ) - )) - ); - - player.showDialog(verificationDialog); - } - - private void handleVerification(Player player, String enteredCode) { - UUID playerUuid = player.getUniqueId(); - VerificationData verificationData = this.authCodesCache.getIfPresent(playerUuid); - - if (verificationData == null) { - this.noticeService.player(playerUuid, messages -> messages.discord.verificationExpired); - return; - } - - if (!verificationData.code().equals(enteredCode)) { - // Invalidate the verification code after a failed attempt to prevent repeated guessing - this.authCodesCache.invalidate(playerUuid); - this.noticeService.player(playerUuid, messages -> messages.discord.invalidCode); - return; - } - - // Code matches - remove from cache and create the link - this.authCodesCache.invalidate(playerUuid); - - this.discordLinkService.createLink(playerUuid, verificationData.discordId()).thenAccept(success -> { - if (success) { - this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); - } else { - this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); - } - }); - } - - private CompletableFuture validateAndLink(UUID playerUuid, String discordIdString) { - return this.discordLinkService.findLinkByPlayer(playerUuid) - .thenCompose(optionalLink -> { - if (optionalLink.isPresent()) { - return CompletableFuture.completedFuture( - ValidationResult.error(messages -> messages.discord.alreadyLinked) - ); + this.validationService.validate(playerUuid, discordIdString) + .thenCompose(validationResult -> { + if (!validationResult.isValid()) { + this.sendValidationErrorToViewer(sender, validationResult); + return CompletableFuture.completedFuture(null); } - return this.discordLinkService.findLinkByDiscordId(discordIdString) - .thenCompose(optionalDiscordLink -> { - if (optionalDiscordLink.isPresent()) { - return CompletableFuture.completedFuture( - ValidationResult.error(messages -> messages.discord.discordAlreadyLinked) - ); + return this.discordLinkService.createLink(playerUuid, discordIdString) + .thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); + this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); + } else { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); } - - return this.client.getUserById(Snowflake.of(discordIdString)) - .map(ValidationResult::success) - .onErrorResume(error -> Mono.just( - ValidationResult.error(messages -> messages.discord.userNotFound) - )) - .toFuture(); }); + }) + .exceptionally(error -> { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + return null; }); } - private String generateVerificationCode() { - int code = ThreadLocalRandom.current().nextInt(1000, 10000); - return String.valueOf(code); - } - - - private record VerificationData(String discordId, String code) {} - - private record ValidationResult( - boolean valid, - User discordUser, - NoticeProvider errorMessageGetter - ) { - static ValidationResult success(User user) { - return new ValidationResult(true, user, null); - } - - static ValidationResult error(NoticeProvider messageGetter) { - return new ValidationResult(false, null, messageGetter); - } - - boolean isValid() { - return this.valid; + private void sendValidationError(UUID playerUuid, ValidationResult result) { + switch (result.errorMessage()) { + case "alreadyLinked" -> this.noticeService.player(playerUuid, messages -> messages.discord.alreadyLinked); + case "discordAlreadyLinked" -> this.noticeService.player(playerUuid, messages -> messages.discord.discordAlreadyLinked); + case "userNotFound" -> this.noticeService.player(playerUuid, messages -> messages.discord.userNotFound); + default -> this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); } + } - NoticeProvider errorMessage() { - return this.errorMessageGetter; + private void sendValidationErrorToViewer(CommandSender sender, ValidationResult result) { + switch (result.errorMessage()) { + case "alreadyLinked" -> this.noticeService.viewer(sender, messages -> messages.discord.playerAlreadyLinked); + case "discordAlreadyLinked" -> this.noticeService.viewer(sender, messages -> messages.discord.discordAlreadyLinked); + case "userNotFound" -> this.noticeService.viewer(sender, messages -> messages.discord.userNotFound); + default -> this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); } } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java new file mode 100644 index 00000000..70078b99 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java @@ -0,0 +1,64 @@ +package com.eternalcode.parcellockers.discord.verification; + +import com.eternalcode.parcellockers.discord.DiscordLinkService; +import com.eternalcode.parcellockers.shared.validation.ValidationResult; +import discord4j.common.util.Snowflake; +import discord4j.core.GatewayDiscordClient; +import discord4j.core.object.entity.User; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import reactor.core.publisher.Mono; + +/** + * Service responsible for validating Discord account linking requests. + */ +public class DiscordLinkValidationService { + + private final DiscordLinkService discordLinkService; + private final GatewayDiscordClient discordClient; + + public DiscordLinkValidationService( + DiscordLinkService discordLinkService, + GatewayDiscordClient discordClient + ) { + this.discordLinkService = discordLinkService; + this.discordClient = discordClient; + } + + /** + * Validates whether a player can link to a Discord account. + * Checks if the player or Discord account is already linked. + * + * @param playerUuid the player's UUID + * @param discordId the Discord user ID + * @return a CompletableFuture containing the validation result + */ + public CompletableFuture validate(UUID playerUuid, String discordId) { + return this.discordLinkService.findLinkByPlayer(playerUuid).thenCompose(optionalLink -> { + if (optionalLink.isPresent()) { + return CompletableFuture.completedFuture(ValidationResult.invalid("alreadyLinked")); + } + + return this.discordLinkService.findLinkByDiscordId(discordId).thenCompose(optionalDiscordLink -> { + if (optionalDiscordLink.isPresent()) { + return CompletableFuture.completedFuture(ValidationResult.invalid("discordAlreadyLinked")); + } + + return this.discordClient.getUserById(Snowflake.of(discordId)) + .map(user -> ValidationResult.valid()) + .onErrorResume(error -> Mono.just(ValidationResult.invalid("userNotFound"))) + .toFuture(); + }); + }); + } + + /** + * Fetches the Discord user by their ID. + * + * @param discordId the Discord user ID + * @return a CompletableFuture containing the Discord user + */ + public CompletableFuture getDiscordUser(String discordId) { + return this.discordClient.getUserById(Snowflake.of(discordId)).toFuture(); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java new file mode 100644 index 00000000..a0bf20e5 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java @@ -0,0 +1,85 @@ +package com.eternalcode.parcellockers.discord.verification; + +import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.dialog.DialogResponseView; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.input.DialogInput; +import io.papermc.paper.registry.data.dialog.type.DialogType; +import java.util.List; +import java.util.function.BiConsumer; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.minimessage.MiniMessage; + +/** + * Factory for creating Discord verification dialogs. + *

+ * 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. + */ +public class DiscordVerificationDialogFactory { + + private final MiniMessage miniMessage; + private final MessageConfig messageConfig; + + public DiscordVerificationDialogFactory(MiniMessage miniMessage, MessageConfig messageConfig) { + this.miniMessage = miniMessage; + this.messageConfig = messageConfig; + } + + /** + * Creates a verification dialog for Discord account linking. + * + * @param onVerify callback when the user clicks verify, receives the entered code + * @param onCancel callback when the user clicks cancel + * @return the created dialog + */ + public Dialog create(BiConsumer onVerify, Runnable onCancel) { + return Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogTitle)) + .canCloseWithEscape(false) + .inputs(List.of( + DialogInput.text("code", this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogPlaceholder)) + .build() + )) + .build() + ) + .type(DialogType.confirmation( + this.createVerifyButton(onVerify), + this.createCancelButton(onCancel) + )) + ); + } + + private ActionButton createVerifyButton(BiConsumer onVerify) { + return ActionButton.create( + this.miniMessage.deserialize("Verify"), + this.miniMessage.deserialize("Click to verify your Discord account"), + 200, + DialogAction.customClick((DialogResponseView view, Audience audience) -> { + String enteredCode = view.getText("code"); + onVerify.accept(view, enteredCode); + }, ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ); + } + + private ActionButton createCancelButton(Runnable onCancel) { + return ActionButton.create( + this.miniMessage.deserialize("Cancel"), + this.miniMessage.deserialize("Click to cancel verification"), + 200, + DialogAction.customClick( + (DialogResponseView view, Audience audience) -> onCancel.run(), + ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java new file mode 100644 index 00000000..308f3de9 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java @@ -0,0 +1,124 @@ +package com.eternalcode.parcellockers.discord.verification; + +import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; +import com.eternalcode.parcellockers.discord.DiscordLinkService; +import com.eternalcode.parcellockers.notification.NoticeService; +import discord4j.core.object.entity.User; +import io.papermc.paper.dialog.Dialog; +import java.util.UUID; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +/** + * Service responsible for handling the Discord verification process. + * Coordinates between cache, dialog, and Discord messaging. + */ +public class DiscordVerificationService { + + private final VerificationCache verificationCache; + private final VerificationCodeGenerator codeGenerator; + private final DiscordVerificationDialogFactory dialogFactory; + private final DiscordLinkService discordLinkService; + private final NoticeService noticeService; + private final MessageConfig messageConfig; + + public DiscordVerificationService( + VerificationCache verificationCache, + VerificationCodeGenerator codeGenerator, + DiscordVerificationDialogFactory dialogFactory, + DiscordLinkService discordLinkService, + NoticeService noticeService, + MessageConfig messageConfig + ) { + this.verificationCache = verificationCache; + this.codeGenerator = codeGenerator; + this.dialogFactory = dialogFactory; + this.discordLinkService = discordLinkService; + this.noticeService = noticeService; + this.messageConfig = messageConfig; + } + + /** + * Checks if a player has a pending verification. + * + * @param playerUuid the player's UUID + * @return true if a pending verification exists + */ + public boolean hasPendingVerification(UUID playerUuid) { + return this.verificationCache.hasPendingVerification(playerUuid); + } + + /** + * Initiates the verification process by sending a code to Discord and showing the dialog. + * + * @param player the Minecraft player + * @param discordId the Discord user ID + * @param discordUser the Discord user + * @return a Mono that completes when the verification message is sent + */ + public Mono startVerification(Player player, String discordId, User discordUser) { + UUID playerUuid = player.getUniqueId(); + String code = this.codeGenerator.generate(); + + VerificationData data = new VerificationData(discordId, code); + if (!this.verificationCache.putIfAbsent(playerUuid, data)) { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); + return Mono.empty(); + } + + return discordUser.getPrivateChannel() + .flatMap(channel -> channel.createMessage( + this.messageConfig.discord.discordDmVerificationMessage + .replace("{CODE}", code) + .replace("{PLAYER}", player.getName()) + )) + .doOnSuccess(msg -> { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationCodeSent); + this.showVerificationDialog(player); + }) + .doOnError(error -> { + this.verificationCache.invalidate(playerUuid); + this.noticeService.player(playerUuid, messages -> messages.discord.cannotSendDm); + }) + .then(); + } + + private void showVerificationDialog(Player player) { + Dialog dialog = this.dialogFactory.create( + (view, enteredCode) -> this.handleVerification(player, enteredCode), + () -> this.handleCancellation(player) + ); + player.showDialog(dialog); + } + + private void handleVerification(Player player, String enteredCode) { + UUID playerUuid = player.getUniqueId(); + + this.verificationCache.get(playerUuid).ifPresentOrElse( + verificationData -> { + if (!verificationData.code().equals(enteredCode)) { + this.verificationCache.invalidate(playerUuid); + this.noticeService.player(playerUuid, messages -> messages.discord.invalidCode); + return; + } + + this.verificationCache.invalidate(playerUuid); + this.discordLinkService.createLink(playerUuid, verificationData.discordId()) + .thenAccept(success -> { + if (success) { + this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); + } else { + this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); + } + }); + }, + () -> this.noticeService.player(playerUuid, messages -> messages.discord.verificationExpired) + ); + } + + private void handleCancellation(Player player) { + UUID playerUuid = player.getUniqueId(); + this.verificationCache.invalidate(playerUuid); + this.noticeService.player(playerUuid, messages -> messages.discord.verificationCancelled); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java new file mode 100644 index 00000000..f966aa08 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java @@ -0,0 +1,59 @@ +package com.eternalcode.parcellockers.discord.verification; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; + +/** + * Cache for storing pending Discord verification requests. + */ +public class VerificationCache { + + private static final Duration EXPIRATION_TIME = Duration.ofMinutes(2); + + private final Cache cache = Caffeine.newBuilder() + .expireAfterWrite(EXPIRATION_TIME) + .build(); + + /** + * Checks if a player has a pending verification. + * + * @param playerUuid the player's UUID + * @return true if a pending verification exists + */ + public boolean hasPendingVerification(UUID playerUuid) { + return this.cache.getIfPresent(playerUuid) != null; + } + + /** + * Retrieves the pending verification data for a player. + * + * @param playerUuid the player's UUID + * @return an Optional containing the verification data, or empty if none exists + */ + public Optional get(UUID playerUuid) { + return Optional.ofNullable(this.cache.getIfPresent(playerUuid)); + } + + /** + * Stores verification data for a player if no pending verification exists. + * + * @param playerUuid the player's UUID + * @param data the verification data to store + * @return true if the data was stored, false if a pending verification already exists + */ + public boolean putIfAbsent(UUID playerUuid, VerificationData data) { + return this.cache.asMap().putIfAbsent(playerUuid, data) == null; + } + + /** + * Removes the pending verification for a player. + * + * @param playerUuid the player's UUID + */ + public void invalidate(UUID playerUuid) { + this.cache.invalidate(playerUuid); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java new file mode 100644 index 00000000..aabcd6df --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java @@ -0,0 +1,22 @@ +package com.eternalcode.parcellockers.discord.verification; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Generates random verification codes for Discord account linking. + */ +public class VerificationCodeGenerator { + + private static final int MIN_CODE = 1000; + private static final int MAX_CODE = 10000; + + /** + * Generates a random 4-digit verification code. + * + * @return a 4-digit verification code as a string + */ + public String generate() { + int code = ThreadLocalRandom.current().nextInt(MIN_CODE, MAX_CODE); + return String.valueOf(code); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java new file mode 100644 index 00000000..348927d0 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java @@ -0,0 +1,9 @@ +package com.eternalcode.parcellockers.discord.verification; + +/** + * Represents a pending Discord verification request. + * + * @param discordId the Discord user ID + * @param code the verification code + */ +public record VerificationData(String discordId, String code) {} From 7dd0cc735a4f855dd098a6e89c7bad0c1d876c0c Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:20:37 +0100 Subject: [PATCH 31/36] Ensure proper encapsulation --- .../parcellockers/ParcelLockers.java | 34 +++++++-------- .../DiscordVerificationDialogFactory.java | 42 +++++++------------ .../DiscordVerificationService.java | 37 ++++++++++++++-- .../verification/VerificationCache.java | 16 ++++--- .../VerificationCodeGenerator.java | 4 +- .../verification/VerificationData.java | 2 +- 6 files changed, 73 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 29382dbb..91ed42a3 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -33,10 +33,7 @@ import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; import com.eternalcode.parcellockers.discord.verification.DiscordLinkValidationService; -import com.eternalcode.parcellockers.discord.verification.DiscordVerificationDialogFactory; import com.eternalcode.parcellockers.discord.verification.DiscordVerificationService; -import com.eternalcode.parcellockers.discord.verification.VerificationCache; -import com.eternalcode.parcellockers.discord.verification.VerificationCodeGenerator; import com.eternalcode.parcellockers.gui.GuiManager; import com.eternalcode.parcellockers.gui.implementation.locker.LockerGui; import com.eternalcode.parcellockers.gui.implementation.remote.MainGui; @@ -105,7 +102,8 @@ public void onEnable() { ConfigService configService = new ConfigService(); PluginConfig config = configService.create(PluginConfig.class, new File(this.getDataFolder(), "config.yml")); - MessageConfig messageConfig = configService.create(MessageConfig.class, new File(this.getDataFolder(), "messages.yml")); + MessageConfig messageConfig = + configService.create(MessageConfig.class, new File(this.getDataFolder(), "messages.yml")); Server server = this.getServer(); NoticeService noticeService = new NoticeService(messageConfig, miniMessage); Scheduler scheduler = new BukkitSchedulerImpl(this); @@ -137,7 +135,8 @@ public void onEnable() { // database repositories ParcelRepositoryOrmLite parcelRepository = new ParcelRepositoryOrmLite(databaseManager, scheduler); LockerRepositoryOrmLite lockerRepository = new LockerRepositoryOrmLite(databaseManager, scheduler); - ParcelContentRepository parcelContentRepository = new ParcelContentRepositoryOrmLite(databaseManager, scheduler); + ParcelContentRepository parcelContentRepository = + new ParcelContentRepositoryOrmLite(databaseManager, scheduler); DeliveryRepositoryOrmLite deliveryRepository = new DeliveryRepositoryOrmLite(databaseManager, scheduler); ItemStorageRepository itemStorageRepository = new ItemStorageRepositoryOrmLite(databaseManager, scheduler); UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, scheduler); @@ -156,7 +155,8 @@ public void onEnable() { UserValidationService userValidationService = new UserValidator(); UserManager userManager = new UserManagerImpl(userRepository, userValidationService, server); LockerValidationService lockerValidationService = new LockerValidator(); - LockerManager lockerManager = new LockerManager(config, lockerRepository, lockerValidationService, parcelRepository, server); + LockerManager lockerManager = + new LockerManager(config, lockerRepository, lockerValidationService, parcelRepository, server); ParcelContentManager parcelContentManager = new ParcelContentManager(parcelContentRepository); ItemStorageManager itemStorageManager = new ItemStorageManager(itemStorageRepository, server); DeliveryManager deliveryManager = new DeliveryManager(deliveryRepository); @@ -243,7 +243,8 @@ public void onEnable() { ); this.discordClientManager.initialize(); - DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); + DiscordLinkRepository discordLinkRepository = + new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); activeLinkService = new DiscordFallbackLinkService(discordLinkRepository); notificationService = new Discord4JNotificationService( this.discordClientManager.getClient(), @@ -255,20 +256,11 @@ public void onEnable() { this.discordClientManager.getClient() ); - VerificationCache verificationCache = new VerificationCache(); - VerificationCodeGenerator codeGenerator = new VerificationCodeGenerator(); - DiscordVerificationDialogFactory dialogFactory = new DiscordVerificationDialogFactory( - miniMessage, - messageConfig - ); - - DiscordVerificationService verificationService = new DiscordVerificationService( - verificationCache, - codeGenerator, - dialogFactory, + DiscordVerificationService verificationService = DiscordVerificationService.create( activeLinkService, noticeService, - messageConfig + messageConfig, + miniMessage ); liteCommandsBuilder.commands( @@ -311,7 +303,9 @@ public void onEnable() { .forEach(parcel -> deliveryRepository.find(parcel.uuid()).thenAccept(optionalDelivery -> optionalDelivery.ifPresent(delivery -> { long delay = Math.max(0, delivery.deliveryTimestamp().toEpochMilli() - System.currentTimeMillis()); - scheduler.runLaterAsync(new ParcelSendTask(parcel, parcelService, deliveryManager), Duration.ofMillis(delay)); + scheduler.runLaterAsync( + new ParcelSendTask(parcel, parcelService, deliveryManager), + Duration.ofMillis(delay)); }) ))); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java index a0bf20e5..38d7f9b6 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java @@ -20,12 +20,12 @@ * 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. */ -public class DiscordVerificationDialogFactory { +class DiscordVerificationDialogFactory { private final MiniMessage miniMessage; private final MessageConfig messageConfig; - public DiscordVerificationDialogFactory(MiniMessage miniMessage, MessageConfig messageConfig) { + DiscordVerificationDialogFactory(MiniMessage miniMessage, MessageConfig messageConfig) { this.miniMessage = miniMessage; this.messageConfig = messageConfig; } @@ -37,21 +37,16 @@ public DiscordVerificationDialogFactory(MiniMessage miniMessage, MessageConfig m * @param onCancel callback when the user clicks cancel * @return the created dialog */ - public Dialog create(BiConsumer onVerify, Runnable onCancel) { + Dialog create(BiConsumer onVerify, Runnable onCancel) { return Dialog.create(builder -> builder.empty() .base(DialogBase.builder(this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogTitle)) .canCloseWithEscape(false) - .inputs(List.of( - DialogInput.text("code", this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogPlaceholder)) - .build() - )) - .build() - ) - .type(DialogType.confirmation( - this.createVerifyButton(onVerify), - this.createCancelButton(onCancel) - )) - ); + .inputs(List.of(DialogInput.text( + "code", + this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogPlaceholder)) + .build())) + .build()) + .type(DialogType.confirmation(this.createVerifyButton(onVerify), this.createCancelButton(onCancel)))); } private ActionButton createVerifyButton(BiConsumer onVerify) { @@ -59,14 +54,11 @@ private ActionButton createVerifyButton(BiConsumer o this.miniMessage.deserialize("Verify"), this.miniMessage.deserialize("Click to verify your Discord account"), 200, - DialogAction.customClick((DialogResponseView view, Audience audience) -> { - String enteredCode = view.getText("code"); - onVerify.accept(view, enteredCode); - }, ClickCallback.Options.builder() - .uses(1) - .lifetime(ClickCallback.DEFAULT_LIFETIME) - .build()) - ); + DialogAction.customClick( + (DialogResponseView view, Audience audience) -> { + String enteredCode = view.getText("code"); + onVerify.accept(view, enteredCode); + }, ClickCallback.Options.builder().uses(1).lifetime(ClickCallback.DEFAULT_LIFETIME).build())); } private ActionButton createCancelButton(Runnable onCancel) { @@ -76,10 +68,6 @@ private ActionButton createCancelButton(Runnable onCancel) { 200, DialogAction.customClick( (DialogResponseView view, Audience audience) -> onCancel.run(), - ClickCallback.Options.builder() - .uses(1) - .lifetime(ClickCallback.DEFAULT_LIFETIME) - .build()) - ); + ClickCallback.Options.builder().uses(1).lifetime(ClickCallback.DEFAULT_LIFETIME).build())); } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java index 308f3de9..399cacfa 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java @@ -6,6 +6,7 @@ import discord4j.core.object.entity.User; import io.papermc.paper.dialog.Dialog; import java.util.UUID; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; import reactor.core.publisher.Mono; @@ -22,7 +23,7 @@ public class DiscordVerificationService { private final NoticeService noticeService; private final MessageConfig messageConfig; - public DiscordVerificationService( + private DiscordVerificationService( VerificationCache verificationCache, VerificationCodeGenerator codeGenerator, DiscordVerificationDialogFactory dialogFactory, @@ -38,6 +39,36 @@ public DiscordVerificationService( this.messageConfig = messageConfig; } + /** + * Creates a new DiscordVerificationService with all required dependencies. + * + * @param discordLinkService the Discord link service + * @param noticeService the notice service + * @param messageConfig the message configuration + * @param miniMessage the MiniMessage instance for text formatting + * @return a new DiscordVerificationService instance + */ + public static DiscordVerificationService create( + DiscordLinkService discordLinkService, + NoticeService noticeService, + MessageConfig messageConfig, + MiniMessage miniMessage + ) { + VerificationCache verificationCache = new VerificationCache(); + VerificationCodeGenerator codeGenerator = new VerificationCodeGenerator(); + DiscordVerificationDialogFactory dialogFactory = + new DiscordVerificationDialogFactory(miniMessage, messageConfig); + + return new DiscordVerificationService( + verificationCache, + codeGenerator, + dialogFactory, + discordLinkService, + noticeService, + messageConfig + ); + } + /** * Checks if a player has a pending verification. * @@ -51,8 +82,8 @@ public boolean hasPendingVerification(UUID playerUuid) { /** * Initiates the verification process by sending a code to Discord and showing the dialog. * - * @param player the Minecraft player - * @param discordId the Discord user ID + * @param player the Minecraft player + * @param discordId the Discord user ID * @param discordUser the Discord user * @return a Mono that completes when the verification message is sent */ diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java index f966aa08..c61a2dba 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java @@ -9,13 +9,11 @@ /** * Cache for storing pending Discord verification requests. */ -public class VerificationCache { +class VerificationCache { private static final Duration EXPIRATION_TIME = Duration.ofMinutes(2); - private final Cache cache = Caffeine.newBuilder() - .expireAfterWrite(EXPIRATION_TIME) - .build(); + private final Cache cache = Caffeine.newBuilder().expireAfterWrite(EXPIRATION_TIME).build(); /** * Checks if a player has a pending verification. @@ -23,7 +21,7 @@ public class VerificationCache { * @param playerUuid the player's UUID * @return true if a pending verification exists */ - public boolean hasPendingVerification(UUID playerUuid) { + boolean hasPendingVerification(UUID playerUuid) { return this.cache.getIfPresent(playerUuid) != null; } @@ -33,7 +31,7 @@ public boolean hasPendingVerification(UUID playerUuid) { * @param playerUuid the player's UUID * @return an Optional containing the verification data, or empty if none exists */ - public Optional get(UUID playerUuid) { + Optional get(UUID playerUuid) { return Optional.ofNullable(this.cache.getIfPresent(playerUuid)); } @@ -41,10 +39,10 @@ public Optional get(UUID playerUuid) { * Stores verification data for a player if no pending verification exists. * * @param playerUuid the player's UUID - * @param data the verification data to store + * @param data the verification data to store * @return true if the data was stored, false if a pending verification already exists */ - public boolean putIfAbsent(UUID playerUuid, VerificationData data) { + boolean putIfAbsent(UUID playerUuid, VerificationData data) { return this.cache.asMap().putIfAbsent(playerUuid, data) == null; } @@ -53,7 +51,7 @@ public boolean putIfAbsent(UUID playerUuid, VerificationData data) { * * @param playerUuid the player's UUID */ - public void invalidate(UUID playerUuid) { + void invalidate(UUID playerUuid) { this.cache.invalidate(playerUuid); } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java index aabcd6df..d76eb980 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java @@ -5,7 +5,7 @@ /** * Generates random verification codes for Discord account linking. */ -public class VerificationCodeGenerator { +class VerificationCodeGenerator { private static final int MIN_CODE = 1000; private static final int MAX_CODE = 10000; @@ -15,7 +15,7 @@ public class VerificationCodeGenerator { * * @return a 4-digit verification code as a string */ - public String generate() { + String generate() { int code = ThreadLocalRandom.current().nextInt(MIN_CODE, MAX_CODE); return String.valueOf(code); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java index 348927d0..4a94f7c5 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java @@ -6,4 +6,4 @@ * @param discordId the Discord user ID * @param code the verification code */ -public record VerificationData(String discordId, String code) {} +record VerificationData(String discordId, String code) {} From 055acc2d10d6df61d92608d8deb76df6c7dba343 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:28:10 +0100 Subject: [PATCH 32/36] Refactor sendPrivateMessage method to return void and improve message handling --- .../implementation/MessageConfig.java | 6 ++++++ .../Discord4JNotificationService.java | 4 +--- .../DiscordNotificationService.java | 7 ++----- .../DiscordSrvNotificationService.java | 19 +++++++++++-------- .../DiscordVerificationDialogFactory.java | 8 ++++---- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index 4ca65530..c11f5e86 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -253,6 +253,12 @@ public static class DiscordMessages extends OkaeriConfig { public String verificationDialogTitle = "&6Enter your Discord verification code:"; public String verificationDialogPlaceholder = "&7Enter 4-digit code"; + @Comment({"", "# Dialog button configuration" }) + public String verificationButtonVerifyText = "Verify"; + public String verificationButtonVerifyDescription = "Click to verify your Discord account"; + public String verificationButtonCancelText = "Cancel"; + public String verificationButtonCancelDescription = "Click to cancel verification"; + @Comment({"", "# The message sent to the Discord user via DM" }) @Comment("# Placeholders: {CODE} - the verification code, {PLAYER} - the Minecraft player name") public String discordDmVerificationMessage = "**📦 ParcelLockers Verification**\n\nPlayer **{PLAYER}** is trying to link their Minecraft account to your Discord account.\n\nYour verification code is: **{CODE}**\n\nThis code will expire in 2 minutes."; diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java index 498d5435..b692c461 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java @@ -21,7 +21,7 @@ public Discord4JNotificationService(GatewayDiscordClient client, Logger logger) } @Override - public CompletableFuture sendPrivateMessage(String discordId, String message) { + public void sendPrivateMessage(String discordId, String message) { CompletableFuture future = new CompletableFuture<>(); this.client.getUserById(Snowflake.of(Long.parseLong(discordId))) @@ -34,7 +34,5 @@ public CompletableFuture sendPrivateMessage(String discordId, String me future.complete(false); }) .subscribe(); - - return future; } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java index 91e6ab2b..1682b57c 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java @@ -1,7 +1,5 @@ package com.eternalcode.parcellockers.discord.notification; -import java.util.concurrent.CompletableFuture; - /** * Service interface for sending Discord notifications. * Implementations can use different Discord libraries (Discord4J, DiscordSRV/JDA, etc.) @@ -12,8 +10,7 @@ public interface DiscordNotificationService { * Sends a private message to a Discord user. * * @param discordId the Discord user ID to send the message to - * @param message the message content to send - * @return a CompletableFuture that completes when the message is sent, returning true on success + * @param message the message content to send */ - CompletableFuture sendPrivateMessage(String discordId, String message); + void sendPrivateMessage(String discordId, String message); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java index aeb4fc19..239cc911 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java @@ -18,8 +18,8 @@ public DiscordSrvNotificationService(Logger logger) { } @Override - public CompletableFuture sendPrivateMessage(String discordId, String message) { - return CompletableFuture.supplyAsync(() -> { + public void sendPrivateMessage(String discordId, String message) { + CompletableFuture.supplyAsync(() -> { try { User user = DiscordUtil.getUserById(discordId); if (user == null) { @@ -28,15 +28,18 @@ public CompletableFuture sendPrivateMessage(String discordId, String me } user.openPrivateChannel() - .flatMap(channel -> channel.sendMessage(message)) - .queue( - success -> {}, - error -> this.logger.warning("Failed to send private message to Discord user " + discordId + ": " + error.getMessage()) - ); + .flatMap(channel -> channel.sendMessage(message)) + .queue( + success -> {}, + error -> this.logger.warning( + "Failed to send private message to Discord user " + discordId + ": " + + error.getMessage()) + ); return true; } catch (Exception e) { - this.logger.warning("Failed to send private message to Discord user " + discordId + ": " + e.getMessage()); + this.logger.warning( + "Failed to send private message to Discord user " + discordId + ": " + e.getMessage()); return false; } }); diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java index 38d7f9b6..bceabc88 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java @@ -51,8 +51,8 @@ Dialog create(BiConsumer onVerify, Runnable onCancel private ActionButton createVerifyButton(BiConsumer onVerify) { return ActionButton.create( - this.miniMessage.deserialize("Verify"), - this.miniMessage.deserialize("Click to verify your Discord account"), + this.miniMessage.deserialize(this.messageConfig.discord.verificationButtonVerifyText), + this.miniMessage.deserialize(this.messageConfig.discord.verificationButtonVerifyDescription), 200, DialogAction.customClick( (DialogResponseView view, Audience audience) -> { @@ -63,8 +63,8 @@ private ActionButton createVerifyButton(BiConsumer o private ActionButton createCancelButton(Runnable onCancel) { return ActionButton.create( - this.miniMessage.deserialize("Cancel"), - this.miniMessage.deserialize("Click to cancel verification"), + this.miniMessage.deserialize(this.messageConfig.discord.verificationButtonCancelText), + this.miniMessage.deserialize(this.messageConfig.discord.verificationButtonCancelDescription), 200, DialogAction.customClick( (DialogResponseView view, Audience audience) -> onCancel.run(), From 57ac290dbd2e871ffa18fe7f50cbb3eed61799fb Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 14:36:10 +0100 Subject: [PATCH 33/36] Improve error logging in DiscordClientManager during login failure --- .../parcellockers/discord/DiscordClientManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java index c6c1f7f0..136c9001 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java @@ -2,6 +2,7 @@ import discord4j.core.DiscordClient; import discord4j.core.GatewayDiscordClient; +import java.util.logging.Level; import java.util.logging.Logger; public class DiscordClientManager { @@ -29,8 +30,7 @@ public void initialize() { this.logger.severe("Failed to log in to Discord: login returned null client."); } } catch (Exception exception) { - this.logger.severe("Failed to log in to Discord: " + exception.getMessage()); - exception.printStackTrace(); + this.logger.log(Level.SEVERE, "Failed to log in to Discord", exception); } } From 201c382bbbc25eacf545a9752764a5d693612b0a Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:02:29 +0100 Subject: [PATCH 34/36] Refactor Discord ID handling to use long, add NoticeHandler, remove redundant javadocs --- .../parcellockers/ParcelLockers.java | 29 ++++++++++--------- .../command/handler/NoticeHandler.java | 26 +++++++++++++++++ .../implementation/MessageConfig.java | 1 + .../discord/DiscordClientManager.java | 14 +++++---- .../discord/DiscordFallbackLinkService.java | 6 ++-- .../parcellockers/discord/DiscordLink.java | 2 +- .../discord/DiscordLinkService.java | 6 ++-- .../discord/DiscordSrvLinkService.java | 25 +++++----------- .../discord/argument/SnowflakeArgument.java | 22 +++++--------- .../discord/command/DiscordLinkCommand.java | 21 ++++++-------- .../command/DiscordSrvLinkCommand.java | 5 +--- .../command/DiscordSrvUnlinkCommand.java | 19 +++++------- .../discord/command/DiscordUnlinkCommand.java | 18 ++++++------ .../DiscordDeliverNotificationController.java | 6 ++-- .../Discord4JNotificationService.java | 4 +-- .../DiscordNotificationService.java | 2 +- .../DiscordSrvNotificationService.java | 20 +++++-------- .../discord/repository/DiscordLinkEntity.java | 8 +++-- .../repository/DiscordLinkRepository.java | 4 +-- .../DiscordLinkRepositoryOrmLite.java | 19 +++++------- .../DiscordLinkValidationService.java | 21 ++------------ .../DiscordVerificationDialogFactory.java | 13 --------- .../DiscordVerificationService.java | 6 ++-- .../verification/VerificationCache.java | 27 ----------------- .../VerificationCodeGenerator.java | 8 ----- .../verification/VerificationData.java | 8 +---- 26 files changed, 134 insertions(+), 206 deletions(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/command/handler/NoticeHandler.java diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index f0387d2b..625e3b55 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -4,9 +4,11 @@ import com.eternalcode.commons.adventure.AdventureLegacyColorPreProcessor; import com.eternalcode.commons.bukkit.scheduler.BukkitSchedulerImpl; import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.multification.notice.Notice; import com.eternalcode.parcellockers.command.debug.DebugCommand; import com.eternalcode.parcellockers.command.handler.InvalidUsageHandlerImpl; import com.eternalcode.parcellockers.command.handler.MissingPermissionsHandlerImpl; +import com.eternalcode.parcellockers.command.handler.NoticeHandler; import com.eternalcode.parcellockers.configuration.ConfigService; import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import com.eternalcode.parcellockers.configuration.implementation.PluginConfig; @@ -102,8 +104,7 @@ public void onEnable() { ConfigService configService = new ConfigService(); PluginConfig config = configService.create(PluginConfig.class, new File(this.getDataFolder(), "config.yml")); - MessageConfig messageConfig = - configService.create(MessageConfig.class, new File(this.getDataFolder(), "messages.yml")); + MessageConfig messageConfig = configService.create(MessageConfig.class, new File(this.getDataFolder(), "messages.yml")); Server server = this.getServer(); NoticeService noticeService = new NoticeService(messageConfig, miniMessage); Scheduler scheduler = new BukkitSchedulerImpl(this); @@ -134,8 +135,7 @@ public void onEnable() { // database repositories ParcelRepositoryOrmLite parcelRepository = new ParcelRepositoryOrmLite(databaseManager, scheduler); LockerRepositoryOrmLite lockerRepository = new LockerRepositoryOrmLite(databaseManager, scheduler); - ParcelContentRepository parcelContentRepository = - new ParcelContentRepositoryOrmLite(databaseManager, scheduler); + ParcelContentRepository parcelContentRepository = new ParcelContentRepositoryOrmLite(databaseManager, scheduler); DeliveryRepositoryOrmLite deliveryRepository = new DeliveryRepositoryOrmLite(databaseManager, scheduler); ItemStorageRepository itemStorageRepository = new ItemStorageRepositoryOrmLite(databaseManager, scheduler); UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, scheduler); @@ -154,8 +154,7 @@ public void onEnable() { UserValidationService userValidationService = new UserValidator(); UserManager userManager = new UserManagerImpl(userRepository, userValidationService, server); LockerValidationService lockerValidationService = new LockerValidator(); - LockerManager lockerManager = - new LockerManager(config, lockerRepository, lockerValidationService, parcelRepository, server); + LockerManager lockerManager = new LockerManager(config, lockerRepository, lockerValidationService, parcelRepository, server); ParcelContentManager parcelContentManager = new ParcelContentManager(parcelContentRepository); ItemStorageManager itemStorageManager = new ItemStorageManager(itemStorageRepository, server); DeliveryManager deliveryManager = new DeliveryManager(deliveryRepository); @@ -200,7 +199,7 @@ public void onEnable() { var liteCommandsBuilder = LiteBukkitFactory.builder(this.getName(), this) .extension(new LiteAdventureExtension<>()) - .argument(Snowflake.class, new SnowflakeArgument()) + .argument(Snowflake.class, new SnowflakeArgument(messageConfig)) .message(LiteBukkitMessages.PLAYER_ONLY, messageConfig.playerOnlyCommand) .message(LiteBukkitMessages.PLAYER_NOT_FOUND, messageConfig.playerNotFound) .commands(LiteCommandsAnnotations.of( @@ -211,7 +210,8 @@ public void onEnable() { noticeService, deliveryManager) )) .invalidUsage(new InvalidUsageHandlerImpl(noticeService)) - .missingPermission(new MissingPermissionsHandlerImpl(noticeService)); + .missingPermission(new MissingPermissionsHandlerImpl(noticeService)) + .result(Notice.class, new NoticeHandler(noticeService)); DiscordSettings discordSettings = config.discord; if (discordSettings.enabled) { @@ -230,8 +230,7 @@ public void onEnable() { ); } else { if (config.discord.botToken == null || config.discord.botToken.isBlank()) { - this.getLogger() - .severe("Discord integration is enabled but some of the properties are not set! Disabling..."); + this.getLogger().severe("Discord integration is enabled but some of the properties are not set! Disabling..."); server.getPluginManager().disablePlugin(this); return; } @@ -240,10 +239,14 @@ public void onEnable() { discordSettings.botToken, this.getLogger() ); - this.discordClientManager.initialize(); - DiscordLinkRepository discordLinkRepository = - new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); + if (!this.discordClientManager.initialize()) { + this.getLogger().severe("Failed to initialize Discord client! Disabling..."); + server.getPluginManager().disablePlugin(this); + return; + } + + DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); activeLinkService = new DiscordFallbackLinkService(discordLinkRepository); notificationService = new Discord4JNotificationService( this.discordClientManager.getClient(), diff --git a/src/main/java/com/eternalcode/parcellockers/command/handler/NoticeHandler.java b/src/main/java/com/eternalcode/parcellockers/command/handler/NoticeHandler.java new file mode 100644 index 00000000..bea276a0 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/command/handler/NoticeHandler.java @@ -0,0 +1,26 @@ +package com.eternalcode.parcellockers.command.handler; + +import com.eternalcode.multification.notice.Notice; +import com.eternalcode.parcellockers.notification.NoticeService; +import dev.rollczi.litecommands.handler.result.ResultHandler; +import dev.rollczi.litecommands.handler.result.ResultHandlerChain; +import dev.rollczi.litecommands.invocation.Invocation; +import org.bukkit.command.CommandSender; + +public class NoticeHandler implements ResultHandler { + + private final NoticeService noticeService; + + public NoticeHandler(NoticeService noticeService) { + this.noticeService = noticeService; + } + + @Override + public void handle(Invocation invocation, Notice result, ResultHandlerChain chain) { + this.noticeService.create() + .viewer(invocation.sender()) + .notice(result) + .send(); + } + +} diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index c11f5e86..a279c70f 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -248,6 +248,7 @@ public static class DiscordMessages extends OkaeriConfig { public Notice adminUnlinkSuccess = Notice.chat("&2✔ &aSuccessfully unlinked the Discord account from the player."); public Notice discordNotLinked = Notice.chat("&4✘ &cNo Minecraft account is linked to this Discord ID!"); public Notice adminUnlinkByDiscordSuccess = Notice.chat("&2✔ &aSuccessfully unlinked the Minecraft account from the Discord ID."); + public Notice invalidDiscordId = Notice.chat("&4✘ &cInvalid Discord ID format! Please provide a valid Discord ID."); @Comment({"", "# Dialog configuration for verification" }) public String verificationDialogTitle = "&6Enter your Discord verification code:"; diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java index 136c9001..e4d1357c 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java @@ -17,20 +17,24 @@ public DiscordClientManager(String token, Logger logger) { this.logger = logger; } - public void initialize() { + public boolean initialize() { this.logger.info("Discord integration is enabled. Logging in to Discord..."); try { GatewayDiscordClient discordClient = DiscordClient.create(this.token) .login() .block(); - if (discordClient != null) { - this.client = discordClient; - this.logger.info("Successfully logged in to Discord."); - } else { + + if (discordClient == null) { this.logger.severe("Failed to log in to Discord: login returned null client."); + return false; } + + this.client = discordClient; + this.logger.info("Successfully logged in to Discord."); + return true; } catch (Exception exception) { this.logger.log(Level.SEVERE, "Failed to log in to Discord", exception); + return false; } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java index 132e9bde..56e4d645 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java @@ -19,12 +19,12 @@ public CompletableFuture> findLinkByPlayer(UUID playerUuid } @Override - public CompletableFuture> findLinkByDiscordId(String discordId) { + public CompletableFuture> findLinkByDiscordId(long discordId) { return this.repository.findByDiscordId(discordId); } @Override - public CompletableFuture createLink(UUID playerUuid, String discordId) { + public CompletableFuture createLink(UUID playerUuid, long discordId) { DiscordLink link = new DiscordLink(playerUuid, discordId); return this.repository.save(link); } @@ -35,7 +35,7 @@ public CompletableFuture unlinkPlayer(UUID playerUuid) { } @Override - public CompletableFuture unlinkDiscordId(String discordId) { + public CompletableFuture unlinkDiscordId(long discordId) { return this.repository.deleteByDiscordId(discordId); } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java index 023817b1..f427eae1 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLink.java @@ -2,5 +2,5 @@ import java.util.UUID; -public record DiscordLink(UUID minecraftUuid, String discordId) { +public record DiscordLink(UUID minecraftUuid, long discordId) { } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java index 11c36592..9f3e6296 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordLinkService.java @@ -6,13 +6,13 @@ public interface DiscordLinkService { - CompletableFuture unlinkDiscordId(String discordId); + CompletableFuture unlinkDiscordId(long discordId); CompletableFuture unlinkPlayer(UUID playerUuid); - CompletableFuture createLink(UUID playerUuid, String discordId); + CompletableFuture createLink(UUID playerUuid, long discordId); - CompletableFuture> findLinkByDiscordId(String discordId); + CompletableFuture> findLinkByDiscordId(long discordId); CompletableFuture> findLinkByPlayer(UUID playerUuid); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java index a1f254b6..320287c2 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java @@ -9,10 +9,6 @@ import java.util.logging.Level; import java.util.logging.Logger; -/** - * DiscordSRV-based implementation of DiscordLinkService. - * Delegates account linking functionality to DiscordSRV plugin. - */ public class DiscordSrvLinkService implements DiscordLinkService { private final Logger logger; @@ -28,14 +24,14 @@ public CompletableFuture> findLinkByPlayer(UUID playerUuid if (discordId == null) { return Optional.empty(); } - return Optional.of(new DiscordLink(playerUuid, discordId)); + return Optional.of(new DiscordLink(playerUuid, Long.parseLong(discordId))); }); } @Override - public CompletableFuture> findLinkByDiscordId(String discordId) { + public CompletableFuture> findLinkByDiscordId(long discordId) { return CompletableFuture.supplyAsync(() -> { - UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(discordId); + UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(Long.toString(discordId)); if (playerUuid == null) { return Optional.empty(); } @@ -44,10 +40,10 @@ public CompletableFuture> findLinkByDiscordId(String disco } @Override - public CompletableFuture createLink(UUID playerUuid, String discordId) { + public CompletableFuture createLink(UUID playerUuid, long discordId) { return CompletableFuture.supplyAsync(() -> { try { - DiscordSRV.getPlugin().getAccountLinkManager().link(discordId, playerUuid); + DiscordSRV.getPlugin().getAccountLinkManager().link(Long.toString(discordId), playerUuid); return true; } catch (Exception e) { this.logger.log(Level.WARNING, "Failed to create DiscordSRV link", e); @@ -70,10 +66,10 @@ public CompletableFuture unlinkPlayer(UUID playerUuid) { } @Override - public CompletableFuture unlinkDiscordId(String discordId) { + public CompletableFuture unlinkDiscordId(long discordId) { return CompletableFuture.supplyAsync(() -> { try { - UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(discordId); + UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(Long.toString(discordId)); if (playerUuid == null) { return false; } @@ -86,9 +82,6 @@ public CompletableFuture unlinkDiscordId(String discordId) { }); } - /** - * Gets the Discord user by their ID using DiscordSRV's JDA instance. - */ public Optional getDiscordUser(String discordId) { try { return Optional.ofNullable(DiscordUtil.getUserById(discordId)); @@ -97,10 +90,6 @@ public Optional getDiscordUser(String discordId) { } } - /** - * Gets the linking code for a player to use in Discord. - * Returns empty if the player is already linked. - */ public Optional getLinkingCode(UUID playerUuid) { String existingDiscordId = DiscordSRV.getPlugin().getAccountLinkManager().getDiscordId(playerUuid); if (existingDiscordId != null) { diff --git a/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java b/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java index 9775fce9..53b398e7 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java @@ -1,38 +1,32 @@ package com.eternalcode.parcellockers.discord.argument; +import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import dev.rollczi.litecommands.argument.Argument; import dev.rollczi.litecommands.argument.parser.ParseResult; import dev.rollczi.litecommands.argument.resolver.ArgumentResolver; import dev.rollczi.litecommands.invocation.Invocation; -import dev.rollczi.litecommands.suggestion.SuggestionContext; -import dev.rollczi.litecommands.suggestion.SuggestionResult; import discord4j.common.util.Snowflake; import org.bukkit.command.CommandSender; public class SnowflakeArgument extends ArgumentResolver { + private final MessageConfig messageConfig; + + public SnowflakeArgument(MessageConfig messageConfig) { + this.messageConfig = messageConfig; + } + @Override protected ParseResult parse( Invocation invocation, Argument context, String argument) { try { - // Try to parse the string as a Snowflake Snowflake snowflake = Snowflake.of(argument); return ParseResult.success(snowflake); } catch (NumberFormatException exception) { - // If parsing fails, return an error - return ParseResult.failure("&4✘ &cInvalid Discord ID format! Please provide a valid Discord ID."); + return ParseResult.failure(this.messageConfig.discord.invalidDiscordId); } } - - @Override - public SuggestionResult suggest( - Invocation invocation, - Argument argument, - SuggestionContext context) { - // No suggestions for Discord IDs (they are unique numeric values) - return SuggestionResult.empty(); - } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index d861903e..860a16da 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -17,9 +17,6 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -/** - * Command responsible for linking a Minecraft account with a Discord account. - */ @Command(name = "parcel linkdiscord") public class DiscordLinkCommand { @@ -43,23 +40,23 @@ public DiscordLinkCommand( @Execute void linkSelf(@Context Player player, @Arg Snowflake discordId) { UUID playerUuid = player.getUniqueId(); - String discordIdString = discordId.asString(); + long discordIdLong = discordId.asLong(); if (this.verificationService.hasPendingVerification(playerUuid)) { this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); return; } - this.validationService.validate(playerUuid, discordIdString) + this.validationService.validate(playerUuid, discordIdLong) .thenCompose(validationResult -> { if (!validationResult.isValid()) { this.sendValidationError(playerUuid, validationResult); return CompletableFuture.completedFuture(null); } - return this.validationService.getDiscordUser(discordIdString) + return this.validationService.getDiscordUser(discordIdLong) .thenCompose(discordUser -> - this.verificationService.startVerification(player, discordIdString, discordUser).toFuture() + this.verificationService.startVerification(player, discordIdLong, discordUser).toFuture() ); }) .exceptionally(error -> { @@ -71,24 +68,24 @@ void linkSelf(@Context Player player, @Arg Snowflake discordId) { @Execute @Permission("parcellockers.admin") void linkOther(@Context CommandSender sender, @Arg OfflinePlayer player, @Arg Snowflake discordId) { - String discordIdString = discordId.asString(); + long discordIdLong = discordId.asLong(); UUID playerUuid = player.getUniqueId(); - this.validationService.validate(playerUuid, discordIdString) + this.validationService.validate(playerUuid, discordIdLong) .thenCompose(validationResult -> { if (!validationResult.isValid()) { this.sendValidationErrorToViewer(sender, validationResult); return CompletableFuture.completedFuture(null); } - return this.discordLinkService.createLink(playerUuid, discordIdString) + return this.discordLinkService.createLink(playerUuid, discordIdLong) .thenAccept(success -> { if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminLinkSuccess); this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + return; } + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); }); }) .exceptionally(error -> { diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java index a4408950..6581adf8 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java @@ -9,10 +9,7 @@ import java.util.UUID; import org.bukkit.entity.Player; -/** - * Command for linking Discord accounts when DiscordSRV is installed. - * Redirects users to use DiscordSRV's linking system. - */ + @Command(name = "parcel linkdiscord") public class DiscordSrvLinkCommand { diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java index 4e6aeae5..7818f628 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java @@ -12,11 +12,6 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -/** - * Command for unlinking Discord accounts when DiscordSRV is installed. - * Regular players are redirected to use DiscordSRV's unlinking system, - * while admins can still forcefully unlink accounts. - */ @Command(name = "parcel unlinkdiscord") public class DiscordSrvUnlinkCommand { @@ -61,9 +56,9 @@ void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkSuccess); this.noticeService.player(targetUuid, messages -> messages.discord.unlinkSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + return; } + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); }); }); } @@ -71,20 +66,20 @@ void unlinkPlayer(@Context CommandSender sender, @Arg Player targetPlayer) { @Execute @Permission("parcellockers.admin") void unlinkByDiscordId(@Context CommandSender sender, @Arg Snowflake discordId) { - String discordIdString = discordId.asString(); + long discordIdLong = discordId.asLong(); - this.discordSrvLinkService.findLinkByDiscordId(discordIdString).thenAccept(optionalLink -> { + this.discordSrvLinkService.findLinkByDiscordId(discordIdLong).thenAccept(optionalLink -> { if (optionalLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); return; } - this.discordSrvLinkService.unlinkDiscordId(discordIdString).thenAccept(success -> { + this.discordSrvLinkService.unlinkDiscordId(discordIdLong).thenAccept(success -> { if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkByDiscordSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + return; } + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); }); }); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java index 51e5b9b6..a034340e 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java @@ -40,9 +40,9 @@ void unlinkSelf(@Context Player player) { this.discordLinkService.unlinkPlayer(playerUuid).thenAccept(success -> { if (success) { this.noticeService.player(playerUuid, messages -> messages.discord.unlinkSuccess); - } else { - this.noticeService.player(playerUuid, messages -> messages.discord.unlinkFailed); + return; } + this.noticeService.player(playerUuid, messages -> messages.discord.unlinkFailed); }); }); } @@ -62,9 +62,9 @@ void unlinkPlayer(@Context CommandSender sender, @Arg OfflinePlayer targetPlayer if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkSuccess); this.noticeService.player(targetUuid, messages -> messages.discord.unlinkSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + return; } + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); }); }); } @@ -72,20 +72,20 @@ void unlinkPlayer(@Context CommandSender sender, @Arg OfflinePlayer targetPlayer @Execute @Permission("parcellockers.admin") void unlinkByDiscordId(@Context CommandSender sender, @Arg Snowflake discordId) { - String discordIdString = discordId.asString(); + long discordIdLong = discordId.asLong(); - this.discordLinkService.findLinkByDiscordId(discordIdString).thenAccept(optionalLink -> { + this.discordLinkService.findLinkByDiscordId(discordIdLong).thenAccept(optionalLink -> { if (optionalLink.isEmpty()) { this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); return; } - this.discordLinkService.unlinkDiscordId(discordIdString).thenAccept(success -> { + this.discordLinkService.unlinkDiscordId(discordIdLong).thenAccept(success -> { if (success) { this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkByDiscordSuccess); - } else { - this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + return; } + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); }); }); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java b/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java index 1c3ce0df..501aa88a 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java @@ -42,12 +42,11 @@ void onParcelDeliver(ParcelDeliverEvent event) { this.discordLinkService.findLinkByPlayer(receiverUuid) .thenAccept(optionalLink -> optionalLink.ifPresent(link -> { - String discordId = link.discordId(); - this.sendDeliveryNotification(parcel, discordId); + this.sendDeliveryNotification(parcel, link.discordId()); })); } - private void sendDeliveryNotification(Parcel parcel, String discordId) { + private void sendDeliveryNotification(Parcel parcel, long discordId) { CompletableFuture senderNameFuture = this.userManager.get(parcel.sender()) .thenApply(optionalUser -> optionalUser.map(User::name).orElse("Unknown")); @@ -64,7 +63,6 @@ private void sendDeliveryNotification(Parcel parcel, String discordId) { .replace("{PRIORITY}", parcel.priority() ? "🔴 High Priority" : "⚪ Normal Priority"); this.notificationService.sendPrivateMessage(discordId, message); - return null; }); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java index b692c461..6e5caadd 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java @@ -21,10 +21,10 @@ public Discord4JNotificationService(GatewayDiscordClient client, Logger logger) } @Override - public void sendPrivateMessage(String discordId, String message) { + public void sendPrivateMessage(long discordId, String message) { CompletableFuture future = new CompletableFuture<>(); - this.client.getUserById(Snowflake.of(Long.parseLong(discordId))) + this.client.getUserById(Snowflake.of(discordId)) .flatMap(user -> user.getPrivateChannel()) .flatMap(channel -> channel.createMessage(message)) .subscribeOn(Schedulers.boundedElastic()) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java index 1682b57c..9bbc9523 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java @@ -12,5 +12,5 @@ public interface DiscordNotificationService { * @param discordId the Discord user ID to send the message to * @param message the message content to send */ - void sendPrivateMessage(String discordId, String message); + void sendPrivateMessage(long discordId, String message); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java index 239cc911..ce349417 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java @@ -18,28 +18,24 @@ public DiscordSrvNotificationService(Logger logger) { } @Override - public void sendPrivateMessage(String discordId, String message) { + public void sendPrivateMessage(long discordId, String message) { CompletableFuture.supplyAsync(() -> { try { - User user = DiscordUtil.getUserById(discordId); + User user = DiscordUtil.getUserById(Long.toString(discordId)); if (user == null) { this.logger.warning("Could not find Discord user with ID: " + discordId); return false; } - user.openPrivateChannel() - .flatMap(channel -> channel.sendMessage(message)) - .queue( - success -> {}, - error -> this.logger.warning( - "Failed to send private message to Discord user " + discordId + ": " - + error.getMessage()) - ); + user.openPrivateChannel().flatMap(channel -> channel.sendMessage(message)).queue( + success -> {}, + error -> this.logger.warning( + "Failed to send private message to Discord user " + discordId + ": " + error.getMessage()) + ); return true; } catch (Exception e) { - this.logger.warning( - "Failed to send private message to Discord user " + discordId + ": " + e.getMessage()); + this.logger.warning("Failed to send private message to Discord user " + discordId + ": " + e.getMessage()); return false; } }); diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java index 8b1ec077..9c2bd00b 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java @@ -8,15 +8,17 @@ @DatabaseTable(tableName = "discord_links") class DiscordLinkEntity { + static final String ID_COLUMN_NAME = "discord_id"; + @DatabaseField(id = true, columnName = "minecraft_uuid") private UUID minecraftUuid; - @DatabaseField(index = true, columnName = "discord_id") - private String discordId; + @DatabaseField(index = true, columnName = ID_COLUMN_NAME) + private long discordId; DiscordLinkEntity() {} - DiscordLinkEntity(UUID minecraftUuid, String discordId) { + DiscordLinkEntity(UUID minecraftUuid, long discordId) { this.minecraftUuid = minecraftUuid; this.discordId = discordId; } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java index 22872ff5..42ccf210 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepository.java @@ -10,8 +10,8 @@ public interface DiscordLinkRepository { CompletableFuture save(DiscordLink link); CompletableFuture> findByPlayerUuid(UUID playerUuid); - CompletableFuture> findByDiscordId(String discordId); + CompletableFuture> findByDiscordId(long discordId); CompletableFuture deleteByPlayerUuid(UUID playerUuid); - CompletableFuture deleteByDiscordId(String discordId); + CompletableFuture deleteByDiscordId(long discordId); } \ No newline at end of file diff --git a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java index 0b96abf7..33433188 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java @@ -14,8 +14,6 @@ public class DiscordLinkRepositoryOrmLite extends AbstractRepositoryOrmLite implements DiscordLinkRepository { - public static final String ID_COLUMN_NAME = "discord_id"; - public DiscordLinkRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) { super(databaseManager, scheduler); @@ -39,13 +37,12 @@ public CompletableFuture> findByPlayerUuid(UUID playerUuid } @Override - public CompletableFuture> findByDiscordId(String discordId) { - return this.action(DiscordLinkEntity.class, dao -> { - var queryBuilder = dao.queryBuilder() - .where() - .eq(ID_COLUMN_NAME, discordId); - return dao.queryForFirst(queryBuilder.prepare()); - }).thenApply(entity -> Optional.ofNullable(entity).map(DiscordLinkEntity::toDomain)); + public CompletableFuture> findByDiscordId(long discordId) { + return this.action(DiscordLinkEntity.class, dao -> dao.queryBuilder() + .where() + .eq(DiscordLinkEntity.ID_COLUMN_NAME, discordId) + .queryForFirst()) + .thenApply(entity -> Optional.ofNullable(entity).map(DiscordLinkEntity::toDomain)); } @Override @@ -55,10 +52,10 @@ public CompletableFuture deleteByPlayerUuid(UUID playerUuid) { } @Override - public CompletableFuture deleteByDiscordId(String discordId) { + public CompletableFuture deleteByDiscordId(long discordId) { return this.action(DiscordLinkEntity.class, dao -> { DeleteBuilder deleteBuilder = dao.deleteBuilder(); - deleteBuilder.where().eq(ID_COLUMN_NAME, discordId); + deleteBuilder.where().eq(DiscordLinkEntity.ID_COLUMN_NAME, discordId); return deleteBuilder.delete(); }).thenApply(deletedRows -> deletedRows > 0); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java index 70078b99..85216044 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java @@ -9,9 +9,6 @@ import java.util.concurrent.CompletableFuture; import reactor.core.publisher.Mono; -/** - * Service responsible for validating Discord account linking requests. - */ public class DiscordLinkValidationService { private final DiscordLinkService discordLinkService; @@ -25,15 +22,7 @@ public DiscordLinkValidationService( this.discordClient = discordClient; } - /** - * Validates whether a player can link to a Discord account. - * Checks if the player or Discord account is already linked. - * - * @param playerUuid the player's UUID - * @param discordId the Discord user ID - * @return a CompletableFuture containing the validation result - */ - public CompletableFuture validate(UUID playerUuid, String discordId) { + public CompletableFuture validate(UUID playerUuid, long discordId) { return this.discordLinkService.findLinkByPlayer(playerUuid).thenCompose(optionalLink -> { if (optionalLink.isPresent()) { return CompletableFuture.completedFuture(ValidationResult.invalid("alreadyLinked")); @@ -52,13 +41,7 @@ public CompletableFuture validate(UUID playerUuid, String disc }); } - /** - * Fetches the Discord user by their ID. - * - * @param discordId the Discord user ID - * @return a CompletableFuture containing the Discord user - */ - public CompletableFuture getDiscordUser(String discordId) { + public CompletableFuture getDiscordUser(long discordId) { return this.discordClient.getUserById(Snowflake.of(discordId)).toFuture(); } } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java index bceabc88..1036e003 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java @@ -14,12 +14,6 @@ import net.kyori.adventure.text.event.ClickCallback; import net.kyori.adventure.text.minimessage.MiniMessage; -/** - * Factory for creating Discord verification dialogs. - *

- * 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. - */ class DiscordVerificationDialogFactory { private final MiniMessage miniMessage; @@ -30,13 +24,6 @@ class DiscordVerificationDialogFactory { this.messageConfig = messageConfig; } - /** - * Creates a verification dialog for Discord account linking. - * - * @param onVerify callback when the user clicks verify, receives the entered code - * @param onCancel callback when the user clicks cancel - * @return the created dialog - */ Dialog create(BiConsumer onVerify, Runnable onCancel) { return Dialog.create(builder -> builder.empty() .base(DialogBase.builder(this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogTitle)) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java index 399cacfa..48720ea2 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java @@ -87,7 +87,7 @@ public boolean hasPendingVerification(UUID playerUuid) { * @param discordUser the Discord user * @return a Mono that completes when the verification message is sent */ - public Mono startVerification(Player player, String discordId, User discordUser) { + public Mono startVerification(Player player, long discordId, User discordUser) { UUID playerUuid = player.getUniqueId(); String code = this.codeGenerator.generate(); @@ -138,9 +138,9 @@ private void handleVerification(Player player, String enteredCode) { .thenAccept(success -> { if (success) { this.noticeService.player(playerUuid, messages -> messages.discord.linkSuccess); - } else { - this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); + return; } + this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); }); }, () -> this.noticeService.player(playerUuid, messages -> messages.discord.verificationExpired) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java index c61a2dba..4647fef6 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java @@ -6,51 +6,24 @@ import java.util.Optional; import java.util.UUID; -/** - * Cache for storing pending Discord verification requests. - */ class VerificationCache { private static final Duration EXPIRATION_TIME = Duration.ofMinutes(2); private final Cache cache = Caffeine.newBuilder().expireAfterWrite(EXPIRATION_TIME).build(); - /** - * Checks if a player has a pending verification. - * - * @param playerUuid the player's UUID - * @return true if a pending verification exists - */ boolean hasPendingVerification(UUID playerUuid) { return this.cache.getIfPresent(playerUuid) != null; } - /** - * Retrieves the pending verification data for a player. - * - * @param playerUuid the player's UUID - * @return an Optional containing the verification data, or empty if none exists - */ Optional get(UUID playerUuid) { return Optional.ofNullable(this.cache.getIfPresent(playerUuid)); } - /** - * Stores verification data for a player if no pending verification exists. - * - * @param playerUuid the player's UUID - * @param data the verification data to store - * @return true if the data was stored, false if a pending verification already exists - */ boolean putIfAbsent(UUID playerUuid, VerificationData data) { return this.cache.asMap().putIfAbsent(playerUuid, data) == null; } - /** - * Removes the pending verification for a player. - * - * @param playerUuid the player's UUID - */ void invalidate(UUID playerUuid) { this.cache.invalidate(playerUuid); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java index d76eb980..5e0b23d9 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java @@ -2,19 +2,11 @@ import java.util.concurrent.ThreadLocalRandom; -/** - * Generates random verification codes for Discord account linking. - */ class VerificationCodeGenerator { private static final int MIN_CODE = 1000; private static final int MAX_CODE = 10000; - /** - * Generates a random 4-digit verification code. - * - * @return a 4-digit verification code as a string - */ String generate() { int code = ThreadLocalRandom.current().nextInt(MIN_CODE, MAX_CODE); return String.valueOf(code); diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java index 4a94f7c5..a4e4014f 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java @@ -1,9 +1,3 @@ package com.eternalcode.parcellockers.discord.verification; -/** - * Represents a pending Discord verification request. - * - * @param discordId the Discord user ID - * @param code the verification code - */ -record VerificationData(String discordId, String code) {} +record VerificationData(long discordId, String code) {} From 5d50f244a3bf1045cd6bd90e7471848dbfb740f2 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:10:00 +0100 Subject: [PATCH 35/36] Delete random javadocs --- .../Discord4JNotificationService.java | 4 --- .../DiscordSrvNotificationService.java | 4 --- .../DiscordVerificationDialogFactory.java | 6 +++-- .../DiscordVerificationService.java | 27 ------------------- 4 files changed, 4 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java index 6e5caadd..ccd1bc1b 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java @@ -6,10 +6,6 @@ import java.util.logging.Logger; import reactor.core.scheduler.Schedulers; -/** - * Discord4J-based implementation of DiscordNotificationService. - * Uses the Discord4J library to send notifications. - */ public class Discord4JNotificationService implements DiscordNotificationService { private final GatewayDiscordClient client; diff --git a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java index ce349417..e2b70ee6 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java @@ -5,10 +5,6 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; -/** - * DiscordSRV-based implementation of DiscordNotificationService. - * Uses DiscordSRV's JDA instance to send notifications. - */ public class DiscordSrvNotificationService implements DiscordNotificationService { private final Logger logger; diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java index 1036e003..83d30dd1 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java @@ -16,6 +16,8 @@ class DiscordVerificationDialogFactory { + private static final String DIALOG_KEY = "code"; + private final MiniMessage miniMessage; private final MessageConfig messageConfig; @@ -29,7 +31,7 @@ Dialog create(BiConsumer onVerify, Runnable onCancel .base(DialogBase.builder(this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogTitle)) .canCloseWithEscape(false) .inputs(List.of(DialogInput.text( - "code", + DIALOG_KEY, this.miniMessage.deserialize(this.messageConfig.discord.verificationDialogPlaceholder)) .build())) .build()) @@ -43,7 +45,7 @@ private ActionButton createVerifyButton(BiConsumer o 200, DialogAction.customClick( (DialogResponseView view, Audience audience) -> { - String enteredCode = view.getText("code"); + String enteredCode = view.getText(DIALOG_KEY); onVerify.accept(view, enteredCode); }, ClickCallback.Options.builder().uses(1).lifetime(ClickCallback.DEFAULT_LIFETIME).build())); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java index 48720ea2..e97c5003 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java @@ -10,10 +10,6 @@ import org.bukkit.entity.Player; import reactor.core.publisher.Mono; -/** - * Service responsible for handling the Discord verification process. - * Coordinates between cache, dialog, and Discord messaging. - */ public class DiscordVerificationService { private final VerificationCache verificationCache; @@ -39,15 +35,6 @@ private DiscordVerificationService( this.messageConfig = messageConfig; } - /** - * Creates a new DiscordVerificationService with all required dependencies. - * - * @param discordLinkService the Discord link service - * @param noticeService the notice service - * @param messageConfig the message configuration - * @param miniMessage the MiniMessage instance for text formatting - * @return a new DiscordVerificationService instance - */ public static DiscordVerificationService create( DiscordLinkService discordLinkService, NoticeService noticeService, @@ -69,24 +56,10 @@ public static DiscordVerificationService create( ); } - /** - * Checks if a player has a pending verification. - * - * @param playerUuid the player's UUID - * @return true if a pending verification exists - */ public boolean hasPendingVerification(UUID playerUuid) { return this.verificationCache.hasPendingVerification(playerUuid); } - /** - * Initiates the verification process by sending a code to Discord and showing the dialog. - * - * @param player the Minecraft player - * @param discordId the Discord user ID - * @param discordUser the Discord user - * @return a Mono that completes when the verification message is sent - */ public Mono startVerification(Player player, long discordId, User discordUser) { UUID playerUuid = player.getUniqueId(); String code = this.codeGenerator.generate(); From cfd75090d4530cd3cb4c5ab2fd187e616facff42 Mon Sep 17 00:00:00 2001 From: Jakubk15 <77227023+Jakubk15@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:00:52 +0100 Subject: [PATCH 36/36] Refactor Discord notification handling, introduce Formatter for message templating, and add DiscordProviderPicker for improved integration --- .../parcellockers/ParcelLockers.java | 93 +---------- .../implementation/MessageConfig.java | 3 + .../discord/DiscordProviderPicker.java | 144 ++++++++++++++++++ .../discord/command/DiscordLinkCommand.java | 3 +- .../command/DiscordSrvLinkCommand.java | 1 - .../DiscordDeliverNotificationController.java | 22 ++- 6 files changed, 169 insertions(+), 97 deletions(-) create mode 100644 src/main/java/com/eternalcode/parcellockers/discord/DiscordProviderPicker.java diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index 625e3b55..8681e82a 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -12,7 +12,6 @@ import com.eternalcode.parcellockers.configuration.ConfigService; import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import com.eternalcode.parcellockers.configuration.implementation.PluginConfig; -import com.eternalcode.parcellockers.configuration.implementation.PluginConfig.DiscordSettings; import com.eternalcode.parcellockers.content.ParcelContentManager; import com.eternalcode.parcellockers.content.repository.ParcelContentRepository; import com.eternalcode.parcellockers.content.repository.ParcelContentRepositoryOrmLite; @@ -20,22 +19,8 @@ import com.eternalcode.parcellockers.delivery.DeliveryManager; import com.eternalcode.parcellockers.delivery.repository.DeliveryRepositoryOrmLite; import com.eternalcode.parcellockers.discord.DiscordClientManager; -import com.eternalcode.parcellockers.discord.DiscordFallbackLinkService; -import com.eternalcode.parcellockers.discord.DiscordLinkService; -import com.eternalcode.parcellockers.discord.DiscordSrvLinkService; +import com.eternalcode.parcellockers.discord.DiscordProviderPicker; import com.eternalcode.parcellockers.discord.argument.SnowflakeArgument; -import com.eternalcode.parcellockers.discord.command.DiscordLinkCommand; -import com.eternalcode.parcellockers.discord.command.DiscordSrvLinkCommand; -import com.eternalcode.parcellockers.discord.command.DiscordSrvUnlinkCommand; -import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; -import com.eternalcode.parcellockers.discord.controller.DiscordDeliverNotificationController; -import com.eternalcode.parcellockers.discord.notification.Discord4JNotificationService; -import com.eternalcode.parcellockers.discord.notification.DiscordNotificationService; -import com.eternalcode.parcellockers.discord.notification.DiscordSrvNotificationService; -import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; -import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; -import com.eternalcode.parcellockers.discord.verification.DiscordLinkValidationService; -import com.eternalcode.parcellockers.discord.verification.DiscordVerificationService; import com.eternalcode.parcellockers.gui.GuiManager; import com.eternalcode.parcellockers.gui.implementation.locker.LockerGui; import com.eternalcode.parcellockers.gui.implementation.remote.MainGui; @@ -213,78 +198,12 @@ public void onEnable() { .missingPermission(new MissingPermissionsHandlerImpl(noticeService)) .result(Notice.class, new NoticeHandler(noticeService)); - DiscordSettings discordSettings = config.discord; - if (discordSettings.enabled) { - DiscordNotificationService notificationService; - DiscordLinkService activeLinkService; - - if (server.getPluginManager().isPluginEnabled("DiscordSRV")) { - this.getLogger().info("DiscordSRV detected! Using DiscordSRV for account linking."); - DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(this.getLogger()); - activeLinkService = discordSrvLinkService; - notificationService = new DiscordSrvNotificationService(this.getLogger()); - - liteCommandsBuilder.commands( - new DiscordSrvLinkCommand(discordSrvLinkService, noticeService), - new DiscordSrvUnlinkCommand(discordSrvLinkService, noticeService) - ); - } else { - if (config.discord.botToken == null || config.discord.botToken.isBlank()) { - this.getLogger().severe("Discord integration is enabled but some of the properties are not set! Disabling..."); - server.getPluginManager().disablePlugin(this); - return; - } - - this.discordClientManager = new DiscordClientManager( - discordSettings.botToken, - this.getLogger() - ); - - if (!this.discordClientManager.initialize()) { - this.getLogger().severe("Failed to initialize Discord client! Disabling..."); - server.getPluginManager().disablePlugin(this); - return; - } - - DiscordLinkRepository discordLinkRepository = new DiscordLinkRepositoryOrmLite(databaseManager, scheduler); - activeLinkService = new DiscordFallbackLinkService(discordLinkRepository); - notificationService = new Discord4JNotificationService( - this.discordClientManager.getClient(), - this.getLogger() - ); - - DiscordLinkValidationService validationService = new DiscordLinkValidationService( - activeLinkService, - this.discordClientManager.getClient() - ); - - DiscordVerificationService verificationService = DiscordVerificationService.create( - activeLinkService, - noticeService, - messageConfig, - miniMessage - ); - - liteCommandsBuilder.commands( - new DiscordLinkCommand( - activeLinkService, - validationService, - verificationService, - noticeService), - new DiscordUnlinkCommand(activeLinkService, noticeService) - ); - } + DiscordProviderPicker discordProviderPicker = new DiscordProviderPicker( + config, messageConfig, server, noticeService, scheduler, databaseManager, + this.getLogger(), userManager, this, miniMessage + ); - server.getPluginManager().registerEvents( - new DiscordDeliverNotificationController( - notificationService, - activeLinkService, - userManager, - messageConfig - ), - this - ); - } + this.discordClientManager = discordProviderPicker.pick(liteCommandsBuilder); this.liteCommands = liteCommandsBuilder.build(); diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index a279c70f..3e9e8482 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -268,6 +268,9 @@ public static class DiscordMessages extends OkaeriConfig { @Comment("# Placeholders: {PARCEL_NAME}, {SENDER}, {RECEIVER}, {DESCRIPTION}, {SIZE}, {PRIORITY}") public String parcelDeliveryNotification = "**📦 Parcel Delivered!**\n\nYour parcel **{PARCEL_NAME}** has been delivered!\n\n**From:** {SENDER}\n**Size:** {SIZE}\n**Priority:** {PRIORITY}\n**Description:** {DESCRIPTION}"; + public String highPriorityPlaceholder = "🔴 High Priority"; + public String normalPriorityPlaceholder = "⚪ Normal Priority"; + @Comment({"", "# DiscordSRV integration messages" }) @Comment("# These messages are shown when DiscordSRV is installed and handles account linking") public Notice discordSrvLinkRedirect = Notice.builder() diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordProviderPicker.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordProviderPicker.java new file mode 100644 index 00000000..07c6ac7e --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordProviderPicker.java @@ -0,0 +1,144 @@ +package com.eternalcode.parcellockers.discord; + +import com.eternalcode.commons.scheduler.Scheduler; +import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; +import com.eternalcode.parcellockers.configuration.implementation.PluginConfig; +import com.eternalcode.parcellockers.database.DatabaseManager; +import com.eternalcode.parcellockers.discord.command.DiscordLinkCommand; +import com.eternalcode.parcellockers.discord.command.DiscordSrvLinkCommand; +import com.eternalcode.parcellockers.discord.command.DiscordSrvUnlinkCommand; +import com.eternalcode.parcellockers.discord.command.DiscordUnlinkCommand; +import com.eternalcode.parcellockers.discord.controller.DiscordDeliverNotificationController; +import com.eternalcode.parcellockers.discord.notification.Discord4JNotificationService; +import com.eternalcode.parcellockers.discord.notification.DiscordNotificationService; +import com.eternalcode.parcellockers.discord.notification.DiscordSrvNotificationService; +import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepository; +import com.eternalcode.parcellockers.discord.repository.DiscordLinkRepositoryOrmLite; +import com.eternalcode.parcellockers.discord.verification.DiscordLinkValidationService; +import com.eternalcode.parcellockers.discord.verification.DiscordVerificationService; +import com.eternalcode.parcellockers.notification.NoticeService; +import com.eternalcode.parcellockers.user.UserManager; +import dev.rollczi.litecommands.LiteCommandsBuilder; +import java.util.logging.Logger; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.java.JavaPlugin; + +public class DiscordProviderPicker { + + private final PluginConfig config; + private final MessageConfig messageConfig; + private final Server server; + private final NoticeService noticeService; + private final Scheduler scheduler; + private final DatabaseManager databaseManager; + private final Logger logger; + private final UserManager userManager; + private final JavaPlugin plugin; + private final MiniMessage miniMessage; + + public DiscordProviderPicker( + PluginConfig config, + MessageConfig messageConfig, + Server server, + NoticeService noticeService, + Scheduler scheduler, + DatabaseManager databaseManager, + Logger logger, + UserManager userManager, + JavaPlugin plugin, + MiniMessage miniMessage + ) { + this.config = config; + this.messageConfig = messageConfig; + this.server = server; + this.noticeService = noticeService; + this.scheduler = scheduler; + this.databaseManager = databaseManager; + this.logger = logger; + this.userManager = userManager; + this.plugin = plugin; + this.miniMessage = miniMessage; + } + + public DiscordClientManager pick(LiteCommandsBuilder liteCommandsBuilder) { + if (!this.config.discord.enabled) { + return null; + } + + DiscordNotificationService notificationService; + DiscordLinkService activeLinkService; + DiscordClientManager discordClientManager = null; + + if (this.server.getPluginManager().isPluginEnabled("DiscordSRV")) { + this.logger.info("DiscordSRV detected! Using DiscordSRV for account linking."); + DiscordSrvLinkService discordSrvLinkService = new DiscordSrvLinkService(this.logger); + activeLinkService = discordSrvLinkService; + notificationService = new DiscordSrvNotificationService(this.logger); + + liteCommandsBuilder.commands( + new DiscordSrvLinkCommand(discordSrvLinkService, this.noticeService), + new DiscordSrvUnlinkCommand(discordSrvLinkService, this.noticeService) + ); + } else { + if (this.config.discord.botToken == null || this.config.discord.botToken.isBlank()) { + this.logger.severe("Discord integration is enabled but some of the properties are not set! Disabling..."); + this.server.getPluginManager().disablePlugin(this.plugin); + return null; + } + + discordClientManager = new DiscordClientManager( + this.config.discord.botToken, + this.logger + ); + + if (!discordClientManager.initialize()) { + this.logger.severe("Failed to initialize Discord client! Disabling..."); + this.server.getPluginManager().disablePlugin(this.plugin); + return null; + } + + DiscordLinkRepository discordLinkRepository = + new DiscordLinkRepositoryOrmLite(this.databaseManager, this.scheduler); + activeLinkService = new DiscordFallbackLinkService(discordLinkRepository); + notificationService = new Discord4JNotificationService( + discordClientManager.getClient(), + this.logger + ); + + DiscordLinkValidationService validationService = new DiscordLinkValidationService( + activeLinkService, + discordClientManager.getClient() + ); + + DiscordVerificationService verificationService = DiscordVerificationService.create( + activeLinkService, + this.noticeService, + this.messageConfig, + this.miniMessage + ); + + liteCommandsBuilder.commands( + new DiscordLinkCommand( + activeLinkService, + validationService, + verificationService, + this.noticeService), + new DiscordUnlinkCommand(activeLinkService, this.noticeService) + ); + } + + this.server.getPluginManager().registerEvents( + new DiscordDeliverNotificationController( + notificationService, + activeLinkService, + this.userManager, + this.messageConfig + ), + this.plugin + ); + + return discordClientManager; + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java index 860a16da..c9233b2f 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -1,5 +1,6 @@ package com.eternalcode.parcellockers.discord.command; +import com.eternalcode.commons.concurrent.FutureHandler; import com.eternalcode.parcellockers.discord.DiscordLinkService; import com.eternalcode.parcellockers.discord.verification.DiscordLinkValidationService; import com.eternalcode.parcellockers.discord.verification.DiscordVerificationService; @@ -90,7 +91,7 @@ void linkOther(@Context CommandSender sender, @Arg OfflinePlayer player, @Arg Sn }) .exceptionally(error -> { this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); - return null; + return FutureHandler.handleException(error); }); } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java index 6581adf8..b1986098 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java @@ -36,7 +36,6 @@ void linkSelf(@Context Player player) { Optional linkingCode = this.discordSrvLinkService.getLinkingCode(playerUuid); if (linkingCode.isEmpty()) { - // This shouldn't happen if the player is not linked, but handle it gracefully this.noticeService.player(playerUuid, messages -> messages.discord.discordSrvAlreadyLinked); return; } diff --git a/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java b/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java index 501aa88a..f101c3dd 100644 --- a/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java @@ -1,5 +1,6 @@ package com.eternalcode.parcellockers.discord.controller; +import com.eternalcode.multification.shared.Formatter; import com.eternalcode.parcellockers.configuration.implementation.MessageConfig; import com.eternalcode.parcellockers.discord.DiscordLinkService; import com.eternalcode.parcellockers.discord.notification.DiscordNotificationService; @@ -54,15 +55,20 @@ private void sendDeliveryNotification(Parcel parcel, long discordId) { .thenApply(optionalUser -> optionalUser.map(User::name).orElse("Unknown")); senderNameFuture.thenCombine(receiverNameFuture, (senderName, receiverName) -> { - String message = this.messageConfig.discord.parcelDeliveryNotification - .replace("{PARCEL_NAME}", parcel.name()) - .replace("{SENDER}", senderName) - .replace("{RECEIVER}", receiverName) - .replace("{DESCRIPTION}", parcel.description() != null ? parcel.description() : "No description") - .replace("{SIZE}", parcel.size().name()) - .replace("{PRIORITY}", parcel.priority() ? "🔴 High Priority" : "⚪ Normal Priority"); + String message = this.messageConfig.discord.parcelDeliveryNotification; + Formatter formatter = new Formatter() + .register("{PARCEL_NAME}", parcel.name()) + .register("{SENDER}", senderName) + .register("{RECEIVER}", receiverName) + .register("{DESCRIPTION}", parcel.description() != null ? parcel.description() : "No description") + .register("{SIZE}", parcel.size().name()) + .register("{PRIORITY}", parcel.priority() + ? this.messageConfig.discord.highPriorityPlaceholder + : this.messageConfig.discord.normalPriorityPlaceholder + ); - this.notificationService.sendPrivateMessage(discordId, message); + + this.notificationService.sendPrivateMessage(discordId, formatter.format(message)); return null; }); }