diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml index c21e1810c..e6484e925 100644 --- a/.github/workflows/build-master.yml +++ b/.github/workflows/build-master.yml @@ -59,6 +59,7 @@ jobs: cp -r EssentialsAntiBuild/build/docs/javadoc/ javadocs/EssentialsAntiBuild/ cp -r EssentialsChat/build/docs/javadoc/ javadocs/EssentialsChat/ 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 EssentialsProtect/build/docs/javadoc/ javadocs/EssentialsProtect/ cp -r EssentialsSpawn/build/docs/javadoc/ javadocs/EssentialsSpawn/ diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index a118891b7..4b03e0aa6 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -24,4 +24,4 @@ - + \ No newline at end of file diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index 021baaa69..ae353dce4 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -202,7 +202,7 @@ public class EssentialsPlayerListener implements Listener, FakeAccessor { to.setY(from.getY()); to.setZ(from.getZ()); try { - event.setTo(LocationUtil.getSafeDestination(to)); + event.setTo(LocationUtil.getSafeDestination(ess, to)); } catch (final Exception ex) { event.setTo(to); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java index b31c6a682..6fa2873af 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java @@ -96,6 +96,7 @@ public class Commandessentials extends EssentialsCommand { "EssentialsAntiBuild", "EssentialsChat", "EssentialsDiscord", + "EssentialsDiscordLink", "EssentialsGeoIP", "EssentialsProtect", "EssentialsSpawn", diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java index e53fa07a8..cf11ee913 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java +++ b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java @@ -33,7 +33,9 @@ import java.lang.reflect.Type; import java.math.BigDecimal; import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -303,6 +305,19 @@ public class EssentialsConfiguration { return ConfigurateUtil.getMap(configurationNode); } + public Map getStringMap(String path) { + final CommentedConfigurationNode node = getInternal(path); + if (node == null || !node.isMap()) { + return Collections.emptyMap(); + } + + final Map map = new LinkedHashMap<>(); + for (Map.Entry entry : node.childrenMap().entrySet()) { + map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue().rawScalar())); + } + return map; + } + public void removeProperty(String path) { final CommentedConfigurationNode node = getInternal(path); if (node != null) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java index 8ba62cc17..70e4a9fb3 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java @@ -13,6 +13,7 @@ import com.google.common.collect.ImmutableSet; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,10 +49,9 @@ public class PermissionsHandler implements IPermissionsHandler { @Override public List getGroups(final OfflinePlayer base) { final long start = System.nanoTime(); - List groups = handler.getGroups(base); - if (groups == null || groups.isEmpty()) { - groups = Collections.singletonList(defaultGroup); - } + final List groups = new ArrayList<>(); + groups.add(defaultGroup); + groups.addAll(handler.getGroups(base)); checkPermLag(start, String.format("Getting groups for %s", base.getName())); return Collections.unmodifiableList(groups); } diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index 3a2637269..eab2065c5 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -240,6 +240,12 @@ discordbroadcastCommandUsage1Description=Sends the given message to the specifie discordbroadcastInvalidChannel=\u00a74Discord channel \u00a7c{0}\u00a74 does not exist. discordbroadcastPermission=\u00a74You do not have permission to send messages to the \u00a7c{0}\u00a74 channel. 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. discordCommandLink=\u00a76Join our Discord server at \u00a7c{0}\u00a76! discordCommandUsage=/ @@ -248,6 +254,14 @@ discordCommandUsage1Description=Sends the discord invite link to the player discordCommandExecuteDescription=Executes a console command on the Minecraft server. discordCommandExecuteArgumentCommand=The command to be executed 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. discordCommandListArgumentGroup=A specific group to limit your search by 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. 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". +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... 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\! 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 @@ -661,6 +687,10 @@ lightningCommandUsage2=/ lightningCommandUsage2Description=Strikes lighting at the target player with the given power lightningSmited=\u00a76Thou hast been smitten\! lightningUse=\u00a76Smiting\u00a7c {0} +linkCommandDescription=Generates a code to link your Minecraft account to Discord. +linkCommandUsage=/ +linkCommandUsage1=/ +linkCommandUsage1Description=Generates a code for the /link command on Discord listAfkTag=\u00a77[AFK]\u00a7r 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. @@ -1028,7 +1058,7 @@ repairCommandUsage2Description=Repairs all items in your inventory repairEnchanted=\u00a74You are not allowed to repair enchanted items. repairInvalidType=\u00a74This item cannot be repaired. 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. replyLastRecipientDisabledFor=\u00a76Replying to last message recipient \u00a7cdisabled \u00a76for \u00a7c{0}\u00a76. replyLastRecipientEnabled=\u00a76Replying to last message recipient \u00a7cenabled\u00a76. @@ -1402,6 +1432,10 @@ unlimitedCommandUsage3=/ clear [player] unlimitedCommandUsage3Description=Clears all unlimited items for yourself or another player if specified unlimitedItemPermission=\u00a74No permission for unlimited item \u00a7c{0}\u00a74. unlimitedItems=\u00a76Unlimited items\:\u00a7r +unlinkCommandDescription=Unlinks your Minecraft account from the currently linked Discord account. +unlinkCommandUsage=/ +unlinkCommandUsage1=/ +unlinkCommandUsage1Description=Unlinks your Minecraft account from the currently linked Discord account. unmutedPlayer=\u00a76Player\u00a7c {0} \u00a76unmuted. 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. diff --git a/EssentialsDiscordLink/README.md b/EssentialsDiscordLink/README.md new file mode 100644 index 000000000..af8c3d758 --- /dev/null +++ b/EssentialsDiscordLink/README.md @@ -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(); +} +``` \ No newline at end of file diff --git a/EssentialsDiscordLink/build.gradle b/EssentialsDiscordLink/build.gradle new file mode 100644 index 000000000..542f00470 --- /dev/null +++ b/EssentialsDiscordLink/build.gradle @@ -0,0 +1,8 @@ +plugins { + id("essentials.module-conventions") +} + +dependencies { + compileOnly project(':EssentialsX') + compileOnly project(':EssentialsXDiscord') +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/events/discordlink/DiscordLinkStatusChangeEvent.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/events/discordlink/DiscordLinkStatusChangeEvent.java new file mode 100644 index 000000000..551b18abd --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/events/discordlink/DiscordLinkStatusChangeEvent.java @@ -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. + *

+ * 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. + *

+ * 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, + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/services/discordlink/DiscordLinkService.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/services/discordlink/DiscordLinkService.java new file mode 100644 index 000000000..8cf09a0bc --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/api/v2/services/discordlink/DiscordLinkService.java @@ -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}. + *

+ * This will automatically trigger role sync (if configured) for the given + * player if this method returns {@code true}. + *

+ * 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). + *

+ * 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). + *

