Skip to content

Commit 57a0230

Browse files
Jakubk15gemini-code-assist[bot]Copilot
authored
GH-75 Add Discord Integration
* Begin working on Discord integration * Finish implementing Discord integration * Add `@Async` annotation to command executors to ensure that the server's main thread is not blocked * Remove `@Async` annotations * Refactor Discord integration to use reactive programming for login and verification processes * Remove redundant check for bot admin role ID in Discord configuration validation * Update src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update src/main/java/com/eternalcode/parcellockers/discord/repository/DiscordLinkRepositoryOrmLite.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Add service layer * Add DiscordSRV hook integration * feat: Update DiscordSRV integration and add parcel delivery notifications * feat: Implement abstract DiscordNotificationService with Discord4J and DiscordSRV support * fix: declare appropriate events as async * fix: avoid DiscordClientManager#getClient race conditions by using blocking discord login * Remove redundant discord properties * Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: Copilot <[email protected]> * fix: initialize DiscordLinkRepository only when fallback Discord integration is enabled * Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: Copilot <[email protected]> * Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: Copilot <[email protected]> * Update src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java Co-authored-by: Copilot <[email protected]> * Update src/main/java/com/eternalcode/parcellockers/discord/command/DiscordLinkCommand.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix: add logging to DiscordSrvLinkService for better error handling * Update PluginConfig.java Co-authored-by: Copilot <[email protected]> * Update ParcelDeliverNotificationController.java Co-authored-by: Copilot <[email protected]> * Update ParcelDeliverNotificationController.java Co-authored-by: Copilot <[email protected]> * Add missing import * WIP - Adjust to DMK suggestions * Use OfflinePlayer instead of String * Use OfflinePlayer in unlinkPlayer executor too * Align with single-responsibility-principle * Ensure proper encapsulation * Refactor sendPrivateMessage method to return void and improve message handling * Improve error logging in DiscordClientManager during login failure * Refactor Discord ID handling to use long, add NoticeHandler, remove redundant javadocs * Delete random javadocs * Refactor Discord notification handling, introduce Formatter for message templating, and add DiscordProviderPicker for improved integration * Refactor notification handling to use thenAcceptBoth for improved message delivery and update LiteCommandsBuilder type * Refactor Discord link/unlink to use result enums * Use FutureHandler * Subscribe to the DiscordClient logout rather than blocking the thread. * Refactor Discord verification handling to utilize configurable link code expiration and improve cache management * Adjust to Gemini suggestions * Adjust to Martin suggestion with delay calculation --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <[email protected]>
1 parent 33e6910 commit 57a0230

36 files changed

+1545
-6
lines changed

build.gradle.kts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ repositories {
1919
maven("https://repo.papermc.io/repository/maven-public/")
2020
maven("https://repo.eternalcode.pl/releases")
2121
maven("https://storehouse.okaeri.eu/repository/maven-public/")
22+
maven("https://nexus.scarsz.me/content/groups/public/") // DiscordSRV
2223
}
2324

2425
dependencies {
@@ -78,6 +79,12 @@ dependencies {
7879
// vault
7980
compileOnly("com.github.MilkBowl:VaultAPI:1.7.1")
8081

82+
// discord integration library
83+
paperLibrary("com.discord4j:discord4j-core:3.3.0")
84+
85+
// discordsrv (optional integration)
86+
compileOnly("com.discordsrv:discordsrv:1.30.4")
87+
8188
testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.2")
8289
testImplementation("org.junit.jupiter:junit-jupiter-params:6.0.2")
8390
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.2")
@@ -108,6 +115,10 @@ paper {
108115
required = true
109116
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
110117
}
118+
register("DiscordSRV") {
119+
required = false
120+
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
121+
}
111122
}
112123
}
113124

@@ -124,6 +135,7 @@ tasks {
124135
downloadPlugins.modrinth("luckperms", "v5.5.17-bukkit")
125136
downloadPlugins.modrinth("vaultunlocked", "2.17.0")
126137
downloadPlugins.modrinth("essentialsx", "2.21.2")
138+
downloadPlugins.modrinth("discordsrv", "1.30.4")
127139
}
128140

