Discord Link Module

This commit is contained in:
Josh Roy 2022-12-25 16:14:20 -05:00 committed by MD
parent 520e8f991f
commit 0936fe80bd
26 changed files with 2877 additions and 7 deletions

View File

@ -59,6 +59,7 @@ jobs:
cp -r EssentialsAntiBuild/build/docs/javadoc/ javadocs/EssentialsAntiBuild/ cp -r EssentialsAntiBuild/build/docs/javadoc/ javadocs/EssentialsAntiBuild/
cp -r EssentialsChat/build/docs/javadoc/ javadocs/EssentialsChat/ cp -r EssentialsChat/build/docs/javadoc/ javadocs/EssentialsChat/
cp -r EssentialsDiscord/build/docs/javadoc/ javadocs/EssentialsDiscord/ cp -r EssentialsDiscord/build/docs/javadoc/ javadocs/EssentialsDiscord/
cp -r EssentialsDiscordLink/build/docs/javadoc/ javadocs/EssentialsDiscordLink/
cp -r EssentialsGeoIP/build/docs/javadoc/ javadocs/EssentialsGeoIP/ cp -r EssentialsGeoIP/build/docs/javadoc/ javadocs/EssentialsGeoIP/
cp -r EssentialsProtect/build/docs/javadoc/ javadocs/EssentialsProtect/ cp -r EssentialsProtect/build/docs/javadoc/ javadocs/EssentialsProtect/
cp -r EssentialsSpawn/build/docs/javadoc/ javadocs/EssentialsSpawn/ cp -r EssentialsSpawn/build/docs/javadoc/ javadocs/EssentialsSpawn/

View File

@ -24,4 +24,4 @@
</list> </list>
</option> </option>
</component> </component>
</project> </project>

View File

