diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java
index c734719df2d..67be6b14d89 100644
--- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java
+++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java
@@ -424,6 +424,8 @@ public interface ISettings extends IConf {
boolean showZeroBaltop();
+ String getNickRegex();
+
BigDecimal getMultiplier(final User user);
int getMaxItemLore();
diff --git a/Essentials/src/main/java/com/earth2me/essentials/IUser.java b/Essentials/src/main/java/com/earth2me/essentials/IUser.java
index bd3b3ec1957..8fce2df96d0 100644
--- a/Essentials/src/main/java/com/earth2me/essentials/IUser.java
+++ b/Essentials/src/main/java/com/earth2me/essentials/IUser.java
@@ -177,6 +177,17 @@ default boolean hasOutstandingTeleportRequest() {
String getFormattedJailTime();
+ /**
+ * Returns last activity time.
+ *
+ * It is used internally to determine if user's afk status should be set to
+ * true because of ACTIVITY {@link AfkStatusChangeEvent.Cause}, or the player
+ * should be kicked for being afk too long.
+ *
+ * @return Last activity time (Epoch Milliseconds)
+ */
+ long getLastActivityTime();
+
@Deprecated
List getMails();
diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java
index f04347ae4af..8980aa97b3f 100644
--- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java
+++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java
@@ -2120,6 +2120,11 @@ public boolean showZeroBaltop() {
return config.getBoolean("show-zero-baltop", true);
}
+ @Override
+ public String getNickRegex() {
+ return config.getString("allowed-nicks-regex", "^[a-zA-Z_0-9§]+$");
+ }
+
@Override
public BigDecimal getMultiplier(final User user) {
BigDecimal multiplier = defaultMultiplier;
diff --git a/Essentials/src/main/java/com/earth2me/essentials/User.java b/Essentials/src/main/java/com/earth2me/essentials/User.java
index df60ff776bf..72f6feaad6c 100644
--- a/Essentials/src/main/java/com/earth2me/essentials/User.java
+++ b/Essentials/src/main/java/com/earth2me/essentials/User.java
@@ -775,6 +775,11 @@ public boolean checkMuteTimeout(final long currentTime) {
return false;
}
+ @Override
+ public long getLastActivityTime() {
+ return this.lastActivity;
+ }
+
@Deprecated
public void updateActivity(final boolean broadcast) {
updateActivity(broadcast, AfkStatusChangeEvent.Cause.UNKNOWN);
diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnick.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnick.java
index 8b47afb024e..7648cbc74f5 100644
--- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnick.java
+++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandnick.java
@@ -63,7 +63,7 @@ protected void updatePlayer(final Server server, final CommandSource sender, fin
private String formatNickname(final User user, final String nick) throws Exception {
final String newNick = user == null ? FormatUtil.replaceFormat(nick) : FormatUtil.formatString(user, "essentials.nick", nick);
- if (!newNick.matches("^[a-zA-Z_0-9" + ChatColor.COLOR_CHAR + "]+$") && user != null && !user.isAuthorized("essentials.nick.allowunsafe")) {
+ if (!newNick.matches(ess.getSettings().getNickRegex()) && user != null && !user.isAuthorized("essentials.nick.allowunsafe")) {
throw new TranslatableException("nickNamesAlpha");
} else if (getNickLength(newNick) > ess.getSettings().getMaxNickLength()) {
throw new TranslatableException("nickTooLong");
diff --git a/Essentials/src/main/java/com/earth2me/essentials/signs/SignBuy.java b/Essentials/src/main/java/com/earth2me/essentials/signs/SignBuy.java
index 8b59f79f62b..ef6508c0987 100644
--- a/Essentials/src/main/java/com/earth2me/essentials/signs/SignBuy.java
+++ b/Essentials/src/main/java/com/earth2me/essentials/signs/SignBuy.java
@@ -3,6 +3,7 @@
import com.earth2me.essentials.ChargeException;
import com.earth2me.essentials.Trade;
import com.earth2me.essentials.User;
+import net.ess3.api.events.SignTransactionEvent;
import net.ess3.api.IEssentials;
import net.ess3.api.MaxMoneyException;
import org.bukkit.inventory.ItemStack;
@@ -45,6 +46,12 @@ protected boolean onSignInteract(final ISign sign, final User player, final Stri
}
charge.isAffordableFor(player);
+ final SignTransactionEvent signTransactionEvent = new SignTransactionEvent(sign, this, player, items.getItemStack(), SignTransactionEvent.TransactionType.BUY, charge.getMoney());
+
+ ess.getServer().getPluginManager().callEvent(signTransactionEvent);
+ if (signTransactionEvent.isCancelled()) {
+ return true;
+ }
if (!items.pay(player)) {
throw new ChargeException("inventoryFull");
}
diff --git a/Essentials/src/main/java/com/earth2me/essentials/signs/SignSell.java b/Essentials/src/main/java/com/earth2me/essentials/signs/SignSell.java
index 5841e2b6ee6..34b8a23ffa3 100644
--- a/Essentials/src/main/java/com/earth2me/essentials/signs/SignSell.java
+++ b/Essentials/src/main/java/com/earth2me/essentials/signs/SignSell.java
@@ -4,6 +4,7 @@
import com.earth2me.essentials.Trade;
import com.earth2me.essentials.Trade.OverflowType;
import com.earth2me.essentials.User;
+import net.ess3.api.events.SignTransactionEvent;
import net.ess3.api.IEssentials;
import net.ess3.api.MaxMoneyException;
import org.bukkit.inventory.ItemStack;
@@ -47,6 +48,13 @@ protected boolean onSignInteract(final ISign sign, final User player, final Stri
}
charge.isAffordableFor(player);
+
+ final SignTransactionEvent signTransactionEvent = new SignTransactionEvent(sign, this, player, charge.getItemStack(), SignTransactionEvent.TransactionType.SELL, money.getMoney());
+ ess.getServer().getPluginManager().callEvent(signTransactionEvent);
+ if (signTransactionEvent.isCancelled()) {
+ return false;
+ }
+
money.pay(player, OverflowType.DROP);
charge.charge(player);
Trade.log("Sign", "Sell", "Interact", username, charge, username, money, sign.getBlock().getLocation(), player.getMoney(), ess);
diff --git a/Essentials/src/main/java/net/ess3/api/events/SignTransactionEvent.java b/Essentials/src/main/java/net/ess3/api/events/SignTransactionEvent.java
new file mode 100644
index 00000000000..18dac137ad1
--- /dev/null
+++ b/Essentials/src/main/java/net/ess3/api/events/SignTransactionEvent.java
@@ -0,0 +1,79 @@
+package net.ess3.api.events;
+
+import com.earth2me.essentials.signs.EssentialsSign;
+import net.ess3.api.IUser;
+import org.bukkit.event.Cancellable;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.bukkit.event.HandlerList;
+
+import java.math.BigDecimal;
+
+/**
+ * Fired when a player either buys or sells from an Essentials sign
+ */
+public final class SignTransactionEvent extends SignInteractEvent implements Cancellable {
+ private static final HandlerList handlers = new HandlerList();
+ private final ItemStack itemStack;
+ private final TransactionType transactionType;
+ private final BigDecimal transactionValue;
+ private boolean isCancelled = false;
+
+ public SignTransactionEvent(EssentialsSign.ISign sign, EssentialsSign essSign, IUser user, ItemStack itemStack, TransactionType transactionType, BigDecimal transactionValue) {
+ super(sign, essSign, user);
+ this.itemStack = itemStack;
+ this.transactionType = transactionType;
+ this.transactionValue = transactionValue;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return this.isCancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.isCancelled = cancelled;
+ }
+
+ /**
+ * Gets the ItemStack that is about to be bought or sold in this transition.
+ * @return The ItemStack being bought or sold.
+ */
+ public @NotNull ItemStack getItemStack() {
+ return itemStack.clone();
+ }
+
+ /**
+ * Gets the type of transaction, either buy or sell.
+ * @return The transaction type.
+ */
+ public @NotNull TransactionType getTransactionType() {
+ return transactionType;
+ }
+
+ /**
+ * Gets the value of the item being bought or sold.
+ * @return The item's value.
+ */
+ public BigDecimal getTransactionValue() {
+ return transactionValue;
+ }
+
+ /**
+ * The type of transaction for this sign transaction.
+ */
+ public enum TransactionType {
+ BUY,
+ SELL
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml
index 168dafa510b..6b8259cacb7 100644
--- a/Essentials/src/main/resources/config.yml
+++ b/Essentials/src/main/resources/config.yml
@@ -33,6 +33,11 @@ nickname-prefix: '~'
# The maximum length allowed in nicknames. The nickname prefix is not included in this.
max-nick-length: 15
+# The regex pattern used to determine if a requested nickname should be allowed for use.
+# If the a requested nickname does not matched this pattern, the nickname will be rejected.
+# Users with essentials.nick.allowunsafe will be able to bypass this check.
+allowed-nicks-regex: '^[a-zA-Z_0-9§]+$'
+
# A list of phrases that cannot be used in nicknames. You can include regular expressions here.
# Users with essentials.nick.blacklist.bypass will be able to bypass this filter.
nick-blacklist:
diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java
index 567bfbde553..4733e7d2a7b 100644
--- a/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java
+++ b/EssentialsDiscord/src/main/java/net/essentialsx/api/v2/services/discord/MessageType.java
@@ -57,6 +57,7 @@ public static final class DefaultTypes {
public final static MessageType FIRST_JOIN = new MessageType("first-join", true);
public final static MessageType LEAVE = new MessageType("leave", true);
public final static MessageType CHAT = new MessageType("chat", true);
+ public final static MessageType PRIVATE_CHAT = new MessageType("private-chat", true);
public final static MessageType DEATH = new MessageType("death", true);
public final static MessageType AFK = new MessageType("afk", true);
public final static MessageType ADVANCEMENT = new MessageType("advancement", true);
@@ -68,7 +69,7 @@ public static final class DefaultTypes {
public final static MessageType LOCAL = new MessageType("local", true);
public final static MessageType QUESTION = new MessageType("question", true);
public final static MessageType SHOUT = new MessageType("shout", true);
- private final static MessageType[] VALUES = new MessageType[]{JOIN, FIRST_JOIN, LEAVE, CHAT, DEATH, AFK, ADVANCEMENT, ACTION, SERVER_START, SERVER_STOP, KICK, MUTE, LOCAL, QUESTION, SHOUT};
+ private final static MessageType[] VALUES = new MessageType[]{JOIN, FIRST_JOIN, LEAVE, CHAT, PRIVATE_CHAT, DEATH, AFK, ADVANCEMENT, ACTION, SERVER_START, SERVER_STOP, KICK, MUTE, LOCAL, QUESTION, SHOUT};
/**
* Gets an array of all the default {@link MessageType MessageTypes}.
diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java
index 72c1ca5b406..50869e8bf34 100644
--- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java
+++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/DiscordSettings.java
@@ -50,6 +50,7 @@ public class DiscordSettings implements IConf {
private MessageFormat permMuteReasonFormat;
private MessageFormat unmuteFormat;
private MessageFormat kickFormat;
+ private MessageFormat pmToDiscordFormat;
public DiscordSettings(EssentialsDiscord plugin) {
this.plugin = plugin;
@@ -445,6 +446,10 @@ public MessageFormat getKickFormat() {
return kickFormat;
}
+ public MessageFormat getPmToDiscordFormat() {
+ return pmToDiscordFormat;
+ }
+
private String getFormatString(String node) {
final String pathPrefix = node.startsWith(".") ? "" : "messages.";
return config.getString(pathPrefix + (pathPrefix.isEmpty() ? node.substring(1) : node), null);
@@ -581,6 +586,8 @@ public void reloadConfig() {
"username", "displayname", "controllername", "controllerdisplayname", "reason");
kickFormat = generateMessageFormat(getFormatString("kick"), "{displayname} was kicked with reason: {reason}", false,
"username", "displayname", "reason");
+ pmToDiscordFormat = generateMessageFormat(getFormatString("private-chat"), "[SocialSpy] {sender-username} -> {receiver-username}: {message}", false,
+ "sender-username", "sender-displayname", "receiver-username", "receiver-displayname", "message");
plugin.onReload();
}
diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java
index 6e471786d52..6a1727cf647 100644
--- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java
+++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java
@@ -4,6 +4,7 @@
import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.VersionUtil;
+import net.ess3.api.events.PrivateMessageSentEvent;
import net.ess3.api.IUser;
import net.ess3.api.events.AfkStatusChangeEvent;
import net.ess3.api.events.MuteStatusChangeEvent;
@@ -47,6 +48,23 @@ public void onDiscordMessage(DiscordMessageEvent event) {
// Bukkit Events
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onPrivateMessage(PrivateMessageSentEvent event) {
+
+ if (event.getSender() instanceof IUser && ((IUser) event.getSender()).isAuthorized("essentials.chat.spy.exempt")) {
+ return;
+ }
+
+ sendDiscordMessage(MessageType.DefaultTypes.PRIVATE_CHAT,
+ MessageUtil.formatMessage(jda.getSettings().getPmToDiscordFormat(),
+ MessageUtil.sanitizeDiscordMarkdown(event.getSender().getName()),
+ MessageUtil.sanitizeDiscordMarkdown(event.getSender().getDisplayName()),
+ MessageUtil.sanitizeDiscordMarkdown(event.getRecipient().getName()),
+ MessageUtil.sanitizeDiscordMarkdown(event.getRecipient().getDisplayName()),
+ MessageUtil.sanitizeDiscordMarkdown(event.getMessage())),
+ event.getSender() instanceof IUser ? ((IUser) event.getSender()).getBase() : null);
+ }
+
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onMute(MuteStatusChangeEvent event) {
if (!event.getValue()) {
diff --git a/EssentialsDiscord/src/main/resources/config.yml b/EssentialsDiscord/src/main/resources/config.yml
index 05382ddde9e..3860166b77e 100644
--- a/EssentialsDiscord/src/main/resources/config.yml
+++ b/EssentialsDiscord/src/main/resources/config.yml
@@ -144,6 +144,8 @@ message-types:
kick: staff
# Message sent when a player's mute state is changed on the Minecraft server.
mute: staff
+ # Message sent when a private message (/msg, /whisper, etc.) is sent on the Minecraft Server.
+ private-chat: none
# Message sent when a player talks in local chat.
# use-essentials-events must be set to "true" for this to work.
local: none
@@ -433,3 +435,11 @@ messages:
# - {displayname}: The display name of the user who got kicked
# - {reason}: The reason the player was kicked
kick: "{displayname} was kicked with reason: {reason}"
+ # This is the message that is used to relay minecraft private messages in Discord.
+ # The following placeholders can be used here:
+ # - {sender-username}: The username of the player sending the message
+ # - {sender-displayname}: The display name of the player sending the message (This would be their nickname)
+ # - {receiver-username}: The username of the player receiving the message
+ # - {receiver-displayname}: The display name of the player receiving the message (This would be their nickname)
+ # - {message}: The content of the message being sent
+ pms-to-discord: "[SocialSpy] {sender-username} -> {receiver-username}: {message}"