diff --git a/build.gradle.kts b/build.gradle.kts index d0ff18dd..2d0e02e5 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 { @@ -78,6 +79,12 @@ dependencies { // vault compileOnly("com.github.MilkBowl:VaultAPI:1.7.1") + // discord integration library + paperLibrary("com.discord4j:discord4j-core:3.3.0") + + // discordsrv (optional integration) + 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") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.2") @@ -108,6 +115,10 @@ paper { required = true load = PaperPluginDescription.RelativeLoadOrder.BEFORE } + register("DiscordSRV") { + required = false + load = PaperPluginDescription.RelativeLoadOrder.BEFORE + } } } @@ -124,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 e5ce1eb0..8681e82a 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; @@ -16,6 +18,9 @@ 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.discord.DiscordProviderPicker; +import com.eternalcode.parcellockers.discord.argument.SnowflakeArgument; import com.eternalcode.parcellockers.gui.GuiManager; import com.eternalcode.parcellockers.gui.implementation.locker.LockerGui; import com.eternalcode.parcellockers.gui.implementation.remote.MainGui; @@ -54,6 +59,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; @@ -72,6 +78,7 @@ public final class ParcelLockers extends JavaPlugin { private SkullAPI skullAPI; private DatabaseManager databaseManager; private Economy economy; + private DiscordClientManager discordClientManager; @Override public void onEnable() { @@ -175,19 +182,30 @@ public void onEnable() { this.skullAPI ); - this.liteCommands = LiteBukkitFactory.builder(this.getName(), this) + var liteCommandsBuilder = LiteBukkitFactory.builder(this.getName(), this) .extension(new LiteAdventureExtension<>()) + .argument(Snowflake.class, new SnowflakeArgument(messageConfig)) .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(); + .result(Notice.class, new NoticeHandler(noticeService)); + + DiscordProviderPicker discordProviderPicker = new DiscordProviderPicker( + config, messageConfig, server, noticeService, scheduler, databaseManager, + this.getLogger(), userManager, this, miniMessage + ); + + this.discordClientManager = discordProviderPicker.pick(liteCommandsBuilder); + + this.liteCommands = liteCommandsBuilder.build(); Stream.of( new LockerInteractionController(lockerManager, lockerGUI, scheduler), @@ -197,8 +215,8 @@ public void onEnable() { new LoadUserController(userManager, server) ).forEach(controller -> server.getPluginManager().registerEvents(controller, this)); - 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() @@ -206,7 +224,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)); }) ))); } @@ -224,6 +244,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/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 531bd961..3e9e8482 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,110 @@ 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 run the command again to restart the verification process.") + .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."); + 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:"; + 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."; + + @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}"; + + 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() + .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/configuration/implementation/PluginConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java index 2132a377..f632052d 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,17 @@ 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 = true; + + @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 = ""; + } } 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..e4d1357c --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordClientManager.java @@ -0,0 +1,51 @@ +package com.eternalcode.parcellockers.discord; + +import discord4j.core.DiscordClient; +import discord4j.core.GatewayDiscordClient; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DiscordClientManager { + + private final String token; + private final Logger logger; + + private GatewayDiscordClient client; + + public DiscordClientManager(String token, Logger logger) { + this.token = token; + this.logger = logger; + } + + 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.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; + } + } + + public void shutdown() { + this.logger.info("Shutting down Discord client..."); + if (this.client != null) { + this.client.logout().block(); + } + } + + public GatewayDiscordClient getClient() { + return this.client; + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.java new file mode 100644 index 00000000..56e4d645 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordFallbackLinkService.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 DiscordFallbackLinkService implements DiscordLinkService { + + private final DiscordLinkRepository repository; + + public DiscordFallbackLinkService(DiscordLinkRepository repository) { + this.repository = repository; + } + + @Override + public CompletableFuture> findLinkByPlayer(UUID playerUuid) { + return this.repository.findByPlayerUuid(playerUuid); + } + + @Override + public CompletableFuture> findLinkByDiscordId(long discordId) { + return this.repository.findByDiscordId(discordId); + } + + @Override + public CompletableFuture createLink(UUID playerUuid, long 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(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 new file mode 100644 index 00000000..f427eae1 --- /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, long discordId) { +} 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..9f3e6296 --- /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(long discordId); + + CompletableFuture unlinkPlayer(UUID playerUuid); + + CompletableFuture createLink(UUID playerUuid, long discordId); + + CompletableFuture> findLinkByDiscordId(long discordId); + + CompletableFuture> findLinkByPlayer(UUID playerUuid); +} 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/DiscordSrvLinkService.java b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java new file mode 100644 index 00000000..320287c2 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/DiscordSrvLinkService.java @@ -0,0 +1,102 @@ +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; +import java.util.logging.Level; +import java.util.logging.Logger; + +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(() -> { + String discordId = DiscordSRV.getPlugin().getAccountLinkManager().getDiscordId(playerUuid); + if (discordId == null) { + return Optional.empty(); + } + return Optional.of(new DiscordLink(playerUuid, Long.parseLong(discordId))); + }); + } + + @Override + public CompletableFuture> findLinkByDiscordId(long discordId) { + return CompletableFuture.supplyAsync(() -> { + UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(Long.toString(discordId)); + if (playerUuid == null) { + return Optional.empty(); + } + return Optional.of(new DiscordLink(playerUuid, discordId)); + }); + } + + @Override + public CompletableFuture createLink(UUID playerUuid, long discordId) { + return CompletableFuture.supplyAsync(() -> { + try { + DiscordSRV.getPlugin().getAccountLinkManager().link(Long.toString(discordId), playerUuid); + return true; + } catch (Exception e) { + this.logger.log(Level.WARNING, "Failed to create DiscordSRV link", e); + return false; + } + }); + } + + @Override + public CompletableFuture unlinkPlayer(UUID playerUuid) { + return CompletableFuture.supplyAsync(() -> { + try { + DiscordSRV.getPlugin().getAccountLinkManager().unlink(playerUuid); + return true; + } catch (Exception exception) { + this.logger.log(Level.WARNING, "Failed to unlink DiscordSRV player", exception); + return false; + } + }); + } + + @Override + public CompletableFuture unlinkDiscordId(long discordId) { + return CompletableFuture.supplyAsync(() -> { + try { + UUID playerUuid = DiscordSRV.getPlugin().getAccountLinkManager().getUuid(Long.toString(discordId)); + if (playerUuid == null) { + return false; + } + DiscordSRV.getPlugin().getAccountLinkManager().unlink(playerUuid); + return true; + } catch (Exception exception) { + this.logger.log(Level.WARNING, "Failed to unlink DiscordSRV user by Discord ID", exception); + return false; + } + }); + } + + public Optional getDiscordUser(String discordId) { + try { + return Optional.ofNullable(DiscordUtil.getUserById(discordId)); + } catch (Exception e) { + return Optional.empty(); + } + } + + 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/argument/SnowflakeArgument.java b/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java new file mode 100644 index 00000000..53b398e7 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/argument/SnowflakeArgument.java @@ -0,0 +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 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 { + Snowflake snowflake = Snowflake.of(argument); + return ParseResult.success(snowflake); + } + catch (NumberFormatException exception) { + return ParseResult.failure(this.messageConfig.discord.invalidDiscordId); + } + } +} 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..c9233b2f --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java @@ -0,0 +1,116 @@ +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; +import com.eternalcode.parcellockers.notification.NoticeService; +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 java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@Command(name = "parcel linkdiscord") +public class DiscordLinkCommand { + + private final DiscordLinkService discordLinkService; + private final DiscordLinkValidationService validationService; + private final DiscordVerificationService verificationService; + private final NoticeService noticeService; + + public DiscordLinkCommand( + DiscordLinkService discordLinkService, + DiscordLinkValidationService validationService, + DiscordVerificationService verificationService, + NoticeService noticeService + ) { + this.discordLinkService = discordLinkService; + this.validationService = validationService; + this.verificationService = verificationService; + this.noticeService = noticeService; + } + + @Execute + void linkSelf(@Context Player player, @Arg Snowflake discordId) { + UUID playerUuid = player.getUniqueId(); + long discordIdLong = discordId.asLong(); + + if (this.verificationService.hasPendingVerification(playerUuid)) { + this.noticeService.player(playerUuid, messages -> messages.discord.verificationAlreadyPending); + return; + } + + this.validationService.validate(playerUuid, discordIdLong) + .thenCompose(validationResult -> { + if (!validationResult.isValid()) { + this.sendValidationError(playerUuid, validationResult); + return CompletableFuture.completedFuture(null); + } + + return this.validationService.getDiscordUser(discordIdLong) + .thenCompose(discordUser -> + this.verificationService.startVerification(player, discordIdLong, discordUser).toFuture() + ); + }) + .exceptionally(error -> { + this.noticeService.player(playerUuid, messages -> messages.discord.linkFailed); + return null; + }); + } + + @Execute + @Permission("parcellockers.admin") + void linkOther(@Context CommandSender sender, @Arg OfflinePlayer player, @Arg Snowflake discordId) { + long discordIdLong = discordId.asLong(); + UUID playerUuid = player.getUniqueId(); + + this.validationService.validate(playerUuid, discordIdLong) + .thenCompose(validationResult -> { + if (!validationResult.isValid()) { + this.sendValidationErrorToViewer(sender, validationResult); + return CompletableFuture.completedFuture(null); + } + + 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); + return; + } + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + }); + }) + .exceptionally(error -> { + this.noticeService.viewer(sender, messages -> messages.discord.linkFailed); + return FutureHandler.handleException(error); + }); + } + + 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); + } + } + + 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/command/DiscordSrvLinkCommand.java b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java new file mode 100644 index 00000000..b1986098 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvLinkCommand.java @@ -0,0 +1,50 @@ +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(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(optionalLink -> { + if (optionalLink.isPresent()) { + this.noticeService.player(playerUuid, messages -> messages.discord.discordSrvAlreadyLinked); + return; + } + + Optional linkingCode = this.discordSrvLinkService.getLinkingCode(playerUuid); + if (linkingCode.isEmpty()) { + 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..7818f628 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordSrvUnlinkCommand.java @@ -0,0 +1,86 @@ +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 discord4j.common.util.Snowflake; +import java.util.UUID; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@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(optionalLink -> { + if (optionalLink.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(optionalLink -> { + if (optionalLink.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); + return; + } + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + }); + }); + } + + @Execute + @Permission("parcellockers.admin") + void unlinkByDiscordId(@Context CommandSender sender, @Arg Snowflake discordId) { + long discordIdLong = discordId.asLong(); + + this.discordSrvLinkService.findLinkByDiscordId(discordIdLong).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { + this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); + return; + } + + this.discordSrvLinkService.unlinkDiscordId(discordIdLong).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkByDiscordSuccess); + 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 new file mode 100644 index 00000000..a034340e --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/command/DiscordUnlinkCommand.java @@ -0,0 +1,92 @@ +package com.eternalcode.parcellockers.discord.command; + +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; +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.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@Command(name = "parcel unlinkdiscord") +public class DiscordUnlinkCommand { + + private final DiscordLinkService discordLinkService; + private final NoticeService noticeService; + + public DiscordUnlinkCommand( + DiscordLinkService discordLinkService, + NoticeService noticeService + ) { + this.discordLinkService = discordLinkService; + this.noticeService = noticeService; + } + + @Execute + void unlinkSelf(@Context Player player) { + UUID playerUuid = player.getUniqueId(); + + this.discordLinkService.findLinkByPlayer(playerUuid).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { + this.noticeService.player(playerUuid, messages -> messages.discord.notLinked); + return; + } + + this.discordLinkService.unlinkPlayer(playerUuid).thenAccept(success -> { + if (success) { + this.noticeService.player(playerUuid, messages -> messages.discord.unlinkSuccess); + return; + } + this.noticeService.player(playerUuid, messages -> messages.discord.unlinkFailed); + }); + }); + } + + @Execute + @Permission("parcellockers.admin") + void unlinkPlayer(@Context CommandSender sender, @Arg OfflinePlayer targetPlayer) { + UUID targetUuid = targetPlayer.getUniqueId(); + + this.discordLinkService.findLinkByPlayer(targetUuid).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { + this.noticeService.viewer(sender, messages -> messages.discord.playerNotLinked); + return; + } + + this.discordLinkService.unlinkPlayer(targetUuid).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkSuccess); + this.noticeService.player(targetUuid, messages -> messages.discord.unlinkSuccess); + return; + } + this.noticeService.viewer(sender, messages -> messages.discord.unlinkFailed); + }); + }); + } + + @Execute + @Permission("parcellockers.admin") + void unlinkByDiscordId(@Context CommandSender sender, @Arg Snowflake discordId) { + long discordIdLong = discordId.asLong(); + + this.discordLinkService.findLinkByDiscordId(discordIdLong).thenAccept(optionalLink -> { + if (optionalLink.isEmpty()) { + this.noticeService.viewer(sender, messages -> messages.discord.discordNotLinked); + return; + } + + this.discordLinkService.unlinkDiscordId(discordIdLong).thenAccept(success -> { + if (success) { + this.noticeService.viewer(sender, messages -> messages.discord.adminUnlinkByDiscordSuccess); + 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 new file mode 100644 index 00000000..f101c3dd --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/controller/DiscordDeliverNotificationController.java @@ -0,0 +1,75 @@ +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; +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 java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +public class DiscordDeliverNotificationController implements Listener { + + private final DiscordNotificationService notificationService; + private final DiscordLinkService discordLinkService; + private final UserManager userManager; + private final MessageConfig messageConfig; + + public DiscordDeliverNotificationController( + DiscordNotificationService notificationService, + DiscordLinkService discordLinkService, + UserManager userManager, + MessageConfig messageConfig + ) { + this.notificationService = notificationService; + this.discordLinkService = discordLinkService; + this.userManager = userManager; + this.messageConfig = messageConfig; + } + + @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 -> { + this.sendDeliveryNotification(parcel, link.discordId()); + })); + } + + private void sendDeliveryNotification(Parcel parcel, long 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; + 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, formatter.format(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..ccd1bc1b --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/Discord4JNotificationService.java @@ -0,0 +1,34 @@ +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; + +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 void sendPrivateMessage(long discordId, String message) { + CompletableFuture future = new CompletableFuture<>(); + + this.client.getUserById(Snowflake.of(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(); + } +} 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..9bbc9523 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordNotificationService.java @@ -0,0 +1,16 @@ +package com.eternalcode.parcellockers.discord.notification; + +/** + * 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 + */ + 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 new file mode 100644 index 00000000..e2b70ee6 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/notification/DiscordSrvNotificationService.java @@ -0,0 +1,39 @@ +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; + +public class DiscordSrvNotificationService implements DiscordNotificationService { + + private final Logger logger; + + public DiscordSrvNotificationService(Logger logger) { + this.logger = logger; + } + + @Override + public void sendPrivateMessage(long discordId, String message) { + CompletableFuture.supplyAsync(() -> { + try { + 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()) + ); + + return true; + } catch (Exception e) { + 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 new file mode 100644 index 00000000..9c2bd00b --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkEntity.java @@ -0,0 +1,40 @@ +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 { + + static final String ID_COLUMN_NAME = "discord_id"; + + @DatabaseField(id = true, columnName = "minecraft_uuid") + private UUID minecraftUuid; + + @DatabaseField(index = true, columnName = ID_COLUMN_NAME) + private long discordId; + + DiscordLinkEntity() {} + + DiscordLinkEntity(UUID minecraftUuid, long discordId) { + this.minecraftUuid = minecraftUuid; + this.discordId = discordId; + } + + public static DiscordLinkEntity fromDomain(DiscordLink link) { + return new DiscordLinkEntity( + link.minecraftUuid(), + link.discordId() + ); + } + + public DiscordLink toDomain() { + return new DiscordLink( + 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..42ccf210 --- /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(long discordId); + + CompletableFuture deleteByPlayerUuid(UUID playerUuid); + 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 new file mode 100644 index 00000000..33433188 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java @@ -0,0 +1,62 @@ +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.stmt.DeleteBuilder; +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 DiscordLinkRepositoryOrmLite(DatabaseManager databaseManager, Scheduler scheduler) { + super(databaseManager, scheduler); + + try { + TableUtils.createTableIfNotExists(databaseManager.connectionSource(), DiscordLinkEntity.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(status -> status.isCreated() || status.isUpdated()); + } + + @Override + public CompletableFuture> findByPlayerUuid(UUID playerUuid) { + return this.selectSafe(DiscordLinkEntity.class, playerUuid.toString()) + .thenApply(optionalEntity -> optionalEntity.map(DiscordLinkEntity::toDomain)); + } + + @Override + 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 + public CompletableFuture deleteByPlayerUuid(UUID playerUuid) { + return this.deleteById(DiscordLinkEntity.class, playerUuid.toString()) + .thenApply(deletedRows -> deletedRows > 0); + } + + @Override + public CompletableFuture deleteByDiscordId(long discordId) { + return this.action(DiscordLinkEntity.class, dao -> { + DeleteBuilder deleteBuilder = dao.deleteBuilder(); + 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 new file mode 100644 index 00000000..85216044 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordLinkValidationService.java @@ -0,0 +1,47 @@ +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; + +public class DiscordLinkValidationService { + + private final DiscordLinkService discordLinkService; + private final GatewayDiscordClient discordClient; + + public DiscordLinkValidationService( + DiscordLinkService discordLinkService, + GatewayDiscordClient discordClient + ) { + this.discordLinkService = discordLinkService; + this.discordClient = discordClient; + } + + public CompletableFuture validate(UUID playerUuid, long 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(); + }); + }); + } + + 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 new file mode 100644 index 00000000..83d30dd1 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationDialogFactory.java @@ -0,0 +1,62 @@ +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; + +class DiscordVerificationDialogFactory { + + private static final String DIALOG_KEY = "code"; + + private final MiniMessage miniMessage; + private final MessageConfig messageConfig; + + DiscordVerificationDialogFactory(MiniMessage miniMessage, MessageConfig messageConfig) { + this.miniMessage = miniMessage; + this.messageConfig = messageConfig; + } + + 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( + DIALOG_KEY, + 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(this.messageConfig.discord.verificationButtonVerifyText), + this.miniMessage.deserialize(this.messageConfig.discord.verificationButtonVerifyDescription), + 200, + DialogAction.customClick( + (DialogResponseView view, Audience audience) -> { + String enteredCode = view.getText(DIALOG_KEY); + 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(this.messageConfig.discord.verificationButtonCancelText), + this.miniMessage.deserialize(this.messageConfig.discord.verificationButtonCancelDescription), + 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..e97c5003 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/DiscordVerificationService.java @@ -0,0 +1,128 @@ +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 net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.entity.Player; +import reactor.core.publisher.Mono; + +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; + + private 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; + } + + 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 + ); + } + + public boolean hasPendingVerification(UUID playerUuid) { + return this.verificationCache.hasPendingVerification(playerUuid); + } + + public Mono startVerification(Player player, long 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); + return; + } + 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..4647fef6 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCache.java @@ -0,0 +1,30 @@ +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; + +class VerificationCache { + + private static final Duration EXPIRATION_TIME = Duration.ofMinutes(2); + + private final Cache cache = Caffeine.newBuilder().expireAfterWrite(EXPIRATION_TIME).build(); + + boolean hasPendingVerification(UUID playerUuid) { + return this.cache.getIfPresent(playerUuid) != null; + } + + Optional get(UUID playerUuid) { + return Optional.ofNullable(this.cache.getIfPresent(playerUuid)); + } + + boolean putIfAbsent(UUID playerUuid, VerificationData data) { + return this.cache.asMap().putIfAbsent(playerUuid, data) == null; + } + + 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..5e0b23d9 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationCodeGenerator.java @@ -0,0 +1,14 @@ +package com.eternalcode.parcellockers.discord.verification; + +import java.util.concurrent.ThreadLocalRandom; + +class VerificationCodeGenerator { + + private static final int MIN_CODE = 1000; + private static final int MAX_CODE = 10000; + + 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..a4e4014f --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/discord/verification/VerificationData.java @@ -0,0 +1,3 @@ +package com.eternalcode.parcellockers.discord.verification; + +record VerificationData(long discordId, String code) {} 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() {