Discord Module (#3844)

Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>
Co-authored-by: pop4959 <pop4959@gmail.com>
Co-authored-by: Riley Park <riley.park@meino.net>
Co-authored-by: Jason <11360596+jpenilla@users.noreply.github.com>
This commit is contained in:
Josh Roy 2021-07-01 09:43:35 -04:00 committed by GitHub
parent 9d3bf337e1
commit 0861427bf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 4247 additions and 171 deletions

View File

@ -3,5 +3,5 @@
<suppressions>
<suppress files=".*[\\/]test[\\/]java[\\/].*" checks="RequireExplicitVisibilityModifier"/>
<!-- TODO: don't suppress I-prefixed API interfaces under com.earth2me.essentials -->
<suppress files="(com[\\/]earth2me[\\/]essentials[\\/]|net[\\/]ess3[\\/])(?:[^\\/]+$|(?!api)[^\\/]+[\\/])" checks="MissingJavadoc(Method|Type)"/>
<suppress files="(com[\\/]earth2me[\\/]essentials[\\/]|net[\\/]ess3[\\/]|net[\\/]essentialsx[\\/])(?:[^\\/]+$|(?!api)[^\\/]+[\\/])" checks="MissingJavadoc(Method|Type)"/>
</suppressions>

View File

@ -58,6 +58,7 @@ jobs:
mv Essentials/build/docs/javadoc/ javadocs/
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 EssentialsGeoIP/build/docs/javadoc/ javadocs/EssentialsGeoIP/
cp -r EssentialsProtect/build/docs/javadoc/ javadocs/EssentialsProtect/
cp -r EssentialsSpawn/build/docs/javadoc/ javadocs/EssentialsSpawn/

View File

@ -21,6 +21,7 @@ import com.earth2me.essentials.commands.EssentialsCommand;
import com.earth2me.essentials.commands.IEssentialsCommand;
import com.earth2me.essentials.commands.NoChargeException;
import com.earth2me.essentials.commands.NotEnoughArgumentsException;
import com.earth2me.essentials.commands.PlayerNotFoundException;
import com.earth2me.essentials.commands.QuietAbortException;
import com.earth2me.essentials.economy.EconomyLayers;
import com.earth2me.essentials.economy.vault.VaultEconomyProvider;
@ -38,6 +39,7 @@ import com.earth2me.essentials.textreader.IText;
import com.earth2me.essentials.textreader.KeywordReplacer;
import com.earth2me.essentials.textreader.SimpleTextInput;
import com.earth2me.essentials.updatecheck.UpdateChecker;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.VersionUtil;
import io.papermc.lib.PaperLib;
import net.ess3.api.Economy;
@ -118,6 +120,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -895,6 +898,94 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
return user;
}
@Override
public User matchUser(final Server server, final User sourceUser, final String searchTerm, final Boolean getHidden, final boolean getOffline) throws PlayerNotFoundException {
final User user;
Player exPlayer;
try {
exPlayer = server.getPlayer(UUID.fromString(searchTerm));
} catch (final IllegalArgumentException ex) {
if (getOffline) {
exPlayer = server.getPlayerExact(searchTerm);
} else {
exPlayer = server.getPlayer(searchTerm);
}
}
if (exPlayer != null) {
user = getUser(exPlayer);
} else {
user = getUser(searchTerm);
}
if (user != null) {
if (!getOffline && !user.getBase().isOnline()) {
throw new PlayerNotFoundException();
}
if (getHidden || canInteractWith(sourceUser, user)) {
return user;
} else { // not looking for hidden and cannot interact (i.e is hidden)
if (getOffline && user.getName().equalsIgnoreCase(searchTerm)) { // if looking for offline and got an exact match
return user;
}
}
throw new PlayerNotFoundException();
}
final List<Player> matches = server.matchPlayer(searchTerm);
if (matches.isEmpty()) {
final String matchText = searchTerm.toLowerCase(Locale.ENGLISH);
for (final User userMatch : getOnlineUsers()) {
if (getHidden || canInteractWith(sourceUser, userMatch)) {
final String displayName = FormatUtil.stripFormat(userMatch.getDisplayName()).toLowerCase(Locale.ENGLISH);
if (displayName.contains(matchText)) {
return userMatch;
}
}
}
} else {
for (final Player player : matches) {
final User userMatch = getUser(player);
if (userMatch.getDisplayName().startsWith(searchTerm) && (getHidden || canInteractWith(sourceUser, userMatch))) {
return userMatch;
}
}
final User userMatch = getUser(matches.get(0));
if (getHidden || canInteractWith(sourceUser, userMatch)) {
return userMatch;
}
}
throw new PlayerNotFoundException();
}
@Override
public boolean canInteractWith(final CommandSource interactor, final User interactee) {
if (interactor == null) {
return !interactee.isHidden();
}
if (interactor.isPlayer()) {
return canInteractWith(getUser(interactor.getPlayer()), interactee);
}
return true; // console
}
@Override
public boolean canInteractWith(final User interactor, final User interactee) {
if (interactor == null) {
return !interactee.isHidden();
}
if (interactor.equals(interactee)) {
return true;
}
return interactor.getBase().canSee(interactee.getBase());
}
//This will create a new user if there is not a match.
@Override
public User getUser(final Player base) {

View File

@ -310,8 +310,6 @@ public class EssentialsPlayerListener implements Listener {
user.setDisplayNick();
updateCompass(user);
ess.runTaskAsynchronously(() -> ess.getServer().getPluginManager().callEvent(new AsyncUserDataLoadEvent(user)));
if (!ess.getVanishedPlayersNew().isEmpty() && !user.isAuthorized("essentials.vanish.see")) {
for (final String p : ess.getVanishedPlayersNew()) {
final Player toVanish = ess.getServer().getPlayerExact(p);
@ -328,12 +326,14 @@ public class EssentialsPlayerListener implements Listener {
user.getBase().setSleepingIgnored(true);
}
final String effectiveMessage;
if (ess.getSettings().allowSilentJoinQuit() && (user.isAuthorized("essentials.silentjoin") || user.isAuthorized("essentials.silentjoin.vanish"))) {
if (user.isAuthorized("essentials.silentjoin.vanish")) {
user.setVanished(true);
}
effectiveMessage = null;
} else if (message == null || hideJoinQuitMessages()) {
//NOOP
effectiveMessage = null;
} else if (ess.getSettings().isCustomJoinMessage()) {
final String msg = ess.getSettings().getCustomJoinMessage()
.replace("{PLAYER}", player.getDisplayName()).replace("{USERNAME}", player.getName())
@ -345,10 +345,16 @@ public class EssentialsPlayerListener implements Listener {
if (!msg.isEmpty()) {
ess.getServer().broadcastMessage(msg);
}
effectiveMessage = msg.isEmpty() ? null : msg;
} else if (ess.getSettings().allowSilentJoinQuit()) {
ess.getServer().broadcastMessage(message);
effectiveMessage = message;
} else {
effectiveMessage = message;
}
ess.runTaskAsynchronously(() -> ess.getServer().getPluginManager().callEvent(new AsyncUserDataLoadEvent(user, effectiveMessage)));
final int motdDelay = ess.getSettings().getMotdDelay() / 50;
final DelayMotdTask motdTask = new DelayMotdTask(user);
if (motdDelay > 0) {

View File

@ -4,6 +4,7 @@ import com.earth2me.essentials.api.IItemDb;
import com.earth2me.essentials.api.IJails;
import com.earth2me.essentials.api.IWarps;
import com.earth2me.essentials.commands.IEssentialsCommand;
import com.earth2me.essentials.commands.PlayerNotFoundException;
import com.earth2me.essentials.perm.PermissionsHandler;
import com.earth2me.essentials.updatecheck.UpdateChecker;
import net.ess3.provider.ContainerProvider;
@ -17,6 +18,7 @@ import net.ess3.provider.SpawnerBlockProvider;
import net.ess3.provider.SpawnerItemProvider;
import net.ess3.provider.SyncCommandsProvider;
import net.essentialsx.api.v2.services.BalanceTop;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -52,6 +54,12 @@ public interface IEssentials extends Plugin {
User getUser(Player base);
User matchUser(Server server, User sourceUser, String searchTerm, Boolean getHidden, boolean getOffline) throws PlayerNotFoundException;
boolean canInteractWith(CommandSource interactor, User interactee);
boolean canInteractWith(User interactor, User interactee);
I18n getI18n();
User getOfflineUser(String name);

View File

@ -1,10 +1,12 @@
package com.earth2me.essentials;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.NumberUtil;
import org.bukkit.ChatColor;
import org.bukkit.Server;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -127,4 +129,88 @@ public final class PlayerList {
return tl("listGroupTag", FormatUtil.replaceFormat(group)) +
message;
}
public static List<String> prepareGroupedList(final IEssentials ess, final String commandLabel, final Map<String, List<User>> playerList) {
final List<String> output = new ArrayList<>();
final Set<String> configGroups = ess.getSettings().getListGroupConfig().keySet();
final List<String> asterisk = new ArrayList<>();
// Loop through the custom defined groups and display them
for (final String oConfigGroup : configGroups) {
final String groupValue = ess.getSettings().getListGroupConfig().get(oConfigGroup).toString().trim();
final String configGroup = oConfigGroup.toLowerCase();
// If the group value is an asterisk, then skip it, and handle it later
if (groupValue.equals("*")) {
asterisk.add(oConfigGroup);
continue;
}
// If the group value is hidden, we don't need to display it
if (groupValue.equalsIgnoreCase("hidden")) {
playerList.remove(configGroup);
continue;
}
final List<User> outputUserList;
final List<User> matchedList = playerList.get(configGroup);
// If the group value is an int, then we might need to truncate it
if (NumberUtil.isInt(groupValue)) {
if (matchedList != null && !matchedList.isEmpty()) {
playerList.remove(configGroup);
outputUserList = new ArrayList<>(matchedList);
final int limit = Integer.parseInt(groupValue);
if (matchedList.size() > limit) {
output.add(outputFormat(oConfigGroup, tl("groupNumber", matchedList.size(), commandLabel, FormatUtil.stripFormat(configGroup))));
} else {
output.add(outputFormat(oConfigGroup, listUsers(ess, outputUserList, ", ")));
}
continue;
}
}
outputUserList = getMergedList(ess, playerList, configGroup);
// If we have no users, than we don't need to continue parsing this group
if (outputUserList.isEmpty()) {
continue;
}
output.add(outputFormat(oConfigGroup, listUsers(ess, outputUserList, ", ")));
}
final Set<String> var = playerList.keySet();
String[] onlineGroups = var.toArray(new String[0]);
Arrays.sort(onlineGroups, String.CASE_INSENSITIVE_ORDER);
// If we have an asterisk group, then merge all remaining groups
if (!asterisk.isEmpty()) {
final List<User> asteriskUsers = new ArrayList<>();
for (final String onlineGroup : onlineGroups) {
asteriskUsers.addAll(playerList.get(onlineGroup));
}
for (final String key : asterisk) {
playerList.put(key, asteriskUsers);
}
onlineGroups = asterisk.toArray(new String[0]);
}
// If we have any groups remaining after the custom groups loop through and display them
for (final String onlineGroup : onlineGroups) {
final List<User> users = playerList.get(onlineGroup);
String groupName = asterisk.isEmpty() ? users.get(0).getGroup() : onlineGroup;
if (ess.getPermissionsHandler().getName().equals("ConfigPermissions")) {
groupName = tl("connectedPlayers");
}
if (users == null || users.isEmpty()) {
continue;
}
output.add(outputFormat(groupName, listUsers(ess, users, ", ")));
}
return output;
}
}

View File

@ -115,7 +115,11 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
@Override
public boolean isPermissionSet(final String node) {
return isPermSetCheck(node);
final boolean result = isPermSetCheck(node);
if (ess.getSettings().isDebug()) {
ess.getLogger().log(Level.INFO, "checking if " + base.getName() + " has " + node + " (set-explicit) - " + result);
}
return result;
}
/**

View File

@ -58,11 +58,13 @@ public class Commandessentials extends EssentialsCommand {
"UltraPermissions",
"PermissionsEx", // permissions (unsupported)
"GroupManager", // permissions (unsupported)
"bPermissions" // permissions (unsupported)
"bPermissions", // permissions (unsupported)
"DiscordSRV" // potential for issues if EssentialsXDiscord is installed
);
private static final List<String> officialPlugins = Arrays.asList(
"EssentialsAntiBuild",
"EssentialsChat",
"EssentialsDiscord",
"EssentialsGeoIP",
"EssentialsProtect",
"EssentialsSpawn",

View File

@ -3,18 +3,12 @@ package com.earth2me.essentials.commands;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.PlayerList;
import com.earth2me.essentials.User;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.NumberUtil;
import org.bukkit.Server;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.earth2me.essentials.I18n.tl;
public class Commandlist extends EssentialsCommand {
public Commandlist() {
@ -41,83 +35,8 @@ public class Commandlist extends EssentialsCommand {
// Output the standard /list output, when no group is specified
private void sendGroupedList(final CommandSource sender, final String commandLabel, final Map<String, List<User>> playerList) {
final Set<String> configGroups = ess.getSettings().getListGroupConfig().keySet();
final List<String> asterisk = new ArrayList<>();
// Loop through the custom defined groups and display them
for (final String oConfigGroup : configGroups) {
final String groupValue = ess.getSettings().getListGroupConfig().get(oConfigGroup).toString().trim();
final String configGroup = oConfigGroup.toLowerCase();
// If the group value is an asterisk, then skip it, and handle it later
if (groupValue.equals("*")) {
asterisk.add(oConfigGroup);
continue;
}
// If the group value is hidden, we don't need to display it
if (groupValue.equalsIgnoreCase("hidden")) {
playerList.remove(configGroup);
continue;
}
final List<User> outputUserList;
final List<User> matchedList = playerList.get(configGroup);
// If the group value is an int, then we might need to truncate it
if (NumberUtil.isInt(groupValue)) {
if (matchedList != null && !matchedList.isEmpty()) {
playerList.remove(configGroup);
outputUserList = new ArrayList<>(matchedList);
final int limit = Integer.parseInt(groupValue);
if (matchedList.size() > limit) {
sender.sendMessage(PlayerList.outputFormat(oConfigGroup, tl("groupNumber", matchedList.size(), commandLabel, FormatUtil.stripFormat(configGroup))));
} else {
sender.sendMessage(PlayerList.outputFormat(oConfigGroup, PlayerList.listUsers(ess, outputUserList, ", ")));
}
continue;
}
}
outputUserList = PlayerList.getMergedList(ess, playerList, configGroup);
// If we have no users, than we don't need to continue parsing this group
if (outputUserList.isEmpty()) {
continue;
}
sender.sendMessage(PlayerList.outputFormat(oConfigGroup, PlayerList.listUsers(ess, outputUserList, ", ")));
}
final Set<String> var = playerList.keySet();
String[] onlineGroups = var.toArray(new String[0]);
Arrays.sort(onlineGroups, String.CASE_INSENSITIVE_ORDER);
// If we have an asterisk group, then merge all remaining groups
if (!asterisk.isEmpty()) {
final List<User> asteriskUsers = new ArrayList<>();
for (final String onlineGroup : onlineGroups) {
asteriskUsers.addAll(playerList.get(onlineGroup));
}
for (final String key : asterisk) {
playerList.put(key, asteriskUsers);
}
onlineGroups = asterisk.toArray(new String[0]);
}
// If we have any groups remaining after the custom groups loop through and display them
for (final String onlineGroup : onlineGroups) {
final List<User> users = playerList.get(onlineGroup);
String groupName = asterisk.isEmpty() ? users.get(0).getGroup() : onlineGroup;
if (ess.getPermissionsHandler().getName().equals("ConfigPermissions")) {
groupName = tl("connectedPlayers");
}
if (users == null || users.isEmpty()) {
continue;
}
sender.sendMessage(PlayerList.outputFormat(groupName, PlayerList.listUsers(ess, users, ", ")));
for (final String str : PlayerList.prepareGroupedList(ess, commandLabel, playerList)) {
sender.sendMessage(str);
}
}

View File

@ -4,7 +4,6 @@ import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.IEssentialsModule;
import com.earth2me.essentials.Trade;
import com.earth2me.essentials.User;
import com.earth2me.essentials.utils.FormatUtil;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@ -14,7 +13,6 @@ import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginIdentifiableCommand;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.StringUtil;
@ -23,10 +21,8 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -94,16 +90,8 @@ public abstract class EssentialsCommand implements IEssentialsCommand {
return bldr.toString();
}
private static boolean canInteractWith(final User interactor, final User interactee) {
if (interactor == null) {
return !interactee.isHidden();
}
if (interactor.equals(interactee)) {
return true;
}
return interactor.getBase().canSee(interactee.getBase());
private boolean canInteractWith(final User interactor, final User interactee) {
return ess.canInteractWith(interactor, interactee);
}
@Override
@ -165,64 +153,7 @@ public abstract class EssentialsCommand implements IEssentialsCommand {
}
private User getPlayer(final Server server, final User sourceUser, final String searchTerm, final boolean getHidden, final boolean getOffline) throws PlayerNotFoundException {
final User user;
Player exPlayer;
try {
exPlayer = server.getPlayer(UUID.fromString(searchTerm));
} catch (final IllegalArgumentException ex) {
if (getOffline) {
exPlayer = server.getPlayerExact(searchTerm);
} else {
exPlayer = server.getPlayer(searchTerm);
}
}
if (exPlayer != null) {
user = ess.getUser(exPlayer);
} else {
user = ess.getUser(searchTerm);
}
if (user != null) {
if (!getOffline && !user.getBase().isOnline()) {
throw new PlayerNotFoundException();
}
if (getHidden || canInteractWith(sourceUser, user)) {
return user;
} else { // not looking for hidden and cannot interact (i.e is hidden)
if (getOffline && user.getName().equalsIgnoreCase(searchTerm)) { // if looking for offline and got an exact match
return user;
}
}
throw new PlayerNotFoundException();
}
final List<Player> matches = server.matchPlayer(searchTerm);
if (matches.isEmpty()) {
final String matchText = searchTerm.toLowerCase(Locale.ENGLISH);
for (final User userMatch : ess.getOnlineUsers()) {
if (getHidden || canInteractWith(sourceUser, userMatch)) {
final String displayName = FormatUtil.stripFormat(userMatch.getDisplayName()).toLowerCase(Locale.ENGLISH);
if (displayName.contains(matchText)) {
return userMatch;
}
}
}
} else {
for (final Player player : matches) {
final User userMatch = ess.getUser(player);
if (userMatch.getDisplayName().startsWith(searchTerm) && (getHidden || canInteractWith(sourceUser, userMatch))) {
return userMatch;
}
}
final User userMatch = ess.getUser(matches.get(0));
if (getHidden || canInteractWith(sourceUser, userMatch)) {
return userMatch;
}
}
throw new PlayerNotFoundException();
return ess.matchUser(server, sourceUser, searchTerm, getHidden, getOffline);
}
@Override
@ -284,15 +215,7 @@ public abstract class EssentialsCommand implements IEssentialsCommand {
}
boolean canInteractWith(final CommandSource interactor, final User interactee) {
if (interactor == null) {
return !interactee.isHidden();
}
if (interactor.isPlayer()) {
return canInteractWith(ess.getUser(interactor.getPlayer()), interactee);
}
return true; // console
return ess.canInteractWith(interactor, interactee);
}
/**

View File

@ -45,6 +45,13 @@ public final class ConfigurateUtil {
return map;
}
public static Map<String, Object> getRawMap(final EssentialsConfiguration config, final String key) {
if (config == null || key == null) {
return Collections.emptyMap();
}
return getRawMap(config.getSection(key));
}
public static Map<String, Object> getRawMap(final CommentedConfigurationNode node) {
if (node == null || !node.isMap()) {
return Collections.emptyMap();

View File

@ -0,0 +1,142 @@
package com.earth2me.essentials.utils;
/**
* Most of this code was "borrowed" from KyoriPowered/Adventure and is subject to their MIT license;
*
* MIT License
*
* Copyright (c) 2017-2020 KyoriPowered
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
public final class DownsampleUtil {
private final static NamedTextColor[] VALUES = new NamedTextColor[] {new NamedTextColor("0", 0x000000), new NamedTextColor("1", 0x0000aa), new NamedTextColor("2", 0x00aa00), new NamedTextColor("3", 0x00aaaa), new NamedTextColor("4", 0xaa0000), new NamedTextColor("5", 0xaa00aa), new NamedTextColor("6", 0xffaa00), new NamedTextColor("7", 0xaaaaaa), new NamedTextColor("8", 0x555555), new NamedTextColor("9", 0x5555ff), new NamedTextColor("a", 0x55ff55), new NamedTextColor("b", 0x55ffff), new NamedTextColor("c", 0xff5555), new NamedTextColor("d", 0xff55ff), new NamedTextColor("e", 0xffff55), new NamedTextColor("f", 0xffffff)};
private DownsampleUtil() {
}
public static String nearestTo(final int rgb) {
final HSVLike any = HSVLike.fromRGB((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff);
float matchedDistance = Float.MAX_VALUE;
NamedTextColor match = VALUES[0];
for (final NamedTextColor potential : VALUES) {
final float distance = distance(any, potential.hsv);
if (distance < matchedDistance) {
match = potential;
matchedDistance = distance;
}
if (distance == 0) {
break;
}
}
return match.code;
}
private static float distance(final HSVLike self, final HSVLike other) {
final float hueDistance = 3 * Math.abs(self.h() - other.h());
final float saturationDiff = self.s() - other.s();
final float valueDiff = self.v() - other.v();
return hueDistance * hueDistance + saturationDiff * saturationDiff + valueDiff * valueDiff;
}
private static final class NamedTextColor {
private final String code;
private final int value;
private final HSVLike hsv;
private NamedTextColor(final String code, final int value) {
this.code = code;
this.value = value;
this.hsv = HSVLike.fromRGB(this.red(), this.green(), this.blue());
}
private int red() {
return (value >> 16) & 0xff;
}
private int green() {
return (value >> 8) & 0xff;
}
private int blue() {
return value & 0xff;
}
}
private static final class HSVLike {
private final float h;
private final float s;
private final float v;
private HSVLike(float h, float s, float v) {
this.h = h;
this.s = s;
this.v = v;
}
public float h() {
return this.h;
}
public float s() {
return this.s;
}
public float v() {
return this.v;
}
static HSVLike fromRGB(final int red, final int green, final int blue) {
final float r = red / 255.0f;
final float g = green / 255.0f;
final float b = blue / 255.0f;
final float min = Math.min(r, Math.min(g, b));
final float max = Math.max(r, Math.max(g, b)); // v
final float delta = max - min;
final float s;
if(max != 0) {
s = delta / max; // s
} else {
// r = g = b = 0
s = 0;
}
if(s == 0) { // s = 0, h is undefined
return new HSVLike(0, s, max);
}
float h;
if(r == max) {
h = (g - b) / delta; // between yellow & magenta
} else if(g == max) {
h = 2 + (b - r) / delta; // between cyan & yellow
} else {
h = 4 + (r - g) / delta; // between magenta & cyan
}
h *= 60; // degrees
if(h < 0) {
h += 360;
}
return new HSVLike(h / 360.0f, s, max);
}
}
}

View File

@ -25,6 +25,8 @@ public final class FormatUtil {
//Used to prepare xmpp output
private static final Pattern LOGCOLOR_PATTERN = Pattern.compile("\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]");
private static final Pattern URL_PATTERN = Pattern.compile("((?:(?:https?)://)?[\\w-_\\.]{2,})\\.([a-zA-Z]{2,3}(?:/\\S+)?)");
//Used to strip ANSI control codes from console
private static final Pattern ANSI_CONTROL_PATTERN = Pattern.compile("\u001B(?:\\[0?m|\\[38;2(?:;\\d{1,3}){3}m|\\[([0-9]{1,2}[;m]?){3})");
private FormatUtil() {
}
@ -45,6 +47,13 @@ public final class FormatUtil {
return stripColor(input, REPLACE_ALL_PATTERN);
}
public static String stripAnsi(final String input) {
if (input == null) {
return null;
}
return stripColor(input, ANSI_CONTROL_PATTERN);
}
//This is the general permission sensitive message format function, checks for urls.
public static String formatMessage(final IUser user, final String permBase, final String input) {
if (input == null) {

View File

@ -83,6 +83,10 @@ public final class VersionUtil {
private VersionUtil() {
}
public static boolean isPaper() {
return PaperLib.isPaper();
}
public static BukkitVersion getServerBukkitVersion() {
if (serverVersion == null) {
serverVersion = BukkitVersion.fromString(Bukkit.getServer().getBukkitVersion());

View File

@ -12,16 +12,28 @@ public class AsyncUserDataLoadEvent extends Event {
private static final HandlerList handlers = new HandlerList();
private final IUser user;
private final String joinMessage;
public AsyncUserDataLoadEvent(IUser user) {
public AsyncUserDataLoadEvent(IUser user, String joinMessage) {
super(true);
this.user = user;
this.joinMessage = joinMessage;
}
/**
* @return The user whose data has been loaded.
*/
public IUser getUser() {
return user;
}
/**
* @return The join message of this user who joined or null if none was displayed.
*/
public String getJoinMessage() {
return joinMessage;
}
@Override
public HandlerList getHandlers() {
return handlers;

View File

@ -232,6 +232,27 @@ destinationNotSet=Destination not set\!
disabled=disabled
disabledToSpawnMob=\u00a74Spawning this mob was disabled in the config file.
disableUnlimited=\u00a76Disabled unlimited placing of\u00a7c {0} \u00a76for\u00a7c {1}\u00a76.
discordCommandExecuteDescription=Executes a console command on the Minecraft server.
discordCommandExecuteArgumentCommand=The command to be executed
discordCommandExecuteReply=Executing command: "/{0}"
discordCommandListDescription=Gets a list of online players.
discordCommandListArgumentGroup=A specific group to limit your search by
discordCommandMessageDescription=Messages a player on the Minecraft server.
discordCommandMessageArgumentUsername=The player to send the message to
discordCommandMessageArgumentMessage=The message to send to the player
discordErrorCommand=You added your bot to your server incorrectly. Please follow the tutorial in the config and add your bot using https://essentialsx.net/discord.html!
discordErrorCommandDisabled=That command is disabled!
discordErrorLogin=An error occurred while logging into Discord, which has caused the plugin to disable itself: \n{0}
discordErrorLoggerInvalidChannel=Discord console logging has been disabled due to an invalid channel definition! If you intend to disable it, set the channel ID to "none"; otherwise check that your channel ID is correct.
discordErrorLoggerNoPerms=Discord console logger has been disabled due to insufficient permissions! Please make sure your bot has the "Manage Webhooks" permissions on the server. After fixing that, run "/ess reload".
discordErrorNoGuild=Invalid or missing server ID! Please follow the tutorial in the config in order to setup the plugin.
discordErrorNoGuildSize=Your bot is not in any servers! Please follow the tutorial in the config in order to setup the plugin.
discordErrorNoPerms=Your bot cannot see or talk in any channel! 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".
discordLoggingIn=Attempting to login to Discord...
discordLoggingInDone=Successfully logged in as {0}
discordNoSendPermission=Cannot send message in channel: #{0} Please ensure the bot has "Send Messages" permission in that channel\!
disposal=Disposal
disposalCommandDescription=Opens a portable disposal menu.
disposalCommandUsage=/<command>
@ -950,6 +971,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}`
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.

462
EssentialsDiscord/README.md Normal file
View File

@ -0,0 +1,462 @@
# EssentialsX Discord
EssentialsX Discord is a module that brings a simple, lightweight, easy-to-use, and bloat-free
bridge between Discord and Minecraft.
EssentialsX Discord offers *essential* features you'd want from a Discord bridge such as:
* MC Chat -> Discord Channel
* Discord Channel -> MC Chat
* Basic MC -> Discord Event Monitoring (Join/Leave/Death/Mute)
* MC Console -> Discord Relay
* Discord Slash Commands
* /execute - Execute console commands from Discord
* /msg - Message Minecraft players from Discord
* /list - List players currently online on Minecraft
* & more...
---
## Table of Contents
> * [Initial Setup](#initial-setup)
> * [Console Relay](#console-relay)
> * [Configuring Messages](#configuring-messages)
> * [Receive Discord Messages in Minecraft](#receive-discord-messages-in-minecraft)
> * [Discord Commands](#discord-commands)
> * [Misc Permissions](#misc-permissions)
> * [Developer API](#developer-api)
---
## Initial Setup
0. Before starting your server, there are a few steps you have to take. First, you must create a new
Discord bot at [discord.com/developers/applications](https://discord.com/developers/applications/).
1. Once on that page, click on "New Application" button on the top right, give your bot a name, and
then click "Create".
> ![Creating Application](https://i.imgur.com/8ffp4R1.gif)
> `New Application` -> Give Application a Name -> `Create`
2. Once you create the application, you'll be directed to its overview. From this screen, you'll
need to copy your "Client ID"/"Application ID" and save it for a later step. To copy your
Client ID, click the upper-left most blue "Copy" button. Make sure to save it for a later step.
> ![Copy Client ID](https://i.imgur.com/W3OMTu5.gif)
> `Copy` -> Paste into Notepad for later step
3. Optionally, you can set an icon for your application as it will be the icon for the bot too.
> ![Avatar](https://i.imgur.com/NuFS9kT.png)
4. The next step is actually creating a bot user for your application. From the overview screen,
this is done by going to the "Bot" tab on the left, then clicking the "Add Bot" on the right,
and finally then clicking "Yes, do it!".
> ![Create Bot](https://i.imgur.com/S14iAFS.gif)
> `Bot` -> `Add Bot` -> `Yes, do it!`
5. Once on this screen, you'll need to uncheck the "Public Bot" setting and then click "Save Changes",
so other people can't add your bot to servers that are not your own.
> ![Disable Public Bot](https://i.imgur.com/HHqWvQ1.gif)
> Uncheck `Public Bot` -> `Save Changes`
6. Finally, you'll need to copy your bot's token and save it for a later step. To copy your bot's token,
click the blue "Copy" button right of your bot's icon. Make sure to save it for a later step.
> ![Copy Token](https://i.imgur.com/OqpaSQH.gif)
> `Copy` -> Paste into Notepad for later step
7. Next up is adding your bot to your Discord server. First, go to [essentialsx.net/discord.html](https://essentialsx.net/discord.html)
and paste your Client ID you copied from step 2 into the text box on that page. Once you do that, click
the "Authorize" button next to the text box. This will redirect you to Discord's authorization website
to chose which server to add the bot to.
Note for advanced users: **Please use the `essentialsx.net` link above even if you already know how
to invite bots.** EssentialsX Discord requires more than just the `bot` scope to work.
> ![OAuth Link Gen](https://i.imgur.com/u6MFJgQ.gif)
> Paste Client ID -> `Authorize`
8. Once on the Discord authorization website, select the server from the "Select a server" dropdown
that you want to add the bot to. Then click the "Authorize" button. You may be prompted to confirm
you are not a bot, proceed with that like you would any other captcha.
> ![Authorize](https://i.imgur.com/KXkESqC.gif)
> Select Server -> `Authorize`
9. For the next few steps, you're going to need to do some stuff in Discord, so start up your
Discord desktop/web client.
10. Once in your Discord client, you'll need to enable Developer Mode. Do this by going into the
Settings, then go to the "Appearance" tab and check on the "Developer Mode" at the bottom of the
page. Once you've checked "Developer Mode" on, click the `X` at the top right to exit Settings.
> ![Developer Mode](https://i.imgur.com/CrW31Up.gif)
> `User Settings` -> `Appearance` -> Check `Developer Mode` -> Exit Settings
11. Next is copying a few IDs. First up, you'll want to copy the server (aka guild) id. Do this by
finding the server you added the bot to, right click its icon, and click "Copy ID". Once you copied
it, make sure to save it for a later step.
> ![Guild ID](https://i.imgur.com/0mg2yT3.gif)
> Right click server -> `Copy ID` -> Paste into Notepad for later step
12. The other ID you need to copy is the ID of the channel you wish to be your primary channel.
In other words, this will be the channel that, by default, receives messages for player chat/join/leave/death
messages as well as mute/kicks. To see how to further configure message types, see [Configuring Messages](#configuring-messages).
> ![Primary Channel ID](https://i.imgur.com/uMODfiQ.gif)
> Right click your 'primary' channel -> `Copy ID` -> Paste into Notepad for later step
13. You've successfully copied all the necessary IDs needed for a basic setup. Next up is generating the
default config for EssentialsX Discord, so you can start setting it up! Do this by putting the
EssentialsX Discord jar (you can download it [here](https://essentialsx.net/downloads.html) if you do not
already have one) in your plugins folder, starting your server, and then stopping it as soon as it finishes
starting up.
> ![Start/Stop Server](https://i.imgur.com/JQX6hqM.gif)
> Drag EssentialsXDiscord jar into plugins folder -> Start Server -> Stop Server
14. Now you can start to configure the plugin with all the stuff you copied from earlier. Open the config
for EssentialsX Discord located at `plugins/EssentialsDiscord/config.yml`. When you open the config, the
first thing to configure is your bot's token. Replace `INSERT-TOKEN-HERE` in the config with the token you
copied earlier from step 6.
> ![Paste Token](https://i.imgur.com/EnD31Wg.gif)
> Re-Copy Token from Step 6 -> Paste as token value
15. Next is the guild ID. Replace the zeros for the guild value in the config with the guild ID you copied
from step 13.
> ![Paste Guild](https://i.imgur.com/YxkHykd.gif)
16. Finally, you'll need to paste the primary channel ID you copied from step 14 and paste it as the
primary value in the channels section and once you've done that save the config file!
> ![Paste Primary](https://i.imgur.com/4xaHMfO.gif)
17. Congratulations, you've completed the initial setup guide! When you start up your server, you should
notice that chat and other messages start showing up in the channel you requested they be. Now that you
completed the initial, go back up to the [Table Of Contents](#table-of-contents) to see what other cool things you can do!
---
## Console Relay
The console relay is pretty self-explanatory: it relays everything on your console into a Discord channel of
your choosing. The console relay is ridiculously easy to setup and if your server is already running, you don't
need to reload it!
0. This assumes you've already done the initial setup.
1. Go to the Discord server that your bot is in and find the channel you wish to use for console output.
Right click on the channel and click "Copy ID". Save this ID for the next step.
> ![Copy ID](https://i.imgur.com/qvDfSLv.gif)
> Find console channel -> Right Click -> `Copy ID`
2. Now that you have that copied, open the EssentialsX Discord config and find the `console` section. In that
section, replace the zeros for the `channel` value with the channel ID you copied from the last step. Once
you paste it, make sure you save the config.
> ![Paste ID](https://i.imgur.com/NicdpGw.gif)
3. Finally, if your server is running, run `ess reload` from your console, otherwise start up your server. You
should notice console output being directed to that channel! That is all you need if you're okay with the default
settings. Otherwise, if you'd like to see what other options you can use to customize console output, stick around.
4. The first thing you can customize is the format of the message sent to Discord. By default, the timestamp,
level (info/warn/error/etc), and message are shown for each console message. Let's say you wanted to make the
timestamp and level bold: since this message would be using Discord's markdown, we can just add \*\* to both sides of
level and timestamp. Then once you've done that, just do `/ess reload` and you should see your changes on Discord.
> ![Bold Format](https://i.imgur.com/jD9mH14.gif)
5. Next, you can also configure the name you wish the to show above console messages. By default, it's "EssX Console
Relay" but can be switched to anything you want.
> ![Change Name](https://i.imgur.com/xtrt1Jt.gif)
6. Finally, you can also choose to enable an option to treat any message by a user in the console channel as a
console command. This will mean that anyone who can send messages in your console channel **will be able to execute
commands as the console**. It is suggested that you stick to the regular `/execute` command
(see [Discord Commands](#discord-commands)) as those can be restricted to specific roles/users and are also not
restricted to the console channel.
> ![Command Relay](https://i.imgur.com/w3cfVUw.gif)
7. That's all the options for the command relay!
---
## Configuring Messages
EssentialsX Discord aims to keep its message-type system basic enough that simple things take little changes, while
giving more fine grain control to those you want it.
To give you a general overview of the system, EssentialsX Discord allows you to define different channel IDs in the
`channels` section of the config. By default, two channels are pre-populated in the `channels` section, `primary`
and `staff`. If you only completed the initial setup, the `staff` channel definition is all zeros. This is fine in
most situations however, as the message system will always fallback to the `primary` channel if a channel ID is
invalid.
Now on the to the types of messages you can receive themselves (which is where you're going to use these channel
definitions). In the `message-types` section of the config, you can see a list of message types (join/leave/chat/etc)
on the left (as the key), and on the right there is a channel definition.
For the sake of example lets say we want to send all chat messages to their own channel. We can do this by creating
a new channel definition and setting the `chat` message type to said channel definition. Below are step-by-step
instructions for said example, you can follow along to get the gist of how to apply this to other use cases
1. Find the channel on Discord you want to only send chat messages to, and then right click the channel and click
"Copy ID".
> ![Copy ID](https://i.imgur.com/ri7NZkD.gif)
2. Next you need to create the actual channel definition, for this example we'll call it `chat`. You create a
channel definition by adding a new entry to the `channels` section with the key as its name and the ID as the one
you copied in the last step.
> ![New Def](https://i.imgur.com/dc7kIkl.gif)
3. Finally, scroll down to the `message-types` section and change the `chat` message type to your newly created
channel definition. Once you do that, save and either run `/ess reload` if your server is running or start your
server.
> ![Move](https://i.imgur.com/qPVWkWF.gif)
4. That's all you need to know about the basics of the message system!
---
## Receive Discord Messages in Minecraft
After reading the [configuring messages section](#configuring-messages), you should now have a few Discord
channels defined in the `channels` of your config. You're probably wondering how you can let your players start
to see messages from Discord in Minecraft chat. Say I defined a channel named `chat` in the `channels` section
of your config, and I wanted to let players see Discord messages from that channel in Minecraft chat; This can
be accomplished very simply by giving players the `essentials.discord.receive.chat` permission. This would relay
all Discord messages from the `chat` channel to players with that permission. Another example: say I have a staff
channel in Discord that I want only staff members in the Minecraft server to see. Provided there is a `staff`
channel defined in the `channels` section of the config, I can give staff members the
`essentials.discord.receive.staff` permission, and they will start to see messages from that channel.
---
## Discord Commands
EssentialsX Discord uses Discord's slash command system to let you type commands into Discord without it being
seen by other people in the server. With this system, you are able to execute console commands, message players,
and see the current player list.
For example, here's what the `/execute` command looks like by default:
> ![/execute](https://i.imgur.com/yPN22bV.gif)
As you can see, you can seamlessly run commands without people seeing the content of your commands or their
response. Additionally, you can also delete the responses once you're done looking at them, so they don't clutter
your chat.
However, this is all configurable! In the `commands` section of the config, lies a ton of options to configure
settings on a per-command basis. Below are explanations of what all the configuration options mean and how to use
them.
* `enabled`
* Default: `true`
* Description: `Whether or not the command should be enabled and therefore shown on Discord. Note that you
must restart your Minecraft server before this option takes effect.`
* `hide-command`
* Default: `true`
* Description: `Whether other people should not be able to see what commands you execute. Setting to false
would allow people in the same channel as you to see exactly what command you execute. In the example below,
you can see how disabling this option shows a message of the user and the command they executed.`
* Example: ![Show Command](https://i.imgur.com/Q61iP4n.gif)
* `allowed-roles`
* Description: `A list of user IDs or role names/IDs that are allowed to use the command. You can also use '*'
in order to allow everyone to use the command.`
* `admin-roles`
* `A list of user IDs or role names/IDs that have extra features in the command. For example, in the list
command, admin-roles allows people to see vanished players.`
---
## Misc Permissions
EssentialsX Discord has a few other permissions that may be important to know about:
* `essentials.discord.markdown` - Allows players to bypass the Markdown filter, so that they can
bold/underline/italic/etc their Minecraft chat messages for Discord.
* `essentials.discord.ping` - Allows players to bypass the ping filter, so that they can ping @everyone/@here
from Minecraft chat.
---
## Developer API
EssentialsX Discord has a pretty extensive API which allows any third party plugin to build
their own integrations into it. Outside the specific examples below, you can also view
javadocs for EssentialsX Discord [here](https://jd-v2.essentialsx.net/EssentialsDiscord).
### Sending Messages to Discord
EssentialsX Discord organizes the types of messages that can be sent along with their
destination on Discord under the `message-types` section of the `config.yml`. The
EssentialsX Discord API uses `message-types` to resolve the channel id you want to send your
message to.
#### Using a built-in message channel
EssentialsX Discord defines a few built in `message-types` which you may fit your use case
already (such as sending a message to the MC->Discord chat relay channel). The list of
built-in message types can be found at [`MessageType.DefaultTypes`](https://github.com/EssentialsX/Essentials/blob/2.x/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java#L47-L67).
Here is an example of what sending a message to the built-in chat channel would look like:
```java
// The built in channel you want to send your message to, in this case the chat channel.
final MessageType channel = MessageType.DefaultTypes.CHAT;
// Set to true if your message should be allowed to ping @everyone, @here, or roles.
// If you are sending user-generated content, you probably should keep this as false.
final boolean allowGroupMentions = false;
// Send the actual message
final DiscordService api = Bukkit.getServicesManager().load(DiscordService.class);
api.sendMessage(channel, "My Epic Message", allowGroupMentions);
```
#### Using your own message channel
If you want to create your own message type to allow your users to explicitly separate your
messages from our other built-in ones, you can do that also by creating a new
[`MessageType`](https://github.com/EssentialsX/Essentials/blob/2.x/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java).
The key provided in the constructor should be the key you'd like your users to use in the
`message-types` section of our config. This key should also be all lowercase and may contain
numbers or dashes. You *can* also put a Discord channel ID as the key if you'd like to
have your users define the channel id in your config rather than ours. Once you create the
`MessageType`, you will also need to register it with Essentialsx Discord by calling
[`DiscordService#registerMessageType`](https://github.com/EssentialsX/Essentials/blob/2.x/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/DiscordService.java#L24-L30).
Here is an example of what sending a message using your own message type:
```java
public class CustomTypeExample {
private final DiscordService api;
private final MessageType type;
public CustomTypeExample(final Plugin plugin) {
// Gets the the EssentialsX Discord API service so we can register our type and
// send a message with it later.
api = Bukkit.getServicesManager().load(DiscordService.class);
// Create a new message type for the user to define in our config.
// Unless you're putting a discord channel id as the type key, it's probably
// a good idea to store this object so you don't create it every time.
type = new MessageType("my-awesome-channel");
// Registers the type we just created with EssentialsX Discord.
api.registerMessageType(plugin, type);
}
@EventHandler()
public void onAwesomeEvent(AwesomeEvent event) {
// Set to true if your message should be allowed to ping @everyone, @here, or roles.
// If you are sending user-generated content, you probably should keep this as false.
final boolean allowGroupMentions = false;
// Send the actual message
api.sendMessage(type, "The player, " + event.getPlayer() + ", did something awesome!", allowPing);
}
}
```
### Prevent certain messages from being sent as chat
Depending on how your plugin sends certain types of chat messages to players, there may be
times when EssentialsX Discord accidentally broadcasts a message that was only intended for a
small group of people. In order for your plugin to stop this from happening you have to
listen to `DiscordChatMessageEvent`.
Here is an example of how a staff chat plugin would cancel a message:
```java
public class StaffChatExample {
private final StaffChatPlugin plugin = ...;
@EventHandler()
public void onDiscordChatMessage(DiscordChatMessageEvent event) {
// Checks if the player is in staff chat mode in this theoretical plugin.
if (plugin.isPlayerInStaffChat(event.getPlayer()) ||
// or we could check if their message started with a # if we use that
// to indicate typing in a specific channel.
event.getMessage().startsWith("#")) {
event.setCanceled(true);
}
}
}
```
Additionally, you can also look at [TownyChat's EssentialsX Discord hook](https://github.com/TownyAdvanced/TownyChat/commit/5bee9611aa4200e3cde1a28af48c25caa4aec649).
### Registering a Discord slash command
EssentialsX Discord also allows you to register slash commands directly with Discord itself
in order to provide your users with a way to interface with your plugins on Discord!
To start writing slash commands, the first thing you'll need to do is create a slash command
class. For the sake of this tutorial, I'm going to use an economy plugin as the
hypothetical plugin creating this slash command.
For this slash command, I'll create a simple command to a string (for player name) and
check their balance.
```java
public class BalanceSlashCommand extends InteractionCommand {
private final MyEconomyPlugin plugin = ...;
@Override
public void onCommand(InteractionEvent event) {
// The name of the argument here has to be the same you used in getArguments()
final String playerName = event.getStringArgument("player");
final Player player = Bukkit.getPlayerExact(playerName);
if (player == null) {
event.reply("A player by that name could not be found!");
return;
}
final int balance = plugin.getBalance(player);
// It is important you reply to the InteractionEvent at least once as discord
// will show your bot is 'thinking' until you do so.
event.reply("The balance of " + player.getName() + " is $" + balance);
}
@Override
public String getName() {
// This should return the name of the command as you want it to appear in discord.
// This method should never return different values.
return "balance";
}
@Override
public String getDescription() {
// This should return the description of the command as you want it
// to appear in discord.
// This method should never return different values.
return "Checks the balance of the given player";
}
@Override
public List<InteractionCommandArgument> getArguments() {
// Should return a list of arguments that will be used in your command.
// If you don't want any arguments, you can return null here.
return List.of(
new InteractionCommandArgument(
// This should be the name of the command argument.
// Keep it a single world, all lower case.
"player",
// This is the description of the argument.
"The player to check the balance of",
// This is the type of the argument you'd like to receive from
// discord.
InteractionCommandArgumentType.STRING,
// Should be set to true if the argument is required to send
// the command from discord.
true));
}
@Override
public boolean isEphemeral() {
// Whether or not the command and response should be hidden to other users on discord.
// Return true here in order to hide command/responses from other discord users.
return false;
}
@Override
public boolean isDisabled() {
// Whether or not the command should be prevented from being registered/executed.
// Return true here in order to mark the command as disabled.
return false;
}
}
```
Once you have created your slash command, it's now time to register it. It is best
practice to register them in your plugin's `onEnable` so your commands make it in the
initial batch of commands sent to Discord.
You can register your command with EssentialsX Discord by doing the following:
```java
...
import net.essentialsx.api.v2.services.discord.DiscordService;
...
public class MyEconomyPlugin {
@Override
public void onEnable() {
final DiscordService api = Bukkit.getServicesManager().load(DiscordService.class);
api.getInteractionController().registerCommand(new BalanceSlashCommand());
}
}
```
---

View File

@ -0,0 +1,58 @@
plugins {
id("essentials.shadow-module")
}
dependencies {
compileOnly project(':EssentialsX')
implementation('net.dv8tion:JDA:4.3.0_277') {
//noinspection GroovyAssignabilityCheck
exclude module: 'opus-java'
}
implementation 'com.vdurmont:emoji-java:5.1.1'
implementation 'club.minnced:discord-webhooks:0.5.6'
compileOnly 'org.apache.logging.log4j:log4j-core:2.0-beta9'
compileOnly 'me.clip:placeholderapi:2.10.9'
}
shadowJar {
dependencies {
// JDA
include(dependency('net.dv8tion:JDA'))
include(dependency('com.neovisionaries:nv-websocket-client'))
include(dependency('com.squareup.okhttp3:okhttp'))
include(dependency('com.squareup.okio:okio'))
include(dependency('org.apache.commons:commons-collections4'))
include(dependency('net.sf.trove4j:trove4j'))
include(dependency('com.fasterxml.jackson.core:jackson-databind'))
include(dependency('com.fasterxml.jackson.core:jackson-core'))
include(dependency('com.fasterxml.jackson.core:jackson-annotations'))
include(dependency('org.slf4j:slf4j-api'))
// Emoji
include(dependency('com.vdurmont:emoji-java'))
include(dependency('org.json:json'))
// discord-webhooks
include(dependency('club.minnced:discord-webhooks'))
}
minimize()
// JDA
relocate 'net.dv8tion.jda', 'net.essentialsx.dep.net.dv8tion.jda'
relocate 'com.neovisionaries.ws', 'net.essentialsx.dep.com.neovisionaries.ws'
relocate 'okhttp3', 'net.essentialsx.dep.okhttp3'
relocate 'okio', 'net.essentialsx.dep.okio'
relocate 'com.iwebpp.crypto', 'net.essentialsx.dep.com.iwebpp.crypto'
relocate 'org.apache.commons.collections4', 'net.essentialsx.dep.org.apache.commons.collections4'
relocate 'com.fasterxml.jackson.databind', 'net.essentialsx.dep.com.fasterxml.jackson.databind'
relocate 'com.fasterxml.jackson.core', 'net.essentialsx.dep.com.fasterxml.jackson.core'
relocate 'com.fasterxml.jackson.annotation', 'net.essentialsx.dep.com.fasterxml.jackson.annotation'
relocate 'gnu.trove', 'net.essentialsx.dep.gnu.trove'
// Emoji
relocate 'com.vdurmont.emoji', 'net.essentialsx.dep.com.vdurmont.emoji'
relocate 'org.json', 'net.essentialsx.dep.org.json'
// discord-webhooks
relocate 'club.minnced.discord.webhook', 'net.essentialsx.dep.club.minnced.discord.webhook'
}

View File

@ -0,0 +1,72 @@
package net.essentialsx.api.v2.events.discord;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
/**
* Fired before a chat message is about to be sent to a Discord channel.
* Should be used to block chat messages (such as staff channels) from appearing in Discord.
*/
public class DiscordChatMessageEvent extends Event implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private final Player player;
private String message;
private boolean cancelled = false;
/**
* @param player The player which caused this event.
* @param message The message of this event.
*/
public DiscordChatMessageEvent(Player player, String message) {
this.player = player;
this.message = message;
}
/**
* The player which which caused this chat message.
* @return the player who caused the event.
*/
public Player getPlayer() {
return player;
}
/**
* The message being sent in this chat event.
* @return the message of this event.
*/
public String getMessage() {
return message;
}
/**
* Sets the message of this event, and thus the chat message relayed to Discord.
* @param message the new message.
*/
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
@NotNull
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,157 @@
package net.essentialsx.api.v2.events.discord;
import net.essentialsx.api.v2.services.discord.MessageType;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Fired before a message is about to be sent to a Discord channel.
*/
public class DiscordMessageEvent extends Event implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private boolean cancelled = false;
private MessageType type;
private String message;
private boolean allowGroupMentions;
private String avatarUrl;
private String name;
private final UUID uuid;
/**
* @param type The message type/destination of this event.
* @param message The raw message content of this event.
* @param allowGroupMentions Whether or not the message should allow the pinging of roles, users, or emotes.
*/
public DiscordMessageEvent(final MessageType type, final String message, final boolean allowGroupMentions) {
this(type, message, allowGroupMentions, null, null, null);
}
/**
* @param type The message type/destination of this event.
* @param message The raw message content of this event.
* @param allowGroupMentions Whether or not the message should allow the pinging of roles, users, or emotes.
* @param avatarUrl The avatar URL to use for this message (if supported) or null to use the default bot avatar.
* @param name The name to use for this message (if supported) or null to use the default bot name.
* @param uuid The UUID of the player which caused this event or null if this wasn't a player triggered event.
*/
public DiscordMessageEvent(final MessageType type, final String message, final boolean allowGroupMentions, final String avatarUrl, final String name, final UUID uuid) {
this.type = type;
this.message = message;
this.allowGroupMentions = allowGroupMentions;
this.avatarUrl = avatarUrl;
this.name = name;
this.uuid = uuid;
}
/**
* Gets the type of this message. This also defines its destination.
* @return The message type.
*/
public MessageType getType() {
return type;
}
/**
* Sets the message type and therefore its destination.
* @param type The new message type.
*/
public void setType(MessageType type) {
this.type = type;
}
/**
* Gets the raw message content that is about to be sent to Discord.
* @return The raw message.
*/
public String getMessage() {
return message;
}
/**
* Sets the raw message content to be sent to Discord.
* @param message The new message content.
*/
public void setMessage(String message) {
this.message = message;
}
/**
* Checks if this message allows pinging of roles/@here/@everyone.
* @return true if this message is allowed to ping of roles/@here/@everyone.
*/
public boolean isAllowGroupMentions() {
return allowGroupMentions;
}
/**
* Sets if this message is allowed to ping roles/@here/@everyone.
* @param allowGroupMentions If pinging of roles/@here/@everyone should be allowed.
*/
public void setAllowGroupMentions(boolean allowGroupMentions) {
this.allowGroupMentions = allowGroupMentions;
}
/**
* Gets the avatar URL to use for this message, or null if none is specified.
* @return The avatar URL or null.
*/
public String getAvatarUrl() {
return avatarUrl;
}
/**
* Sets the avatar URL for this message, or null to use the bot's avatar.
* @param avatarUrl The avatar URL or null.
*/
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
/**
* Gets the name to use for this message, or null if none is specified.
* @return The name or null.
*/
public String getName() {
return name;
}
/**
* Sets the name for this message, or null to use the bot's name.
* @param name The name or null.
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the UUID of the player which caused this event, or null if it wasn't a player triggered event.
* @return The UUID or null.
*/
public UUID getUUID() {
return uuid;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,44 @@
package net.essentialsx.api.v2.services.discord;
import org.bukkit.plugin.Plugin;
/**
* A class which provides numerous methods to interact with EssentialsX Discord.
*/
public interface DiscordService {
/**
* Sends a message to a message type channel.
* @param type The message type/destination of this message.
* @param message The exact message to be sent.
* @param allowGroupMentions Whether or not the message should allow the pinging of roles, users, or emotes.
*/
void sendMessage(final MessageType type, final String message, final boolean allowGroupMentions);
/**
* Checks if a {@link MessageType} by the given key is already registered.
* @param key The {@link MessageType} key to check.
* @return true if a {@link MessageType} with the provided key is registered, otherwise false.
*/
boolean isRegistered(final String key);
/**
* Registers a message type to be used in the future.
* <p>
* In the future, this method will automatically populate the message type in the EssentialsX Discord config.
* @param type The {@link MessageType} to be registered.
*/
void registerMessageType(final Plugin plugin, final MessageType type);
/**
* Gets the {@link InteractionController} instance.
* @return the {@link InteractionController} instance.
*/
InteractionController getInteractionController();
/**
* Gets unstable API that is subject to change at any time.
* @return {@link Unsafe the unsafe} instance.
* @see Unsafe
*/
Unsafe getUnsafe();
}

View File

@ -0,0 +1,18 @@
package net.essentialsx.api.v2.services.discord;
/**
* Represents a interaction channel argument as a guild channel.
*/
public interface InteractionChannel {
/**
* Gets the name of this channel.
* @return this channel's name.
*/
String getName();
/**
* Gets the ID of this channel.
* @return this channel's ID.
*/
String getId();
}

View File

@ -0,0 +1,46 @@
package net.essentialsx.api.v2.services.discord;
import java.util.List;
/**
* Represents a command to be registered with the Discord client.
*/
public interface InteractionCommand {
/**
* Whether or not the command has been disabled and should not be registered at the request of the user.
* @return true if the command has been disabled.
*/
boolean isDisabled();
/**
* Whether or not the command is ephemeral and if its usage/replies should be private for the user on in Discord client.
* @return true if the command is ephemeral.
*/
boolean isEphemeral();
/**
* Gets the name of this command as it appears in Discord.
* @return the name of the command.
*/
String getName();
/**
* Gets the brief description of the command as it appears in Discord.
* @return the description of the command.
*/
String getDescription();
/**
* Gets the list of arguments registered to this command.
* <p>
* Note: Arguments can only be registered before the command itself is registered, others will be ignored.
* @return the list of arguments.
*/
List<InteractionCommandArgument> getArguments();
/**
* Called when an interaction command is received from Discord.
* @param event The {@link InteractionEvent} which caused this command to be executed.
*/
void onCommand(InteractionEvent event);
}

View File

@ -0,0 +1,57 @@
package net.essentialsx.api.v2.services.discord;
/**
* Represents an argument for a command to be shown to Discord users.
*/
public class InteractionCommandArgument {
private final String name;
private final String description;
private final InteractionCommandArgumentType type;
private final boolean required;
/**
* Builds a command argument.
* @param name The name of the argument to be shown to the Discord client.
* @param description A brief description of the argument to be shown to the Discord client.
* @param type The type of argument.
* @param required Whether or not the argument is required in order to send the command in the Discord client.
*/
public InteractionCommandArgument(String name, String description, InteractionCommandArgumentType type, boolean required) {
this.name = name;
this.description = description;
this.type = type;
this.required = required;
}
/**
* Gets the name of this argument.
* @return the name of the argument.
*/
public String getName() {
return name;
}
/**
* Gets the description of this argument.
* @return the description of the argument.
*/
public String getDescription() {
return description;
}
/**
* Gets the type of this argument.
* @return the argument type.
*/
public InteractionCommandArgumentType getType() {
return type;
}
/**
* Whether or not this argument is required or not.
* @return true if the argument is required.
*/
public boolean isRequired() {
return required;
}
}

View File

@ -0,0 +1,25 @@
package net.essentialsx.api.v2.services.discord;
/**
* Represents an argument type to be shown on the Discord client.
*/
public enum InteractionCommandArgumentType {
STRING(3),
INTEGER(4),
BOOLEAN(5),
USER(6),
CHANNEL(7);
private final int id;
InteractionCommandArgumentType(int id) {
this.id = id;
}
/**
* Gets the internal Discord ID for this argument type.
* @return the internal Discord ID.
*/
public int getId() {
return id;
}
}

View File

@ -0,0 +1,20 @@
package net.essentialsx.api.v2.services.discord;
/**
* A class which provides numerous methods to interact with Discord slash commands.
*/
public interface InteractionController {
/**
* Gets the command with the given name or null if no command by that name exists.
* @param name The name of the command.
* @return The {@link InteractionCommand command} by the given name, or null.
*/
InteractionCommand getCommand(String name);
/**
* Registers the given slash command with Discord.
* @param command The slash command to be registered.
* @throws InteractionException if a command with that name was already registered or if the given command was already registered.
*/
void registerCommand(InteractionCommand command) throws InteractionException;
}

View File

@ -0,0 +1,59 @@
package net.essentialsx.api.v2.services.discord;
/**
* Represents a triggered interaction event.
*/
public interface InteractionEvent {
/**
* Appends the given string to the initial response message and creates one if it doesn't exist.
* @param message The message to append.
*/
void reply(String message);
/**
* Gets the member which caused this event.
* @return the member which caused the event.
*/
InteractionMember getMember();
/**
* Get the value of the argument matching the given key represented as a String, or null if no argument by that name is present.
* @param key The key of the argument to lookup.
* @return the string value or null.
*/
String getStringArgument(String key);
/**
* Get the Long representation of the argument by the given key or null if none by that key is present.
* @param key The key of the argument to lookup.
* @return the long value or null
*/
Long getIntegerArgument(String key);
/**
* Helper method to get the Boolean representation of the argument by the given key or null if none by that key is present.
* @param key The key of the argument to lookup.
* @return the boolean value or null
*/
Boolean getBooleanArgument(String key);
/**
* Helper method to get the user representation of the argument by the given key or null if none by that key is present.
* @param key The key of the argument to lookup.
* @return the user value or null
*/
InteractionMember getUserArgument(String key);
/**
* Helper method to get the channel representation of the argument by the given key or null if none by that key is present.
* @param key The key of the argument to lookup.
* @return the channel value or null
*/
InteractionChannel getChannelArgument(String key);
/**
* Gets the channel ID where this interaction occurred.
* @return the channel ID.
*/
String getChannelId();
}

View File

@ -0,0 +1,10 @@
package net.essentialsx.api.v2.services.discord;
/**
* Thrown when an error occurs during an operation dealing with Discord interactions.
*/
public class InteractionException extends Exception {
public InteractionException(String message) {
super(message);
}
}

View File

@ -0,0 +1,67 @@
package net.essentialsx.api.v2.services.discord;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Represents the interaction command executor as a guild member.
*/
public interface InteractionMember {
/**
* Gets the username of this member.
* @return this member's username.
*/
String getName();
/**
* Gets the four numbers after the {@code #} in the member's username.
* @return this member's discriminator.
*/
String getDiscriminator();
/**
* Gets this member's name and discriminator split by a {@code #}.
* @return this member's tag.
*/
default String getTag() {
return getName() + "#" + getDiscriminator();
}
/**
* Gets the nickname of this member or their username if they don't have one.
* @return this member's nickname or username if none is present.
*/
String getEffectiveName();
/**
* Gets the nickname of this member or null if they do not have one.
* @return this member's nickname or null.
*/
String getNickname();
/**
* Gets the ID of this member.
* @return this member's ID.
*/
String getId();
/**
* Checks if this member has the administrator permission on Discord.
* @return true if this user has administrative permissions.
*/
boolean isAdmin();
/**
* Returns true if the user has one of the specified roles.
* @param roleDefinitions A list of role definitions from the config.
* @return true if the member has one of the given roles.
*/
boolean hasRoles(List<String> roleDefinitions);
/**
* Sends a private message to this member with the given content.
* @param content The message to send.
* @return A future which will complete a boolean stating the success of the message.
*/
CompletableFuture<Boolean> sendPrivateMessage(String content);
}

View File

@ -0,0 +1,73 @@
package net.essentialsx.api.v2.services.discord;
/**
* Indicates the type of message being sent and its literal channel name used in the config.
*/
public final class MessageType {
private final String key;
private final boolean player;
/**
* Creates a {@link MessageType} which will send channels to the specified channel key.
* <p>
* The message type key may only contain: lowercase letters, numbers, and dashes.
* @param key The channel key defined in the {@code message-types} section of the config.
*/
public MessageType(final String key) {
this(key, false);
}
/**
* Internal constructor used by EssentialsX Discord
*/
private MessageType(String key, boolean player) {
if (!key.matches("^[a-z0-9-]*$")) {
throw new IllegalArgumentException("Key must match \"^[a-z0-9-]*$\"");
}
this.key = key;
this.player = player;
}
/**
* Gets the key used in {@code message-types} section of the config.
* @return The config key.
*/
public String getKey() {
return key;
}
/**
* Checks if this message type should be beholden to player-specific config settings.
* @return true if message type should be beholden to player-specific config settings.
*/
public boolean isPlayer() {
return player;
}
@Override
public String toString() {
return key;
}
/**
* Default {@link MessageType MessageTypes} provided and documented by EssentialsX Discord.
*/
public static final class DefaultTypes {
public final static MessageType JOIN = new MessageType("join", true);
public final static MessageType LEAVE = new MessageType("leave", true);
public final static MessageType CHAT = new MessageType("chat", true);
public final static MessageType DEATH = new MessageType("death", true);
public final static MessageType AFK = new MessageType("afk", true);
public final static MessageType KICK = new MessageType("kick", false);
public final static MessageType MUTE = new MessageType("mute", false);
private final static MessageType[] VALUES = new MessageType[]{JOIN, LEAVE, CHAT, DEATH, AFK, KICK, MUTE};
/**
* Gets an array of all the default {@link MessageType MessageTypes}.
* @return An array of all the default {@link MessageType MessageTypes}.
*/
public static MessageType[] values() {
return VALUES;
}
}
}

View File

@ -0,0 +1,15 @@
package net.essentialsx.api.v2.services.discord;
import net.dv8tion.jda.api.JDA;
/**
* Unstable methods that may vary with our implementation.
* These methods have no guarantee of remaining consistent and may change at any time.
*/
public interface Unsafe {
/**
* Gets the JDA instance associated with this EssentialsX Discord instance, if available.
* @return the {@link JDA} instance or null if not ready.
*/
JDA getJDAInstance();
}

View File

@ -0,0 +1,354 @@
package net.essentialsx.discord;
import com.earth2me.essentials.IConf;
import com.earth2me.essentials.config.ConfigurateUtil;
import com.earth2me.essentials.config.EssentialsConfiguration;
import com.earth2me.essentials.utils.FormatUtil;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity;
import org.apache.logging.log4j.Level;
import org.bukkit.entity.Player;
import java.io.File;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class DiscordSettings implements IConf {
private final EssentialsConfiguration config;
private final EssentialsDiscord plugin;
private final Map<String, Long> nameToChannelIdMap = new HashMap<>();
private final Map<Long, List<String>> channelIdToNamesMap = new HashMap<>();
private OnlineStatus status;
private Activity statusActivity;
private Pattern discordFilter;
private MessageFormat consoleFormat;
private Level consoleLogLevel;
private MessageFormat discordToMcFormat;
private MessageFormat tempMuteFormat;
private MessageFormat tempMuteReasonFormat;
private MessageFormat permMuteFormat;
private MessageFormat permMuteReasonFormat;
private MessageFormat unmuteFormat;
private MessageFormat kickFormat;
public DiscordSettings(EssentialsDiscord plugin) {
this.plugin = plugin;
this.config = new EssentialsConfiguration(new File(plugin.getDataFolder(), "config.yml"), "/config.yml", EssentialsDiscord.class);
reloadConfig();
}
public String getBotToken() {
return config.getString("token", "");
}
public long getGuildId() {
return config.getLong("guild", 0);
}
public long getPrimaryChannelId() {
return config.getLong("channels.primary", 0);
}
public long getChannelId(String key) {
try {
return Long.parseLong(key);
} catch (NumberFormatException ignored) {
return nameToChannelIdMap.getOrDefault(key, 0L);
}
}
public List<String> getKeysFromChannelId(long channelId) {
return channelIdToNamesMap.get(channelId);
}
public String getMessageChannel(String key) {
return config.getString("message-types." + key, "none");
}
public boolean isShowDiscordAttachments() {
return config.getBoolean("show-discord-attachments", true);
}
public List<String> getPermittedFormattingRoles() {
return config.getList("permit-formatting-roles", String.class);
}
public OnlineStatus getStatus() {
return status;
}
public Activity getStatusActivity() {
return statusActivity;
}
public boolean isAlwaysReceivePrimary() {
return config.getBoolean("always-receive-primary", false);
}
public int getChatDiscordMaxLength() {
return config.getInt("chat.discord-max-length", 2000);
}
public boolean isChatFilterNewlines() {
return config.getBoolean("chat.filter-newlines", true);
}
public Pattern getDiscordFilter() {
return discordFilter;
}
public boolean isShowWebhookMessages() {
return config.getBoolean("chat.show-webhook-messages", false);
}
public boolean isShowBotMessages() {
return config.getBoolean("chat.show-bot-messages", false);
}
public boolean isShowAllChat() {
return config.getBoolean("chat.show-all-chat", false);
}
public String getConsoleChannelDef() {
return config.getString("console.channel", "none");
}
public MessageFormat getConsoleFormat() {
return consoleFormat;
}
public String getConsoleWebhookName() {
return config.getString("console.webhook-name", "EssentialsX Console Relay");
}
public boolean isConsoleCommandRelay() {
return config.getBoolean("console.command-relay", false);
}
public Level getConsoleLogLevel() {
return consoleLogLevel;
}
public boolean isShowAvatar() {
return config.getBoolean("show-avatar", false);
}
public boolean isShowName() {
return config.getBoolean("show-name", false);
}
// General command settings
public boolean isCommandEnabled(String command) {
return config.getBoolean("commands." + command + ".enabled", true);
}
public boolean isCommandEphemeral(String command) {
return config.getBoolean("commands." + command + ".hide-command", true);
}
public List<String> getCommandSnowflakes(String command) {
return config.getList("commands." + command + ".allowed-roles", String.class);
}
public List<String> getCommandAdminSnowflakes(String command) {
return config.getList("commands." + command + ".admin-roles", String.class);
}
// Message formats
public MessageFormat getDiscordToMcFormat() {
return discordToMcFormat;
}
public MessageFormat getMcToDiscordFormat(Player player) {
final String format = getFormatString("mc-to-discord");
final String filled;
if (plugin.isPAPI() && format != null) {
filled = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, format);
} else {
filled = format;
}
return generateMessageFormat(filled, "{displayname}: {message}", false,
"username", "displayname", "message", "world", "prefix", "suffix");
}
public MessageFormat getTempMuteFormat() {
return tempMuteFormat;
}
public MessageFormat getTempMuteReasonFormat() {
return tempMuteReasonFormat;
}
public MessageFormat getPermMuteFormat() {
return permMuteFormat;
}
public MessageFormat getPermMuteReasonFormat() {
return permMuteReasonFormat;
}
public MessageFormat getUnmuteFormat() {
return unmuteFormat;
}
public MessageFormat getJoinFormat(Player player) {
final String format = getFormatString("join");
final String filled;
if (plugin.isPAPI() && format != null) {
filled = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, format);
} else {
filled = format;
}
return generateMessageFormat(filled, ":arrow_right: {displayname} has joined!", false,
"username", "displayname", "joinmessage");
}
public MessageFormat getQuitFormat(Player player) {
final String format = getFormatString("quit");
final String filled;
if (plugin.isPAPI() && format != null) {
filled = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, format);
} else {
filled = format;
}
return generateMessageFormat(filled, ":arrow_left: {displayname} has left!", false,
"username", "displayname", "quitmessage");
}
public MessageFormat getDeathFormat(Player player) {
final String format = getFormatString("death");
final String filled;
if (plugin.isPAPI() && format != null) {
filled = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, format);
} else {
filled = format;
}
return generateMessageFormat(filled, ":skull: {deathmessage}", false,
"username", "displayname", "deathmessage");
}
public MessageFormat getAfkFormat(Player player) {
final String format = getFormatString("afk");
final String filled;
if (plugin.isPAPI() && format != null) {
filled = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, format);
} else {
filled = format;
}
return generateMessageFormat(filled, ":person_walking: {displayname} is now AFK!", false,
"username", "displayname");
}
public MessageFormat getUnAfkFormat(Player player) {
final String format = getFormatString("un-afk");
final String filled;
if (plugin.isPAPI() && format != null) {
filled = me.clip.placeholderapi.PlaceholderAPI.setPlaceholders(player, format);
} else {
filled = format;
}
return generateMessageFormat(filled, ":keyboard: {displayname} is no longer AFK!", false,
"username", "displayname");
}
public MessageFormat getKickFormat() {
return kickFormat;
}
private String getFormatString(String node) {
final String pathPrefix = node.startsWith(".") ? "" : "messages.";
return config.getString(pathPrefix + (pathPrefix.isEmpty() ? node.substring(1) : node), null);
}
private MessageFormat generateMessageFormat(String content, String defaultStr, boolean format, String... arguments) {
content = content == null ? defaultStr : content;
content = format ? FormatUtil.replaceFormat(content) : FormatUtil.stripFormat(content);
for (int i = 0; i < arguments.length; i++) {
content = content.replace("{" + arguments[i] + "}", "{" + i + "}");
}
return new MessageFormat(content);
}
@Override
public void reloadConfig() {
config.load();
// Build channel maps
nameToChannelIdMap.clear();
channelIdToNamesMap.clear();
final Map<String, Object> section = ConfigurateUtil.getRawMap(config, "channels");
for (Map.Entry<String, Object> entry : section.entrySet()) {
if (entry.getValue() instanceof Long) {
final long value = (long) entry.getValue();
nameToChannelIdMap.put(entry.getKey(), value);
channelIdToNamesMap.computeIfAbsent(value, o -> new ArrayList<>()).add(entry.getKey());
}
}
// Presence stuff
status = OnlineStatus.fromKey(config.getString("presence.status", "online"));
if (status == OnlineStatus.UNKNOWN) {
// Default invalid status to online
status = OnlineStatus.ONLINE;
}
final String activity = config.getString("presence.activity", "default").trim().toUpperCase().replace("CUSTOM_STATUS", "DEFAULT");
statusActivity = null;
Activity.ActivityType activityType = null;
try {
if (!activity.equals("NONE")) {
activityType = Activity.ActivityType.valueOf(activity);
}
} catch (IllegalArgumentException e) {
activityType = Activity.ActivityType.DEFAULT;
}
if (activityType != null) {
statusActivity = Activity.of(activityType, config.getString("presence.message", "Minecraft"));
}
final String filter = config.getString("chat.discord-filter", null);
if (filter != null) {
try {
discordFilter = Pattern.compile(filter);
} catch (PatternSyntaxException e) {
plugin.getLogger().log(java.util.logging.Level.WARNING, "Invalid pattern for \"chat.discord-filter\": " + e.getMessage());
discordFilter = null;
}
} else {
discordFilter = null;
}
consoleLogLevel = Level.toLevel(config.getString("console.log-level", null), Level.INFO);
consoleFormat = generateMessageFormat(getFormatString(".console.format"), "[{timestamp} {level}] {message}", false,
"timestamp", "level", "message");
discordToMcFormat = generateMessageFormat(getFormatString("discord-to-mc"), "&6[#{channel}] &3{fullname}&7: &f{message}", true,
"channel", "username", "discriminator", "fullname", "nickname", "color", "message");
unmuteFormat = generateMessageFormat(getFormatString("unmute"), "{displayname} unmuted.", false, "username", "displayname");
tempMuteFormat = generateMessageFormat(getFormatString("temporary-mute"), "{controllerdisplayname} has muted player {displayname} for {time}.", false,
"username", "displayname", "controllername", "controllerdisplayname", "time");
permMuteFormat = generateMessageFormat(getFormatString("permanent-mute"), "{controllerdisplayname} permanently muted {displayname}.", false,
"username", "displayname", "controllername", "controllerdisplayname");
tempMuteReasonFormat = generateMessageFormat(getFormatString("temporary-mute-reason"), "{controllerdisplayname} has muted player {displayname} for {time}. Reason: {reason}.", false,
"username", "displayname", "controllername", "controllerdisplayname", "time", "reason");
permMuteReasonFormat = generateMessageFormat(getFormatString("permanent-mute-reason"), "{controllerdisplayname} has muted player {displayname}. Reason: {reason}.", false,
"username", "displayname", "controllername", "controllerdisplayname", "reason");
kickFormat = generateMessageFormat(getFormatString("kick"), "{displayname} was kicked with reason: {reason}", false,
"username", "displayname", "reason");
plugin.onReload();
}
}

View File

@ -0,0 +1,86 @@
package net.essentialsx.discord;
import com.earth2me.essentials.IEssentials;
import com.earth2me.essentials.IEssentialsModule;
import com.earth2me.essentials.metrics.MetricsWrapper;
import net.essentialsx.discord.interactions.InteractionControllerImpl;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.earth2me.essentials.I18n.tl;
public class EssentialsDiscord extends JavaPlugin implements IEssentialsModule {
private final static Logger logger = Logger.getLogger("EssentialsDiscord");
private transient IEssentials ess;
private transient MetricsWrapper metrics = null;
private JDADiscordService jda;
private DiscordSettings settings;
private boolean isPAPI = false;
@Override
public void onEnable() {
ess = (IEssentials) getServer().getPluginManager().getPlugin("Essentials");
if (ess == null || !ess.isEnabled()) {
setEnabled(false);
return;
}
if (!getDescription().getVersion().equals(ess.getDescription().getVersion())) {
getLogger().log(Level.WARNING, tl("versionMismatchAll"));
}
isPAPI = getServer().getPluginManager().getPlugin("PlaceholderAPI") != null;
settings = new DiscordSettings(this);
ess.addReloadListener(settings);
if (jda == null) {
jda = new JDADiscordService(this);
try {
jda.startup();
ess.scheduleSyncDelayedTask(() -> ((InteractionControllerImpl) jda.getInteractionController()).processBatchRegistration());
} catch (Exception e) {
logger.log(Level.SEVERE, tl("discordErrorLogin", e.getMessage()));
if (ess.getSettings().isDebug()) {
e.printStackTrace();
}
setEnabled(false);
return;
}
}
if (metrics == null) {
metrics = new MetricsWrapper(this, 9824, false);
}
}
public void onReload() {
if (jda != null) {
jda.updatePresence();
jda.updatePrimaryChannel();
jda.updateConsoleRelay();
jda.updateTypesRelay();
}
}
public IEssentials getEss() {
return ess;
}
public DiscordSettings getSettings() {
return settings;
}
public boolean isPAPI() {
return isPAPI;
}
@Override
public void onDisable() {
if (jda != null) {
jda.shutdown();
}
}
}

View File

@ -0,0 +1,412 @@
package net.essentialsx.discord;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.WebhookClientBuilder;
import club.minnced.discord.webhook.send.WebhookMessage;
import club.minnced.discord.webhook.send.WebhookMessageBuilder;
import com.earth2me.essentials.utils.FormatUtil;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.Webhook;
import net.dv8tion.jda.api.events.ShutdownEvent;
import net.dv8tion.jda.api.hooks.EventListener;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.essentialsx.api.v2.events.discord.DiscordMessageEvent;
import net.essentialsx.api.v2.services.discord.DiscordService;
import net.essentialsx.api.v2.services.discord.InteractionController;
import net.essentialsx.api.v2.services.discord.InteractionException;
import net.essentialsx.api.v2.services.discord.MessageType;
import net.essentialsx.api.v2.services.discord.Unsafe;
import net.essentialsx.discord.interactions.InteractionControllerImpl;
import net.essentialsx.discord.interactions.commands.ExecuteCommand;
import net.essentialsx.discord.interactions.commands.ListCommand;
import net.essentialsx.discord.interactions.commands.MessageCommand;
import net.essentialsx.discord.listeners.BukkitListener;
import net.essentialsx.discord.listeners.DiscordCommandDispatcher;
import net.essentialsx.discord.listeners.DiscordListener;
import net.essentialsx.discord.util.ConsoleInjector;
import net.essentialsx.discord.util.DiscordUtil;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.ServicePriority;
import org.jetbrains.annotations.NotNull;
import javax.security.auth.login.LoginException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import static com.earth2me.essentials.I18n.tl;
public class JDADiscordService implements DiscordService {
private final static Logger logger = Logger.getLogger("EssentialsDiscord");
private final EssentialsDiscord plugin;
private final Unsafe unsafe = this::getJda;
private JDA jda;
private Guild guild;
private TextChannel primaryChannel;
private WebhookClient consoleWebhook;
private String lastConsoleId;
private final Map<String, MessageType> registeredTypes = new HashMap<>();
private final Map<MessageType, String> typeToChannelId = new HashMap<>();
private final Map<String, WebhookClient> channelIdToWebhook = new HashMap<>();
private ConsoleInjector injector;
private DiscordCommandDispatcher commandDispatcher;
private InteractionControllerImpl interactionController;
public JDADiscordService(EssentialsDiscord plugin) {
this.plugin = plugin;
for (final MessageType type : MessageType.DefaultTypes.values()) {
registerMessageType(plugin, type);
}
}
public TextChannel getChannel(String key, boolean primaryFallback) {
long resolvedId;
try {
resolvedId = Long.parseLong(key);
} catch (NumberFormatException ignored) {
resolvedId = getSettings().getChannelId(getSettings().getMessageChannel(key));
}
if (isDebug()) {
logger.log(Level.INFO, "Channel definition " + key + " resolved as " + resolvedId);
}
TextChannel channel = guild.getTextChannelById(resolvedId);
if (channel == null && primaryFallback) {
if (isDebug()) {
logger.log(Level.WARNING, "Resolved channel id " + resolvedId + " was not found! Falling back to primary channel.");
}
channel = primaryChannel;
}
return channel;
}
public WebhookMessage getWebhookMessage(String message) {
return getWebhookMessage(message, jda.getSelfUser().getAvatarUrl(), getSettings().getConsoleWebhookName(), false);
}
public WebhookMessage getWebhookMessage(String message, String avatarUrl, String name, boolean groupMentions) {
return new WebhookMessageBuilder()
.setAvatarUrl(avatarUrl)
.setAllowedMentions(groupMentions ? DiscordUtil.ALL_MENTIONS_WEBHOOK : DiscordUtil.NO_GROUP_MENTIONS_WEBHOOK)
.setUsername(name)
.setContent(message)
.build();
}
public void sendMessage(DiscordMessageEvent event, String message, boolean groupMentions) {
final TextChannel channel = getChannel(event.getType().getKey(), true);
final String strippedContent = FormatUtil.stripFormat(message);
final String webhookChannelId = typeToChannelId.get(event.getType());
if (webhookChannelId != null) {
final WebhookClient client = channelIdToWebhook.get(webhookChannelId);
if (client != null) {
final String avatarUrl = event.getAvatarUrl() != null ? event.getAvatarUrl() : jda.getSelfUser().getAvatarUrl();
final String name = event.getName() != null ? event.getName() : guild.getSelfMember().getEffectiveName();
client.send(getWebhookMessage(strippedContent, avatarUrl, name, groupMentions));
return;
}
}
if (!channel.canTalk()) {
logger.warning(tl("discordNoSendPermission", channel.getName()));
return;
}
channel.sendMessage(strippedContent)
.allowedMentions(groupMentions ? null : DiscordUtil.NO_GROUP_MENTIONS)
.queue();
}
public void startup() throws LoginException, InterruptedException {
shutdown();
logger.log(Level.INFO, tl("discordLoggingIn"));
if (plugin.getSettings().getBotToken().replace("INSERT-TOKEN-HERE", "").trim().isEmpty()) {
throw new IllegalArgumentException(tl("discordErrorNoToken"));
}
jda = JDABuilder.createDefault(plugin.getSettings().getBotToken())
.addEventListeners(new DiscordListener(this))
.setContextEnabled(false)
.setRawEventsEnabled(true)
.build()
.awaitReady();
updatePresence();
logger.log(Level.INFO, tl("discordLoggingInDone", jda.getSelfUser().getAsTag()));
if (jda.getGuilds().isEmpty()) {
throw new IllegalArgumentException(tl("discordErrorNoGuildSize"));
}
guild = jda.getGuildById(plugin.getSettings().getGuildId());
if (guild == null) {
throw new IllegalArgumentException(tl("discordErrorNoGuild"));
}
interactionController = new InteractionControllerImpl(this);
try {
interactionController.registerCommand(new ExecuteCommand(this));
interactionController.registerCommand(new MessageCommand(this));
interactionController.registerCommand(new ListCommand(this));
} catch (InteractionException ignored) {
// won't happen
}
updatePrimaryChannel();
updateConsoleRelay();
updateTypesRelay();
Bukkit.getPluginManager().registerEvents(new BukkitListener(this), plugin);
Bukkit.getServicesManager().register(DiscordService.class, this, plugin, ServicePriority.Normal);
}
@Override
public boolean isRegistered(String key) {
return registeredTypes.containsKey(key);
}
@Override
public void registerMessageType(Plugin plugin, MessageType type) {
if (!type.getKey().matches("^[a-z0-9-]*$")) {
throw new IllegalArgumentException("MessageType key must match \"^[a-z0-9-]*$\"");
}
if (registeredTypes.containsKey(type.getKey())) {
throw new IllegalArgumentException("A MessageType with that key is already registered!");
}
registeredTypes.put(type.getKey(), type);
}
@Override
public void sendMessage(MessageType type, String message, boolean allowGroupMentions) {
if (!registeredTypes.containsKey(type.getKey())) {
logger.warning("Sending message to channel \"" + type.getKey() + "\" which is an unregistered type! If you are a plugin author, you should be registering your MessageType before using them.");
}
final DiscordMessageEvent event = new DiscordMessageEvent(type, FormatUtil.stripFormat(message), allowGroupMentions);
if (Bukkit.getServer().isPrimaryThread()) {
Bukkit.getPluginManager().callEvent(event);
} else {
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.getPluginManager().callEvent(event));
}
}
@Override
public InteractionController getInteractionController() {
return interactionController;
}
public void updatePrimaryChannel() {
TextChannel channel = guild.getTextChannelById(plugin.getSettings().getPrimaryChannelId());
if (channel == null) {
channel = guild.getDefaultChannel();
if (channel == null || !channel.canTalk()) {
throw new RuntimeException(tl("discordErrorNoPerms"));
}
}
primaryChannel = channel;
}
public void updatePresence() {
jda.getPresence().setPresence(plugin.getSettings().getStatus(), plugin.getSettings().getStatusActivity());
}
public void updateTypesRelay() {
if (!getSettings().isShowAvatar() && !getSettings().isShowName()) {
for (WebhookClient webhook : channelIdToWebhook.values()) {
webhook.close();
}
typeToChannelId.clear();
channelIdToWebhook.clear();
return;
}
for (MessageType type : MessageType.DefaultTypes.values()) {
if (!type.isPlayer()) {
continue;
}
final TextChannel channel = getChannel(type.getKey(), true);
if (channel.getId().equals(typeToChannelId.get(type))) {
continue;
}
final String webhookName = "EssX Advanced Relay";
Webhook webhook = DiscordUtil.getAndCleanWebhooks(channel, webhookName).join();
webhook = webhook == null ? DiscordUtil.createWebhook(channel, webhookName).join() : webhook;
if (webhook == null) {
final WebhookClient current = channelIdToWebhook.get(channel.getId());
if (current != null) {
current.close();
}
channelIdToWebhook.remove(channel.getId());
continue;
}
typeToChannelId.put(type, channel.getId());
channelIdToWebhook.put(channel.getId(), DiscordUtil.getWebhookClient(webhook.getIdLong(), webhook.getToken(), jda.getHttpClient()));
}
}
public void updateConsoleRelay() {
final String consoleDef = getSettings().getConsoleChannelDef();
final Matcher matcher = WebhookClientBuilder.WEBHOOK_PATTERN.matcher(consoleDef);
final long webhookId;
final String webhookToken;
if (matcher.matches()) {
webhookId = Long.parseUnsignedLong(matcher.group(1));
webhookToken = matcher.group(2);
if (commandDispatcher != null) {
jda.removeEventListener(commandDispatcher);
commandDispatcher = null;
}
} else {
final TextChannel channel = getChannel(consoleDef, false);
if (channel != null) {
if (getSettings().isConsoleCommandRelay()) {
if (commandDispatcher == null) {
commandDispatcher = new DiscordCommandDispatcher(this);
jda.addEventListener(commandDispatcher);
}
commandDispatcher.setChannelId(channel.getId());
} else if (commandDispatcher != null) {
jda.removeEventListener(commandDispatcher);
commandDispatcher = null;
}
if (channel.getId().equals(lastConsoleId)) {
return;
}
final String webhookName = "EssX Console Relay";
Webhook webhook = DiscordUtil.getAndCleanWebhooks(channel, webhookName).join();
webhook = webhook == null ? DiscordUtil.createWebhook(channel, webhookName).join() : webhook;
if (webhook == null) {
logger.info(tl("discordErrorLoggerNoPerms"));
return;
}
webhookId = webhook.getIdLong();
webhookToken = webhook.getToken();
lastConsoleId = channel.getId();
} else if (!getSettings().getConsoleChannelDef().equals("none") && !getSettings().getConsoleChannelDef().startsWith("0")) {
logger.info(tl("discordErrorLoggerInvalidChannel"));
shutdownConsoleRelay(true);
return;
} else {
// It's either not configured at all or knowingly disabled.
shutdownConsoleRelay(true);
return;
}
}
shutdownConsoleRelay(false);
consoleWebhook = DiscordUtil.getWebhookClient(webhookId, webhookToken, jda.getHttpClient());
if (injector == null) {
injector = new ConsoleInjector(this);
injector.start();
}
}
private void shutdownConsoleRelay(final boolean closeInjector) {
if (consoleWebhook != null && !consoleWebhook.isShutdown()) {
consoleWebhook.close();
}
consoleWebhook = null;
if (closeInjector) {
if (injector != null) {
injector.remove();
injector = null;
}
if (commandDispatcher != null) {
jda.removeEventListener(commandDispatcher);
commandDispatcher = null;
}
}
}
public void shutdown() {
if (interactionController != null) {
interactionController.shutdown();
}
if (jda != null) {
shutdownConsoleRelay(true);
// Unregister leftover jda listeners
for (Object obj : jda.getRegisteredListeners()) {
if (!(obj instanceof EventListener)) { // Yeah bro I wish I knew too :/
jda.removeEventListener(obj);
}
}
// Unregister Bukkit Events
HandlerList.unregisterAll(plugin);
// Creates a future which will be completed when JDA fully shutdowns
final CompletableFuture<Void> future = new CompletableFuture<>();
jda.addEventListener(new ListenerAdapter() {
@Override
public void onShutdown(@NotNull ShutdownEvent event) {
future.complete(null);
}
});
// Tell JDA to wrap it up
jda.shutdown();
try {
// Wait for JDA to wrap it up
future.get(5, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
logger.warning("JDA took longer than expected to shutdown, this may have caused some problems.");
} finally {
jda = null;
}
}
}
public JDA getJda() {
return jda;
}
@Override
public Unsafe getUnsafe() {
return unsafe;
}
public Guild getGuild() {
return guild;
}
public EssentialsDiscord getPlugin() {
return plugin;
}
public DiscordSettings getSettings() {
return plugin.getSettings();
}
public WebhookClient getConsoleWebhook() {
return consoleWebhook;
}
public boolean isDebug() {
return plugin.getEss().getSettings().isDebug();
}
}

View File

@ -0,0 +1,26 @@
package net.essentialsx.discord.interactions;
import net.dv8tion.jda.api.entities.GuildChannel;
import net.essentialsx.api.v2.services.discord.InteractionChannel;
public class InteractionChannelImpl implements InteractionChannel {
private final GuildChannel channel;
public InteractionChannelImpl(GuildChannel channel) {
this.channel = channel;
}
@Override
public String getName() {
return channel.getName();
}
public GuildChannel getJdaObject() {
return channel;
}
@Override
public String getId() {
return channel.getId();
}
}

View File

@ -0,0 +1,54 @@
package net.essentialsx.discord.interactions;
import net.essentialsx.api.v2.services.discord.InteractionCommand;
import net.essentialsx.api.v2.services.discord.InteractionCommandArgument;
import net.essentialsx.discord.JDADiscordService;
import java.util.ArrayList;
import java.util.List;
public abstract class InteractionCommandImpl implements InteractionCommand {
protected final JDADiscordService jda;
private final String name;
private final String description;
private final List<InteractionCommandArgument> arguments = new ArrayList<>();
public InteractionCommandImpl(JDADiscordService jda, String name, String description) {
this.jda = jda;
this.name = name;
this.description = description;
}
@Override
public final boolean isDisabled() {
return !jda.getSettings().isCommandEnabled(name);
}
@Override
public final boolean isEphemeral() {
return jda.getSettings().isCommandEphemeral(name);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public List<InteractionCommandArgument> getArguments() {
return arguments;
}
public List<String> getAdminSnowflakes() {
return jda.getSettings().getCommandAdminSnowflakes(name);
}
public void addArgument(InteractionCommandArgument argument) {
arguments.add(argument);
}
}

View File

@ -0,0 +1,161 @@
package net.essentialsx.discord.interactions;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.requests.ErrorResponse;
import net.essentialsx.api.v2.services.discord.InteractionCommand;
import net.essentialsx.api.v2.services.discord.InteractionCommandArgument;
import net.essentialsx.api.v2.services.discord.InteractionController;
import net.essentialsx.api.v2.services.discord.InteractionEvent;
import net.essentialsx.api.v2.services.discord.InteractionException;
import net.essentialsx.discord.JDADiscordService;
import net.essentialsx.discord.util.DiscordUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.earth2me.essentials.I18n.tl;
public class InteractionControllerImpl extends ListenerAdapter implements InteractionController {
private final static Logger logger = Logger.getLogger("EssentialsDiscord");
private final JDADiscordService jda;
private final Map<String, InteractionCommand> commandMap = new ConcurrentHashMap<>();
private final Map<String, InteractionCommand> batchRegistrationQueue = new HashMap<>();
private boolean initialBatchRegistration = false;
public InteractionControllerImpl(JDADiscordService jda) {
this.jda = jda;
jda.getJda().addEventListener(this);
}
@Override
public void onSlashCommand(@NotNull SlashCommandEvent event) {
if (event.getGuild() == null || event.getMember() == null || !commandMap.containsKey(event.getName())) {
return;
}
final InteractionCommand command = commandMap.get(event.getName());
if (command.isDisabled()) {
event.reply(tl("discordErrorCommandDisabled")).setEphemeral(true).queue();
return;
}
event.deferReply(command.isEphemeral()).queue(null, failure -> logger.log(Level.SEVERE, "Error while deferring Discord command", failure));
final InteractionEvent interactionEvent = new InteractionEventImpl(event);
if (!DiscordUtil.hasRoles(event.getMember(), jda.getSettings().getCommandSnowflakes(command.getName()))) {
interactionEvent.reply(tl("noAccessCommand"));
return;
}
jda.getPlugin().getEss().scheduleSyncDelayedTask(() -> command.onCommand(interactionEvent));
}
@Override
public InteractionCommand getCommand(String name) {
return commandMap.get(name);
}
public void processBatchRegistration() {
if (!initialBatchRegistration && !batchRegistrationQueue.isEmpty()) {
initialBatchRegistration = true;
final List<CommandData> list = new ArrayList<>();
for (final InteractionCommand command : batchRegistrationQueue.values()) {
final CommandData data = new CommandData(command.getName(), command.getDescription());
if (command.getArguments() != null) {
for (final InteractionCommandArgument argument : command.getArguments()) {
data.addOption(OptionType.valueOf(argument.getType().name()), argument.getName(), argument.getDescription(), argument.isRequired());
}
}
list.add(data);
}
jda.getGuild().updateCommands().addCommands(list).queue(success -> {
for (final Command command : success) {
commandMap.put(command.getName(), batchRegistrationQueue.get(command.getName()));
batchRegistrationQueue.remove(command.getName());
if (jda.isDebug()) {
logger.info("Registered guild command " + command.getName() + " with id " + command.getId());
}
}
if (!batchRegistrationQueue.isEmpty()) {
logger.warning(batchRegistrationQueue.size() + " Discord commands were lost during command registration!");
if (jda.isDebug()) {
logger.warning("Lost commands: " + batchRegistrationQueue.keySet());
}
batchRegistrationQueue.clear();
}
}, failure -> {
if (failure instanceof ErrorResponseException && ((ErrorResponseException) failure).getErrorResponse() == ErrorResponse.MISSING_ACCESS) {
logger.severe(tl("discordErrorCommand"));
return;
}
logger.log(Level.SEVERE, "Error while registering command", failure);
});
}
}
@Override
public void registerCommand(InteractionCommand command) throws InteractionException {
if (command.isDisabled()) {
throw new InteractionException("The given command has been disabled!");
}
if (commandMap.containsKey(command.getName())) {
throw new InteractionException("A command with that name is already registered!");
}
if (!initialBatchRegistration) {
if (jda.isDebug()) {
logger.info("Marked guild command for batch registration: " + command.getName());
}
batchRegistrationQueue.put(command.getName(), command);
return;
}
final CommandData data = new CommandData(command.getName(), command.getDescription());
if (command.getArguments() != null) {
for (final InteractionCommandArgument argument : command.getArguments()) {
data.addOption(OptionType.valueOf(argument.getType().name()), argument.getName(), argument.getDescription(), argument.isRequired());
}
}
jda.getGuild().upsertCommand(data).queue(success -> {
commandMap.put(command.getName(), command);
if (jda.isDebug()) {
logger.info("Registered guild command " + success.getName() + " with id " + success.getId());
}
}, failure -> {
if (failure instanceof ErrorResponseException && ((ErrorResponseException) failure).getErrorResponse() == ErrorResponse.MISSING_ACCESS) {
logger.severe(tl("discordErrorCommand"));
return;
}
logger.log(Level.SEVERE, "Error while registering command", failure);
});
}
public void shutdown() {
try {
jda.getGuild().updateCommands().complete();
} catch (Throwable e) {
logger.severe("Error while deleting commands: " + e.getMessage());
if (jda.isDebug()) {
e.printStackTrace();
}
}
commandMap.clear();
}
}

View File

@ -0,0 +1,79 @@
package net.essentialsx.discord.interactions;
import com.earth2me.essentials.utils.FormatUtil;
import com.google.common.base.Joiner;
import net.dv8tion.jda.api.MessageBuilder;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.essentialsx.api.v2.services.discord.InteractionChannel;
import net.essentialsx.api.v2.services.discord.InteractionEvent;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import net.essentialsx.discord.util.DiscordUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A class which provides information about what triggered an interaction event.
*/
public class InteractionEventImpl implements InteractionEvent {
private final static Logger logger = Logger.getLogger("EssentialsDiscord");
private final SlashCommandEvent event;
private final InteractionMember member;
private final List<String> replyBuffer = new ArrayList<>();
public InteractionEventImpl(final SlashCommandEvent jdaEvent) {
this.event = jdaEvent;
this.member = new InteractionMemberImpl(jdaEvent.getMember());
}
@Override
public void reply(String message) {
message = FormatUtil.stripFormat(message).replace("§", ""); // Don't ask
replyBuffer.add(message);
event.getHook().editOriginal(new MessageBuilder().setContent(Joiner.on('\n').join(replyBuffer)).setAllowedMentions(DiscordUtil.NO_GROUP_MENTIONS).build())
.queue(null, error -> logger.log(Level.SEVERE, "Error while editing command interaction response", error));
}
@Override
public InteractionMember getMember() {
return member;
}
@Override
public String getStringArgument(String key) {
final OptionMapping mapping = event.getOption(key);
return mapping == null ? null : mapping.getAsString();
}
@Override
public Long getIntegerArgument(String key) {
final OptionMapping mapping = event.getOption(key);
return mapping == null ? null : mapping.getAsLong();
}
@Override
public Boolean getBooleanArgument(String key) {
final OptionMapping mapping = event.getOption(key);
return mapping == null ? null : mapping.getAsBoolean();
}
@Override
public InteractionMember getUserArgument(String key) {
final OptionMapping mapping = event.getOption(key);
return mapping == null ? null : new InteractionMemberImpl(mapping.getAsMember());
}
@Override
public InteractionChannel getChannelArgument(String key) {
final OptionMapping mapping = event.getOption(key);
return mapping == null ? null : new InteractionChannelImpl(mapping.getAsGuildChannel());
}
@Override
public String getChannelId() {
return event.getChannel().getId();
}
}

View File

@ -0,0 +1,72 @@
package net.essentialsx.discord.interactions;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.PrivateChannel;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import net.essentialsx.discord.util.DiscordUtil;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class InteractionMemberImpl implements InteractionMember {
private final Member member;
public InteractionMemberImpl(Member member) {
this.member = member;
}
@Override
public String getName() {
return member.getUser().getName();
}
@Override
public String getDiscriminator() {
return member.getUser().getDiscriminator();
}
@Override
public String getEffectiveName() {
return member.getEffectiveName();
}
@Override
public String getNickname() {
return member.getNickname();
}
@Override
public String getId() {
return member.getId();
}
@Override
public boolean isAdmin() {
return member.hasPermission(Permission.ADMINISTRATOR);
}
@Override
public boolean hasRoles(List<String> roleDefinitions) {
return DiscordUtil.hasRoles(member, roleDefinitions);
}
public Member getJdaObject() {
return member;
}
@Override
public CompletableFuture<Boolean> sendPrivateMessage(String content) {
final CompletableFuture<Boolean> future = new CompletableFuture<>();
final CompletableFuture<PrivateChannel> privateFuture = member.getUser().openPrivateChannel().submit();
privateFuture.thenCompose(privateChannel -> privateChannel.sendMessage(content).submit())
.whenComplete((m, error) -> {
if (error != null) {
future.complete(false);
return;
}
future.complete(true);
});
return future;
}
}

View File

@ -0,0 +1,26 @@
package net.essentialsx.discord.interactions.commands;
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.discord.JDADiscordService;
import net.essentialsx.discord.interactions.InteractionCommandImpl;
import net.essentialsx.discord.util.DiscordCommandSender;
import org.bukkit.Bukkit;
import static com.earth2me.essentials.I18n.tl;
public class ExecuteCommand extends InteractionCommandImpl {
public ExecuteCommand(JDADiscordService jda) {
super(jda, "execute", tl("discordCommandExecuteDescription"));
addArgument(new InteractionCommandArgument("command", tl("discordCommandExecuteArgumentCommand"), InteractionCommandArgumentType.STRING, true));
}
@Override
public void onCommand(InteractionEvent event) {
final String command = event.getStringArgument("command");
event.reply(tl("discordCommandExecuteReply", command));
Bukkit.getScheduler().runTask(jda.getPlugin(), () ->
Bukkit.dispatchCommand(new DiscordCommandSender(jda, Bukkit.getConsoleSender(), event::reply).getSender(), command));
}
}

View File

@ -0,0 +1,51 @@
package net.essentialsx.discord.interactions.commands;
import com.earth2me.essentials.IEssentials;
import com.earth2me.essentials.PlayerList;
import com.earth2me.essentials.User;
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.discord.JDADiscordService;
import net.essentialsx.discord.interactions.InteractionCommandImpl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.earth2me.essentials.I18n.tl;
public class ListCommand extends InteractionCommandImpl {
public ListCommand(JDADiscordService jda) {
super(jda, "list", tl("discordCommandListDescription"));
addArgument(new InteractionCommandArgument("group", tl("discordCommandListArgumentGroup"), InteractionCommandArgumentType.STRING, false));
}
@Override
public void onCommand(InteractionEvent event) {
final boolean showHidden = event.getMember().hasRoles(getAdminSnowflakes());
final List<String> output = new ArrayList<>();
final IEssentials ess = jda.getPlugin().getEss();
output.add(PlayerList.listSummary(ess, null, showHidden));
final Map<String, List<User>> playerList = PlayerList.getPlayerLists(ess, null, showHidden);
final String group = event.getStringArgument("group");
if (group != null) {
try {
output.add(PlayerList.listGroupUsers(ess, playerList, group));
} catch (Exception e) {
output.add(tl("errorWithMessage", e.getMessage()));
}
} else {
output.addAll(PlayerList.prepareGroupedList(ess, getName(), playerList));
}
final StringBuilder stringBuilder = new StringBuilder();
for (final String str : output) {
stringBuilder.append(str).append("\n");
}
event.reply(stringBuilder.substring(0, stringBuilder.length() - 2));
}
}

View File

@ -0,0 +1,59 @@
package net.essentialsx.discord.interactions.commands;
import com.earth2me.essentials.User;
import com.earth2me.essentials.commands.PlayerNotFoundException;
import com.earth2me.essentials.utils.FormatUtil;
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.discord.JDADiscordService;
import net.essentialsx.discord.interactions.InteractionCommandImpl;
import net.essentialsx.discord.util.DiscordMessageRecipient;
import org.bukkit.Bukkit;
import java.util.concurrent.atomic.AtomicReference;
import static com.earth2me.essentials.I18n.tl;
public class MessageCommand extends InteractionCommandImpl {
public MessageCommand(JDADiscordService jda) {
super(jda, "msg", tl("discordCommandMessageDescription"));
addArgument(new InteractionCommandArgument("username", tl("discordCommandMessageArgumentUsername"), InteractionCommandArgumentType.STRING, true));
addArgument(new InteractionCommandArgument("message", tl("discordCommandMessageArgumentMessage"), InteractionCommandArgumentType.STRING, true));
}
@Override
public void onCommand(InteractionEvent event) {
final boolean getHidden = event.getMember().hasRoles(getAdminSnowflakes());
final User user;
try {
user = jda.getPlugin().getEss().matchUser(Bukkit.getServer(), null, event.getStringArgument("username"), getHidden, false);
} catch (PlayerNotFoundException e) {
event.reply(tl("errorWithMessage", e.getMessage()));
return;
}
if (!getHidden && user.isIgnoreMsg()) {
event.reply(tl("msgIgnore", user.getDisplayName()));
return;
}
if (user.isAfk()) {
if (user.getAfkMessage() != null) {
event.reply(tl("userAFKWithMessage", user.getDisplayName(), user.getAfkMessage()));
} else {
event.reply(tl("userAFK", user.getDisplayName()));
}
}
final String message = event.getMember().hasRoles(jda.getSettings().getPermittedFormattingRoles()) ?
FormatUtil.replaceFormat(event.getStringArgument("message")) : FormatUtil.stripFormat(event.getStringArgument("message"));
event.reply(tl("msgFormat", tl("meSender"), user.getDisplayName(), message));
user.sendMessage(tl("msgFormat", event.getMember().getTag(), tl("meRecipient"), message));
// We use an atomic reference here so that java will garbage collect the recipient
final AtomicReference<DiscordMessageRecipient> ref = new AtomicReference<>(new DiscordMessageRecipient(event.getMember()));
jda.getPlugin().getEss().runTaskLaterAsynchronously(() -> ref.set(null), 6000); // Expires after 5 minutes
user.setReplyRecipient(ref.get());
}
}

View File

@ -0,0 +1,191 @@
package net.essentialsx.discord.listeners;
import com.earth2me.essentials.Console;
import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.FormatUtil;
import net.ess3.api.events.AfkStatusChangeEvent;
import net.ess3.api.events.MuteStatusChangeEvent;
import net.essentialsx.api.v2.events.AsyncUserDataLoadEvent;
import net.essentialsx.api.v2.events.discord.DiscordChatMessageEvent;
import net.essentialsx.api.v2.events.discord.DiscordMessageEvent;
import net.essentialsx.api.v2.services.discord.MessageType;
import net.essentialsx.discord.JDADiscordService;
import net.essentialsx.discord.util.MessageUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.text.MessageFormat;
import java.util.UUID;
public class BukkitListener implements Listener {
private final static String AVATAR_URL = "https://crafthead.net/helm/{uuid}";
private final JDADiscordService jda;
public BukkitListener(JDADiscordService jda) {
this.jda = jda;
}
/**
* Processes messages from all other events.
* This way it allows other plugins to modify route/message or just cancel it.
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDiscordMessage(DiscordMessageEvent event) {
jda.sendMessage(event, event.getMessage(), event.isAllowGroupMentions());
}
// Bukkit Events
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onMute(MuteStatusChangeEvent event) {
if (!event.getValue()) {
sendDiscordMessage(MessageType.DefaultTypes.MUTE,
MessageUtil.formatMessage(jda.getSettings().getUnmuteFormat(),
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getDisplayName())));
} else if (event.getTimestamp().isPresent()) {
final boolean console = event.getController() == null;
final MessageFormat msg = event.getReason() == null ? jda.getSettings().getTempMuteFormat() : jda.getSettings().getTempMuteReasonFormat();
sendDiscordMessage(MessageType.DefaultTypes.MUTE,
MessageUtil.formatMessage(msg,
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getDisplayName()),
MessageUtil.sanitizeDiscordMarkdown(console ? Console.NAME : event.getController().getName()),
MessageUtil.sanitizeDiscordMarkdown(console ? Console.DISPLAY_NAME : event.getController().getDisplayName()),
DateUtil.formatDateDiff(event.getTimestamp().get()),
MessageUtil.sanitizeDiscordMarkdown(event.getReason())));
} else {
final boolean console = event.getController() == null;
final MessageFormat msg = event.getReason() == null ? jda.getSettings().getPermMuteFormat() : jda.getSettings().getPermMuteReasonFormat();
sendDiscordMessage(MessageType.DefaultTypes.MUTE,
MessageUtil.formatMessage(msg,
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getDisplayName()),
MessageUtil.sanitizeDiscordMarkdown(console ? Console.NAME : event.getController().getName()),
MessageUtil.sanitizeDiscordMarkdown(console ? Console.DISPLAY_NAME : event.getController().getDisplayName()),
MessageUtil.sanitizeDiscordMarkdown(event.getReason())));
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onChat(AsyncPlayerChatEvent event) {
final Player player = event.getPlayer();
Bukkit.getScheduler().runTask(jda.getPlugin(), () -> {
final DiscordChatMessageEvent chatEvent = new DiscordChatMessageEvent(event.getPlayer(), event.getMessage());
chatEvent.setCancelled(!jda.getSettings().isShowAllChat() && !event.getRecipients().containsAll(Bukkit.getOnlinePlayers()));
Bukkit.getPluginManager().callEvent(chatEvent);
if (chatEvent.isCancelled()) {
return;
}
sendDiscordMessage(MessageType.DefaultTypes.CHAT,
MessageUtil.formatMessage(jda.getSettings().getMcToDiscordFormat(player),
MessageUtil.sanitizeDiscordMarkdown(player.getName()),
MessageUtil.sanitizeDiscordMarkdown(player.getDisplayName()),
player.hasPermission("essentials.discord.markdown") ? chatEvent.getMessage() : MessageUtil.sanitizeDiscordMarkdown(chatEvent.getMessage()),
MessageUtil.sanitizeDiscordMarkdown(player.getWorld().getName()),
MessageUtil.sanitizeDiscordMarkdown(FormatUtil.stripEssentialsFormat(jda.getPlugin().getEss().getPermissionsHandler().getPrefix(player))),
MessageUtil.sanitizeDiscordMarkdown(FormatUtil.stripEssentialsFormat(jda.getPlugin().getEss().getPermissionsHandler().getSuffix(player)))),
player.hasPermission("essentials.discord.ping"),
jda.getSettings().isShowAvatar() ? AVATAR_URL.replace("{uuid}", player.getUniqueId().toString()) : null,
jda.getSettings().isShowName() ? player.getName() : null,
player.getUniqueId());
});
}
@EventHandler(priority = EventPriority.MONITOR)
public void onJoin(AsyncUserDataLoadEvent event) {
// Delay join to let nickname load
if (event.getJoinMessage() != null) {
sendDiscordMessage(MessageType.DefaultTypes.JOIN,
MessageUtil.formatMessage(jda.getSettings().getJoinFormat(event.getUser().getBase()),
MessageUtil.sanitizeDiscordMarkdown(event.getUser().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getUser().getDisplayName()),
MessageUtil.sanitizeDiscordMarkdown(event.getJoinMessage())),
false,
jda.getSettings().isShowAvatar() ? AVATAR_URL.replace("{uuid}", event.getUser().getBase().getUniqueId().toString()) : null,
jda.getSettings().isShowName() ? event.getUser().getName() : null,
event.getUser().getBase().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onQuit(PlayerQuitEvent event) {
if (event.getQuitMessage() != null) {
sendDiscordMessage(MessageType.DefaultTypes.LEAVE,
MessageUtil.formatMessage(jda.getSettings().getQuitFormat(event.getPlayer()),
MessageUtil.sanitizeDiscordMarkdown(event.getPlayer().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getPlayer().getDisplayName()),
MessageUtil.sanitizeDiscordMarkdown(event.getQuitMessage())),
false,
jda.getSettings().isShowAvatar() ? AVATAR_URL.replace("{uuid}", event.getPlayer().getUniqueId().toString()) : null,
jda.getSettings().isShowName() ? event.getPlayer().getName() : null,
event.getPlayer().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDeath(PlayerDeathEvent event) {
sendDiscordMessage(MessageType.DefaultTypes.DEATH,
MessageUtil.formatMessage(jda.getSettings().getDeathFormat(event.getEntity()),
MessageUtil.sanitizeDiscordMarkdown(event.getEntity().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getEntity().getDisplayName()),
MessageUtil.sanitizeDiscordMarkdown(event.getDeathMessage())),
false,
jda.getSettings().isShowAvatar() ? AVATAR_URL.replace("{uuid}", event.getEntity().getUniqueId().toString()) : null,
jda.getSettings().isShowName() ? event.getEntity().getName() : null,
event.getEntity().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onAfk(AfkStatusChangeEvent event) {
final MessageFormat format;
if (event.getValue()) {
format = jda.getSettings().getAfkFormat(event.getAffected().getBase());
} else {
format = jda.getSettings().getUnAfkFormat(event.getAffected().getBase());
}
sendDiscordMessage(MessageType.DefaultTypes.AFK,
MessageUtil.formatMessage(format,
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getAffected().getDisplayName())),
false,
jda.getSettings().isShowAvatar() ? AVATAR_URL.replace("{uuid}", event.getAffected().getBase().getUniqueId().toString()) : null,
jda.getSettings().isShowName() ? event.getAffected().getName() : null,
event.getAffected().getBase().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onKick(PlayerKickEvent event) {
sendDiscordMessage(MessageType.DefaultTypes.KICK,
MessageUtil.formatMessage(jda.getSettings().getKickFormat(),
MessageUtil.sanitizeDiscordMarkdown(event.getPlayer().getName()),
MessageUtil.sanitizeDiscordMarkdown(event.getPlayer().getDisplayName()),
MessageUtil.sanitizeDiscordMarkdown(event.getReason())));
}
private void sendDiscordMessage(final MessageType messageType, final String message) {
sendDiscordMessage(messageType, message, false, null, null, null);
}
private void sendDiscordMessage(final MessageType messageType, final String message, final boolean allowPing, final String avatarUrl, final String name, final UUID uuid) {
if (jda.getPlugin().getSettings().getMessageChannel(messageType.getKey()).equalsIgnoreCase("none")) {
return;
}
final DiscordMessageEvent event = new DiscordMessageEvent(messageType, FormatUtil.stripFormat(message), allowPing, avatarUrl, name, uuid);
if (Bukkit.getServer().isPrimaryThread()) {
Bukkit.getPluginManager().callEvent(event);
} else {
Bukkit.getScheduler().runTask(jda.getPlugin(), () -> Bukkit.getPluginManager().callEvent(event));
}
}
}

View File

@ -0,0 +1,31 @@
package net.essentialsx.discord.listeners;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.essentialsx.discord.JDADiscordService;
import net.essentialsx.discord.util.DiscordCommandSender;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
public class DiscordCommandDispatcher extends ListenerAdapter {
private final JDADiscordService jda;
private String channelId = null;
public DiscordCommandDispatcher(JDADiscordService jda) {
this.jda = jda;
}
@Override
public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) {
if (jda.getConsoleWebhook() != null && event.getChannel().getId().equals(channelId)
&& !event.isWebhookMessage() && !event.getAuthor().isBot()) {
Bukkit.getScheduler().runTask(jda.getPlugin(), () ->
Bukkit.dispatchCommand(new DiscordCommandSender(jda, Bukkit.getConsoleSender(), message ->
event.getMessage().reply(message).queue()).getSender(), event.getMessage().getContentRaw()));
}
}
public void setChannelId(String channelId) {
this.channelId = channelId;
}
}

View File

@ -0,0 +1,101 @@
package net.essentialsx.discord.listeners;
import com.earth2me.essentials.utils.FormatUtil;
import com.vdurmont.emoji.EmojiParser;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.ess3.api.IUser;
import net.essentialsx.discord.JDADiscordService;
import net.essentialsx.discord.util.DiscordUtil;
import net.essentialsx.discord.util.MessageUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DiscordListener extends ListenerAdapter {
private final static Logger logger = Logger.getLogger("EssentialsDiscord");
private final JDADiscordService plugin;
public DiscordListener(JDADiscordService plugin) {
this.plugin = plugin;
}
@Override
public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) {
if (event.getAuthor().isBot() && !event.isWebhookMessage() && (!plugin.getSettings().isShowBotMessages() || event.getAuthor().getId().equals(plugin.getJda().getSelfUser().getId()))) {
return;
}
if (event.isWebhookMessage() && (!plugin.getSettings().isShowWebhookMessages() || DiscordUtil.ACTIVE_WEBHOOKS.contains(event.getAuthor().getId()))) {
return;
}
// Get list of channel names that have this channel id mapped
final List<String> keys = plugin.getPlugin().getSettings().getKeysFromChannelId(event.getChannel().getIdLong());
if (keys == null || keys.size() == 0) {
if (plugin.isDebug()) {
logger.log(Level.INFO, "Skipping message due to no channel keys for id " + event.getChannel().getIdLong() + "!");
}
return;
}
final User user = event.getAuthor();
final Member member = event.getMember();
final Message message = event.getMessage();
assert member != null; // Member will never be null
if (plugin.getSettings().getDiscordFilter() != null && plugin.getSettings().getDiscordFilter().matcher(message.getContentDisplay()).find()) {
if (plugin.isDebug()) {
logger.log(Level.INFO, "Skipping message " + message.getId() + " with content, \"" + message.getContentDisplay() + "\" as it matched the filter!");
}
return;
}
final StringBuilder messageBuilder = new StringBuilder(message.getContentDisplay());
if (plugin.getPlugin().getSettings().isShowDiscordAttachments()) {
for (final Message.Attachment attachment : message.getAttachments()) {
messageBuilder.append(" ").append(attachment.getUrl());
}
}
// Strip message
final String strippedMessage = StringUtils.abbreviate(
messageBuilder.toString()
.replace(plugin.getSettings().isChatFilterNewlines() ? '\n' : ' ', ' ')
.trim(), plugin.getSettings().getChatDiscordMaxLength());
// Apply or strip color formatting
final String finalMessage = DiscordUtil.hasRoles(member, plugin.getPlugin().getSettings().getPermittedFormattingRoles()) ?
FormatUtil.replaceFormat(strippedMessage) : FormatUtil.stripFormat(strippedMessage);
// Don't send blank messages
if (finalMessage.trim().length() == 0) {
if (plugin.isDebug()) {
logger.log(Level.INFO, "Skipping finalized empty message " + message.getId());
}
return;
}
final String formattedMessage = EmojiParser.parseToAliases(MessageUtil.formatMessage(plugin.getPlugin().getSettings().getDiscordToMcFormat(),
event.getChannel().getName(), user.getName(), user.getDiscriminator(), user.getAsTag(),
member.getEffectiveName(), DiscordUtil.getRoleColorFormat(member), finalMessage), EmojiParser.FitzpatrickAction.REMOVE);
for (IUser essUser : plugin.getPlugin().getEss().getOnlineUsers()) {
for (String group : keys) {
final String perm = "essentials.discord.receive." + group;
final boolean primaryOverride = plugin.getSettings().isAlwaysReceivePrimary() && group.equalsIgnoreCase("primary");
if (primaryOverride || (essUser.isPermissionSet(perm) && essUser.isAuthorized(perm))) {
essUser.sendMessage(formattedMessage);
}
}
}
}
}

View File

@ -0,0 +1,89 @@
package net.essentialsx.discord.util;
import com.earth2me.essentials.utils.FormatUtil;
import com.google.common.base.Splitter;
import net.dv8tion.jda.api.entities.Message;
import net.essentialsx.discord.JDADiscordService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.bukkit.Bukkit;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import static com.earth2me.essentials.I18n.tl;
@Plugin(name = "EssentialsX-ConsoleInjector", category = "Core", elementType = "appender", printObject = true)
public class ConsoleInjector extends AbstractAppender {
private final static java.util.logging.Logger logger = java.util.logging.Logger.getLogger("EssentialsDiscord");
private final JDADiscordService jda;
private final BlockingQueue<String> messageQueue = new LinkedBlockingQueue<>();
private final SimpleDateFormat timestampFormat = new SimpleDateFormat("HH:mm:ss");
private final int taskId;
public ConsoleInjector(JDADiscordService jda) {
super("EssentialsX-ConsoleInjector", null, null, false);
this.jda = jda;
((Logger) LogManager.getRootLogger()).addAppender(this);
taskId = Bukkit.getScheduler().runTaskTimerAsynchronously(jda.getPlugin(), () -> {
final StringBuilder buffer = new StringBuilder();
String curLine;
while ((curLine = messageQueue.peek()) != null) {
if (buffer.length() + curLine.length() > Message.MAX_CONTENT_LENGTH - 2) {
sendMessage(buffer.toString());
buffer.setLength(0);
continue;
}
buffer.append("\n").append(messageQueue.poll());
}
if (buffer.length() != 0) {
sendMessage(buffer.toString());
}
}, 20, 40).getTaskId();
}
private void sendMessage(String content) {
jda.getConsoleWebhook().send(jda.getWebhookMessage(content)).exceptionally(e -> {
logger.severe(tl("discordErrorWebhook"));
remove();
return null;
});
}
@Override
public void append(LogEvent event) {
if (event.getLevel().intLevel() > jda.getSettings().getConsoleLogLevel().intLevel()) {
return;
}
// Ansi strip is for normal colors, normal strip is for 1.16 hex color codes as they are not formatted correctly
String entry = FormatUtil.stripFormat(FormatUtil.stripAnsi(event.getMessage().getFormattedMessage())).trim();
if (entry.isEmpty()) {
return;
}
final String loggerName = event.getLoggerName();
if (!loggerName.isEmpty() && !loggerName.contains(".")) {
entry = "[" + event.getLoggerName() + "] " + entry;
}
//noinspection UnstableApiUsage
messageQueue.addAll(Splitter.fixedLength(Message.MAX_CONTENT_LENGTH).splitToList(
MessageUtil.formatMessage(jda.getSettings().getConsoleFormat(),
timestampFormat.format(new Date()),
event.getLevel().name(),
MessageUtil.sanitizeDiscordMarkdown(entry))));
}
public void remove() {
((Logger) LogManager.getRootLogger()).removeAppender(this);
Bukkit.getScheduler().cancelTask(taskId);
messageQueue.clear();
}
}

View File

@ -0,0 +1,46 @@
package net.essentialsx.discord.util;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.VersionUtil;
import net.ess3.provider.providers.BukkitSenderProvider;
import net.ess3.provider.providers.PaperCommandSender;
import net.essentialsx.discord.JDADiscordService;
import org.bukkit.Bukkit;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.scheduler.BukkitTask;
public class DiscordCommandSender {
private final BukkitSenderProvider sender;
private BukkitTask task;
private String responseBuffer = "";
private long lastTime = System.currentTimeMillis();
public DiscordCommandSender(JDADiscordService jda, ConsoleCommandSender sender, CmdCallback callback) {
final BukkitSenderProvider.MessageHook hook = message -> {
responseBuffer = responseBuffer + (responseBuffer.isEmpty() ? "" : "\n") + MessageUtil.sanitizeDiscordMarkdown(FormatUtil.stripFormat(message));
lastTime = System.currentTimeMillis();
};
this.sender = (VersionUtil.isPaper() && VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_16_5_R01)) ? new PaperCommandSender(sender, hook) : new BukkitSenderProvider(sender, hook);
task = Bukkit.getScheduler().runTaskTimerAsynchronously(jda.getPlugin(), () -> {
if (!responseBuffer.isEmpty() && System.currentTimeMillis() - lastTime >= 1000) {
callback.onMessage(responseBuffer);
responseBuffer = "";
lastTime = System.currentTimeMillis();
return;
}
if (System.currentTimeMillis() - lastTime >= 20000) {
task.cancel();
}
}, 0, 20);
}
public interface CmdCallback {
void onMessage(String message);
}
public BukkitSenderProvider getSender() {
return sender;
}
}

View File

@ -0,0 +1,75 @@
package net.essentialsx.discord.util;
import com.earth2me.essentials.messaging.IMessageRecipient;
import com.earth2me.essentials.utils.FormatUtil;
import net.essentialsx.api.v2.services.discord.InteractionMember;
import org.bukkit.entity.Player;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.earth2me.essentials.I18n.tl;
public class DiscordMessageRecipient implements IMessageRecipient {
private final InteractionMember member;
private final AtomicBoolean died = new AtomicBoolean(false);
public DiscordMessageRecipient(InteractionMember member) {
this.member = member;
}
@Override
public void sendMessage(String message) {
}
@Override
public MessageResponse sendMessage(IMessageRecipient recipient, String message) {
return MessageResponse.UNREACHABLE;
}
@Override
public MessageResponse onReceiveMessage(IMessageRecipient sender, String message) {
if (died.get()) {
sender.setReplyRecipient(null);
return MessageResponse.UNREACHABLE;
}
final String cleanMessage = MessageUtil.sanitizeDiscordMarkdown(FormatUtil.stripFormat(message));
member.sendPrivateMessage(tl("replyFromDiscord", sender.getName(), cleanMessage)).thenAccept(success -> {
if (!success) {
died.set(true);
}
});
return MessageResponse.SUCCESS;
}
@Override
public String getName() {
return member.getTag();
}
@Override
public String getDisplayName() {
return member.getTag();
}
@Override
public boolean isReachable() {
return !died.get();
}
@Override
public IMessageRecipient getReplyRecipient() {
return null;
}
@Override
public void setReplyRecipient(IMessageRecipient recipient) {
}
@Override
public boolean isHiddenFrom(Player player) {
return died.get();
}
}

View File

@ -0,0 +1,168 @@
package net.essentialsx.discord.util;
import club.minnced.discord.webhook.WebhookClient;
import club.minnced.discord.webhook.WebhookClientBuilder;
import club.minnced.discord.webhook.send.AllowedMentions;
import com.earth2me.essentials.utils.DownsampleUtil;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.VersionUtil;
import com.google.common.collect.ImmutableList;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.entities.Webhook;
import okhttp3.OkHttpClient;
import java.awt.Color;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public final class DiscordUtil {
public final static List<Message.MentionType> NO_GROUP_MENTIONS;
public final static AllowedMentions ALL_MENTIONS_WEBHOOK = AllowedMentions.all();
public final static AllowedMentions NO_GROUP_MENTIONS_WEBHOOK = new AllowedMentions().withParseEveryone(false).withParseRoles(false).withParseUsers(true);
public final static CopyOnWriteArrayList<String> ACTIVE_WEBHOOKS = new CopyOnWriteArrayList<>();
static {
final ImmutableList.Builder<Message.MentionType> types = new ImmutableList.Builder<>();
types.add(Message.MentionType.USER);
types.add(Message.MentionType.CHANNEL);
types.add(Message.MentionType.EMOTE);
NO_GROUP_MENTIONS = types.build();
}
private DiscordUtil() {
}
/**
* Creates a {@link WebhookClient}.
*
* @param id The id of the webhook.
* @param token The token of the webhook.
* @param client The http client of the webhook.
* @return The {@link WebhookClient}.
*/
public static WebhookClient getWebhookClient(long id, String token, OkHttpClient client) {
return new WebhookClientBuilder(id, token)
.setAllowedMentions(AllowedMentions.none())
.setHttpClient(client)
.setDaemon(true)
.build();
}
/**
* Gets and cleans webhooks with the given name from channels other than the specified one.
*
* @param channel The channel to search for webhooks in.
* @param webhookName The name of the webhook to validate it.
*
* @return A future which completes with the webhook by the given name in the given channel, if present, otherwise null.
*/
public static CompletableFuture<Webhook> getAndCleanWebhooks(final TextChannel channel, final String webhookName) {
final Member self = channel.getGuild().getSelfMember();
final CompletableFuture<Webhook> future = new CompletableFuture<>();
final Consumer<List<Webhook>> consumer = webhooks -> {
boolean foundWebhook = false;
for (final Webhook webhook : webhooks) {
if (webhook.getName().equalsIgnoreCase(webhookName)) {
if (foundWebhook || !webhook.getChannel().equals(channel)) {
ACTIVE_WEBHOOKS.remove(webhook.getId());
webhook.delete().reason("EssX Webhook Cleanup").queue();
continue;
}
ACTIVE_WEBHOOKS.addIfAbsent(webhook.getId());
future.complete(webhook);
foundWebhook = true;
}
}
if (!foundWebhook) {
future.complete(null);
}
};
if (self.hasPermission(Permission.MANAGE_WEBHOOKS)) {
channel.getGuild().retrieveWebhooks().queue(consumer);
} else if (self.hasPermission(channel, Permission.MANAGE_WEBHOOKS)) {
channel.retrieveWebhooks().queue(consumer);
} else {
return CompletableFuture.completedFuture(null);
}
return future;
}
/**
* Creates a webhook with the given name in the given channel.
*
* @param channel The channel to search for webhooks in.
* @param webhookName The name of the webhook to look for.
* @return A future which completes with the webhook by the given name in the given channel or null if no permissions.
*/
public static CompletableFuture<Webhook> createWebhook(TextChannel channel, String webhookName) {
if (!channel.getGuild().getSelfMember().hasPermission(channel, Permission.MANAGE_WEBHOOKS)) {
return CompletableFuture.completedFuture(null);
}
final CompletableFuture<Webhook> future = new CompletableFuture<>();
channel.createWebhook(webhookName).queue(webhook -> {
future.complete(webhook);
ACTIVE_WEBHOOKS.addIfAbsent(webhook.getId());
});
return future;
}
/**
* Gets the uppermost bukkit color code of a given member or an empty string if the server version is &lt; 1.16.
*
* @param member The target member.
* @return The bukkit color code or blank string.
*/
public static String getRoleColorFormat(Member member) {
final Color color = member.getColor();
if (color == null) {
return "";
}
if (VersionUtil.getServerBukkitVersion().isHigherThanOrEqualTo(VersionUtil.v1_16_1_R01)) {
// Essentials' FormatUtil allows us to not have to use bungee's chatcolor since bukkit's own one doesn't support rgb
return FormatUtil.replaceFormat("&#" + Integer.toHexString(color.getRGB()).substring(2));
}
return FormatUtil.replaceFormat("&" + DownsampleUtil.nearestTo(color.getRGB()));
}
/**
* Checks is the supplied user has at least one of the supplied roles.
*
* @param member The member to check.
* @param roleDefinitions A list with either the name or id of roles.
* @return true if member has role.
*/
public static boolean hasRoles(Member member, List<String> roleDefinitions) {
if (member.hasPermission(Permission.ADMINISTRATOR)) {
return true;
}
final List<Role> roles = member.getRoles();
for (String roleDefinition : roleDefinitions) {
roleDefinition = roleDefinition.trim();
if (roleDefinition.equals("*") || member.getId().equals(roleDefinition)) {
return true;
}
for (final Role role : roles) {
if (role.getId().equals(roleDefinition) || role.getName().equalsIgnoreCase(roleDefinition)) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,31 @@
package net.essentialsx.discord.util;
import java.text.MessageFormat;
public final class MessageUtil {
private MessageUtil() {
}
/**
* Sanitizes text to be sent to Discord, escaping any Markdown syntax.
*/
public static String sanitizeDiscordMarkdown(String message) {
if (message == null) {
return null;
}
return message.replace("*", "\\*")
.replace("~", "\\~")
.replace("_", "\\_")
.replace("`", "\\`")
.replace(">", "\\>")
.replace("|", "\\|");
}
/**
* Shortcut method allowing for use of varags in {@link MessageFormat} instances
*/
public static String formatMessage(MessageFormat format, Object... args) {
return format.format(args);
}
}

View File

@ -0,0 +1,299 @@
#############################################################
# +-------------------------------------------------------+ #
# | EssentialsX Discord | #
# +-------------------------------------------------------+ #
#############################################################
# This is the config file for EssentialsX Discord.
# This config was generated for version ${full.version}.
# You need to create a bot user in order to connect your server to Discord.
# You can find instructions on this here: https://essentialsx.net/discord-tutorial
# The token for your bot from the Discord Developers site.
# Please make sure to use this site to add the bot to your server as it grants special permissions you may not be familiar with: https://essentialsx.net/discord.html
token: "INSERT-TOKEN-HERE"
# The ID of your server.
guild: 000000000000000000
# Defined text channels
# =====================
#
# Channels defined here can be used for two different purposes:
#
# Firstly, channels defined here can be used to give players permission to see messages from said channel.
# This can be done by give your players the permission node "essentials.discord.receive.<channel>".
# For example, if you wanted to let a player see messages from the primary channel, you'd give them "essentials.discord.receive.primary".
#
# Secondly, channels defined here can be used in the section below to specify which channel a message goes to.
# If a defined channel ID is invalid, the primary channel will be used as a fallback.
# If the primary channel is not defined or invalid, the default channel of the server will be used.
# If your server doesn't have any text channels, the plugin will be disabled.
# By default, two channels are defined:
# - primary, which will send basic join/leave/death/chat messages
# - staff, which will send kick/mute messages
# (note: you will need to replace the zeros with the actual channel ID you want to use)
channels:
primary: 000000000000000000
staff: 000000000000000000
# Should all players receive Discord messages from the primary channel, regardless of their permissions?
# This is intended for use for people without permission plugins. If you have a permission plugin, please give your
# players the essentials.discord.receive.primary permission.
always-receive-primary: false
# Chat relay settings
# General settings for chat relays between Minecraft and Discord.
# To configure the channel Minecraft chat is sent to, see the "message-types" section of the config.
chat:
# The maximum amount of characters messages from Discord should be before being truncated.
discord-max-length: 2000
# Whether or not new lines from Discord should be filtered or not.
filter-newlines: true
# A regex pattern which will not send matching messages through to Discord.
# By default, this will ignore messages starting with '!' and '?'.
discord-filter: "^[!?]"
# Whether or not webhook messages from Discord should be shown in Minecraft.
show-webhook-messages: false
# Whether or not bot messages from Discord should be shown in Minecraft.
show-bot-messages: false
# Whether or not to show all Minecraft chat messages that are not shown to all players.
# You shouldn't need to enable this unless you're not seeing all chat messages go through to Discord.
show-all-chat: false
# Console relay settings
# The console relay sends every message shown in the console to a Discord channel.
console:
# The channel ID (or webhook URL) to send the console output to.
# If the channel ID/webhook URL is invalid or set to 'none', the console relay will be disabled.
# Note: If you use a channel ID, the bot must have the "Manage Webhooks" permission in Discord or else the console relay will not work.
channel: 000000000000000000
# The format of the console message. The following placeholders can be used:
# - {timestamp}: Timestamp in HH:mm:ss format
# - {level}: The console logging level
# - {message}: The actual message being logged
format: "[{timestamp} {level}] {message}"
# The name of the webhook that will be created, if a channel ID is provided.
webhook-name: "EssentialsX Console Relay"
# Set to true if all messages in the console relay channel should be treated as commands.
# Note: Enabling this means everyone who can send messages in the console channel will be able to send commands as the
# console. It's recommended you stick to the /execute command which has role permission checks (see command configuration below).
# Note 2: This option requires a channel ID and is not supported if you specify a webhook URL above. You'll need to use /execute in Discord if you use a webhook URL.
command-relay: false
# The maximum log level of messages to send to the console relay.
# The following is a list of available log levels in order of lowest to highest.
# Changing the log level will send all log levels above it to the console relay.
# For example, setting this to 'info' will display log messages with info, warn, error, and fatal levels.
# Log Levels:
# - fatal
# - error
# - warn
# - info
# - debug
# - trace
log-level: info
# Configure which Discord channels different messages will be sent to.
# You can either use the names of the channels listed above or just the id of a channel.
# If an invalid channel is used, the primary channel will be used instead.
#
# To disable a message from showing, use 'none' as the channel name.
message-types:
# Join messages sent when a player joins the Minecraft server.
join: primary
# Leave messages sent when a player leaves the Minecraft server.
leave: primary
# Chat messages sent when a player chats on the Minecraft server.
chat: primary
# Death messages sent when a player dies on the Minecraft server.
death: primary
# AFK status change messages sent when a player's AFK status changes.
afk: primary
# Message sent when a player is kicked from the Minecraft server.
kick: staff
# Message sent when a player's mute state is changed on the Minecraft server.
mute: staff
# Whether or not player messages should show their avatar as the profile picture in Discord.
show-avatar: false
# Whether or not player messages should show their name as the bot name in Discord.
show-name: false
# Settings pertaining to the varies commands registered by EssentialsX on Discord.
commands:
# The execute command allows for discord users to execute MC commands from Discord.
# MC commands executed by this will be ran as the console and you should therefore be careful to who you grant this to.
execute:
# Set to false if you do not want this command to show up on the Discord command selector.
# You must restart your server after changing this.
enabled: true
# Whether or not the command should be hidden from other people in the channel.
# If set to false, members of the Discord guild will be able to see the exact command you executed as well as its response.
hide-command: true
# List of user IDs or role names/IDs allowed to use this command (or * to allow anyone to access it).
allowed-roles:
- "Admins"
- "123456789012345678"
# The msg command allows for Discord users to message MC players from Discord.
msg:
# Set to false if you do not want this command to show up on the Discord command selector.
# You must restart your server after changing this.
enabled: true
# Whether or not the command should be hidden from other people in the channel.
# If set to false, members of the Discord guild will be able to see the target and content of your message.
hide-command: true
# List of user IDs or role names/IDs allowed to use this command (or '*' to allow anyone to access it).
allowed-roles:
- "*"
# List of user IDs or role names/IDs who can message vanished players or players who disable messages. If '*' is
# used, all people on Discord would be allowed to message vanished players (and therefore expose they are actually online)
# and message players who disable messages.
admin-roles:
- "Admins"
- "123456789012345678"
# The list command allows Discord users to see a list of players currently online on Minecraft.
list:
# Set to false if you do not want this command to show up on the Discord command selector.
# You must restart your server after changing this.
enabled: true
# Whether or not the command should be hidden from other people in the channel.
# If set to false, members of the Discord guild will be able to see when you use this command as well as its response.
hide-command: true
# List of user IDs or role names/IDs allowed to use this command (or '*' to allow anyone to access it).
allowed-roles:
- "*"
# List of user IDs or role names/IDs who can see vanished players in the player list. If '*' is used, all people
# on Discord would be allowed to see vanished players (and therefore expose they are actually online).
admin-roles:
- "Admins"
- "123456789012345678"
# Whether or not links to attachments in Discord messages should be displayed in chat or not.
# If this is set to false and a message from Discord only contains an image/file and not any text, nothing will be sent.
show-discord-attachments: true
# A list of roles allowed to send Minecraft color/formatting codes from Discord to MC.
# This applies to all aspects such as that Discord->MC chat relay as well as commands.
# You can either use '*' (for everyone), a role name/ID, or a user ID.
permit-formatting-roles:
- "Admins"
- "Color Codes"
# The presence of the bot, including its status, activity and status message.
presence:
# The online status of the bot. Must be one of the following:
# - "online": Shows as green circle (Online)
# - "idle": Shows as yellow half-circle (Away)
# - "dnd": Shows as red circle (Do Not Disturb)
# - "invisible": Makes the bot appear offline
status: online
# The activity of the bot to be prefixed before your message below. Must be one of the following;
# - "playing": Shows up as "Playing <message>"
# - "listening": Shows up as "Listening to <message>"
# - "watching": Shows up as "Watching <message>"
# - "competing": Shows up as "Competing in <message>"
# - "none": Don't show any activity message
activity: "playing"
# The activity status message.
message: "Minecraft"
# The following entries allow you to customize the formatting of messages sent by the plugin.
# Each message has a description of how it is used along with placeholders that can be used.
messages:
# This is the message that is used to show discord chat to players in game.
# Color/formatting codes and the follow placeholders can be used here:
# - {channel}: The name of the discord channel the message was sent from
# - {username}: The username of the user who sent the message
# - {discriminator}: The four numbers displayed after the user's name
# - {fullname}: Equivalent to typing "{username}#{discriminator}"
# - {nickname}: The nickname of the user who sent the message. (Will return username if user has no nickname)
# - {color}: The minecraft color representative of the user's topmost role color on discord. If the user doesn't have a role color, the placeholder is empty.
# - {message}: The content of the message being sent
discord-to-mc: "&6[#{channel}] &3{fullname}&7: &f{message}"
# This is the message that is used to relay minecraft chat in discord.
# The following placeholders can be used here:
# - {username}: The username of the player sending the message
# - {displayname}: The display name of the player sending the message (This would be their nickname)
# - {message}: The content of the message being sent
# - {world}: The name of the world the player sending the message is in
# - {prefix}: The prefix of the player sending the message
# - {suffix}: The suffix of the player sending the message
# ... PlaceholderAPI placeholders are also supported here too!
mc-to-discord: "{displayname}: {message}"
# This is the message sent to discord when a player is temporarily muted in minecraft.
# The following placeholders can be used here:
# - {username}: The username of the player being muted
# - {displayname}: The display name of the player being muted
# - {controllername}: The username of the user who muted the player
# - {controllerdisplayname}: The display name of the user who muted the player
# - {time}: The amount of time the player was muted for
temporary-mute: "{controllerdisplayname} has muted player {displayname} for {time}."
# This is the message sent to discord when a player is temporarily muted (with a reason specified) in minecraft.
# The following placeholders can be used here:
# - {username}: The username of the player being muted
# - {displayname}: The display name of the player being muted
# - {controllername}: The username of the user who muted the player
# - {controllerdisplayname}: The display name of the user who muted the player
# - {time}: The amount of time the player was muted for
# - {reason}: The reason the player was muted for
temporary-mute-reason: "{controllerdisplayname} has muted player {displayname} for {time}. Reason: {reason}."
# This is the message sent to discord when a player is permanently muted in minecraft.
# The following placeholders can be used here:
# - {username}: The username of the player being muted
# - {displayname}: The display name of the player being muted
# - {controllername}: The username of the user who muted the player
# - {controllerdisplayname}: The display name of the user who muted the player
permanent-mute: "{controllerdisplayname} has muted player {displayname}."
# This is the message sent to discord when a player is permanently muted (with a reason specified) in minecraft.
# The following placeholders can be used here:
# - {username}: The username of the player being muted
# - {displayname}: The display name of the player being muted
# - {controllername}: The username of the user who muted the player
# - {controllerdisplayname}: The display name of the user who muted the player
# - {reason}: The reason the player was muted for
permanent-mute-reason: "{controllerdisplayname} has permanently muted player {displayname}. Reason: {reason}."
# This is the message sent to discord when a player is unmuted in minecraft.
# The following placeholders can be used here:
# - {username}: The username of the player being unmuted
# - {displayname}: The display name of the player being unmuted
unmute: "{displayname} unmuted."
# This is the message sent to discord when a player joins the minecraft server.
# The following placeholders can be used here:
# - {username}: The name of the user joining
# - {displayname}: The display name of the user joining
# - {joinmessage}: The full default join message used in game
# ... PlaceholderAPI placeholders are also supported here too!
join: ":arrow_right: {displayname} has joined!"
# This is the message sent to discord when a player leaves the minecraft server.
# The following placeholders can be used here:
# - {username}: The name of the user leaving
# - {displayname}: The display name of the user leaving
# - {quitmessage}: The full default leave message used in game
# ... PlaceholderAPI placeholders are also supported here too!
quit: ":arrow_left: {displayname} has left!"
# This is the message sent to discord when a player dies.
# The following placeholders can be used here:
# - {username}: The name of the user who died
# - {displayname}: The display name of the user who died
# - {deathmessage}: The full default death message used in game
# ... PlaceholderAPI placeholders are also supported here too!
death: ":skull: {deathmessage}"
# This is the message sent to discord when a player becomes afk.
# The following placeholders can be used here:
# - {username}: The name of the user who became afk
# - {displayname}: The display name of the user who became afk
# ... PlaceholderAPI placeholders are also supported here too!
afk: ":person_walking: {displayname} is now AFK!"
# This is the message sent to discord when a player is no longer afk.
# The following placeholders can be used here:
# - {username}: The name of the user who is no longer afk
# - {displayname}: The display name of the user who is no longer afk
# ... PlaceholderAPI placeholders are also supported here too!
un-afk: ":keyboard: {displayname} is no longer AFK!"
# This is the message sent to discord when a player is kicked from the server.
# The following placeholders can be used here:
# - {username}: The name of the user who got kicked
# - {displayname}: The display name of the user who got kicked
# - {reason}: The reason the player was kicked
kick: "{displayname} was kicked with reason: {reason}"

View File

@ -0,0 +1,10 @@
name: EssentialsDiscord
main: net.essentialsx.discord.EssentialsDiscord
# Note to developers: This next line cannot change, or the automatic versioning system will break.
version: ${full.version}
website: https://essentialsx.net/
description: Provides integration between Minecraft and Discord servers.
authors: [mdcfe, JRoy, pop4959, Glare]
depend: [Essentials]
softdepend: [EssentialsChat, PlaceholderAPI]
api-version: 1.13

View File

@ -0,0 +1,150 @@
package net.ess3.provider.providers;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.Plugin;
import java.util.Set;
import java.util.UUID;
public class BukkitSenderProvider implements CommandSender {
private final ConsoleCommandSender base;
private final MessageHook hook;
public BukkitSenderProvider(ConsoleCommandSender base, MessageHook hook) {
this.base = base;
this.hook = hook;
}
public interface MessageHook {
void sendMessage(String message);
}
@Override
public void sendMessage(String message) {
hook.sendMessage(message);
}
@Override
public void sendMessage(String[] messages) {
for (String msg : messages) {
sendMessage(msg);
}
}
@Override
public void sendMessage(UUID uuid, String message) {
sendMessage(message);
}
@Override
public void sendMessage(UUID uuid, String[] messages) {
sendMessage(messages);
}
@Override
public Server getServer() {
return base.getServer();
}
@Override
public String getName() {
return base.getName();
}
@Override
public Spigot spigot() {
return new Spigot() {
@Override
public void sendMessage(BaseComponent component) {
BukkitSenderProvider.this.sendMessage(component.toLegacyText());
}
@Override
public void sendMessage(BaseComponent... components) {
sendMessage(new TextComponent(components));
}
@Override
public void sendMessage(UUID sender, BaseComponent... components) {
sendMessage(components);
}
@Override
public void sendMessage(UUID sender, BaseComponent component) {
sendMessage(component);
}
};
}
@Override
public boolean isPermissionSet(String name) {
return base.isPermissionSet(name);
}
@Override
public boolean isPermissionSet(Permission perm) {
return base.isPermissionSet(perm);
}
@Override
public boolean hasPermission(String name) {
return base.hasPermission(name);
}
@Override
public boolean hasPermission(Permission perm) {
return base.hasPermission(perm);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
return base.addAttachment(plugin, name, value);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin) {
return base.addAttachment(plugin);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
return base.addAttachment(plugin, name, value, ticks);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, int ticks) {
return base.addAttachment(plugin, ticks);
}
@Override
public void removeAttachment(PermissionAttachment attachment) {
base.removeAttachment(attachment);
}
@Override
public void recalculatePermissions() {
base.recalculatePermissions();
}
@Override
public Set<PermissionAttachmentInfo> getEffectivePermissions() {
return base.getEffectivePermissions();
}
@Override
public boolean isOp() {
return base.isOp();
}
@Override
public void setOp(boolean value) {
base.setOp(value);
}
}

View File

@ -0,0 +1,79 @@
package net.ess3.provider.providers;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identified;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.bukkit.Bukkit;
import org.bukkit.command.ConsoleCommandSender;
public class PaperCommandSender extends BukkitSenderProvider {
public PaperCommandSender(ConsoleCommandSender base, MessageHook hook) {
super(base, hook);
}
@Override
public void sendMessage(Identity identity, Component message, MessageType type) {
sendDumbComponent(message);
}
@Override
public void sendMessage(ComponentLike message) {
sendDumbComponent(message.asComponent());
}
@Override
public void sendMessage(Identified source, ComponentLike message) {
sendDumbComponent(message.asComponent());
}
@Override
public void sendMessage(Identity source, ComponentLike message) {
sendDumbComponent(message.asComponent());
}
@Override
public void sendMessage(Component message) {
sendDumbComponent(message);
}
@Override
public void sendMessage(Identified source, Component message) {
sendDumbComponent(message);
}
@Override
public void sendMessage(Identity source, Component message) {
sendDumbComponent(message);
}
@Override
public void sendMessage(ComponentLike message, MessageType type) {
sendDumbComponent(message.asComponent());
}
@Override
public void sendMessage(Identified source, ComponentLike message, MessageType type) {
sendDumbComponent(message.asComponent());
}
@Override
public void sendMessage(Identity source, ComponentLike message, MessageType type) {
sendDumbComponent(message.asComponent());
}
@Override
public void sendMessage(Component message, MessageType type) {
sendDumbComponent(message);
}
@Override
public void sendMessage(Identified source, Component message, MessageType type) {
sendDumbComponent(message);
}
public void sendDumbComponent(Component message) {
sendMessage(Bukkit.getUnsafe().legacyComponentSerializer().serialize(message));
}
}

View File

@ -8,6 +8,12 @@ dependencyResolutionManagement {
maven("https://repo.codemc.org/repository/maven-public") {
content { includeGroup("org.bstats") }
}
maven("https://m2.dv8tion.net/releases/") {
content { includeGroup("net.dv8tion") }
}
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") {
content { includeGroup("me.clip") }
}
mavenCentral {
content { includeGroup("net.kyori") }
}
@ -26,6 +32,7 @@ sequenceOf(
"",
"AntiBuild",
"Chat",
"Discord",
"GeoIP",
"Protect",
"Spawn",