Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c5a6d1e
Begin working on Discord integration
Jakubk15 Jan 16, 2026
6307b0e
Finish implementing Discord integration
Jakubk15 Jan 17, 2026
9e4a998
Add `@Async` annotation to command executors to ensure that the serve…
Jakubk15 Jan 17, 2026
1109f11
Remove `@Async` annotations
Jakubk15 Jan 17, 2026
67ac9a2
Refactor Discord integration to use reactive programming for login an…
Jakubk15 Jan 17, 2026
e98e263
Remove redundant check for bot admin role ID in Discord configuration…
Jakubk15 Jan 17, 2026
8751030
Update src/main/java/com/eternalcode/parcellockers/discord/repository…
Jakubk15 Jan 17, 2026
031d466
Update src/main/java/com/eternalcode/parcellockers/discord/repository…
Jakubk15 Jan 17, 2026
fc95d79
Merge branch 'master' into discord
Jakubk15 Jan 17, 2026
ed98f10
Add service layer
Jakubk15 Jan 22, 2026
14ae198
Add DiscordSRV hook integration
Jakubk15 Jan 22, 2026
7de214f
feat: Update DiscordSRV integration and add parcel delivery notificat…
Jakubk15 Jan 24, 2026
143d8a1
feat: Implement abstract DiscordNotificationService with Discord4J an…
Jakubk15 Jan 24, 2026
161cec0
fix: declare appropriate events as async
Jakubk15 Jan 24, 2026
f2b470a
fix: avoid DiscordClientManager#getClient race conditions by using bl…
Jakubk15 Jan 24, 2026
9342e82
Remove redundant discord properties
Jakubk15 Jan 24, 2026
a271e9f
Update src/main/java/com/eternalcode/parcellockers/discord/command/Di…
Jakubk15 Jan 24, 2026
8809808
fix: initialize DiscordLinkRepository only when fallback Discord inte…
Jakubk15 Jan 24, 2026
457490b
Update src/main/java/com/eternalcode/parcellockers/discord/command/Di…
Jakubk15 Jan 24, 2026
dee0d6c
Update src/main/java/com/eternalcode/parcellockers/discord/command/Di…
Jakubk15 Jan 24, 2026
73477f6
Update src/main/java/com/eternalcode/parcellockers/configuration/impl…
Jakubk15 Jan 24, 2026
152dc6f
Merge remote-tracking branch 'origin/discord' into discord
Jakubk15 Jan 24, 2026
b8d54bf
Update src/main/java/com/eternalcode/parcellockers/discord/command/Di…
Jakubk15 Jan 24, 2026
a7f2a35
fix: add logging to DiscordSrvLinkService for better error handling
Jakubk15 Jan 24, 2026
2ddcf51
Merge remote-tracking branch 'origin/discord' into discord
Jakubk15 Jan 24, 2026
6d1c84e
Update PluginConfig.java
Jakubk15 Jan 24, 2026
5cbffe7
Update ParcelDeliverNotificationController.java
Jakubk15 Jan 24, 2026
77b19aa
Update ParcelDeliverNotificationController.java
Jakubk15 Jan 24, 2026
d0ed324
Add missing import
Jakubk15 Jan 24, 2026
3555651
WIP - Adjust to DMK suggestions
Jakubk15 Jan 25, 2026
8df0ed9
Use OfflinePlayer instead of String
Jakubk15 Jan 25, 2026
a31f0c2
Use OfflinePlayer in unlinkPlayer executor too
Jakubk15 Jan 25, 2026
e4070c3
Align with single-responsibility-principle
Jakubk15 Jan 25, 2026
7dd0cc7
Ensure proper encapsulation
Jakubk15 Jan 25, 2026
055acc2
Refactor sendPrivateMessage method to return void and improve message…
Jakubk15 Jan 25, 2026
57ac290
Improve error logging in DiscordClientManager during login failure
Jakubk15 Jan 25, 2026
00cf84d
Merge branch 'master' into discord
Jakubk15 Jan 25, 2026
201c382
Refactor Discord ID handling to use long, add NoticeHandler, remove r…
Jakubk15 Jan 25, 2026
5d50f24
Delete random javadocs
Jakubk15 Jan 25, 2026
cfd7509
Refactor Discord notification handling, introduce Formatter for messa…
Jakubk15 Jan 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -108,6 +115,10 @@ paper {
required = true
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("DiscordSRV") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
}
}

Expand All @@ -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 {
Expand Down
36 changes: 30 additions & 6 deletions src/main/java/com/eternalcode/parcellockers/ParcelLockers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -175,19 +182,30 @@ public void onEnable() {
this.skullAPI
);

this.liteCommands = LiteBukkitFactory.builder(this.getName(), this)
var liteCommandsBuilder = LiteBukkitFactory.builder(this.getName(), this)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

.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),
Expand All @@ -197,16 +215,18 @@ 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()
.filter(parcel -> parcel.status() != ParcelStatus.DELIVERED)
.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));
})
)));
}
Expand All @@ -224,6 +244,10 @@ public void onDisable() {
if (this.skullAPI != null) {
this.skullAPI.shutdown();
}

if (this.discordClientManager != null) {
this.discordClientManager.shutdown();
}
}

private boolean setupEconomy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CommandSender, Notice> {

private final NoticeService noticeService;

public NoticeHandler(NoticeService noticeService) {
this.noticeService = noticeService;
}

@Override
public void handle(Invocation<CommandSender> invocation, Notice result, ResultHandlerChain<CommandSender> chain) {
this.noticeService.create()
.viewer(invocation.sender())
.notice(result)
.send();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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 = "<dark_green>Verify";
public String verificationButtonVerifyDescription = "<green>Click to verify your Discord account";
public String verificationButtonCancelText = "<dark_red>Cancel";
public String verificationButtonCancelDescription = "<red>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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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?")
Expand Down Expand Up @@ -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 = "";
}
}
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking main thread?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a problem on shutdown. We don't have to block, but then the discord bot will be shown as online for few minutes despite server shutdown

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking the main thread is generally a bad idea, even during shutdown. The main thread handles all server ticks, events, and scheduled tasks. Using .block() here will freeze the server until the logout completes, which can cause delays, timeouts, or even deadlocks if the logout takes longer than expected.

A better approach is to use a non-blocking call, like .subscribe(), so the Discord logout happens in the background and the server can continue shutting down immediately. This way you avoid hanging the main thread while still performing a clean shutdown.

}
Comment on lines +43 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to initialize(), using .block() here during plugin shutdown can cause delays and potentially server hangs if the logout process takes time. It's better to use a non-blocking approach, such as .subscribe(), to ensure a swift shutdown.

}

public GatewayDiscordClient getClient() {
return this.client;
}
}
Loading