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}"