修复三处bug:时间解析支持26-10-08 14:43短横线格式、禁言期间屏蔽一切私信收发、URL拦截baidu . com空格变形绕过

This commit is contained in:
youpaishidifu
2026-10-08 15:41:46 +08:00
parent 581475e760
commit 881fb85408
3 changed files with 163 additions and 6 deletions
@@ -652,7 +652,15 @@ public class ChatFilterManager {
+ "[_.][\\p{L}]{2,}");
private static final Pattern DOT_VARIANTS =
Pattern.compile(
"[\uff0e\u3002\u2025\u2026\u00b7]");
"[\uff0e\u3002\u2025\u2026\u00b7\uff65\ufe52]");
/** 零宽/不可见字符:变形URL归一前剔除(如 baidu.<零宽空格>com) */
private static final Pattern INVISIBLE =
Pattern.compile(
"[\u200b\u200c\u200d\u2060\ufeff\u00ad]");
/** 点周带空白的变形域名:baidu . com(2026-10-08 加强拦截) */
private static final Pattern SPACED_DOT_URL =
Pattern.compile(
"(?i)(?:[\\p{L}0-9](?:[\\p{L}0-9\\-]*[\\p{L}0-9])?[\\s\\u3000]*\\.)+[\\s\\u3000]*[\\p{L}]{2,}");
private static final Pattern COLOR_CODES =
Pattern.compile(
"\u00a7[0-9a-fk-orA-FK-OR]");
@@ -977,6 +985,8 @@ public class ChatFilterManager {
// ========== URL检测 ==========
private String normalizeMessage(String msg) {
msg = INVISIBLE.matcher(msg)
.replaceAll("");
msg = COLOR_CODES.matcher(msg)
.replaceAll("");
msg = DOT_VARIANTS.matcher(msg)
@@ -999,6 +1009,19 @@ public class ChatFilterManager {
"");
if (!url.isEmpty()) urls.add(url);
}
// ★ 变形URL加强(2026-10-08):点周带空白(baidu . com)压缩空白后再判定;
// 仅当顶级域命中「非法域名后缀」才收录,避免误伤 "OK. Good" 之类正常英文句子
Matcher m2 = SPACED_DOT_URL.matcher(normalized);
while (m2.find()) {
String token = m2.group();
if (token.indexOf(' ') < 0 && token.indexOf('\t') < 0
&& token.indexOf('\u3000') < 0) continue; // 无空白 = 常规域名,上面已提取
String compact = token.replaceAll("[\\s\u3000]+", "").toLowerCase();
int dot = compact.lastIndexOf('.');
if (dot < 0) continue;
if (!illegalDomainSuffixes.contains(compact.substring(dot + 1))) continue;
if (!urls.contains(compact)) urls.add(compact);
}
return urls;
}
+75 -1
View File
@@ -69,8 +69,15 @@ public class DurationParser {
throws IllegalArgumentException {
if (input == null || input.trim().isEmpty())
throw new IllegalArgumentException("时长为空");
String in = input.trim();
// ★ 0) 日期+时间(绝对):26-10-08 14:43 / 2026-10-08 14:43 / 2026年10月8日 14:43
// 必须在去空格之前解析:否则日期与时间粘连(26-10-08 14:43 → 26-10-0814:43)无法区分
long dt = parseDateTime(in);
if (dt >= 0) return dt;
// ★ 先把英文数字词换成阿拉伯数字(one min -> 1 min)
String s = normalizeEnglishNumber(input.trim()).replace(" ", "");
String s = normalizeEnglishNumber(in).replace(" ", "");
// 1) 纯数字 → Unix 秒时间戳
if (s.matches("\\d+")) {
@@ -170,6 +177,73 @@ public class DurationParser {
}
}
// ===== 日期+时间绝对解析(2026-10-08 新增:支持 26-10-08 14:43 写法) =====
/** 中文日期+时间:2026年10月8日 14:43(“日”已界定日期边界,时间可紧贴) */
private static final Pattern DT_CN = Pattern.compile(
"(\\d{4})年(\\d{1,2})月(\\d{1,2})日(?:[ T\\u3000]+)?"
+ "(\\d{1,2}):(\\d{2})(?::(\\d{2}))?(?!\\d|[A-Za-z])");
/** 分隔日期+时间:26-10-08 14:43 / 2026-10-08 14:43 / 2026.10.08T14:43:00
* 两位年份按 20xx 解释;日期与时间之间必须有空格或 T,
* 防止 2026-1-8 14:43 被粘连误读成「日=81、时=4」 */
private static final Pattern DT_SEP = Pattern.compile(
"(?<!\\d)(\\d{4}|\\d{2})[-/.](\\d{1,2})[-/.](\\d{1,2})"
+ "[ T\\u3000]+(\\d{1,2}):(\\d{2})(?::(\\d{2}))?(?!\\d|[A-Za-z])");
/** 两位年份无时间:26-10-08
* lookbehind 防止吃掉 4 位年份的后半段(2026-07-09 不能被读成 07-09) */
private static final Pattern DT_YMD2 = Pattern.compile(
"(?<!\\d{2}[-/.])(\\d{2})[-/.](\\d{1,2})[-/.](\\d{1,2})(?!\\d|[A-Za-z])");
/** 解析「日期+时间」为绝对毫秒;认不出来返回 -1 交后续分支处理 */
private static long parseDateTime(String in) {
Matcher m = DT_CN.matcher(in);
if (m.find()) {
return buildDateTimeMillis(Integer.parseInt(m.group(1)),
Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)),
timeOf(m.group(4), m.group(5), m.group(6)));
}
m = DT_SEP.matcher(in);
if (m.find()) {
int year = Integer.parseInt(m.group(1));
if (year < 100) year += 2000; // 26 → 2026
return buildDateTimeMillis(year, Integer.parseInt(m.group(2)),
Integer.parseInt(m.group(3)),
timeOf(m.group(4), m.group(5), m.group(6)));
}
m = DT_YMD2.matcher(in);
if (m.find()) {
return buildDateTimeMillis(2000 + Integer.parseInt(m.group(1)),
Integer.parseInt(m.group(2)), Integer.parseInt(m.group(3)),
new int[]{-1, -1, -1});
}
return -1;
}
/** 解析 HH:mm[:ss];无时间(null)返回 {-1,-1,-1} 表示取 00:00:00 */
private static int[] timeOf(String h, String mi, String s) {
if (h == null) return new int[]{-1, -1, -1};
int H = Integer.parseInt(h), M = Integer.parseInt(mi);
int S = (s == null) ? 0 : Integer.parseInt(s);
if (H > 23 || M > 59 || S > 59)
throw new IllegalArgumentException("无效的时间: " + h + ":" + mi);
return new int[]{H, M, S};
}
/** 绝对日期+时间(t[0..2] 为 -1 表示 00:00:00) */
private static long buildDateTimeMillis(int y, int mo, int d, int[] t) {
if (mo < 1 || mo > 12 || d < 1 || d > 31)
throw new IllegalArgumentException("无效的日期: " + y + "-" + mo + "-" + d);
Calendar c = Calendar.getInstance();
c.set(y, mo - 1, d,
t[0] < 0 ? 0 : t[0],
t[1] < 0 ? 0 : t[1],
t[2] < 0 ? 0 : t[2]);
c.set(Calendar.MILLISECOND, 0);
return c.getTimeInMillis();
}
private static long parseRelative(String s) {
// 按「数字 + 单位」逐段匹配并累加
StringBuilder units = new StringBuilder();
+64 -4
View File
@@ -3447,16 +3447,76 @@ public class Main extends JavaPlugin
// 告示牌通过 onSignChange 拦截,编辑书通过 onPlayerEditBook 拦截
// 私信通过 PlayerCommandPreprocessEvent 拦截(/tell, /msg, /w, /r)
/** 私信命令标签(/r 回复类单独处理),2026-10-08 扩展命名空间与别名 */
private static final Set<String> TELL_CMDS = new HashSet<>(java.util.Arrays.asList(
"tell", "msg", "w", "whisper", "pm"));
/** 最近私信会话:本玩家 → 对方玩家(供 /r 定位接收方,禁言收发屏蔽用) */
private final java.util.Map<String, String> lastTellPeer =
new java.util.concurrent.ConcurrentHashMap<>();
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPrivateMessage(org.bukkit.event.player.PlayerCommandPreprocessEvent e) {
Player p = e.getPlayer();
String cmd = e.getMessage().toLowerCase();
boolean isTellCmd = cmd.startsWith("/tell ") || cmd.startsWith("/msg ") || cmd.startsWith("/w ") || cmd.startsWith("/r ");
if (!isTellCmd) return;
String raw = e.getMessage();
// ★ 解析命令标签(兼容 /minecraft:msg 命名空间、连续空格)
String[] tok = raw.trim().split("\\s+");
if (tok.length == 0) return;
String label = tok[0].startsWith("/") ? tok[0].substring(1) : tok[0];
label = label.toLowerCase(java.util.Locale.ROOT);
int colon = label.indexOf(':');
if (colon >= 0) label = label.substring(colon + 1);
boolean isTellCmd = TELL_CMDS.contains(label);
boolean isReplyCmd = label.equals("r") || label.equals("reply");
if (!isTellCmd && !isReplyCmd) return;
if (isFrozen(p)) return;
if (chatFilter == null || !chatFilter.isEnabled()) return;
// ===== 禁言屏蔽(2026-10-08):禁言期间屏蔽一切私信收发 =====
// 1) 发送方被禁言 → 拦
if (chatFilter.isMuted(p.getName())) {
e.setCancelled(true);
String r = chatFilter.getMuteReason(p.getName());
long expire = chatFilter.getMuteExpire(p.getName());
StringBuilder sb = new StringBuilder("§c§l[私信] §f你已被禁言,禁言期间无法发送私信");
if (r != null && !r.isEmpty()) sb.append("\n§7原因: §f").append(r);
if (expire > 0) sb.append("\n§7到期: §f")
.append(new java.text.SimpleDateFormat("yyyy年M月d日 HH点mm分")
.format(new java.util.Date(expire)));
p.sendMessage(sb.toString());
return;
}
// 2) 解析接收方(/r 取最近会话对方)
Player peer = null;
if (isTellCmd && tok.length >= 2 && !tok[1].startsWith("@")) {
peer = org.bukkit.Bukkit.getPlayerExact(tok[1]);
if (peer == null) peer = org.bukkit.Bukkit.getPlayer(tok[1]);
if (peer == null) {
try {
peer = org.bukkit.Bukkit.getPlayer(java.util.UUID.fromString(tok[1]));
} catch (Exception ignore) {
}
}
} else if (isReplyCmd) {
String prev = lastTellPeer.get(p.getName());
if (prev != null) peer = org.bukkit.Bukkit.getPlayerExact(prev);
}
// 3) 接收方被禁言 → 拦(禁言期间任何人不得私信被禁言者)
if (peer != null && chatFilter.isMuted(peer.getName())) {
e.setCancelled(true);
p.sendMessage("§c§l[私信] §f对方 §e" + peer.getName() + " §f已被禁言,暂时无法接收私信");
return;
}
if (chatFilter.isPlayerWhitelisted(p.getName())) return;
if (filterLinkInContent(p, e.getMessage())) e.setCancelled(true);
if (filterLinkInContent(p, raw)) {
e.setCancelled(true);
return;
}
// 记录会话(供 /r 判断接收方);链接被拦则不记
if (peer != null) {
lastTellPeer.put(p.getName(), peer.getName());
lastTellPeer.put(peer.getName(), p.getName());
}
}
@EventHandler