306 lines
13 KiB
Java
306 lines
13 KiB
Java
/*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package fr.neatmonster.nocheatplus.checks.chat;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import org.bukkit.Location;
|
|
import org.bukkit.command.Command;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.EventPriority;
|
|
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
|
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
|
import org.bukkit.event.player.PlayerLoginEvent;
|
|
import org.bukkit.event.player.PlayerLoginEvent.Result;
|
|
|
|
import fr.neatmonster.nocheatplus.NCPAPIProvider;
|
|
import fr.neatmonster.nocheatplus.checks.CheckListener;
|
|
import fr.neatmonster.nocheatplus.checks.CheckType;
|
|
import fr.neatmonster.nocheatplus.checks.moving.MovingConfig;
|
|
import fr.neatmonster.nocheatplus.checks.moving.util.MovingUtil;
|
|
import fr.neatmonster.nocheatplus.command.CommandUtil;
|
|
import fr.neatmonster.nocheatplus.compat.BridgeMisc;
|
|
import fr.neatmonster.nocheatplus.components.registry.feature.INotifyReload;
|
|
import fr.neatmonster.nocheatplus.components.registry.feature.JoinLeaveListener;
|
|
import fr.neatmonster.nocheatplus.config.ConfPaths;
|
|
import fr.neatmonster.nocheatplus.config.ConfigFile;
|
|
import fr.neatmonster.nocheatplus.config.ConfigManager;
|
|
import fr.neatmonster.nocheatplus.logging.Streams;
|
|
import fr.neatmonster.nocheatplus.players.DataManager;
|
|
import fr.neatmonster.nocheatplus.players.IPlayerData;
|
|
import fr.neatmonster.nocheatplus.utilities.StringUtil;
|
|
import fr.neatmonster.nocheatplus.utilities.ds.prefixtree.SimpleCharPrefixTree;
|
|
|
|
/**
|
|
* Central location to listen to events that are relevant for the chat checks.
|
|
*
|
|
* @see ChatEvent
|
|
*/
|
|
public class ChatListener extends CheckListener implements INotifyReload, JoinLeaveListener {
|
|
|
|
// Checks.
|
|
|
|
/** Captcha handler. */
|
|
private final Captcha captcha = addCheck(new Captcha());
|
|
|
|
/** The color check. */
|
|
private final Color color = addCheck(new Color());
|
|
|
|
/** Commands repetition check. */
|
|
private final Commands commands = addCheck(new Commands());
|
|
|
|
/** Logins check (global) */
|
|
private final Logins logins = addCheck(new Logins());
|
|
|
|
/** Chat message check. */
|
|
private final Text text = addCheck(new Text());
|
|
|
|
/** Relogging check. */
|
|
private final Relog relog = addCheck(new Relog());
|
|
|
|
// Auxiliary stuff.
|
|
|
|
/** Commands to be ignored completely. */
|
|
private final SimpleCharPrefixTree commandExclusions = new SimpleCharPrefixTree();
|
|
|
|
/** Commands to be handled as chat. */
|
|
private final SimpleCharPrefixTree chatCommands = new SimpleCharPrefixTree();
|
|
|
|
/** Commands not to be executed in-game. */
|
|
private final SimpleCharPrefixTree consoleOnlyCommands = new SimpleCharPrefixTree();
|
|
|
|
/** Set world to null after use, primary thread only. */
|
|
private final Location useLoc = new Location(null, 0, 0, 0);
|
|
|
|
public ChatListener() {
|
|
super(CheckType.CHAT);
|
|
ConfigFile config = ConfigManager.getConfigFile();
|
|
initFilters(config);
|
|
// (text inits in constructor.)
|
|
}
|
|
|
|
private void initFilters(ConfigFile config) {
|
|
CommandUtil.feedCommands(consoleOnlyCommands, config, ConfPaths.PROTECT_COMMANDS_CONSOLEONLY_CMDS, true);
|
|
CommandUtil.feedCommands(chatCommands, config, ConfPaths.CHAT_COMMANDS_HANDLEASCHAT, true);
|
|
CommandUtil.feedCommands(commandExclusions, config, ConfPaths.CHAT_COMMANDS_EXCLUSIONS, true);
|
|
}
|
|
|
|
/**
|
|
* We listen to PlayerChat events for obvious reasons.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST)
|
|
public void onPlayerChat(final AsyncPlayerChatEvent event) {
|
|
|
|
final Player player = event.getPlayer();
|
|
final boolean alreadyCancelled = event.isCancelled();
|
|
|
|
// Tell TickTask to update cached permissions.
|
|
// (Might omit this if already cancelled.)
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final ChatConfig cc = pData.getGenericInstance(ChatConfig.class);
|
|
|
|
// First the color check.
|
|
if (!alreadyCancelled && color.isEnabled(player, pData)) {
|
|
event.setMessage(color.check(player, event.getMessage(), false));
|
|
}
|
|
|
|
// Then the no chat check.
|
|
// TODO: isMainThread: Could consider event.isAsync ?
|
|
if (textChecks(player, event.getMessage(), cc, pData, false, alreadyCancelled)) {
|
|
event.setCancelled(true);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* We listen to PlayerCommandPreprocess events because commands can be used for spamming too.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(priority = EventPriority.LOWEST)
|
|
public void onPlayerCommandPreprocess(final PlayerCommandPreprocessEvent event) {
|
|
final Player player = event.getPlayer();
|
|
|
|
// Tell TickTask to update cached permissions.
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final ChatConfig cc = pData.getGenericInstance(ChatConfig.class);
|
|
|
|
// Checks that replace parts of the message (color).
|
|
if (color.isEnabled(player, pData)) {
|
|
event.setMessage(color.check(player, event.getMessage(), true));
|
|
}
|
|
|
|
// Left-trim is necessary because the server accepts leading spaces with commands.
|
|
final String message = event.getMessage();
|
|
final String lcMessage = StringUtil.leftTrim(message).toLowerCase();
|
|
// TODO: Remove bukkit: etc.
|
|
|
|
final String[] split = lcMessage.split(" ", 2);
|
|
final String alias = split[0].substring(1);
|
|
final Command command = CommandUtil.getCommand(alias);
|
|
|
|
final List<String> messageVars = new ArrayList<String>(); // Could as well use an array and allow null on input of SimpleCharPrefixTree.
|
|
messageVars.add(lcMessage);
|
|
String checkMessage = message; // Message to run chat checks on.
|
|
if (command != null) {
|
|
messageVars.add("/" + command.getLabel().toLowerCase() + (split.length > 1 ? (" " + split[1]) : ""));
|
|
}
|
|
if (alias.indexOf(":") != -1) {
|
|
final int index = message.indexOf(":") + 1;
|
|
if (index < message.length()) {
|
|
checkMessage = message.substring(index);
|
|
messageVars.add(checkMessage.toLowerCase());
|
|
}
|
|
}
|
|
// Prevent commands from being used by players (e.g. /op /deop /reload).
|
|
if (cc.consoleOnlyCheck && consoleOnlyCommands.hasAnyPrefixWords(messageVars)) {
|
|
if (command == null || command.testPermission(player)) {
|
|
player.sendMessage(cc.consoleOnlyMessage);
|
|
}
|
|
event.setCancelled(true);
|
|
return;
|
|
}
|
|
|
|
// Handle as chat or command.
|
|
if (chatCommands.hasAnyPrefixWords(messageVars)) {
|
|
// Treat as chat.
|
|
// TODO: Consider requesting permission updates on these, for consistency.
|
|
// TODO: Cut off the command ?.
|
|
if (textChecks(player, checkMessage, cc, pData, true, false)) {
|
|
event.setCancelled(true);
|
|
}
|
|
}
|
|
else if (!commandExclusions.hasAnyPrefixWords(messageVars)) {
|
|
// Treat as command.
|
|
if (commands.isEnabled(player, pData) && commands.check(player, checkMessage, cc, pData, captcha)) {
|
|
event.setCancelled(true);
|
|
}
|
|
else {
|
|
// TODO: Consider always checking these?
|
|
// Note that this checks for prefixes, not prefix words.
|
|
final MovingConfig mcc = pData.getGenericInstance(MovingConfig.class);
|
|
if (mcc.passableUntrackedCommandCheck && mcc.passableUntrackedCommandPrefixes.hasAnyPrefix(messageVars)) {
|
|
if (checkUntrackedLocation(player, message, mcc, pData)) {
|
|
event.setCancelled(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private boolean checkUntrackedLocation(final Player player, final String message,
|
|
final MovingConfig mcc, final IPlayerData pData) {
|
|
final Location loc = player.getLocation(useLoc);
|
|
boolean cancel = false;
|
|
if (MovingUtil.shouldCheckUntrackedLocation(player, loc, pData)) {
|
|
final Location newTo = MovingUtil.checkUntrackedLocation(loc);
|
|
if (newTo != null) {
|
|
if (mcc.passableUntrackedCommandTryTeleport
|
|
&& player.teleport(newTo, BridgeMisc.TELEPORT_CAUSE_CORRECTION_OF_POSITION)) {
|
|
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().info(Streams.TRACE_FILE, player.getName() + " runs the command '" + message + "' at an untracked location: " + loc + " , teleport to: " + newTo);
|
|
} else {
|
|
// TODO: Allow disabling cancel?
|
|
// TODO: Should message the player?
|
|
NCPAPIProvider.getNoCheatPlusAPI().getLogManager().info(Streams.TRACE_FILE, player.getName() + " runs the command '" + message + "' at an untracked location: " + loc + " , cancel the command.");
|
|
cancel = true;
|
|
}
|
|
}
|
|
}
|
|
useLoc.setWorld(null); // Cleanup.
|
|
return cancel;
|
|
}
|
|
|
|
private boolean textChecks(final Player player, final String message,
|
|
final ChatConfig cc, final IPlayerData pData,
|
|
final boolean isMainThread, final boolean alreadyCancelled) {
|
|
return text.isEnabled(player, pData) && text.check(player, message, cc, pData,
|
|
captcha, isMainThread, alreadyCancelled);
|
|
}
|
|
|
|
/**
|
|
* We listen to this type of events to prevent spambots from login to the server.
|
|
*
|
|
* @param event
|
|
* the event
|
|
*/
|
|
@EventHandler(
|
|
priority = EventPriority.NORMAL)
|
|
public void onPlayerLogin(final PlayerLoginEvent event) {
|
|
if (event.getResult() != Result.ALLOWED) return;
|
|
final Player player = event.getPlayer();
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final ChatConfig cc = pData.getGenericInstance(ChatConfig.class);
|
|
final ChatData data = pData.getGenericInstance(ChatData.class);
|
|
|
|
// (No forced permission update, because the associated permissions are treated as hints rather.)
|
|
|
|
// Reset captcha of player if needed.
|
|
synchronized(data) {
|
|
captcha.resetCaptcha(player, cc, data, pData);
|
|
}
|
|
// Fast relog check.
|
|
if (relog.isEnabled(player, pData) && relog.unsafeLoginCheck(player, cc, data,pData)) {
|
|
event.disallow(Result.KICK_OTHER, cc.relogKickMessage);
|
|
}
|
|
else if (logins.isEnabled(player, pData) && logins.check(player, cc, data)) {
|
|
event.disallow(Result.KICK_OTHER, cc.loginsKickMessage);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onReload() {
|
|
// Read some things from the global config file.
|
|
ConfigFile config = ConfigManager.getConfigFile();
|
|
initFilters(config);
|
|
text.onReload();
|
|
logins.onReload();
|
|
}
|
|
|
|
@Override
|
|
public void playerJoins(final Player player) {
|
|
final IPlayerData pData = DataManager.getPlayerData(player);
|
|
final ChatConfig cc = pData.getGenericInstance(ChatConfig.class);
|
|
final ChatData data = pData.getGenericInstance(ChatData.class);
|
|
/*
|
|
* TODO: The isEnabled check must be done with IWorldData (no locking).
|
|
* Specifically because enabling/disabling checks should be done in the
|
|
* primary thread, regardless of the capabilities of WorldDataManager
|
|
* implementation.
|
|
*/
|
|
synchronized (data) {
|
|
if (captcha.isEnabled(player, pData)) {
|
|
if (captcha.shouldCheckCaptcha(player, cc, data, pData)) {
|
|
// shouldCheckCaptcha: only if really enabled.
|
|
// TODO: Later: add check for cc.captchaOnLogin or so (before shouldCheckCaptcha).
|
|
// TODO: maybe schedule this to come after other plugins messages.
|
|
captcha.sendNewCaptcha(player, cc, data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void playerLeaves(final Player player) {
|
|
}
|
|
|
|
}
|