+ * 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); +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountLinkManager.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountLinkManager.java new file mode 100644 index 000000000..10b33d9d5 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountLinkManager.java @@ -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 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> 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; + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountStorage.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountStorage.java new file mode 100644 index 000000000..68bf0552a --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/AccountStorage.java @@ -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 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 map = gson.fromJson(reader, new TypeToken>() {}.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 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 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(); + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/DiscordLinkSettings.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/DiscordLinkSettings.java new file mode 100644 index 000000000..ad5b668af --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/DiscordLinkSettings.java @@ -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 roleSyncGroups; + private Map 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 getRoleSyncGroups() { + return roleSyncGroups; + } + + private Map _getRoleSyncGroups() { + return config.getStringMap("role-sync.groups"); + } + + public Map getRoleSyncRoles() { + return roleSyncRoles; + } + + private Map _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(); + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/EssentialsDiscordLink.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/EssentialsDiscordLink.java new file mode 100644 index 000000000..f34898e54 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/EssentialsDiscordLink.java @@ -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(); + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandlink.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandlink.java new file mode 100644 index 000000000..36831fefb --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandlink.java @@ -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())); + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandunlink.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandunlink.java new file mode 100644 index 000000000..4e30f92d2 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/bukkit/Commandunlink.java @@ -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")); + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/AccountInteractionCommand.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/AccountInteractionCommand.java new file mode 100644 index 000000000..7df811759 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/AccountInteractionCommand.java @@ -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 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 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())); + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/LinkInteractionCommand.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/LinkInteractionCommand.java new file mode 100644 index 000000000..8b1f4345f --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/LinkInteractionCommand.java @@ -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 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 getArguments() { + return arguments; + } +} + diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/UnlinkInteractionCommand.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/UnlinkInteractionCommand.java new file mode 100644 index 000000000..690e8102f --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/commands/discord/UnlinkInteractionCommand.java @@ -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 getArguments() { + return null; + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/listeners/LinkBukkitListener.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/listeners/LinkBukkitListener.java new file mode 100644 index 000000000..4ea071ff6 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/listeners/LinkBukkitListener.java @@ -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; + } + } + } +} diff --git a/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/rolesync/RoleSyncManager.java b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/rolesync/RoleSyncManager.java new file mode 100644 index 000000000..820678754 --- /dev/null +++ b/EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/rolesync/RoleSyncManager.java @@ -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 groupToRoleMap = new HashMap<>(); + private final Map 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 uuidToDiscordCopy = ess.getAccountStorage().getRawStorageMap(); + final Map groupToRoleMapCopy = new HashMap<>(groupToRoleMap); + final Map 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 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 groupToRoleMapCopy = new HashMap<>(groupToRoleMap); + final Map 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 groupToRoleMap, final Map roleIdToGroupMap, + final boolean primaryOnly, final boolean removeGroups, final boolean removeRoles) { + final List 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 toAdd = new ArrayList<>(); + final List toRemove = new ArrayList<>(); + + for (final Map.Entry 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 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 groupToRoleMapCopy = new HashMap<>(groupToRoleMap); + final Map 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 groups = ess.getEss().getPermissionsHandler().getGroups(); + + for (final Map.Entry 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 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"); + } +} diff --git a/EssentialsDiscordLink/src/main/resources/config.yml b/EssentialsDiscordLink/src/main/resources/config.yml new file mode 100644 index 000000000..f7604bc90 --- /dev/null +++ b/EssentialsDiscordLink/src/main/resources/config.yml @@ -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 diff --git a/EssentialsDiscordLink/src/main/resources/plugin.yml b/EssentialsDiscordLink/src/main/resources/plugin.yml new file mode 100644 index 000000000..49df5f7f3 --- /dev/null +++ b/EssentialsDiscordLink/src/main/resources/plugin.yml @@ -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: / + aliases: [elink, discordlink, ediscordlink] + unlink: + description: Unlinks your Minecraft account from any associated Discord account. + usage: / + aliases: [eunlink, discordunlink, ediscordunlink] diff --git a/providers/1_8Provider/src/main/java/com/earth2me/essentials/UUIDPlayer.java b/providers/1_8Provider/src/main/java/com/earth2me/essentials/UUIDPlayer.java new file mode 100644 index 000000000..5994a25f9 --- /dev/null +++ b/providers/1_8Provider/src/main/java/com/earth2me/essentials/UUIDPlayer.java @@ -0,0 +1,1314 @@ +package com.earth2me.essentials; + +import org.bukkit.Achievement; +import org.bukkit.Bukkit; +import org.bukkit.Effect; +import org.bukkit.EntityEffect; +import org.bukkit.GameMode; +import org.bukkit.Instrument; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Note; +import org.bukkit.Server; +import org.bukkit.Sound; +import org.bukkit.Statistic; +import org.bukkit.WeatherType; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.conversations.Conversation; +import org.bukkit.conversations.ConversationAbandonedEvent; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Egg; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.entity.Snowball; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.map.MapView; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.util.Vector; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class UUIDPlayer implements Player { + private final UUID uuid; + + public UUIDPlayer(UUID uuid) { + this.uuid = uuid; + } + + @Override + public UUID getUniqueId() { + return uuid; + } + + @Override + public boolean isOnline() { + return false; + } + + @Override + public String getName() { + return uuid.toString(); + } + + @Override + public Server getServer() { + return Bukkit.getServer(); + } + + @Override + public String getDisplayName() { + throw new UnsupportedOperationException(); + } + + @Override + public void setDisplayName(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public String getPlayerListName() { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayerListName(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void setCompassTarget(Location loc) { + throw new UnsupportedOperationException(); + } + + @Override + public Location getCompassTarget() { + throw new UnsupportedOperationException(); + } + + @Override + public InetSocketAddress getAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isConversing() { + throw new UnsupportedOperationException(); + } + + @Override + public void acceptConversationInput(String input) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean beginConversation(Conversation conversation) { + throw new UnsupportedOperationException(); + } + + @Override + public void abandonConversation(Conversation conversation) { + throw new UnsupportedOperationException(); + } + + @Override + public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendRawMessage(String message) { + throw new UnsupportedOperationException(); + } + + @Override + public void kickPlayer(String message) { + throw new UnsupportedOperationException(); + } + + @Override + public void chat(String msg) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean performCommand(String command) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSneaking() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSneaking(boolean sneak) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSprinting() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSprinting(boolean sprinting) { + throw new UnsupportedOperationException(); + } + + @Override + public void saveData() { + throw new UnsupportedOperationException(); + } + + @Override + public void loadData() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSleepingIgnored(boolean isSleeping) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSleepingIgnored() { + throw new UnsupportedOperationException(); + } + + @Override + public void playNote(Location loc, byte instrument, byte note) { + throw new UnsupportedOperationException(); + } + + @Override + public void playNote(Location loc, Instrument instrument, Note note) { + throw new UnsupportedOperationException(); + } + + @Override + public void playSound(Location location, Sound sound, float volume, float pitch) { + throw new UnsupportedOperationException(); + } + + @Override + public void playSound(Location location, String sound, float volume, float pitch) { + throw new UnsupportedOperationException(); + } + + @Override + public void playEffect(Location loc, Effect effect, int data) { + throw new UnsupportedOperationException(); + } + + @Override + public void playEffect(Location loc, Effect effect, T data) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendBlockChange(Location loc, Material material, byte data) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendBlockChange(Location loc, int material, byte data) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendSignChange(Location loc, String[] lines) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMap(MapView map) { + throw new UnsupportedOperationException(); + } + + @Override + public void updateInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public void awardAchievement(Achievement achievement) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAchievement(Achievement achievement) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasAchievement(Achievement achievement) { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void setStatistic(Statistic statistic, int newValue) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public int getStatistic(Statistic statistic) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void setStatistic(Statistic statistic, Material material, int newValue) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void incrementStatistic(Statistic statistic, EntityType entityType, int amount) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void setStatistic(Statistic statistic, EntityType entityType, int newValue) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayerTime(long time, boolean relative) { + throw new UnsupportedOperationException(); + } + + @Override + public long getPlayerTime() { + throw new UnsupportedOperationException(); + } + + @Override + public long getPlayerTimeOffset() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayerTimeRelative() { + throw new UnsupportedOperationException(); + } + + @Override + public void resetPlayerTime() { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayerWeather(WeatherType type) { + throw new UnsupportedOperationException(); + } + + @Override + public WeatherType getPlayerWeather() { + throw new UnsupportedOperationException(); + } + + @Override + public void resetPlayerWeather() { + throw new UnsupportedOperationException(); + } + + @Override + public void giveExp(int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void giveExpLevels(int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public float getExp() { + throw new UnsupportedOperationException(); + } + + @Override + public void setExp(float exp) { + throw new UnsupportedOperationException(); + } + + @Override + public int getLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLevel(int level) { + throw new UnsupportedOperationException(); + } + + @Override + public int getTotalExperience() { + throw new UnsupportedOperationException(); + } + + @Override + public void setTotalExperience(int exp) { + throw new UnsupportedOperationException(); + } + + @Override + public float getExhaustion() { + throw new UnsupportedOperationException(); + } + + @Override + public void setExhaustion(float value) { + throw new UnsupportedOperationException(); + } + + @Override + public float getSaturation() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSaturation(float value) { + throw new UnsupportedOperationException(); + } + + @Override + public int getFoodLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFoodLevel(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isBanned() { + throw new UnsupportedOperationException(); + } + + @Override + public void setBanned(boolean banned) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isWhitelisted() { + throw new UnsupportedOperationException(); + } + + @Override + public void setWhitelisted(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public Player getPlayer() { + throw new UnsupportedOperationException(); + } + + @Override + public long getFirstPlayed() { + throw new UnsupportedOperationException(); + } + + @Override + public long getLastPlayed() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPlayedBefore() { + throw new UnsupportedOperationException(); + } + + @Override + public Location getBedSpawnLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public void setBedSpawnLocation(Location location) { + throw new UnsupportedOperationException(); + } + + @Override + public void setBedSpawnLocation(Location location, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getAllowFlight() { + throw new UnsupportedOperationException(); + } + + @Override + public void setAllowFlight(boolean flight) { + throw new UnsupportedOperationException(); + } + + @Override + public void hidePlayer(Player player) { + throw new UnsupportedOperationException(); + } + + @Override + public void showPlayer(Player player) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean canSee(Player player) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFlying() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFlying(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public void setFlySpeed(float value) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public void setWalkSpeed(float value) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public float getFlySpeed() { + throw new UnsupportedOperationException(); + } + + @Override + public float getWalkSpeed() { + throw new UnsupportedOperationException(); + } + + @Override + public void setTexturePack(String url) { + throw new UnsupportedOperationException(); + } + + @Override + public void setResourcePack(String url) { + throw new UnsupportedOperationException(); + } + + @Override + public Scoreboard getScoreboard() { + throw new UnsupportedOperationException(); + } + + @Override + public void setScoreboard(Scoreboard scoreboard) throws IllegalArgumentException, IllegalStateException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isHealthScaled() { + throw new UnsupportedOperationException(); + } + + @Override + public void setHealthScaled(boolean scale) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHealthScale(double scale) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public double getHealthScale() { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getSpectatorTarget() { + throw new UnsupportedOperationException(); + } + + @Override + public void setSpectatorTarget(Entity entity) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendTitle(String title, String subtitle) { + throw new UnsupportedOperationException(); + } + + @Override + public void resetTitle() { + throw new UnsupportedOperationException(); + } + + @Override + public Spigot spigot() { + throw new UnsupportedOperationException(); + } + + @Override + public Map serialize() { + throw new UnsupportedOperationException(); + } + + @Override + public PlayerInventory getInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public Inventory getEnderChest() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setWindowProperty(InventoryView.Property prop, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView getOpenInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView openInventory(Inventory inventory) { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView openWorkbench(Location location, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public InventoryView openEnchanting(Location location, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public void openInventory(InventoryView inventory) { + throw new UnsupportedOperationException(); + } + + @Override + public void closeInventory() { + throw new UnsupportedOperationException(); + } + + @Override + public ItemStack getItemInHand() { + throw new UnsupportedOperationException(); + } + + @Override + public void setItemInHand(ItemStack item) { + throw new UnsupportedOperationException(); + } + + @Override + public ItemStack getItemOnCursor() { + throw new UnsupportedOperationException(); + } + + @Override + public void setItemOnCursor(ItemStack item) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSleeping() { + throw new UnsupportedOperationException(); + } + + @Override + public int getSleepTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public GameMode getGameMode() { + throw new UnsupportedOperationException(); + } + + @Override + public void setGameMode(GameMode mode) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isBlocking() { + throw new UnsupportedOperationException(); + } + + @Override + public int getExpToLevel() { + throw new UnsupportedOperationException(); + } + + @Override + public double getEyeHeight() { + throw new UnsupportedOperationException(); + } + + @Override + public double getEyeHeight(boolean ignorePose) { + throw new UnsupportedOperationException(); + } + + @Override + public Location getEyeLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public List getLineOfSight(HashSet transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public List getLineOfSight(Set transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public Block getTargetBlock(HashSet transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public Block getTargetBlock(Set transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public List getLastTwoTargetBlocks(HashSet transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public List getLastTwoTargetBlocks(Set transparent, int maxDistance) { + throw new UnsupportedOperationException(); + } + + @Override + public Egg throwEgg() { + throw new UnsupportedOperationException(); + } + + @Override + public Snowball throwSnowball() { + throw new UnsupportedOperationException(); + } + + @Override + public Arrow shootArrow() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRemainingAir() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRemainingAir(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaximumAir() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaximumAir(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaximumNoDamageTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaximumNoDamageTicks(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public double getLastDamage() { + throw new UnsupportedOperationException(); + } + + @Override + public int _INVALID_getLastDamage() { + throw new UnsupportedOperationException(); + } + + @Override + public void setLastDamage(double damage) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_setLastDamage(int damage) { + throw new UnsupportedOperationException(); + } + + @Override + public int getNoDamageTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public void setNoDamageTicks(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public Player getKiller() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addPotionEffect(PotionEffect effect) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addPotionEffect(PotionEffect effect, boolean force) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addPotionEffects(Collection effects) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPotionEffect(PotionEffectType type) { + throw new UnsupportedOperationException(); + } + + @Override + public void removePotionEffect(PotionEffectType type) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getActivePotionEffects() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLineOfSight(Entity other) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getRemoveWhenFarAway() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRemoveWhenFarAway(boolean remove) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityEquipment getEquipment() { + throw new UnsupportedOperationException(); + } + + @Override + public void setCanPickupItems(boolean pickup) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getCanPickupItems() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLeashed() { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getLeashHolder() throws IllegalStateException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setLeashHolder(Entity holder) { + throw new UnsupportedOperationException(); + } + + @Override + public void damage(double amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_damage(int amount) { + throw new UnsupportedOperationException(); + } + + @Override + public void damage(double amount, Entity source) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_damage(int amount, Entity source) { + throw new UnsupportedOperationException(); + } + + @Override + public double getHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public int _INVALID_getHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public void setHealth(double health) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_setHealth(int health) { + throw new UnsupportedOperationException(); + } + + @Override + public double getMaxHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public int _INVALID_getMaxHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMaxHealth(double health) { + throw new UnsupportedOperationException(); + } + + @Override + public void _INVALID_setMaxHealth(int health) { + throw new UnsupportedOperationException(); + } + + @Override + public void resetMaxHealth() { + throw new UnsupportedOperationException(); + } + + @Override + public Location getLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public Location getLocation(Location loc) { + throw new UnsupportedOperationException(); + } + + @Override + public void setVelocity(Vector velocity) { + throw new UnsupportedOperationException(); + } + + @Override + public Vector getVelocity() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOnGround() { + throw new UnsupportedOperationException(); + } + + @Override + public World getWorld() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Location location) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Entity destination) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean teleport(Entity destination, PlayerTeleportEvent.TeleportCause cause) { + throw new UnsupportedOperationException(); + } + + @Override + public List getNearbyEntities(double x, double y, double z) { + throw new UnsupportedOperationException(); + } + + @Override + public int getEntityId() { + throw new UnsupportedOperationException(); + } + + @Override + public int getFireTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public int getMaxFireTicks() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFireTicks(int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDead() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isValid() { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessage(String message) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessage(String[] messages) { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getPassenger() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setPassenger(Entity passenger) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean eject() { + throw new UnsupportedOperationException(); + } + + @Override + public float getFallDistance() { + throw new UnsupportedOperationException(); + } + + @Override + public void setFallDistance(float distance) { + throw new UnsupportedOperationException(); + } + + @Override + public void setLastDamageCause(EntityDamageEvent event) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityDamageEvent getLastDamageCause() { + throw new UnsupportedOperationException(); + } + + @Override + public int getTicksLived() { + throw new UnsupportedOperationException(); + } + + @Override + public void setTicksLived(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public void playEffect(EntityEffect type) { + throw new UnsupportedOperationException(); + } + + @Override + public EntityType getType() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInsideVehicle() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean leaveVehicle() { + throw new UnsupportedOperationException(); + } + + @Override + public Entity getVehicle() { + throw new UnsupportedOperationException(); + } + + @Override + public void setCustomNameVisible(boolean flag) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCustomNameVisible() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCustomName() { + throw new UnsupportedOperationException(); + } + + @Override + public void setCustomName(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { + throw new UnsupportedOperationException(); + } + + @Override + public List getMetadata(String metadataKey) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasMetadata(String metadataKey) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeMetadata(String metadataKey, Plugin owningPlugin) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPermissionSet(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPermissionSet(Permission perm) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPermission(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasPermission(Permission perm) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public PermissionAttachment addAttachment(Plugin plugin, int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAttachment(PermissionAttachment attachment) { + throw new UnsupportedOperationException(); + } + + @Override + public void recalculatePermissions() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getEffectivePermissions() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOp() { + throw new UnsupportedOperationException(); + } + + @Override + public void setOp(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendPluginMessage(Plugin source, String channel, byte[] message) { + throw new UnsupportedOperationException(); + } + + @Override + public Set getListeningPluginChannels() { + throw new UnsupportedOperationException(); + } + + @Override + public T launchProjectile(Class projectile) { + throw new UnsupportedOperationException(); + } + + @Override + public T launchProjectile(Class projectile, Vector velocity) { + throw new UnsupportedOperationException(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8b8c0b786..f133a0cd1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,7 @@ sequenceOf( "AntiBuild", "Chat", "Discord", + "DiscordLink", "GeoIP", "Protect", "Spawn",