@ -202,7 +202,7 @@ public class EssentialsPlayerListener implements Listener, FakeAccessor {
to.setY(from.getY()); to.setY(from.getY());
to.setZ(from.getZ()); to.setZ(from.getZ());
try { try {
event.setTo(LocationUtil.getSafeDestination(to)); event.setTo(LocationUtil.getSafeDestination(ess, to));
} catch (final Exception ex) { } catch (final Exception ex) {
event.setTo(to); event.setTo(to);
} }

View File

@ -96,6 +96,7 @@ public class Commandessentials extends EssentialsCommand {
"EssentialsAntiBuild", "EssentialsAntiBuild",
"EssentialsChat", "EssentialsChat",
"EssentialsDiscord", "EssentialsDiscord",
"EssentialsDiscordLink",
"EssentialsGeoIP", "EssentialsGeoIP",
"EssentialsProtect", "EssentialsProtect",
"EssentialsSpawn", "EssentialsSpawn",

View File

@ -33,7 +33,9 @@ import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -303,6 +305,19 @@ public class EssentialsConfiguration {
return ConfigurateUtil.getMap(configurationNode); return ConfigurateUtil.getMap(configurationNode);
} }
public Map<String, String> getStringMap(String path) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null || !node.isMap()) {
return Collections.emptyMap();
}
final Map<String, String> map = new LinkedHashMap<>();
for (Map.Entry<Object, CommentedConfigurationNode> entry : node.childrenMap().entrySet()) {
map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue().rawScalar()));
}
return map;
}
public void removeProperty(String path) { public void removeProperty(String path) {
final CommentedConfigurationNode node = getInternal(path); final CommentedConfigurationNode node = getInternal(path);
if (node != null) { if (node != null) {

View File

@ -13,6 +13,7 @@ import com.google.common.collect.ImmutableSet;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -48,10 +49,9 @@ public class PermissionsHandler implements IPermissionsHandler {
@Override @Override
public List<String> getGroups(final OfflinePlayer base) { public List<String> getGroups(final OfflinePlayer base) {
final long start = System.nanoTime(); final long start = System.nanoTime();
List<String> groups = handler.getGroups(base); final List<String> groups = new ArrayList<>();
if (groups == null || groups.isEmpty()) { groups.add(defaultGroup);
groups = Collections.singletonList(defaultGroup); groups.addAll(handler.getGroups(base));
}
checkPermLag(start, String.format("Getting groups for %s", base.getName())); checkPermLag(start, String.format("Getting groups for %s", base.getName()));
return Collections.unmodifiableList(groups); return Collections.unmodifiableList(groups);
} }

View File

@ -240,6 +240,12 @@ discordbroadcastCommandUsage1Description=Sends the given message to the specifie
discordbroadcastInvalidChannel=\u00a74Discord channel \u00a7c{0}\u00a74 does not exist. discordbroadcastInvalidChannel=\u00a74Discord channel \u00a7c{0}\u00a74 does not exist.
discordbroadcastPermission=\u00a74You do not have permission to send messages to the \u00a7c{0}\u00a74 channel. discordbroadcastPermission=\u00a74You do not have permission to send messages to the \u00a7c{0}\u00a74 channel.
discordbroadcastSent=\u00a76Message sent to \u00a7c{0}\u00a76! discordbroadcastSent=\u00a76Message sent to \u00a7c{0}\u00a76!
discordCommandAccountArgumentUser=The Discord account to look up
discordCommandAccountDescription=Looks up the linked Minecraft account for either yourself or another Discord user
discordCommandAccountResponseLinked=Your account is linked to the Minecraft account: **{0}**
discordCommandAccountResponseLinkedOther={0}'s account is linked to the Minecraft account: **{1}**
discordCommandAccountResponseNotLinked=You do not have a linked Minecraft account.
discordCommandAccountResponseNotLinkedOther={0} does not have a linked Minecraft account.
discordCommandDescription=Sends the discord invite link to the player. discordCommandDescription=Sends the discord invite link to the player.
discordCommandLink=\u00a76Join our Discord server at \u00a7c{0}\u00a76! discordCommandLink=\u00a76Join our Discord server at \u00a7c{0}\u00a76!
discordCommandUsage=/<command> discordCommandUsage=/<command>
@ -248,6 +254,14 @@ discordCommandUsage1Description=Sends the discord invite link to the player
discordCommandExecuteDescription=Executes a console command on the Minecraft server. discordCommandExecuteDescription=Executes a console command on the Minecraft server.
discordCommandExecuteArgumentCommand=The command to be executed discordCommandExecuteArgumentCommand=The command to be executed
discordCommandExecuteReply=Executing command: "/{0}" discordCommandExecuteReply=Executing command: "/{0}"
discordCommandUnlinkDescription=Unlinks the Minecraft account currently linked to your Discord account
discordCommandUnlinkInvalidCode=You do not currently have a Minecraft account linked to Discord!
discordCommandUnlinkUnlinked=Your Discord account has been unlinked from all associated Minecraft accounts.
discordCommandLinkArgumentCode=The code provided in-game to link your Minecraft account
discordCommandLinkDescription=Links your Discord account with your Minecraft account using a code from the in-game /link command
discordCommandLinkHasAccount=You already have an account linked! To unlink your current account, type /unlink.
discordCommandLinkInvalidCode=Invalid linking code! Make sure you've run /link in-game and copied the code correctly.
discordCommandLinkLinked=Successfully linked your account!
discordCommandListDescription=Gets a list of online players. discordCommandListDescription=Gets a list of online players.
discordCommandListArgumentGroup=A specific group to limit your search by discordCommandListArgumentGroup=A specific group to limit your search by
discordCommandMessageDescription=Messages a player on the Minecraft server. discordCommandMessageDescription=Messages a player on the Minecraft server.
@ -265,8 +279,20 @@ discordErrorNoPrimary=You did not define a primary channel or your defined prima
discordErrorNoPrimaryPerms=Your bot cannot speak in your primary channel, #{0}. Please make sure your bot has read and write permissions in all channels you wish to use. discordErrorNoPrimaryPerms=Your bot cannot speak in your primary channel, #{0}. Please make sure your bot has read and write permissions in all channels you wish to use.
discordErrorNoToken=No token provided! Please follow the tutorial in the config in order to setup the plugin. discordErrorNoToken=No token provided! Please follow the tutorial in the config in order to setup the plugin.
discordErrorWebhook=An error occurred while sending messages to your console channel\! This was likely caused by accidentally deleting your console webhook. This can usually by fixed by ensuring your bot has the "Manage Webhooks" permission and running "/ess reload". discordErrorWebhook=An error occurred while sending messages to your console channel\! This was likely caused by accidentally deleting your console webhook. This can usually by fixed by ensuring your bot has the "Manage Webhooks" permission and running "/ess reload".
discordLinkInvalidGroup=Invalid group {0} was provided for role {1}. The following groups are available: {2}
discordLinkInvalidRole=An invalid role ID, {0}, was provided for group: {1}. You can see the ID of roles with the /roleinfo command in Discord.
discordLinkInvalidRoleInteract=The role, {0} ({1}), cannot be used for group->role synchronization because it above your bot''s upper most role. Either move your bot''s role above "{0}" or move "{0}" below your bot''s role.
discordLinkInvalidRoleManaged=The role, {0} ({1}), cannot be used for group->role synchronization because it is managed by another bot or integration.
discordLinkLinked=\u00a76To link your Minecraft account to Discord, type \u00a7c{0} \u00a76in the Discord server.
discordLinkLinkedAlready=\u00a76You have already linked your Discord account! If you wish to unlink your discord account use \u00a7c/unlink\u00a76.
discordLinkLoginKick=\u00a76You must link your Discord account before you can join this server.\n\u00a76To link your Minecraft account to Discord, type\:\n\u00a7c{0}\n\u00a76in this server''s Discord server\:\n\u00a7c{1}
discordLinkLoginPrompt=\u00a76You must link your Discord account before you can move, chat on or interact with this server. To link your Minecraft account to Discord, type \u00a7c{0} \u00a76in this server''s Discord server\: \u00a7c{1}
discordLinkNoAccount=\u00a76You do not currently have a Discord account linked to your Minecraft account.
discordLinkPending=\u00a76You already have a link code. To complete linking your Minecraft account to Discord, type \u00a7c{0} \u00a76in the Discord server.
discordLinkUnlinked=\u00a76Unlinked your Minecraft account from all associated discord accounts.
discordLoggingIn=Attempting to login to Discord... discordLoggingIn=Attempting to login to Discord...
discordLoggingInDone=Successfully logged in as {0} discordLoggingInDone=Successfully logged in as {0}
discordMailLine=**New mail from {0}:** {1}
discordNoSendPermission=Cannot send message in channel: #{0} Please ensure the bot has "Send Messages" permission in that channel\! discordNoSendPermission=Cannot send message in channel: #{0} Please ensure the bot has "Send Messages" permission in that channel\!
discordReloadInvalid=Tried to reload EssentialsX Discord config while the plugin is in an invalid state! If you've modified your config, restart your server. discordReloadInvalid=Tried to reload EssentialsX Discord config while the plugin is in an invalid state! If you've modified your config, restart your server.
disposal=Disposal disposal=Disposal
@ -661,6 +687,10 @@ lightningCommandUsage2=/<command> <player> <power>
lightningCommandUsage2Description=Strikes lighting at the target player with the given power lightningCommandUsage2Description=Strikes lighting at the target player with the given power
lightningSmited=\u00a76Thou hast been smitten\! lightningSmited=\u00a76Thou hast been smitten\!
lightningUse=\u00a76Smiting\u00a7c {0} lightningUse=\u00a76Smiting\u00a7c {0}
linkCommandDescription=Generates a code to link your Minecraft account to Discord.
linkCommandUsage=/<command>
linkCommandUsage1=/<command>
linkCommandUsage1Description=Generates a code for the /link command on Discord
listAfkTag=\u00a77[AFK]\u00a7r listAfkTag=\u00a77[AFK]\u00a7r
listAmount=\u00a76There are \u00a7c{0}\u00a76 out of maximum \u00a7c{1}\u00a76 players online. listAmount=\u00a76There are \u00a7c{0}\u00a76 out of maximum \u00a7c{1}\u00a76 players online.
listAmountHidden=\u00a76There are \u00a7c{0}\u00a76/\u00a7c{1}\u00a76 out of maximum \u00a7c{2}\u00a76 players online. listAmountHidden=\u00a76There are \u00a7c{0}\u00a76/\u00a7c{1}\u00a76 out of maximum \u00a7c{2}\u00a76 players online.
@ -1028,7 +1058,7 @@ repairCommandUsage2Description=Repairs all items in your inventory
repairEnchanted=\u00a74You are not allowed to repair enchanted items. repairEnchanted=\u00a74You are not allowed to repair enchanted items.
repairInvalidType=\u00a74This item cannot be repaired. repairInvalidType=\u00a74This item cannot be repaired.
repairNone=\u00a74There were no items that needed repairing. repairNone=\u00a74There were no items that needed repairing.
replyFromDiscord=**Reply from {0}\:** `{1}` replyFromDiscord=**Reply from {0}\:** {1}
replyLastRecipientDisabled=\u00a76Replying to last message recipient \u00a7cdisabled\u00a76. replyLastRecipientDisabled=\u00a76Replying to last message recipient \u00a7cdisabled\u00a76.
replyLastRecipientDisabledFor=\u00a76Replying to last message recipient \u00a7cdisabled \u00a76for \u00a7c{0}\u00a76. replyLastRecipientDisabledFor=\u00a76Replying to last message recipient \u00a7cdisabled \u00a76for \u00a7c{0}\u00a76.
replyLastRecipientEnabled=\u00a76Replying to last message recipient \u00a7cenabled\u00a76. replyLastRecipientEnabled=\u00a76Replying to last message recipient \u00a7cenabled\u00a76.
@ -1402,6 +1432,10 @@ unlimitedCommandUsage3=/<command> clear [player]
unlimitedCommandUsage3Description=Clears all unlimited items for yourself or another player if specified unlimitedCommandUsage3Description=Clears all unlimited items for yourself or another player if specified
unlimitedItemPermission=\u00a74No permission for unlimited item \u00a7c{0}\u00a74. unlimitedItemPermission=\u00a74No permission for unlimited item \u00a7c{0}\u00a74.
unlimitedItems=\u00a76Unlimited items\:\u00a7r unlimitedItems=\u00a76Unlimited items\:\u00a7r
unlinkCommandDescription=Unlinks your Minecraft account from the currently linked Discord account.
unlinkCommandUsage=/<command>
unlinkCommandUsage1=/<command>
unlinkCommandUsage1Description=Unlinks your Minecraft account from the currently linked Discord account.
unmutedPlayer=\u00a76Player\u00a7c {0} \u00a76unmuted. unmutedPlayer=\u00a76Player\u00a7c {0} \u00a76unmuted.
unsafeTeleportDestination=\u00a74The teleport destination is unsafe and teleport-safety is disabled. unsafeTeleportDestination=\u00a74The teleport destination is unsafe and teleport-safety is disabled.
unsupportedBrand=\u00a74The server platform you are currently running does not provide the capabilities for this feature. unsupportedBrand=\u00a74The server platform you are currently running does not provide the capabilities for this feature.

View File

@ -0,0 +1,102 @@
# EssentialsX Discord Link
EssentialsX Discord Link is an addon for EssentialsX Discord which provides numerous features related to
group/role synchronization.
EssentialsX Discord Link offers features such as:
* Vault Group -> Discord Role Synchronization
* Discord Role -> Vault Group Synchronization
* Prevent unlinked players from joining
* Prevent unlinked players from moving/chatting
* & more...
---
## Table of Contents
> * [Setting Up Role Sync](#setting-up-role-sync)
> * [Linking an Account](#linking-an-account)
> * [Developer API](#developer-api)
---
## Setting Up Role Sync
In EssentialsX Discord Link, you can define a synchronizations for both Vault groups -> Discord roles and for
Discord roles -> Vault groups.
The following tutorial (as an example) will show how to give players with the Discord role `Patreon` the `donator`
Vault group and how to give players with the `vip` Vault group the `VIP` Discord role.
0. First, head to your server's role page in order to get their IDs.
1. For both the `Patreon` and `VIP` role, right click them and click on "Copy ID".
> ![Copy Role ID](https://i.imgur.com/YS9P2ej.gif)
> Right Click on Role(s) -> `Copy ID` -> Paste into Notepad for later step
2. Now that you have the IDs you need from Discord, you can begin configuring the plugin. First place the
EssentialsX Discord Link jar (you can download it [here](https://essentialsx.net/downloads.html) if you do not
already have it) in your plugins folder and then start your server.
> ![Start Server](https://i.imgur.com/64IwqoO.gif)
> Drag EssentialsXDiscordLink jar into plugins folder -> Start Server
3. Once the server started, open the config for EssentialsX Discord Link at
`plugins/EssentialsDiscordLink/config.yml`. Once opened, put `group-name: role-id` in the `groups` section
to create a Vault group -> Discord role synchronization (`vip: 882835722640433242` for this example); Then put
`role-id: group-name` in the `roles` section to create a Discord role -> Vault group synchronization
(`882835662280224818: donator` for this example). When done, save the file.
> ![Paste Synchronizations](https://i.imgur.com/JYZHzW0.gif)
> Paste Vault->Discord syncs in the group section & Discord->Vault syncs in the roles section
5. Finally, once the file is saved, run `ess reload` from your console and then linked accounts should now have
their groups/roles linked between Minecraft/Discord! Now that you completed the basics of group/role syncing,
go back up to the [Table of Contents](#table-of-contents) to see what else you can do!
---
## Linking an Account
0. This assumes the server has started and you have joined the server.
1. Once on the server, run `/link` in Minecraft and take note of the code if gives you.
> ![Run /link](https://i.imgur.com/1EdqdOa.gif)
> Run `/link` in Minecraft
2. Next, all you have to do is run the `/link` command in discord with the code provided.
> ![Run /link in Discord](https://i.imgur.com/yXkvMDX.gif)
> Run `/link` with the code in Discord
3. That's it! Now that you've learned how to link an account, go back up to the
[Table of Contents](#table-of-contents) to see what else you can do!
---
## Developer API
EssentialsX Discord Link has a simple API to provide very simple methods to check if players are linked,
link players, unlink players, and to get linked player data.
Outside the specific examples below, you can also view javadocs for EssentialsX Discord Link
[here](https://jd-v2.essentialsx.net/EssentialsDiscordLink).
### Get a linked player's Discord tag
The following example shows how to get a linked player's Discord tag (in `Name#0000` format) or null if the player
isn't linked.
```java
public String getDiscordTag(final Player player) {
// Gets the API service for EssentialsX Discord Link
final DiscordLinkService linkApi = Bukkit.getServicesManager().load(DiscordLinkService.class);
final String discordId = linkApi.getDiscordId(player.getUniqueId());
if (discordId == null) {
return null;
}
// Gets the API service for EssentialsX Discord which we will use to get the actual user
final DiscordService discordApi = Bukkit.getServicesManager().load(DiscordService.class);
final InteractionMember member = discordApi.getMemberById(discordId).join();
return member == null ? null : member.getTag();
}
```

View File

@ -0,0 +1,8 @@
plugins {
id("essentials.module-conventions")
}
dependencies {
compileOnly project(':EssentialsX')
compileOnly project(':EssentialsXDiscord')
}

View File

@ -0,0 +1,109 @@
package net.essentialsx.api.v2.events.discordlink;
import net.ess3.api.IUser;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Fired when a User's link status has changed.
*/
public class DiscordLinkStatusChangeEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private final IUser user;
private final InteractionMember member;
private final String memberId;
private final boolean state;
private final Cause cause;
public DiscordLinkStatusChangeEvent(IUser user, InteractionMember member, String memberId, boolean state, Cause cause) {
this.user = user;
this.member = member;
this.memberId = memberId;
this.state = state;
this.cause = cause;
}
/**
* Gets the Essentials {@link IUser user} whose link status has been changed in this event.
* @return the user.
*/
public IUser getUser() {
return user;
}
/**
* Gets the Discord {@link InteractionMember member} whose link status has been changed in this event.
* <p>
* This will return {@code null} if {@link #getCause()} returns {@link Cause#UNSYNC_LEAVE}.
* @see #getCause()
* @see #getMemberId()
* @return the member or null.
*/
public InteractionMember getMember() {
return member;
}
/**
* Gets the ID of the Discord member whose link status has been changed in this event.
* <p>
* Unlink {@link #getMember()}, this method will never return null.
* @return the member's id.
*/
public String getMemberId() {
return memberId;
}
/**
* Gets the new link status of this {@link #getUser() user} after this event.
* @return true if the user is linked to a discord account.
*/
public boolean isLinked() {
return state;
}
/**
* The cause which triggered this event.
* @see Cause
* @return the cause.
*/
public Cause getCause() {
return cause;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
/**
* The cause of the link status change.
*/
public enum Cause {
/**
* Used when a player successfully completes an account link with the /link account in Minecraft.
*/
SYNC_PLAYER,
/**
* Used when a player is linked via an external plugin using API.
*/
SYNC_API,
/**
* Used when a player unlinks their account via the /unlink Discord or Minecraft command.
*/
UNSYNC_PLAYER,
/**
* Used when a player is unlinked via an external plugin using API.
*/
UNSYNC_API,
/**
* Used when a player is unlinked due to them leaving the Discord server.
*/
UNSYNC_LEAVE,
}
}

View File

@ -0,0 +1,85 @@
package net.essentialsx.api.v2.services.discordlink;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import java.util.UUID;
/**
* A class which provides numerous methods to interact with the link module for EssentialsX Discord.
*/
public interface DiscordLinkService {
/**
* Gets the Discord ID linked to the given {@link UUID} or {@code null} if none is present.
* @param uuid the {@link UUID} of the player to lookup.
* @return the Discord ID or {@code null}.
*/
String getDiscordId(final UUID uuid);
/**
* Checks if there is a Discord account linked to the given {@link UUID}.
* @param uuid the {@link UUID} to check.
* @return true if there is a Discord account linked to the given {@link UUID}.
*/
default boolean isLinked(final UUID uuid) {
return getDiscordId(uuid) != null;
}
/**
* Gets the {@link UUID} linked to the given Discord ID or {@code null} if none is present.
* @param discordId The Discord ID to lookup.
* @return the {@link UUID} or {@code null}.
*/
UUID getUUID(final String discordId);
/**
* Checks if there is a Minecraft account linked to the given Discord ID.
* @param discordId the Discord ID to check.
* @return true if there is a Minecraft account linked to the given Discord ID.
*/
default boolean isLinked(final String discordId) {
return getUUID(discordId) != null;
}
/**
* Links the given {@link UUID} to the given {@link InteractionMember}.
* <p>
* This will automatically trigger role sync (if configured) for the given
* player if this method returns {@code true}.
* <p>
* This method will return true if the accounts are successfully linked, or
* false if either the provided {@link UUID} or {@link InteractionMember} are
* already linked to another account.
* @param uuid The {@link UUID} of the target player.
* @param member The {@link InteractionMember} to link to the target player.
* @see net.essentialsx.api.v2.services.discord.DiscordService#getMemberById(String) to get an
* {@link InteractionMember} by their ID.
* @see #isLinked(UUID) to ensure the given {@link UUID} isn't already linked to an account.
* @see #isLinked(String) to ensure the given {@link InteractionMember} isn't already linked to an account.
* @throws IllegalArgumentException if either of the {@link UUID} or {@link InteractionMember} are null.
* @return true if the accounts were linked successfully, otherwise false.
*/
boolean linkAccount(final UUID uuid, final InteractionMember member);
/**
* Unlinks the given {@link UUID} with its associated Discord account (if present).
* <p>
* This will automatically trigger role unsync (if configured) for the given player if this method
* returns {@code true}.
* @param uuid The {@link UUID} of the player to unlink.
* @throws IllegalArgumentException if the provided {@link UUID} is null.
* @return true if there was an account associated with the given {@link UUID}, otherwise false.
*/
boolean unlinkAccount(final UUID uuid);
/**
* Unlinks the given {@link InteractionMember} with its associated Minecraft account (if present).
* <p>
* This will automatically trigger role unsync (if configured) for the given {@link InteractionMember}
* if this method returns {@code true}.
* @param member The {@link InteractionMember} to unlink.
* @throws IllegalArgumentException if the provided {@link InteractionMember} is null.
* @return true if there was a linked Minecraft account associated with the given
* {@link InteractionMember}, otherwise false.
*/
boolean unlinkAccount(final InteractionMember member);
}

View File

@ -0,0 +1,168 @@
package net.essentialsx.discordlink;
import com.earth2me.essentials.IEssentialsModule;
import com.google.common.base.Preconditions;
import net.ess3.api.IUser;
import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import net.essentialsx.api.v2.services.discordlink.DiscordLinkService;
import net.essentialsx.discordlink.rolesync.RoleSyncManager;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class AccountLinkManager implements IEssentialsModule, DiscordLinkService {
private static final char[] CODE_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
private final EssentialsDiscordLink ess;
private final AccountStorage storage;
private final RoleSyncManager roleSyncManager;
private final Map<String, UUID> codeToUuidMap = new ConcurrentHashMap<>();
public AccountLinkManager(EssentialsDiscordLink ess, AccountStorage storage, RoleSyncManager roleSyncManager) {
this.ess = ess;
this.storage = storage;
this.roleSyncManager = roleSyncManager;
}
public String createCode(final UUID uuid) throws IllegalArgumentException {
final Optional<Map.Entry<String, UUID>> prevCode = codeToUuidMap.entrySet().stream().filter(stringUUIDEntry -> stringUUIDEntry.getValue().equals(uuid)).findFirst();
if (prevCode.isPresent()) {
throw new IllegalArgumentException(prevCode.get().getKey());
}
final String code = generateCode();
codeToUuidMap.put(code, uuid);
return code;
}
public UUID getPendingUUID(final String code) {
return codeToUuidMap.remove(code);
}
@Override
public String getDiscordId(final UUID uuid) {
return storage.getDiscordId(uuid);
}
public IUser getUser(final String discordId) {
final UUID uuid = getUUID(discordId);
if (uuid == null) {
return null;
}
return ess.getEss().getUser(uuid);
}
@Override
public UUID getUUID(final String discordId) {
return storage.getUUID(discordId);
}
@Override
public boolean unlinkAccount(InteractionMember member) {
Preconditions.checkNotNull(member, "member cannot be null");
if (!isLinked(member.getId())) {
return false;
}
removeAccount(member, DiscordLinkStatusChangeEvent.Cause.UNSYNC_API);
return true;
}
public boolean removeAccount(final InteractionMember member, final DiscordLinkStatusChangeEvent.Cause cause) {
final UUID uuid = getUUID(member.getId());
if (storage.remove(member.getId())) {
ensureAsync(() -> {
final IUser user = ess.getEss().getUser(uuid);
ensureSync(() -> ess.getServer().getPluginManager().callEvent(new DiscordLinkStatusChangeEvent(user, member, member.getId(), false, cause)));
});
return true;
}
ensureAsync(() -> roleSyncManager.unSync(uuid, member.getId()));
return false;
}
@Override
public boolean unlinkAccount(UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid cannot be null");
if (!isLinked(uuid)) {
return false;
}
ensureAsync(() -> removeAccount(ess.getEss().getUser(uuid), DiscordLinkStatusChangeEvent.Cause.UNSYNC_API));
return true;
}
public boolean removeAccount(final IUser user, final DiscordLinkStatusChangeEvent.Cause cause) {
final String id = getDiscordId(user.getBase().getUniqueId());
if (storage.remove(user.getBase().getUniqueId())) {
ess.getApi().getMemberById(id).thenAccept(member -> ensureSync(() ->
ess.getServer().getPluginManager().callEvent(new DiscordLinkStatusChangeEvent(user, member, id, false, cause))));
return true;
}
ensureAsync(() -> roleSyncManager.unSync(user.getBase().getUniqueId(), id));
return false;
}
@Override
public boolean linkAccount(UUID uuid, InteractionMember member) {
Preconditions.checkNotNull(uuid, "uuid cannot be null");
Preconditions.checkNotNull(member, "member cannot be null");
if (isLinked(uuid) || isLinked(member.getId())) {
return false;
}
registerAccount(uuid, member, DiscordLinkStatusChangeEvent.Cause.SYNC_API);
return true;
}
public void registerAccount(final UUID uuid, final InteractionMember member, final DiscordLinkStatusChangeEvent.Cause cause) {
storage.add(uuid, member.getId());
ensureAsync(() -> roleSyncManager.sync(uuid, member.getId()));
ensureAsync(() -> {
final IUser user = ess.getEss().getUser(uuid);
ensureSync(() -> ess.getServer().getPluginManager().callEvent(new DiscordLinkStatusChangeEvent(user, member, member.getId(), true, cause)));
});
}
private void ensureSync(final Runnable runnable) {
if (ess.getServer().isPrimaryThread()) {
runnable.run();
return;
}
ess.getEss().scheduleSyncDelayedTask(runnable);
}
private void ensureAsync(final Runnable runnable) {
if (!ess.getServer().isPrimaryThread()) {
runnable.run();
return;
}
ess.getEss().runTaskAsynchronously(runnable);
}
private String generateCode() {
final char[] code = new char[8];
final Random random = ThreadLocalRandom.current();
for (int i = 0; i < 8; i++) {
code[i] = CODE_CHARACTERS[random.nextInt(CODE_CHARACTERS.length)];
}
final String result = new String(code);
if (codeToUuidMap.containsKey(result)) {
// If this happens, buy a lottery ticket.
return generateCode();
}
return result;
}
}

View File

@ -0,0 +1,118 @@
package net.essentialsx.discordlink;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
public class AccountStorage {
private final Gson gson = new Gson();
private final EssentialsDiscordLink plugin;
private final File accountFile;
private final BiMap<String, String> uuidToDiscordIdMap;
private final AtomicBoolean mapDirty = new AtomicBoolean(false);
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public AccountStorage(final EssentialsDiscordLink plugin) throws IOException {
this.plugin = plugin;
this.accountFile = new File(plugin.getDataFolder(), "accounts.json");
if (!plugin.getDataFolder().exists() && !plugin.getDataFolder().mkdirs()) {
throw new IOException("Unable to create account file!");
}
if (!accountFile.exists() && !accountFile.createNewFile()) {
throw new IOException("Unable to create account file!");
}
try (final Reader reader = new FileReader(accountFile)) {
//noinspection UnstableApiUsage
final Map<String, String> map = gson.fromJson(reader, new TypeToken<Map<String, String>>() {}.getType());
uuidToDiscordIdMap = map == null ? Maps.synchronizedBiMap(HashBiMap.create()) : Maps.synchronizedBiMap(HashBiMap.create(map));
}
executorService.scheduleWithFixedDelay(() -> {
if (!mapDirty.compareAndSet(true, false)) {
return;
}
if (plugin.getEss().getSettings().isDebug()) {
plugin.getLogger().log(Level.INFO, "Saving linked discord accounts to disk...");
}
final Map<String, String> clone;
clone = new HashMap<>(uuidToDiscordIdMap);
try (final Writer writer = new FileWriter(accountFile)) {
gson.toJson(clone, writer);
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to save link accounts!", e);
mapDirty.set(true); // mark the map as dirty and pray it fixes itself :D
}
}, 10, 10, TimeUnit.SECONDS);
}
public BiMap<String, String> getRawStorageMap() {
return HashBiMap.create(uuidToDiscordIdMap);
}
public void add(final UUID uuid, final String discordId) {
uuidToDiscordIdMap.forcePut(uuid.toString(), discordId);
queueSave();
}
public boolean remove(final UUID uuid) {
final boolean success = uuidToDiscordIdMap.remove(uuid.toString()) != null;
queueSave();
return success;
}
public boolean remove(final String discordId) {
final boolean success = uuidToDiscordIdMap.values().removeIf(discordId::equals);
queueSave();
return success;
}
public UUID getUUID(final String discordId) {
final String uuid = uuidToDiscordIdMap.inverse().get(discordId);
return uuid == null ? null : UUID.fromString(uuid);
}
public String getDiscordId(final UUID uuid) {
return uuidToDiscordIdMap.get(uuid.toString());
}
public void queueSave() {
mapDirty.set(true);
}
public void shutdown() {
try {
executorService.shutdown();
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
plugin.getLogger().log(Level.SEVERE, "Timed out while saving!");
executorService.shutdownNow();
}
if (mapDirty.get()) {
try (final Writer writer = new FileWriter(accountFile)) {
gson.toJson(uuidToDiscordIdMap, writer);
}
}
} catch (InterruptedException | IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to shutdown link accounts save!", e);
executorService.shutdownNow();
}
}
}

View File

@ -0,0 +1,96 @@
package net.essentialsx.discordlink;
import com.earth2me.essentials.IConf;
import com.earth2me.essentials.config.EssentialsConfiguration;
import java.io.File;
import java.util.Map;
public class DiscordLinkSettings implements IConf {
private final EssentialsDiscordLink plugin;
private final EssentialsConfiguration config;
private LinkPolicy linkPolicy;
private Map<String, String> roleSyncGroups;
private Map<String, String> roleSyncRoles;
public DiscordLinkSettings(EssentialsDiscordLink plugin) {
this.plugin = plugin;
this.config = new EssentialsConfiguration(new File(plugin.getDataFolder(), "config.yml"), "/config.yml", EssentialsDiscordLink.class);
reloadConfig();
}
public LinkPolicy getLinkPolicy() {
return linkPolicy;
}
public boolean isBlockUnlinkedChat() {
return config.getBoolean("block-unlinked-chat", false);
}
public boolean isUnlinkOnLeave() {
return config.getBoolean("unlink-on-leave", true);
}
public boolean isRelayMail() {
return config.getBoolean("relay-mail", true);
}
public boolean isRoleSyncRemoveRoles() {
return config.getBoolean("role-sync.remove-roles", true);
}
public boolean isRoleSyncRemoveGroups() {
return config.getBoolean("role-sync.remove-groups", true);
}
public int getRoleSyncResyncDelay() {
return config.getInt("role-sync.resync-delay", 5);
}
public boolean isRoleSyncPrimaryGroupOnly() {
return config.getBoolean("role-sync.primary-group-only", false);
}
public Map<String, String> getRoleSyncGroups() {
return roleSyncGroups;
}
private Map<String, String> _getRoleSyncGroups() {
return config.getStringMap("role-sync.groups");
}
public Map<String, String> getRoleSyncRoles() {
return roleSyncRoles;
}
private Map<String, String> _getRoleSyncRoles() {
return config.getStringMap("role-sync.roles");
}
public enum LinkPolicy {
KICK,
FREEZE,
NONE;
static LinkPolicy fromName(final String name) {
for (LinkPolicy policy : values()) {
if (policy.name().equalsIgnoreCase(name)) {
return policy;
}
}
return LinkPolicy.NONE;
}
}
@Override
public void reloadConfig() {
config.load();
linkPolicy = LinkPolicy.fromName(config.getString("link-policy", "none"));
roleSyncGroups = _getRoleSyncGroups();
roleSyncRoles = _getRoleSyncRoles();
plugin.onReload();
}
}

View File

@ -0,0 +1,135 @@
package net.essentialsx.discordlink;
import com.earth2me.essentials.EssentialsLogger;
import com.earth2me.essentials.IEssentials;
import com.earth2me.essentials.metrics.MetricsWrapper;
import com.google.common.collect.ImmutableSet;
import net.essentialsx.api.v2.services.discord.DiscordService;
import net.essentialsx.api.v2.services.discord.InteractionException;
import net.essentialsx.api.v2.services.discordlink.DiscordLinkService;
import net.essentialsx.discord.EssentialsDiscord;
import net.essentialsx.discordlink.commands.discord.AccountInteractionCommand;
import net.essentialsx.discordlink.commands.discord.LinkInteractionCommand;
import net.essentialsx.discordlink.commands.discord.UnlinkInteractionCommand;
import net.essentialsx.discordlink.listeners.LinkBukkitListener;
import net.essentialsx.discordlink.rolesync.RoleSyncManager;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.plugin.ServicePriority;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.IOException;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.earth2me.essentials.I18n.tl;
public class EssentialsDiscordLink extends JavaPlugin {
private transient IEssentials ess;
private transient MetricsWrapper metrics = null;
private DiscordService api;
private DiscordLinkSettings settings;
private AccountStorage accounts;
private AccountLinkManager linkManager;
private RoleSyncManager roleSyncManager;
@Override
public void onEnable() {
ess = (IEssentials) getServer().getPluginManager().getPlugin("Essentials");
final EssentialsDiscord essDiscord = (EssentialsDiscord) getServer().getPluginManager().getPlugin("EssentialsDiscord");
if (ess == null || !ess.isEnabled() || essDiscord == null || !essDiscord.isEnabled()) {
setEnabled(false);
return;
}
if (!getDescription().getVersion().equals(ess.getDescription().getVersion())) {
getLogger().log(Level.WARNING, tl("versionMismatchAll"));
}
api = getServer().getServicesManager().load(DiscordService.class);
settings = new DiscordLinkSettings(this);
ess.addReloadListener(settings);
try {
accounts = new AccountStorage(this);
} catch (IOException e) {
getLogger().log(Level.SEVERE, "Unable to create link accounts file", e);
setEnabled(false);
return;
}
roleSyncManager = new RoleSyncManager(this);
linkManager = new AccountLinkManager(this, accounts, roleSyncManager);
getServer().getPluginManager().registerEvents(new LinkBukkitListener(this), this);
getServer().getServicesManager().register(DiscordLinkService.class, linkManager, this, ServicePriority.Normal);
if (!(api.getInteractionController().getCommand("link") instanceof LinkInteractionCommand)) {
try {
api.getInteractionController().registerCommand(new AccountInteractionCommand(linkManager));
api.getInteractionController().registerCommand(new LinkInteractionCommand(linkManager));
api.getInteractionController().registerCommand(new UnlinkInteractionCommand(linkManager));
} catch (InteractionException e) {
e.printStackTrace();
setEnabled(false);
return;
}
}
ess.getPermissionsHandler().registerContext("essentials:linked", user ->
Collections.singleton(String.valueOf(linkManager.isLinked(user.getUUID()))), () -> ImmutableSet.of("true", "false"));
if (metrics == null) {
metrics = new MetricsWrapper(this, 11462, false);
}
}
@Override
public void onDisable() {
if (accounts != null) {
accounts.shutdown();
}
}
public void onReload() {
if (roleSyncManager != null) {
roleSyncManager.onReload();
}
}
public IEssentials getEss() {
return ess;
}
public DiscordService getApi() {
return api;
}
public DiscordLinkSettings getSettings() {
return settings;
}
public AccountStorage getAccountStorage() {
return accounts;
}
public AccountLinkManager getLinkManager() {
return linkManager;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
return ess.onCommandEssentials(sender, command, label, args, EssentialsDiscordLink.class.getClassLoader(), "net.essentialsx.discordlink.commands.bukkit.Command", "essentials.", linkManager);
}
@Override
public Logger getLogger() {
try {
return EssentialsLogger.getLoggerProvider(this);
} catch (Throwable ignored) {
// In case Essentials isn't installed/loaded
return super.getLogger();
}
}
}

View File

@ -0,0 +1,30 @@
package net.essentialsx.discordlink.commands.bukkit;
import com.earth2me.essentials.User;
import com.earth2me.essentials.commands.EssentialsCommand;
import net.essentialsx.discordlink.AccountLinkManager;
import org.bukkit.Server;
import static com.earth2me.essentials.I18n.tl;
public class Commandlink extends EssentialsCommand {
public Commandlink() {
super("link");
}
@Override
protected void run(Server server, User user, String commandLabel, String[] args) {
final AccountLinkManager manager = (AccountLinkManager) module;
if (manager.isLinked(user.getUUID())) {
user.sendMessage(tl("discordLinkLinkedAlready"));
return;
}
try {
final String code = manager.createCode(user.getBase().getUniqueId());
user.sendMessage(tl("discordLinkLinked", "/link " + code));
} catch (final IllegalArgumentException e) {
user.sendMessage(tl("discordLinkPending", "/link " + e.getMessage()));
}
}
}

View File

@ -0,0 +1,26 @@
package net.essentialsx.discordlink.commands.bukkit;
import com.earth2me.essentials.User;
import com.earth2me.essentials.commands.EssentialsCommand;
import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent;
import net.essentialsx.discordlink.AccountLinkManager;
import org.bukkit.Server;
import static com.earth2me.essentials.I18n.tl;
public class Commandunlink extends EssentialsCommand {
public Commandunlink() {
super("unlink");
}
@Override
protected void run(Server server, User user, String commandLabel, String[] args) {
final AccountLinkManager manager = (AccountLinkManager) module;
if (!manager.removeAccount(user, DiscordLinkStatusChangeEvent.Cause.UNSYNC_PLAYER)) {
user.sendMessage(tl("discordLinkNoAccount"));
return;
}
user.sendMessage(tl("discordLinkUnlinked"));
}
}

View File

@ -0,0 +1,66 @@
package net.essentialsx.discordlink.commands.discord;
import com.google.common.collect.ImmutableList;
import net.ess3.api.IUser;
import net.essentialsx.api.v2.services.discord.InteractionCommand;
import net.essentialsx.api.v2.services.discord.InteractionCommandArgument;
import net.essentialsx.api.v2.services.discord.InteractionCommandArgumentType;
import net.essentialsx.api.v2.services.discord.InteractionEvent;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import net.essentialsx.discordlink.AccountLinkManager;
import java.util.List;
import static com.earth2me.essentials.I18n.tl;
public class AccountInteractionCommand implements InteractionCommand {
private final List<InteractionCommandArgument> arguments;
private final AccountLinkManager accounts;
public AccountInteractionCommand(AccountLinkManager accounts) {
this.arguments = ImmutableList.of(new InteractionCommandArgument("user", tl("discordCommandAccountArgumentUser"), InteractionCommandArgumentType.USER, false));
this.accounts = accounts;
}
@Override
public boolean isDisabled() {
return false;
}
@Override
public boolean isEphemeral() {
return true;
}
@Override
public String getName() {
return "account";
}
@Override
public String getDescription() {
return tl("discordCommandAccountDescription");
}
@Override
public List<InteractionCommandArgument> getArguments() {
return arguments;
}
@Override
public void onCommand(InteractionEvent event) {
final InteractionMember userArg = event.getUserArgument("user");
final InteractionMember effectiveUser = userArg == null ? event.getMember() : userArg;
final IUser user = accounts.getUser(effectiveUser.getId());
if (user == null) {
event.reply(tl(event.getMember().getId().equals(effectiveUser.getId()) ? "discordCommandAccountResponseNotLinked" : "discordCommandAccountResponseNotLinkedOther", effectiveUser.getAsMention()));
return;
}
if (event.getMember().getId().equals(effectiveUser.getId())) {
event.reply(tl("discordCommandAccountResponseLinked", user.getName()));
return;
}
event.reply(tl("discordCommandAccountResponseLinkedOther", effectiveUser.getAsMention(), user.getName()));
}
}

View File

@ -0,0 +1,67 @@
package net.essentialsx.discordlink.commands.discord;
import com.google.common.collect.ImmutableList;
import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent;
import net.essentialsx.api.v2.services.discord.InteractionCommand;
import net.essentialsx.api.v2.services.discord.InteractionCommandArgument;
import net.essentialsx.api.v2.services.discord.InteractionCommandArgumentType;
import net.essentialsx.api.v2.services.discord.InteractionEvent;
import net.essentialsx.discordlink.AccountLinkManager;
import java.util.List;
import java.util.UUID;
import static com.earth2me.essentials.I18n.tl;
public class LinkInteractionCommand implements InteractionCommand {
private final List<InteractionCommandArgument> arguments;
private final AccountLinkManager accounts;
public LinkInteractionCommand(final AccountLinkManager accounts) {
this.arguments = ImmutableList.of(new InteractionCommandArgument("code", tl("discordCommandLinkArgumentCode"), InteractionCommandArgumentType.STRING, true));
this.accounts = accounts;
}
@Override
public void onCommand(InteractionEvent event) {
if (accounts.isLinked(event.getMember().getId())) {
event.reply(tl("discordCommandLinkHasAccount"));
return;
}
final UUID uuid = accounts.getPendingUUID(event.getStringArgument("code"));
if (uuid == null) {
event.reply(tl("discordCommandLinkInvalidCode"));
return;
}
accounts.registerAccount(uuid, event.getMember(), DiscordLinkStatusChangeEvent.Cause.SYNC_PLAYER);
event.reply(tl("discordCommandLinkLinked"));
}
@Override
public boolean isDisabled() {
return false;
}
@Override
public boolean isEphemeral() {
return true;
}
@Override
public String getName() {
return "link";
}
@Override
public String getDescription() {
return tl("discordCommandLinkDescription");
}
@Override
public List<InteractionCommandArgument> getArguments() {
return arguments;
}
}

View File

@ -0,0 +1,53 @@
package net.essentialsx.discordlink.commands.discord;
import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent;
import net.essentialsx.api.v2.services.discord.InteractionCommand;
import net.essentialsx.api.v2.services.discord.InteractionCommandArgument;
import net.essentialsx.api.v2.services.discord.InteractionEvent;
import net.essentialsx.discordlink.AccountLinkManager;
import java.util.List;
import static com.earth2me.essentials.I18n.tl;
public class UnlinkInteractionCommand implements InteractionCommand {
private final AccountLinkManager accounts;
public UnlinkInteractionCommand(final AccountLinkManager accounts) {
this.accounts = accounts;
}
@Override
public void onCommand(InteractionEvent event) {
if (!accounts.removeAccount(event.getMember(), DiscordLinkStatusChangeEvent.Cause.UNSYNC_PLAYER)) {
event.reply(tl("discordCommandUnlinkInvalidCode"));
return;
}
event.reply(tl("discordCommandUnlinkUnlinked"));
}
@Override
public boolean isDisabled() {
return false;
}
@Override
public boolean isEphemeral() {
return true;
}
@Override
public String getName() {
return "unlink";
}
@Override
public String getDescription() {
return tl("discordCommandUnlinkDescription");
}
@Override
public List<InteractionCommandArgument> getArguments() {
return null;
}
}

View File

@ -0,0 +1,179 @@
package net.essentialsx.discordlink.listeners;
import com.earth2me.essentials.utils.FormatUtil;
import net.essentialsx.api.v2.events.AsyncUserDataLoadEvent;
import net.essentialsx.api.v2.events.UserMailEvent;
import net.essentialsx.api.v2.events.discord.DiscordMessageEvent;
import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent;
import net.essentialsx.api.v2.services.discord.MessageType;
import net.essentialsx.discord.util.MessageUtil;
import net.essentialsx.discordlink.DiscordLinkSettings;
import net.essentialsx.discordlink.EssentialsDiscordLink;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import static com.earth2me.essentials.I18n.tl;
public class LinkBukkitListener implements Listener {
private final EssentialsDiscordLink ess;
public LinkBukkitListener(EssentialsDiscordLink ess) {
this.ess = ess;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onMail(final UserMailEvent event) {
if (!ess.getSettings().isRelayMail()) {
return;
}
final String discordId = ess.getLinkManager().getDiscordId(event.getRecipient().getBase().getUniqueId());
if (discordId == null) {
return;
}
final String sanitizedName = MessageUtil.sanitizeDiscordMarkdown(event.getMessage().getSenderUsername());
final String sanitizedMessage = MessageUtil.sanitizeDiscordMarkdown(FormatUtil.stripFormat(event.getMessage().getMessage()));
ess.getApi().getMemberById(discordId).thenAccept(member -> {
member.sendPrivateMessage(tl("discordMailLine", sanitizedName, sanitizedMessage));
});
}
@EventHandler(priority = EventPriority.HIGH)
public void onConnect(final AsyncPlayerPreLoginEvent event) {
if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.KICK) {
return;
}
if (!ess.getLinkManager().isLinked(event.getUniqueId())) {
String code;
try {
code = ess.getLinkManager().createCode(event.getUniqueId());
} catch (IllegalArgumentException e) {
code = e.getMessage();
}
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, tl("discordLinkLoginKick", "/link " + code, ess.getApi().getInviteUrl()));
}
}
@EventHandler(priority = EventPriority.LOW)
public void onInteract(final PlayerInteractEvent event) {
if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) {
return;
}
if (!ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler(priority = EventPriority.LOW)
public void onCommand(final PlayerCommandPreprocessEvent event) {
if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) {
return;
}
//todo maybe allowed commands
if (!ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
String code;
try {
code = ess.getLinkManager().createCode(event.getPlayer().getUniqueId());
} catch (IllegalArgumentException e) {
code = e.getMessage();
}
event.getPlayer().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl()));
}
}
@EventHandler(priority = EventPriority.LOW)
public void onChat(final AsyncPlayerChatEvent event) {
if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) {
return;
}
if (!ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
String code;
try {
code = ess.getLinkManager().createCode(event.getPlayer().getUniqueId());
} catch (IllegalArgumentException e) {
code = e.getMessage();
}
event.getPlayer().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl()));
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onUserDataLoad(final AsyncUserDataLoadEvent event) {
if (ess.getSettings().getLinkPolicy() != DiscordLinkSettings.LinkPolicy.FREEZE) {
return;
}
if (!ess.getLinkManager().isLinked(event.getUser().getBase().getUniqueId())) {
event.getUser().setFreeze(true);
String code;
try {
code = ess.getLinkManager().createCode(event.getUser().getBase().getUniqueId());
} catch (IllegalArgumentException e) {
code = e.getMessage();
}
event.getUser().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl()));
}
}
@EventHandler(priority = EventPriority.HIGH)
public void onDiscordMessage(final DiscordMessageEvent event) {
if (ess.getSettings().isBlockUnlinkedChat() && event.getType() == MessageType.DefaultTypes.CHAT && !ess.getLinkManager().isLinked(event.getUUID())) {
event.setCancelled(true);
}
}
@EventHandler
public void onUserLinkStatusChange(final DiscordLinkStatusChangeEvent event) {
if (event.isLinked()) {
event.getUser().setFreeze(false);
return;
}
switch (ess.getSettings().getLinkPolicy()) {
case KICK: {
String code;
try {
code = ess.getLinkManager().createCode(event.getUser().getBase().getUniqueId());
} catch (IllegalArgumentException e) {
code = e.getMessage();
}
final String finalCode = code;
final Runnable kickTask = () -> event.getUser().getBase().kickPlayer(tl("discordLinkLoginKick", "/link " + finalCode, ess.getApi().getInviteUrl()));
if (Bukkit.isPrimaryThread()) {
kickTask.run();
} else {
ess.getEss().scheduleSyncDelayedTask(kickTask);
}
break;
}
case FREEZE: {
String code;
try {
code = ess.getLinkManager().createCode(event.getUser().getBase().getUniqueId());
} catch (IllegalArgumentException e) {
code = e.getMessage();
}
event.getUser().sendMessage(tl("discordLinkLoginPrompt", "/link " + code, ess.getApi().getInviteUrl()));
event.getUser().setFreeze(true);
break;
}
default: {
break;
}
}
}
}

View File

@ -0,0 +1,192 @@
package net.essentialsx.discordlink.rolesync;
import com.earth2me.essentials.UUIDPlayer;
import com.google.common.collect.BiMap;
import net.essentialsx.api.v2.events.discordlink.DiscordLinkStatusChangeEvent;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import net.essentialsx.api.v2.services.discord.InteractionRole;
import net.essentialsx.discordlink.EssentialsDiscordLink;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static com.earth2me.essentials.I18n.tl;
public class RoleSyncManager implements Listener {
private final EssentialsDiscordLink ess;
private final Map<String, InteractionRole> groupToRoleMap = new HashMap<>();
private final Map<String, String> roleIdToGroupMap = new HashMap<>();
public RoleSyncManager(final EssentialsDiscordLink ess) {
this.ess = ess;
Bukkit.getPluginManager().registerEvents(this, ess);
onReload();
this.ess.getEss().runTaskTimerAsynchronously(() -> {
if (groupToRoleMap.isEmpty() && roleIdToGroupMap.isEmpty()) {
return;
}
final BiMap<String, String> uuidToDiscordCopy = ess.getAccountStorage().getRawStorageMap();
final Map<String, InteractionRole> groupToRoleMapCopy = new HashMap<>(groupToRoleMap);
final Map<String, String> roleIdToGroupMapCopy = new HashMap<>(roleIdToGroupMap);
final boolean primaryOnly = ess.getSettings().isRoleSyncPrimaryGroupOnly();
final boolean removeGroups = ess.getSettings().isRoleSyncRemoveGroups();
final boolean removeRoles = ess.getSettings().isRoleSyncRemoveRoles();
for (final Map.Entry<String, String> entry : uuidToDiscordCopy.entrySet()) {
sync(new UUIDPlayer(UUID.fromString(entry.getKey())), entry.getValue(), groupToRoleMapCopy, roleIdToGroupMapCopy, primaryOnly, removeGroups, removeRoles);
}
}, 0, ess.getSettings().getRoleSyncResyncDelay() * 1200L);
}
public void sync(final UUID uuid, final String discordId) {
final Map<String, InteractionRole> groupToRoleMapCopy = new HashMap<>(groupToRoleMap);
final Map<String, String> roleIdToGroupMapCopy = new HashMap<>(roleIdToGroupMap);
final boolean primaryOnly = ess.getSettings().isRoleSyncPrimaryGroupOnly();
final boolean removeGroups = ess.getSettings().isRoleSyncRemoveGroups();
final boolean removeRoles = ess.getSettings().isRoleSyncRemoveRoles();
sync(new UUIDPlayer(uuid), discordId, groupToRoleMapCopy, roleIdToGroupMapCopy, primaryOnly, removeGroups, removeRoles);
}
public void sync(final Player player, final String discordId, final Map<String, InteractionRole> groupToRoleMap, final Map<String, String> roleIdToGroupMap,
final boolean primaryOnly, final boolean removeGroups, final boolean removeRoles) {
final List<String> groups = primaryOnly ?
Collections.singletonList(ess.getEss().getPermissionsHandler().getGroup(player)) : ess.getEss().getPermissionsHandler().getGroups(player);
final InteractionMember member = ess.getApi().getMemberById(discordId).join();
if (member == null) {
if (ess.getSettings().isUnlinkOnLeave()) {
ess.getLinkManager().removeAccount(ess.getEss().getUser(player.getUniqueId()), DiscordLinkStatusChangeEvent.Cause.UNSYNC_LEAVE);
} else {
unSync(player.getUniqueId(), discordId);
}
return;
}
final List<InteractionRole> toAdd = new ArrayList<>();
final List<InteractionRole> toRemove = new ArrayList<>();
for (final Map.Entry<String, InteractionRole> entry : groupToRoleMap.entrySet()) {
if (groups.contains(entry.getKey()) && !member.hasRole(entry.getValue())) {
toAdd.add(entry.getValue());
} else if (removeRoles && !groups.contains(entry.getKey()) && member.hasRole(entry.getValue())) {
toRemove.add(entry.getValue());
}
}
for (final Map.Entry<String, String> entry : roleIdToGroupMap.entrySet()) {
if (member.hasRole(entry.getKey()) && !groups.contains(entry.getValue())) {
ess.getEss().getPermissionsHandler().addToGroup(player, entry.getValue());
} else if (removeGroups && !member.hasRole(entry.getKey()) && groups.contains(entry.getValue())) {
ess.getEss().getPermissionsHandler().removeFromGroup(player, entry.getValue());
}
}
if (toAdd.isEmpty() && toRemove.isEmpty()) {
return;
}
ess.getApi().modifyMemberRoles(member, toAdd, toRemove);
}
public void unSync(final UUID uuid, final String discordId) {
final boolean removeGroups = ess.getSettings().isRoleSyncRemoveGroups();
final boolean removeRoles = ess.getSettings().isRoleSyncRemoveRoles();
if (!removeGroups && !removeRoles) {
return;
}
final Map<String, InteractionRole> groupToRoleMapCopy = new HashMap<>(groupToRoleMap);
final Map<String, String> roleIdToGroupMapCopy = new HashMap<>(roleIdToGroupMap);
final Player player = new UUIDPlayer(uuid);
final InteractionMember member = ess.getApi().getMemberById(discordId).join();
if (removeGroups) {
for (final String group : roleIdToGroupMapCopy.values()) {
ess.getEss().getPermissionsHandler().removeFromGroup(player, group);
}
}
// Check if the member is no longer in the guild (null), they don't have any roles anyway.
if (removeRoles && member != null) {
ess.getApi().modifyMemberRoles(member, null, groupToRoleMapCopy.values());
}
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
ess.getEss().runTaskAsynchronously(() -> {
if (ess.getLinkManager().isLinked(event.getPlayer().getUniqueId())) {
sync(event.getPlayer().getUniqueId(), ess.getLinkManager().getDiscordId(event.getPlayer().getUniqueId()));
}
});
}
public void onReload() {
groupToRoleMap.clear();
roleIdToGroupMap.clear();
final List<String> groups = ess.getEss().getPermissionsHandler().getGroups();
for (final Map.Entry<String, String> entry : ess.getSettings().getRoleSyncGroups().entrySet()) {
if (isExampleRole(entry.getValue())) {
continue;
}
final String group = entry.getKey();
final InteractionRole role = ess.getApi().getRole(entry.getValue());
if (!groups.contains(group)) {
ess.getLogger().warning(tl("discordLinkInvalidGroup", group, entry.getValue(), groups));
continue;
}
if (role == null) {
ess.getLogger().warning(tl("discordLinkInvalidRole", entry.getValue(), group));
continue;
}
if (role.isManaged() || role.isPublicRole()) {
ess.getLogger().warning(tl("discordLinkInvalidRoleManaged", role.getName(), role.getId()));
continue;
}
if (!role.canInteract()) {
ess.getLogger().warning(tl("discordLinkInvalidRoleInteract", role.getName(), role.getId()));
continue;
}
groupToRoleMap.put(group, role);
}
for (final Map.Entry<String, String> entry : ess.getSettings().getRoleSyncRoles().entrySet()) {
if (isExampleRole(entry.getKey())) {
continue;
}
final InteractionRole role = ess.getApi().getRole(entry.getKey());
final String group = entry.getValue();
if (role == null) {
ess.getLogger().warning(tl("discordLinkInvalidRole", entry.getKey(), group));
continue;
}
if (!groups.contains(group)) {
ess.getLogger().warning(tl("discordLinkInvalidGroup", group, entry.getKey(), groups));
continue;
}
roleIdToGroupMap.put(role.getId(), group);
}
}
private boolean isExampleRole(final String role) {
return role.equals("0") || role.equals("11111111111111111") || role.equals("22222222222222222") || role.equals("33333333333333333");
}
}

View File

@ -0,0 +1,52 @@
#############################################################
# +-------------------------------------------------------+ #
# | EssentialsX DiscordLink | #
# +-------------------------------------------------------+ #
#############################################################
# This is the config file for EssentialsXDiscordLink.
# This config was generated for version ${full.version}.
# The desired behavior when a player hasn't linked their Minecraft account to Discord.
# Accepts the following values:
# - kick: Kicks the player with a link code and requires they link their discord account before they can join.
# - freeze: Prevents player from moving/interacting/doing commands when they join until they link their discord account.
# - none: Places no restrictions on players for unlinked accounts.
link-policy: none
# Whether to ignore Discord messages from unlinked members and hide them from Minecraft chat.
block-unlinked-chat: false
# Whether someone's Minecraft account should be unlinked when they leave the Discord server.
unlink-on-leave: true
# Whether linked player's incoming mail should be DM'd to them on Discord.
relay-mail: true
# MC group to Discord role sync settings
# Allows for the ability to give players discord roles based on their Minecraft groups and/or give players Minecraft
# groups based on their Discord roles.
role-sync:
# Whether EssentialsX DiscordLink should remove synced Discord roles from players who unlink their Minecraft account,
# leave the Discord server, or who no longer have the groups that awarded them the role in the first place.
remove-roles: true
# Whether EssentialsX DiscordLink should remove synced Minecraft groups from players who unlink their Discord account,
# or who no longer have the Discord roles that awarded them the group in the first place.
remove-groups: true
# The amount of time (in minutes) between which EssentialsX DiscordLink should audit player groups/roles.
# Requires a restart after changing.
resync-delay: 5
# Whether EssentialsX DiscordLink should only consider the primary group of Minecraft users
primary-group-only: false
# Minecraft group to Discord role ID synchronization.
# Players in the following groups listed here will receive the corresponding role ID on discord when they link
# their Minecraft account to their Discord account.
groups:
default: 000000000000000000
admin: 11111111111111111
# Discord role ID to Minecraft group synchronization.
# Users with the following roles listed here will receive the corresponding group in Minecraft when they link
# their Discord account to their Minecraft account.
roles:
22222222222222222: vip
33333333333333333: booster

View File

@ -0,0 +1,18 @@
name: EssentialsDiscordLink
main: net.essentialsx.discordlink.EssentialsDiscordLink
# Note to developers: This next line cannot change, or the automatic versioning system will break.
version: ${full.version}
website: https://essentialsx.net/
description: EssentialsX Discord addon which allows you link your Minecraft and Discord accounts together.
authors: [JRoy]
depend: [EssentialsDiscord]
api-version: 1.13
commands:
link:
description: Generates a code to link your Minecraft account to Discord.
usage: /<command>
aliases: [elink, discordlink, ediscordlink]
unlink:
description: Unlinks your Minecraft account from any associated Discord account.
usage: /<command>
aliases: [eunlink, discordunlink, ediscordunlink]

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,7 @@ sequenceOf(
"AntiBuild", "AntiBuild",
"Chat", "Chat",
"Discord", "Discord",
"DiscordLink",
"GeoIP", "GeoIP",
"Protect", "Protect",
"Spawn", "Spawn",