feat: enhance anti-spam logic to allow messages between player interactions
This commit is contained in:
3 files changed
+39
-16
No files matched your search
@@ -22,7 +22,8 @@ import java.util.UUID;
|
||||
* <ul>
|
||||
* <li><b>频率限制</b>:{@link #WINDOW_MILLIS} 毫秒内最多放行 {@link #MAX_MESSAGES} 条。</li>
|
||||
* <li><b>重复拦截</b>:与上一条内容相同(忽略大小写与首尾空白)且间隔不足
|
||||
* {@link #DUPLICATE_WINDOW_MILLIS} 毫秒的消息被拦截。</li>
|
||||
* {@link #DUPLICATE_WINDOW_MILLIS} 毫秒的消息被拦截。<br>
|
||||
* 但若在这两条之间有<b>其他人</b>发过公屏消息(对话已经推进),则重复内容放行。</li>
|
||||
* </ul>
|
||||
* 判定完全在服务端进行,走 Fabric {@code ALLOW_CHAT_MESSAGE} 钩子——返回 false 即取消该条消息。
|
||||
* 只影响公屏聊天;命令(如 /msg)走的是 {@code ALLOW_COMMAND_MESSAGE},不受影响。
|
||||
@@ -39,6 +40,8 @@ public class AntiSpamModule extends ServerModule {
|
||||
|
||||
// 仅在服务端主线程访问(聊天与断线回调都在主线程),无需并发容器。
|
||||
private final Map<UUID, Tracker> trackers = new HashMap<>();
|
||||
// 全局放行消息计数器,每放行一条公屏消息 +1。用于判断某玩家两次发言之间是否有其他人插话。
|
||||
private long messageSeq;
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
@@ -53,13 +56,14 @@ public class AntiSpamModule extends ServerModule {
|
||||
var tracker = trackers.computeIfAbsent(sender.getUUID(), ignored -> new Tracker());
|
||||
var content = message.signedContent().strip();
|
||||
|
||||
var reasonKey = tracker.check(content, now);
|
||||
var reasonKey = tracker.check(content, now, messageSeq);
|
||||
if (reasonKey != null) {
|
||||
// 被拦截的消息不计入放行窗口、也不刷新「上一条」,以免刷屏者借此绕过判定。
|
||||
warn(sender, tracker, now, reasonKey);
|
||||
return false;
|
||||
}
|
||||
tracker.accept(content, now);
|
||||
// 先自增再记录:tracker 存下的是「本条消息之后」的全局序号。
|
||||
tracker.accept(content, now, ++messageSeq);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -74,13 +78,19 @@ public class AntiSpamModule extends ServerModule {
|
||||
private final Deque<Long> recent = new ArrayDeque<>();
|
||||
private String lastContent = "";
|
||||
private long lastContentTime;
|
||||
/** 上一条放行消息时的全局序号;若之后全局序号增长,说明有其他人插过话。 */
|
||||
private long lastSeq;
|
||||
private long lastWarnTime;
|
||||
|
||||
/** @return 拦截原因的翻译键,{@code null} 表示放行。 */
|
||||
String check(String content, long now) {
|
||||
/**
|
||||
* @param seq 当前全局放行序号;等于 {@link #lastSeq} 表示自本玩家上次发言以来无人插话。
|
||||
* @return 拦截原因的翻译键,{@code null} 表示放行。
|
||||
*/
|
||||
String check(String content, long now, long seq) {
|
||||
if (!content.isEmpty()
|
||||
&& content.equalsIgnoreCase(lastContent)
|
||||
&& now - lastContentTime < DUPLICATE_WINDOW_MILLIS) {
|
||||
&& now - lastContentTime < DUPLICATE_WINDOW_MILLIS
|
||||
&& seq == lastSeq) {
|
||||
return "message.sfcraft.antispam.duplicate";
|
||||
}
|
||||
while (!recent.isEmpty() && now - recent.peekFirst() >= WINDOW_MILLIS) {
|
||||
@@ -92,10 +102,11 @@ public class AntiSpamModule extends ServerModule {
|
||||
return null;
|
||||
}
|
||||
|
||||
void accept(String content, long now) {
|
||||
void accept(String content, long now, long seq) {
|
||||
recent.addLast(now);
|
||||
lastContent = content;
|
||||
lastContentTime = now;
|
||||
lastSeq = seq;
|
||||
}
|
||||
|
||||
boolean canWarn(long now) {
|
||||
|
||||
@@ -71,7 +71,7 @@ public class ChatPingModule extends ServerModule implements ChatDecorator {
|
||||
var r = match.group();
|
||||
if (r.isEmpty()) continue;
|
||||
final var d = r.substring(1).toLowerCase();
|
||||
return playerNames.stream()
|
||||
return "@"+playerNames.stream()
|
||||
.filter(it -> it.toLowerCase().startsWith(d))
|
||||
.peek(it -> foundPlayers.add(serverSupplier.get().getPlayerList().getPlayerByName(it)))
|
||||
.findFirst()
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
package io.ib67.sfcraft.module.chat.decorator;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import io.ib67.kiwi.routine.Fail;
|
||||
import io.ib67.kiwi.routine.Result;
|
||||
import io.ib67.kiwi.routine.Some;
|
||||
import io.ib67.sfcraft.ServerModule;
|
||||
import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator;
|
||||
import net.minecraft.network.chat.ChatDecorator;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class ChatShowoffModule extends ServerModule implements ChatDecorator {
|
||||
@Inject
|
||||
SimpleMessageDecorator messageDecorator;
|
||||
@@ -24,15 +30,21 @@ public class ChatShowoffModule extends ServerModule implements ChatDecorator {
|
||||
if (!isEnabled()) {
|
||||
return message;
|
||||
}
|
||||
if(player == null || player.getMainHandItem().is(Items.AIR)) { return message; }
|
||||
|
||||
return ReplaceHelper.replace(player, message, ".item", this::generateShowoffText);
|
||||
if (player == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
private Component generateShowoffText(ServerPlayer serverPlayer) {
|
||||
var item = serverPlayer.getMainHandItem();
|
||||
return item.count() > 1 ?
|
||||
Component.literal(" "+item.count()+"x ").append(item.getDisplayName())
|
||||
: Component.literal(" ").append(item.getDisplayName());
|
||||
var comp = generateShowoffText(player.getMainHandItem(), player)
|
||||
.orElseGet(generateShowoffText(player.getOffhandItem(), player)::result);
|
||||
|
||||
if (comp == null) return message;
|
||||
return ReplaceHelper.replace(player, message, ".item", p -> comp);
|
||||
}
|
||||
|
||||
private Result<Component> generateShowoffText(@Nullable ItemStack item, ServerPlayer serverPlayer) {
|
||||
if (item == null || item.is(Items.AIR)) return Fail.none();
|
||||
return new Some<>(item.count() > 1 ?
|
||||
Component.literal(" " + item.count() + "x ").append(item.getDisplayName())
|
||||
: Component.literal(" ").append(item.getDisplayName()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user