修复三处bug:时间解析支持26-10-08 14:43短横线格式、禁言期间屏蔽一切私信收发、URL拦截baidu . com空格变形绕过
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user