129141
test {

src/main/java/com/eternalcode/parcellockers/ParcelLockers.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import com.eternalcode.commons.adventure.AdventureLegacyColorPreProcessor;
55
import com.eternalcode.commons.bukkit.scheduler.BukkitSchedulerImpl;
66
import com.eternalcode.commons.scheduler.Scheduler;
7+
import com.eternalcode.multification.notice.Notice;
78
import com.eternalcode.parcellockers.command.debug.DebugCommand;
89
import com.eternalcode.parcellockers.command.handler.InvalidUsageHandlerImpl;
910
import com.eternalcode.parcellockers.command.handler.MissingPermissionsHandlerImpl;
11+
import com.eternalcode.parcellockers.command.handler.NoticeHandler;
1012
import com.eternalcode.parcellockers.configuration.ConfigService;
1113
import com.eternalcode.parcellockers.configuration.implementation.MessageConfig;
1214
import com.eternalcode.parcellockers.configuration.implementation.PluginConfig;
@@ -16,6 +18,9 @@
1618
import com.eternalcode.parcellockers.database.DatabaseManager;
1719
import com.eternalcode.parcellockers.delivery.DeliveryManager;
1820
import com.eternalcode.parcellockers.delivery.repository.DeliveryRepositoryOrmLite;
21+
import com.eternalcode.parcellockers.discord.DiscordClientManager;
22+
import com.eternalcode.parcellockers.discord.DiscordProviderPicker;
23+
import com.eternalcode.parcellockers.discord.argument.SnowflakeArgument;
1924
import com.eternalcode.parcellockers.gui.GuiManager;
2025
import com.eternalcode.parcellockers.gui.implementation.locker.LockerGui;
2126
import com.eternalcode.parcellockers.gui.implementation.remote.MainGui;
@@ -47,16 +52,21 @@
4752
import com.eternalcode.parcellockers.user.validation.UserValidationService;
4853
import com.eternalcode.parcellockers.user.validation.UserValidator;
4954
import dev.rollczi.litecommands.LiteCommands;
55+
import dev.rollczi.litecommands.LiteCommandsBuilder;
5056
import dev.rollczi.litecommands.adventure.LiteAdventureExtension;
5157
import dev.rollczi.litecommands.annotations.LiteCommandsAnnotations;
5258
import dev.rollczi.litecommands.bukkit.LiteBukkitFactory;
5359
import dev.rollczi.litecommands.bukkit.LiteBukkitMessages;
60+
import dev.rollczi.litecommands.bukkit.LiteBukkitSettings;
5461
import dev.rollczi.liteskullapi.LiteSkullFactory;
5562
import dev.rollczi.liteskullapi.SkullAPI;
5663
import dev.triumphteam.gui.TriumphGui;
64+
import discord4j.common.util.Snowflake;
5765
import java.io.File;
5866
import java.sql.SQLException;
67+
import java.time.Clock;
5968
import java.time.Duration;
69+
import java.time.Instant;
6070
import java.util.stream.Stream;
6171
import net.kyori.adventure.text.minimessage.MiniMessage;
6272
import net.milkbowl.vault.economy.Economy;
@@ -72,6 +82,7 @@ public final class ParcelLockers extends JavaPlugin {
7282
private SkullAPI skullAPI;
7383
private DatabaseManager databaseManager;
7484
private Economy economy;
85+
private DiscordClientManager discordClientManager;
7586

7687
@Override
7788
public void onEnable() {
@@ -175,19 +186,30 @@ public void onEnable() {
175186
this.skullAPI
176187
);
177188

178-
this.liteCommands = LiteBukkitFactory.builder(this.getName(), this)
189+
LiteCommandsBuilder<CommandSender, LiteBukkitSettings, ?> liteCommandsBuilder = LiteBukkitFactory.builder(this.getName(), this)
179190
.extension(new LiteAdventureExtension<>())
191+
.argument(Snowflake.class, new SnowflakeArgument(messageConfig))
180192
.message(LiteBukkitMessages.PLAYER_ONLY, messageConfig.playerOnlyCommand)
181193
.message(LiteBukkitMessages.PLAYER_NOT_FOUND, messageConfig.playerNotFound)
182194
.commands(LiteCommandsAnnotations.of(
183195
new ParcelCommand(mainGUI),
184196
new ParcelLockersCommand(configService, config, noticeService),
185-
new DebugCommand(parcelService, lockerManager, itemStorageManager, parcelContentManager,
197+
new DebugCommand(
198+
parcelService, lockerManager, itemStorageManager, parcelContentManager,
186199
noticeService, deliveryManager)
187200
))
188201
.invalidUsage(new InvalidUsageHandlerImpl(noticeService))
189202
.missingPermission(new MissingPermissionsHandlerImpl(noticeService))
190-
.build();
203+
.result(Notice.class, new NoticeHandler(noticeService));
204+
205+
DiscordProviderPicker discordProviderPicker = new DiscordProviderPicker(
206+
config, messageConfig, server, noticeService, scheduler, databaseManager,
207+
this.getLogger(), userManager, this, miniMessage
208+
);
209+
210+
this.discordClientManager = discordProviderPicker.pick(liteCommandsBuilder);
211+
212+
this.liteCommands = liteCommandsBuilder.build();
191213

192214
Stream.of(
193215
new LockerInteractionController(lockerManager, lockerGUI, scheduler),
@@ -205,8 +227,13 @@ public void onEnable() {
205227
.filter(parcel -> parcel.status() != ParcelStatus.DELIVERED)
206228
.forEach(parcel -> deliveryRepository.find(parcel.uuid()).thenAccept(optionalDelivery ->
207229
optionalDelivery.ifPresent(delivery -> {
208-
long delay = Math.max(0, delivery.deliveryTimestamp().toEpochMilli() - System.currentTimeMillis());
209-
scheduler.runLaterAsync(new ParcelSendTask(parcel, parcelService, deliveryManager), Duration.ofMillis(delay));
230+
long delay = Math.max(
231+
0,
232+
Duration.between(Instant.now(Clock.systemDefaultZone()), delivery.deliveryTimestamp()).toMillis()
233+
);
234+
scheduler.runLaterAsync(
235+
new ParcelSendTask(parcel, parcelService, deliveryManager),
236+
Duration.ofMillis(delay));
210237
})
211238
)));
212239
}
@@ -224,6 +251,10 @@ public void onDisable() {
224251
if (this.skullAPI != null) {
225252
this.skullAPI.shutdown();
226253
}
254+
255+
if (this.discordClientManager != null) {
256+
this.discordClientManager.shutdown();
257+
}
227258
}
228259

229260
private boolean setupEconomy() {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.eternalcode.parcellockers.command.handler;
2+
3+
import com.eternalcode.multification.notice.Notice;
4+
import com.eternalcode.parcellockers.notification.NoticeService;
5+
import dev.rollczi.litecommands.handler.result.ResultHandler;
6+
import dev.rollczi.litecommands.handler.result.ResultHandlerChain;
7+
import dev.rollczi.litecommands.invocation.Invocation;
8+
import org.bukkit.command.CommandSender;
9+
10+
public class NoticeHandler implements ResultHandler<CommandSender, Notice> {
11+
12+
private final NoticeService noticeService;
13+
14+
public NoticeHandler(NoticeService noticeService) {
15+
this.noticeService = noticeService;
16+
}
17+
18+
@Override
19+
public void handle(Invocation<CommandSender> invocation, Notice result, ResultHandlerChain<CommandSender> chain) {
20+
this.noticeService.create()
21+
.viewer(invocation.sender())
22+
.notice(result)
23+
.send();
24+
}
25+
26+
}

src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public class MessageConfig extends OkaeriConfig {
3737
@Comment("# These messages are used for administrative actions such as deleting all lockers or parcels.")
3838
public AdminMessages admin = new AdminMessages();
3939

40+
@Comment({"", "# Messages related to Discord integration can be configured here." })
41+
@Comment("# These messages are used for linking Discord accounts with Minecraft accounts.")
42+
public DiscordMessages discord = new DiscordMessages();
43+
4044
public static class ParcelMessages extends OkaeriConfig {
4145
public Notice sent = Notice.builder()
4246
.chat("&2✔ &aParcel sent successfully.")
@@ -178,4 +182,110 @@ public static class AdminMessages extends OkaeriConfig {
178182
public Notice deletedContents = Notice.chat("&4⚠ &cAll ({COUNT}) parcel contents have been deleted!");
179183
public Notice deletedDeliveries = Notice.chat("&4⚠ &cAll ({COUNT}) deliveries have been deleted!");
180184
}
185+
186+
public static class DiscordMessages extends OkaeriConfig {
187+
public Notice verificationAlreadyPending = Notice.builder()
188+
.chat("&4✘ &cYou already have a pending verification. Please complete it or wait for it to expire.")
189+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
190+
.build();
191+
public Notice alreadyLinked = Notice.builder()
192+
.chat("&4✘ &cYour Minecraft account is already linked to a Discord account!")
193+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
194+
.build();
195+
public Notice discordAlreadyLinked = Notice.builder()
196+
.chat("&4✘ &cThis Discord account is already linked to another Minecraft account!")
197+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
198+
.build();
199+
public Notice userNotFound = Notice.builder()
200+
.chat("&4✘ &cCould not find a Discord user with that ID!")
201+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
202+
.build();
203+
public Notice verificationCodeSent = Notice.builder()
204+
.chat("&2✔ &aA verification code has been sent to your Discord DM. Please check your messages.")
205+
.sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP)
206+
.build();
207+
public Notice cannotSendDm = Notice.builder()
208+
.chat("&4✘ &cCould not send a DM to your Discord account. Please make sure your DMs are open.")
209+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
210+
.build();
211+
public Notice verificationExpired = Notice.builder()
212+
.chat("&4✘ &cYour verification code has expired. Please run the command again.")
213+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
214+
.build();
215+
public Notice invalidCode = Notice.builder()
216+
.chat("&4✘ &cInvalid verification code. Please run the command again to restart the verification process.")
217+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
218+
.build();
219+
public Notice linkSuccess = Notice.builder()
220+
.chat("&2✔ &aYour Discord account has been successfully linked!")
221+
.sound(SoundEventKeys.ENTITY_PLAYER_LEVELUP)
222+
.build();
223+
public Notice linkFailed = Notice.builder()
224+
.chat("&4✘ &cFailed to link your Discord account. Please try again later.")
225+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
226+
.build();
227+
public Notice verificationCancelled = Notice.builder()
228+
.chat("&6⚠ &eVerification cancelled.")
229+
.sound(SoundEventKeys.BLOCK_NOTE_BLOCK_BASS)
230+
.build();
231+
public Notice playerAlreadyLinked = Notice.chat("&4✘ &cThis player already has a linked Discord account!");
232+
public Notice adminLinkSuccess = Notice.chat("&2✔ &aSuccessfully linked the Discord account to the player.");
233+
234+
@Comment({"", "# Unlink messages" })
235+
public Notice notLinked = Notice.builder()
236+
.chat("&4✘ &cYour Minecraft account is not linked to any Discord account!")
237+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
238+
.build();
239+
public Notice unlinkSuccess = Notice.builder()
240+
.chat("&2✔ &aYour Discord account has been successfully unlinked!")
241+
.sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP)
242+
.build();
243+
public Notice unlinkFailed = Notice.builder()
244+
.chat("&4✘ &cFailed to unlink the Discord account. Please try again later.")
245+
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
246+
.build();
247+
public Notice playerNotLinked = Notice.chat("&4✘ &cThis player does not have a linked Discord account!");
248+
public Notice adminUnlinkSuccess = Notice.chat("&2✔ &aSuccessfully unlinked the Discord account from the player.");
249+
public Notice discordNotLinked = Notice.chat("&4✘ &cNo Minecraft account is linked to this Discord ID!");
250+
public Notice adminUnlinkByDiscordSuccess = Notice.chat("&2✔ &aSuccessfully unlinked the Minecraft account from the Discord ID.");
251+
public Notice invalidDiscordId = Notice.chat("&4✘ &cInvalid Discord ID format! Please provide a valid Discord ID.");
252+
253+
@Comment({"", "# Dialog configuration for verification" })
254+
public String verificationDialogTitle = "&6Enter your Discord verification code:";
255+
public String verificationDialogPlaceholder = "&7Enter 4-digit code";
256+
257+
@Comment({"", "# Dialog button configuration" })
258+
public String verificationButtonVerifyText = "<dark_green>Verify";
259+
public String verificationButtonVerifyDescription = "<green>Click to verify your Discord account";
260+
public String verificationButtonCancelText = "<dark_red>Cancel";
261+
public String verificationButtonCancelDescription = "<red>Click to cancel verification";
262+
263+
@Comment({"", "# The message sent to the Discord user via DM" })
264+
@Comment("# Placeholders: {CODE} - the verification code, {PLAYER} - the Minecraft player name")
265+
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.";
266+
267+
@Comment({"", "# The message sent to the Discord user when a parcel is delivered" })
268+
@Comment("# Placeholders: {PARCEL_NAME}, {SENDER}, {RECEIVER}, {DESCRIPTION}, {SIZE}, {PRIORITY}")
269+
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}";
270+
271+
public String highPriorityPlaceholder = "🔴 High Priority";
272+
public String normalPriorityPlaceholder = "⚪ Normal Priority";
273+
274+
@Comment({"", "# DiscordSRV integration messages" })
275+
@Comment("# These messages are shown when DiscordSRV is installed and handles account linking")
276+
public Notice discordSrvLinkRedirect = Notice.builder()
277+
.chat("&6⚠ &eTo link your Discord account, use the DiscordSRV linking system.")
278+
.chat("&6⚠ &eYour linking code is: &a{CODE}")
279+
.chat("&6⚠ &eSend this code to the Discord bot in a private message.")
280+
.sound(SoundEventKeys.BLOCK_NOTE_BLOCK_CHIME)
281+
.build();
282+
public Notice discordSrvAlreadyLinked = Notice.builder()
283+
.chat("&2✔ &aYour account is already linked via DiscordSRV!")
284+
.sound(SoundEventKeys.ENTITY_VILLAGER_YES)
285+
.build();
286+
public Notice discordSrvUnlinkRedirect = Notice.builder()
287+
.chat("&6⚠ &eTo unlink your Discord account, please use the DiscordSRV unlinking system.")
288+
.sound(SoundEventKeys.BLOCK_NOTE_BLOCK_CHIME)
289+
.build();
290+
}
181291
}

src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public class PluginConfig extends OkaeriConfig {
2424
@Comment({ "", "# The plugin GUI settings." })
2525
public GuiSettings guiSettings = new GuiSettings();
2626

27+
@Comment({ "", "# The plugin Discord integration settings." })
28+
public DiscordSettings discord = new DiscordSettings();
29+
2730
public static class Settings extends OkaeriConfig {
2831

2932
@Comment("# Whether the player after entering the server should receive information about the new version of the plugin?")
@@ -357,4 +360,16 @@ public static class GuiSettings extends OkaeriConfig {
357360
@Comment({ "", "# The lore line showing when the parcel has arrived. Placeholders: {DATE} - arrival date" })
358361
public String parcelArrivedLine = "&aArrived on: &2{DATE}";
359362
}
363+
364+
public static class DiscordSettings extends OkaeriConfig {
365+
366+
@Comment("# Whether Discord integration is enabled.")
367+
public boolean enabled = true;
368+
369+
@Comment("# The Discord bot token used by the bot to connect.")
370+
public String botToken = "";
371+
372+
@Comment("# The expiration duration of the Discord account linking codes.")
373+
public Duration linkCodeExpiration = Duration.ofMinutes(2);
374+
}
360375
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.eternalcode.parcellockers.discord;
2+
3+
import discord4j.core.DiscordClient;
4+
import discord4j.core.GatewayDiscordClient;
5+
import java.util.logging.Level;
6+
import java.util.logging.Logger;
7+
8+
public class DiscordClientManager {
9+
10+
private final String token;
11+
private final Logger logger;
12+
13+
private GatewayDiscordClient client;
14+
15+
public DiscordClientManager(String token, Logger logger) {
16+
this.token = token;
17+
this.logger = logger;
18+
}
19+
20+
public boolean initialize() {
21+
this.logger.info("Discord integration is enabled. Logging in to Discord...");
22+
try {
23+
GatewayDiscordClient discordClient = DiscordClient.create(this.token)
24+
.login()
25+
.block();
26+
27+
if (discordClient == null) {
28+
this.logger.severe("Failed to log in to Discord: login returned null client.");
29+
return false;
30+
}
31+
32+
this.client = discordClient;
33+
this.logger.info("Successfully logged in to Discord.");
34+
return true;
35+
} catch (Exception exception) {
36+
this.logger.log(Level.SEVERE, "Failed to log in to Discord", exception);
37+
return false;
38+
}
39+
}
40+
41+
public void shutdown() {
42+
this.logger.info("Shutting down Discord client...");
43+
if (this.client != null) {
44+
this.client.logout().subscribe();
45+
}
46+
}
47+
48+
public GatewayDiscordClient getClient() {
49+
return this.client;
50+
}
51+
}

0 commit comments

Comments
 (0)