Add more chat control.
* A certain amount of characters in the message must be latin-ish, if the message is longer than a certain length. * There must be a certain number of spaces in the message, if the message is longer than a certain length. * The same message cannot be repeated twice in the same location. * Switched the separator between parts for the chat control config value from _ to . for the scope.
This commit is contained in:
parent
0cf261f6e4
commit
4bddace596
|
@ -148,8 +148,10 @@ cah.$.ErrorCode.CARDCAST_INVALID_ID = "cii";
|
|||
cah.$.ErrorCode.TOO_FAST = "tf";
|
||||
cah.$.ErrorCode.NOT_ENOUGH_CARDS = "nec";
|
||||
cah.$.ErrorCode.NO_CARD_SPECIFIED = "ncs";
|
||||
cah.$.ErrorCode.REPEAT_MESSAGE = "rm";
|
||||
cah.$.ErrorCode.NO_GAME_SPECIFIED = "ngs";
|
||||
cah.$.ErrorCode.OP_NOT_SPECIFIED = "ons";
|
||||
cah.$.ErrorCode.TOO_MANY_SPECIAL_CHARACTERS = "tmsc";
|
||||
cah.$.ErrorCode.BAD_REQUEST = "br";
|
||||
cah.$.ErrorCode.NOT_ENOUGH_PLAYERS = "nep";
|
||||
cah.$.ErrorCode.CARDCAST_CANNOT_FIND = "ccf";
|
||||
|
@ -159,6 +161,7 @@ cah.$.ErrorCode.NOT_REGISTERED = "nr";
|
|||
cah.$.ErrorCode.BAD_OP = "bo";
|
||||
cah.$.ErrorCode.DO_NOT_HAVE_CARD = "dnhc";
|
||||
cah.$.ErrorCode.NOT_YOUR_TURN = "nyt";
|
||||
cah.$.ErrorCode.NOT_ENOUGH_SPACES = "nes";
|
||||
cah.$.ErrorCode.ALREADY_STOPPED = "aS";
|
||||
cah.$.ErrorCode.SESSION_EXPIRED = "se";
|
||||
cah.$.ErrorCode.GAME_FULL = "gf";
|
||||
|
@ -184,6 +187,7 @@ cah.$.ErrorCode_msg['wp'] = "That password is incorrect.";
|
|||
cah.$.ErrorCode_msg['ic'] = "Invalid card specified.";
|
||||
cah.$.ErrorCode_msg['niu'] = "Nickname is already in use.";
|
||||
cah.$.ErrorCode_msg['ngs'] = "No game specified.";
|
||||
cah.$.ErrorCode_msg['nes'] = "You must use more words in a message that long.";
|
||||
cah.$.ErrorCode_msg['nitg'] = "You are not in that game.";
|
||||
cah.$.ErrorCode_msg['tmu'] = "There are too many users connected. Either join another server, or wait for a user to disconnect.";
|
||||
cah.$.ErrorCode_msg['ig'] = "Invalid game specified.";
|
||||
|
@ -201,8 +205,10 @@ cah.$.ErrorCode_msg['nns'] = "No nickname specified.";
|
|||
cah.$.ErrorCode_msg['tf'] = "You are chatting too fast. Wait a few seconds and try again.";
|
||||
cah.$.ErrorCode_msg['na'] = "You are not an administrator.";
|
||||
cah.$.ErrorCode_msg['ons'] = "Operation not specified.";
|
||||
cah.$.ErrorCode_msg['rm'] = "You can't repeat the same message multiple times in a row.";
|
||||
cah.$.ErrorCode_msg['nj'] = "You are not the judge.";
|
||||
cah.$.ErrorCode_msg['rn'] = "That nick is reserved.";
|
||||
cah.$.ErrorCode_msg['tmsc'] = "You used too many special characters in that message.";
|
||||
|
||||
cah.$.ErrorInformation = function() {
|
||||
// Dummy constructor to make Eclipse auto-complete.
|
||||
|
|
|
@ -10,14 +10,34 @@ pyx.insecure_id_allowed=true
|
|||
pyx.id_code_salt=
|
||||
# comma-separated listed of IP addresses (v4 or v6) from which users are considered admins.
|
||||
pyx.admin_addrs=127.0.0.1,::1
|
||||
|
||||
# Settings for global chat protection. Some of these do not apply to game chats.
|
||||
# Ratio of 'basic' characters to length of message. Basic characters are defined by
|
||||
# Character.isJavaIdentifierPart, which stipulates:
|
||||
# -it is a letter
|
||||
# -it is a currency symbol (such as '$')
|
||||
# -it is a connecting punctuation character (such as '_')
|
||||
# -it is a digit
|
||||
# -it is a numeric letter (such as a Roman numeral character)
|
||||
# -it is a combining mark
|
||||
# -it is a non-spacing mark
|
||||
# -isIdentifierIgnorable(codePoint) returns true for the character
|
||||
pyx.global.basic_ratio=.5
|
||||
# A message must have at least this many characters for that ratio to apply.
|
||||
pyx.global.basic_min_len=10
|
||||
# messages longer than min_len characters require at least min_count spaces between words
|
||||
pyx.global.spaces_min_len=50
|
||||
pyx.global.spaces_min_count=4
|
||||
# this many messages to global chat in that many seconds is considered chatting too fast.
|
||||
pyx.global_flood_count=3
|
||||
pyx.global.flood_count=3
|
||||
# seconds
|
||||
pyx.global_flood_time=25
|
||||
pyx.global.flood_time=25
|
||||
|
||||
# Settings for game chat protection. If it isn't listed here, it isn't supported.
|
||||
# same but for game chats
|
||||
pyx.game_flood_count=5
|
||||
pyx.game.flood_count=5
|
||||
# seconds
|
||||
pyx.game_flood_time=30
|
||||
pyx.game.flood_time=30
|
||||
|
||||
|
||||
# for production use, use postgres
|
||||
|
|
|
@ -6,10 +6,14 @@ pyx.server.broadcast_connects_and_disconnects=${pyx.broadcast_connects_and_disco
|
|||
pyx.server.global_chat_enabled=${pyx.global_chat_enabled}
|
||||
pyx.server.id_code_salt=${pyx.id_code_salt}
|
||||
pyx.server.admin_addrs=${pyx.admin_addrs}
|
||||
pyx.chat.global.flood_count=${pyx.global_flood_count}
|
||||
pyx.chat.global.flood_time=${pyx.global_flood_time}
|
||||
pyx.chat.game.flood_count=${pyx.game_flood_count}
|
||||
pyx.chat.game.flood_time=${pyx.game_flood_time}
|
||||
pyx.chat.global.flood_count=${pyx.global.flood_count}
|
||||
pyx.chat.global.flood_time=${pyx.global.flood_time}
|
||||
pyx.chat.global.basic_ratio=${pyx.global.basic_ratio}
|
||||
pyx.chat.global.basic_min_len=${pyx.global.basic_min_len}
|
||||
pyx.chat.global.spaces_min_len=${pyx.global.spaces_min_len}
|
||||
pyx.chat.global.spaces_min_count=${pyx.global.spaces_min_count}
|
||||
pyx.chat.game.flood_count=${pyx.game.flood_count}
|
||||
pyx.chat.game.flood_time=${pyx.game.flood_time}
|
||||
pyx.build=${buildNumber}
|
||||
|
||||
# this is NOT allowed to be changed during a reload, as metrics depend on previous events
|
||||
|
|
|
@ -382,6 +382,7 @@ public class Constants {
|
|||
+ Game.MINIMUM_BLACK_CARDS + " black cards and " + Game.MINIMUM_WHITE_CARDS_PER_PLAYER
|
||||
+ " times the player limit white cards."),
|
||||
NOT_ENOUGH_PLAYERS("nep", "There are not enough players to start the game."),
|
||||
NOT_ENOUGH_SPACES("nes", "You must use more words in a message that long."),
|
||||
NOT_GAME_HOST("ngh", "Only the game host can do that."),
|
||||
NOT_IN_THAT_GAME("nitg", "You are not in that game."),
|
||||
NOT_JUDGE("nj", "You are not the judge."),
|
||||
|
@ -389,11 +390,15 @@ public class Constants {
|
|||
NOT_YOUR_TURN("nyt", "It is not your turn to play a card."),
|
||||
OP_NOT_SPECIFIED("ons", "Operation not specified."),
|
||||
RESERVED_NICK("rn", "That nick is reserved."),
|
||||
REPEAT_MESSAGE("rm",
|
||||
"You can't repeat the same message multiple times in a row."),
|
||||
SERVER_ERROR("serr", "An error occured on the server."),
|
||||
SESSION_EXPIRED("se", "Your session has expired. Refresh the page."),
|
||||
TOO_FAST("tf", "You are chatting too fast. Wait a few seconds and try again."),
|
||||
TOO_MANY_GAMES("tmg", "There are too many games already in progress. Either join " +
|
||||
"an existing game, or wait for one to become available."),
|
||||
TOO_MANY_SPECIAL_CHARACTERS("tmsc",
|
||||
"You used too many special characters in that message."),
|
||||
TOO_MANY_USERS("tmu", "There are too many users connected. Either join another server, or " +
|
||||
"wait for a user to disconnect."),
|
||||
WRONG_PASSWORD("wp", "That password is incorrect.");
|
||||
|
|
|
@ -96,12 +96,18 @@ public class ChatHandler extends Handler {
|
|||
case OK:
|
||||
// nothing to do
|
||||
break;
|
||||
case NO_MESSAGE:
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
case NOT_ENOUGH_SPACES:
|
||||
return error(ErrorCode.NOT_ENOUGH_SPACES);
|
||||
case REPEAT:
|
||||
return error(ErrorCode.REPEAT_MESSAGE);
|
||||
case TOO_FAST:
|
||||
return error(ErrorCode.TOO_FAST);
|
||||
case TOO_LONG:
|
||||
return error(ErrorCode.MESSAGE_TOO_LONG);
|
||||
case NO_MESSAGE:
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
case TOO_MANY_SPECIALS:
|
||||
return error(ErrorCode.TOO_MANY_SPECIAL_CHARACTERS);
|
||||
default:
|
||||
LOG.error(String.format("Unknown chat filter result %s", filterResult));
|
||||
}
|
||||
|
|
|
@ -81,12 +81,18 @@ public class GameChatHandler extends GameWithPlayerHandler {
|
|||
case OK:
|
||||
// nothing to do
|
||||
break;
|
||||
case NO_MESSAGE:
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
case NOT_ENOUGH_SPACES:
|
||||
return error(ErrorCode.NOT_ENOUGH_SPACES);
|
||||
case REPEAT:
|
||||
return error(ErrorCode.REPEAT_MESSAGE);
|
||||
case TOO_FAST:
|
||||
return error(ErrorCode.TOO_FAST);
|
||||
case TOO_LONG:
|
||||
return error(ErrorCode.MESSAGE_TOO_LONG);
|
||||
case NO_MESSAGE:
|
||||
return error(ErrorCode.NO_MSG_SPECIFIED);
|
||||
case TOO_MANY_SPECIALS:
|
||||
return error(ErrorCode.TOO_MANY_SPECIAL_CHARACTERS);
|
||||
default:
|
||||
LOG.error(String.format("Unknown chat filter result %s", filterResult));
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import java.util.Properties;
|
|||
import java.util.TreeMap;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
|
@ -50,13 +51,20 @@ public class ChatFilter {
|
|||
private static final Logger LOG = Logger.getLogger(ChatFilter.class);
|
||||
|
||||
private static final int DEFAULT_CHAT_FLOOD_MESSAGE_COUNT = 4;
|
||||
private static final long DEFAULT_CHAT_FLOOD_TIME = TimeUnit.SECONDS.toMillis(30);
|
||||
private static final int DEFAULT_CHAT_FLOOD_TIME_SECONDS = 30;
|
||||
private static final int DEFAULT_BASIC_MIN_MSG_LENGTH = 10;
|
||||
private static final double DEFAULT_BASIC_CHARACTER_RATIO = .5;
|
||||
private static final int DEFAULT_SPACES_MIN_MSG_LENGTH = 75;
|
||||
private static final int DEFAULT_SPACES_REQUIRED = 3;
|
||||
|
||||
public static final Pattern SIMPLE_MESSAGE_PATTERN = Pattern
|
||||
.compile("^[a-zA-Z0-9 _\\-=+*()\\[\\]\\\\/|,.!:'\"`~#]+$");
|
||||
|
||||
private final Provider<Properties> propsProvider;
|
||||
private final Map<User, FilterData> filterData = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
public enum Result {
|
||||
OK, TOO_FAST, TOO_LONG, NO_MESSAGE
|
||||
OK, NO_MESSAGE, NOT_ENOUGH_SPACES, REPEAT, TOO_FAST, TOO_LONG, TOO_MANY_SPECIALS
|
||||
}
|
||||
|
||||
private enum Scope {
|
||||
|
@ -74,10 +82,26 @@ public class ChatFilter {
|
|||
return result;
|
||||
}
|
||||
|
||||
// TODO
|
||||
final long total = message.codePoints().count();
|
||||
|
||||
if (!SIMPLE_MESSAGE_PATTERN.matcher(message).matches()
|
||||
&& total >= getIntParameter(Scope.global, "basic_min_len", DEFAULT_BASIC_MIN_MSG_LENGTH)) {
|
||||
// do some more in-depth analysis. we don't want too many emoji or non-latin characters
|
||||
final long basic = message.codePoints().filter(c -> Character.isJavaIdentifierPart(c))
|
||||
.count();
|
||||
if (((double) basic) / total < getBasicCharacterRatio(Scope.global)) {
|
||||
return Result.TOO_MANY_SPECIALS;
|
||||
}
|
||||
}
|
||||
|
||||
final int spaces = message.split("\\s+").length + 1;
|
||||
if (total >= getIntParameter(Scope.global, "spaces_min_len", DEFAULT_SPACES_MIN_MSG_LENGTH)
|
||||
&& spaces < getIntParameter(Scope.global, "spaces_min_count", DEFAULT_SPACES_REQUIRED)) {
|
||||
return Result.NOT_ENOUGH_SPACES;
|
||||
}
|
||||
|
||||
getMessageTimes(user, Scope.global).add(System.currentTimeMillis());
|
||||
return result;
|
||||
return Result.OK;
|
||||
}
|
||||
|
||||
public Result filterGame(final User user, final String message) {
|
||||
|
@ -86,19 +110,17 @@ public class ChatFilter {
|
|||
return result;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// TODO?
|
||||
|
||||
getMessageTimes(user, Scope.game).add(System.currentTimeMillis());
|
||||
return result;
|
||||
return Result.OK;
|
||||
}
|
||||
|
||||
private Result filterCommon(final Scope scope, final User user, final String message) {
|
||||
// TODO
|
||||
|
||||
final List<Long> messageTimes = getMessageTimes(user, scope);
|
||||
if (messageTimes.size() >= getFloodCount(scope)) {
|
||||
final Long head = messageTimes.get(0);
|
||||
if (System.currentTimeMillis() - head < getFloodTime(scope)) {
|
||||
if (System.currentTimeMillis() - head < getFloodTimeMillis(scope)) {
|
||||
return Result.TOO_FAST;
|
||||
}
|
||||
messageTimes.remove(0);
|
||||
|
@ -110,34 +132,51 @@ public class ChatFilter {
|
|||
return Result.NO_MESSAGE;
|
||||
}
|
||||
|
||||
final FilterData data = getFilterData(user);
|
||||
synchronized (data.lastMessages) {
|
||||
if (message.equals(data.lastMessages.get(scope))) {
|
||||
return Result.REPEAT;
|
||||
} else {
|
||||
data.lastMessages.put(scope, message);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.OK;
|
||||
}
|
||||
|
||||
private int getFloodCount(final Scope scope) {
|
||||
private int getIntParameter(final Scope scope, final String name, final int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(propsProvider.get().getProperty(
|
||||
String.format("pyx.chat.%s.flood_count", scope),
|
||||
String.valueOf(DEFAULT_CHAT_FLOOD_MESSAGE_COUNT)));
|
||||
String.format("pyx.chat.%s.%s", scope, name), String.valueOf(defaultValue)));
|
||||
} catch (final NumberFormatException e) {
|
||||
LOG.warn(String.format("Unable to parse pyx.chat.%s.flood_count as a number,"
|
||||
+ " using default of %d", scope, DEFAULT_CHAT_FLOOD_MESSAGE_COUNT), e);
|
||||
return DEFAULT_CHAT_FLOOD_MESSAGE_COUNT;
|
||||
LOG.warn(String.format("Unable to parse pyx.chat.%s.%s as a number,"
|
||||
+ " using default of %d", scope, name, defaultValue), e);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private long getFloodTime(final Scope scope) {
|
||||
private int getFloodCount(final Scope scope) {
|
||||
return getIntParameter(scope, "flood_count", DEFAULT_CHAT_FLOOD_MESSAGE_COUNT);
|
||||
}
|
||||
|
||||
private long getFloodTimeMillis(final Scope scope) {
|
||||
return TimeUnit.SECONDS
|
||||
.toMillis(getIntParameter(scope, "flood_time", DEFAULT_CHAT_FLOOD_TIME_SECONDS));
|
||||
}
|
||||
|
||||
private double getBasicCharacterRatio(final Scope scope) {
|
||||
try {
|
||||
return TimeUnit.SECONDS.toMillis(Integer.parseInt(propsProvider.get().getProperty(
|
||||
String.format("pyx.chat.%s.flood_time", scope),
|
||||
String.valueOf(DEFAULT_CHAT_FLOOD_TIME))));
|
||||
return Double.parseDouble(propsProvider.get().getProperty(
|
||||
String.format("pyx.chat.%s.basic_ratio", scope),
|
||||
String.valueOf(DEFAULT_BASIC_CHARACTER_RATIO)));
|
||||
} catch (final NumberFormatException e) {
|
||||
LOG.warn(String.format("Unable to parse pyx.chat.%s.flood_time as a number,"
|
||||
+ " using default of %d", scope, DEFAULT_CHAT_FLOOD_TIME), e);
|
||||
return DEFAULT_CHAT_FLOOD_TIME;
|
||||
LOG.warn(String.format("Unable to parse pyx.chat.%s.basic_ratio as a number,"
|
||||
+ " using default of %d", scope, DEFAULT_BASIC_CHARACTER_RATIO), e);
|
||||
return DEFAULT_BASIC_CHARACTER_RATIO;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Long> getMessageTimes(final User user, final Scope scope) {
|
||||
private FilterData getFilterData(final User user) {
|
||||
FilterData data;
|
||||
synchronized (filterData) {
|
||||
data = filterData.get(user);
|
||||
|
@ -148,11 +187,16 @@ public class ChatFilter {
|
|||
filterData.put(user, data);
|
||||
}
|
||||
}
|
||||
return data.lastMessageTimes.get(scope);
|
||||
return data;
|
||||
}
|
||||
|
||||
private List<Long> getMessageTimes(final User user, final Scope scope) {
|
||||
return getFilterData(user).lastMessageTimes.get(scope);
|
||||
}
|
||||
|
||||
private static class FilterData {
|
||||
final Map<Scope, List<Long>> lastMessageTimes;
|
||||
final Map<Scope, String> lastMessages = Collections.synchronizedMap(new TreeMap<>());
|
||||
|
||||
private FilterData() {
|
||||
final Map<Scope, List<Long>> map = new TreeMap<>();
|
||||
|
|
Loading…
Reference in New Issue