package Sdf1_login; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; import java.io.*; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class ChatFilterManager { private final JavaPlugin plugin; private final List whitelistUrls = new ArrayList<>(); private final Set whitelistPlayers = new TreeSet<>( String.CASE_INSENSITIVE_ORDER); private final Map punishmentRules = new LinkedHashMap<>(); private final Map punishmentDurations = new LinkedHashMap<>(); private final Map violationCount = new ConcurrentHashMap<>(); private final Map mutedPlayers = new ConcurrentHashMap<>(); private final Map messages = new LinkedHashMap<>(); private boolean notifyAdmin = true; private boolean notifyAll = false; private int muteDuration = 300; private static final Pattern URL_PATTERN = Pattern.compile( "(?i)(?:https?://\\S+)" + "|(?:(?:[a-zA-Z0-9]" + "(?:[a-zA-Z0-9\\-]*" + "[a-zA-Z0-9])?\\.)+" + "[a-zA-Z]{2,})" + "(?:/\\S*)?"); private static final Pattern DOT_VARIANTS = Pattern.compile( "[\uff0e\u3002\u2025\u2026\u00b7]"); private static final Pattern COLOR_CODES = Pattern.compile( "\u00a7[0-9a-fk-orA-FK-OR]"); private static final Set FILE_EXT; static { Set s = new HashSet<>( Arrays.asList( "txt", "yml", "yaml", "json", "xml", "java", "class", "jar", "png", "jpg", "jpeg", "gif", "bmp", "svg", "ico", "webp", "mp3", "mp4", "avi", "mkv", "wav", "flac", "ogg", "zip", "rar", "7z", "tar", "gz", "cfg", "conf", "ini", "log", "db", "sql", "sh", "bat", "html", "htm", "css", "js", "md", "pdf", "dll", "so", "exe", "bin", "c", "cpp", "py", "rb", "go", "rs", "kt", "lua", "php", "asp", "jsp")); FILE_EXT = Collections.unmodifiableSet(s); } public ChatFilterManager(JavaPlugin plugin) { this.plugin = plugin; } // ========== 配置加载 ========== public void loadConfig() { whitelistUrls.clear(); whitelistPlayers.clear(); punishmentRules.clear(); punishmentDurations.clear(); messages.clear(); File f = new File( plugin.getDataFolder(), "chat.txt"); if (!f.exists()) writeDefaultFile(f); try (BufferedReader r = new BufferedReader( new InputStreamReader( new FileInputStream(f), StandardCharsets.UTF_8))) { String section = ""; String line; while ((line = r.readLine()) != null) { if (line.startsWith("\uFEFF")) line = line.substring(1); String t = line.trim(); if (t.isEmpty() || t.startsWith("#")) continue; if (t.equals("白名单") || t.equals("白名单:")) { section = "whitelist"; continue; } if (t.equals("处罚规则") || t.equals("处罚规则:")) { section = "punishment"; continue; } if (t.equals("白名单玩家") || t.equals("白名单玩家:")) { section = "players"; continue; } if (t.contains(":")) { String[] kv = t.split(":", 2); String k = kv[0].trim(); String v = kv[1].trim(); if (k.equals("通知管理员")) { notifyAdmin = parseBool(v); continue; } if (k.equals("全服通报")) { notifyAll = parseBool(v); continue; } if (k.equals("禁言时长")) { try { muteDuration = Integer.parseInt(v); } catch (Exception ignored) { } continue; } } switch (section) { case "whitelist": whitelistUrls.add( t.toLowerCase()); break; case "punishment": String[] parts = t.split(":"); if (parts.length >= 2) { try { int cnt = Integer.parseInt( parts[0] .trim()); String type = parts[1].trim() .toLowerCase(); int dur = parts.length >= 3 ? Integer.parseInt( parts[2] .trim()) : muteDuration; punishmentRules.put( cnt, type); punishmentDurations.put( cnt, dur); } catch (Exception ignored) { } } break; case "players": whitelistPlayers.add(t); break; } } } catch (Exception e) { plugin.getLogger().severe( "[Sdf1_chat] 配置加载失败: " + e.getMessage()); } // 从消息.txt读取chat_开头的消息 loadMessages(); } private void loadMessages() { File msgFile = new File( plugin.getDataFolder(), "消息.txt"); if (!msgFile.exists()) return; try (BufferedReader mr = new BufferedReader( new InputStreamReader( new FileInputStream( msgFile), StandardCharsets.UTF_8))) { String mline; while ((mline = mr.readLine()) != null) { mline = mline.trim(); if (mline.isEmpty() || mline.startsWith("#")) continue; // 支持 = 和 : 两种分隔符 int eqIdx = mline.indexOf('='); int coIdx = mline.indexOf(':'); int splitIdx = -1; if (eqIdx >= 0 && coIdx >= 0) { splitIdx = Math.min(eqIdx, coIdx); } else if (eqIdx >= 0) { splitIdx = eqIdx; } else if (coIdx >= 0) { splitIdx = coIdx; } if (splitIdx < 0) continue; String k = mline.substring(0, splitIdx).trim(); String v = mline.substring( splitIdx + 1).trim(); if (k.startsWith("chat_")) { messages.put(k, v); } } } catch (Exception ignored) { } } private void writeDefaultFile(File f) { try (PrintWriter pw = new PrintWriter( new OutputStreamWriter( new FileOutputStream(f), StandardCharsets.UTF_8))) { pw.println("# Sdf1_chat 聊天过滤配置"); pw.println("通知管理员: true"); pw.println("全服通报: false"); pw.println("禁言时长: 300"); pw.println(); pw.println("白名单:"); pw.println("*.minecraft.net"); pw.println("baidu.com"); pw.println("github.com"); pw.println(); pw.println("处罚规则:"); pw.println("1:warn:0"); pw.println("2:warn:0"); pw.println("3:mute:300"); pw.println("5:mute:600"); pw.println("8:kick:0"); pw.println("10:ban:0"); pw.println(); pw.println("白名单玩家:"); } catch (IOException ignored) { } } public void saveConfig() { File f = new File( plugin.getDataFolder(), "chat.txt"); try (PrintWriter pw = new PrintWriter( new OutputStreamWriter( new FileOutputStream(f), StandardCharsets.UTF_8))) { pw.println("# Sdf1_chat 聊天过滤配置"); pw.println("通知管理员: " + notifyAdmin); pw.println("全服通报: " + notifyAll); pw.println("禁言时长: " + muteDuration); pw.println(); pw.println("白名单:"); for (String u : whitelistUrls) pw.println(u); pw.println(); pw.println("处罚规则:"); for (Map.Entry entry : punishmentRules.entrySet()) { int dur = punishmentDurations .getOrDefault(entry.getKey(), muteDuration); pw.println(entry.getKey() + ":" + entry.getValue() + ":" + dur); } pw.println(); pw.println("白名单玩家:"); for (String p : whitelistPlayers) pw.println(p); } catch (IOException e) { plugin.getLogger().severe( "[Sdf1_chat] 保存失败: " + e.getMessage()); } } // ========== 消息 ========== public String msg(String key, String... args) { String template = messages.getOrDefault( key, key); for (int i = 0; i < args.length - 1; i += 2) { template = template.replace( "{" + args[i] + "}", args[i + 1]); } return template; } // ========== URL检测 ========== private String normalizeMessage(String msg) { msg = COLOR_CODES.matcher(msg) .replaceAll(""); msg = DOT_VARIANTS.matcher(msg) .replaceAll("."); return msg; } public List extractUrls( String message) { String normalized = normalizeMessage(message); List urls = new ArrayList<>(); Matcher m = URL_PATTERN .matcher(normalized); while (m.find()) { String url = m.group().toLowerCase(); url = url.replaceAll( "[\\s.,;!?,。;!?、)》」』】" + "\\]\\[\\((]+$", ""); if (!url.isEmpty()) urls.add(url); } return urls; } public boolean isWhitelisted(String url) { String clean = url .replaceAll("^https?://", "") .toLowerCase(); int colon = clean.indexOf(':'); int slash = clean.indexOf('/'); if (colon > 0 && (slash < 0 || colon < slash)) { clean = clean.substring(0, colon); } slash = clean.indexOf('/'); if (slash > 0) { clean = clean.substring(0, slash); } for (String entry : whitelistUrls) { String e = entry.toLowerCase().trim(); if (e.startsWith("*.")) { String suffix = e.substring(1); if (clean.endsWith(suffix) || clean.equals( suffix.substring(1))) return true; } else { if (clean.equals(e) || clean.endsWith( "." + e)) return true; } } return false; } public boolean isLikelyDomain(String url) { int lastDot = url.lastIndexOf('.'); if (lastDot < 0 || lastDot >= url.length() - 1) return false; String ext = url.substring(lastDot + 1) .toLowerCase(); return !FILE_EXT.contains(ext); } // ========== 玩家操作 ========== public boolean isMuted(String name) { Long expiry = mutedPlayers.get(name); if (expiry == null) return false; if (System.currentTimeMillis() >= expiry) { mutedPlayers.remove(name); return false; } return true; } public String fmtDuration(int seconds) { if (seconds <= 0) return "0秒"; int day = seconds / 86400; int hr = (seconds % 86400) / 3600; int min = (seconds % 3600) / 60; int sec = seconds % 60; StringBuilder sb = new StringBuilder(); if (day > 0) sb.append(day).append("天"); if (hr > 0) sb.append(hr).append("小时"); if (min > 0) sb.append(min).append("分"); if (sec > 0 || sb.length() == 0) sb.append(sec).append("秒"); return sb.toString(); } public void applyPunishment(Player player, int violation) { String type = "warn"; int duration = muteDuration; int maxRule = 0; for (Map.Entry entry : punishmentRules.entrySet()) { if (violation >= entry.getKey() && entry.getKey() > maxRule) { maxRule = entry.getKey(); type = entry.getValue(); duration = punishmentDurations .getOrDefault( entry.getKey(), muteDuration); } } if (type.equals("mute")) { if (isMuted(player.getName())) return; mutedPlayers.put(player.getName(), System.currentTimeMillis() + (long) duration * 1000L); } final String fType = type; final int fDur = duration; final int fV = violation; final String fName = player.getName(); Bukkit.getScheduler().runTask(plugin, () -> { Player p = Bukkit.getPlayer(fName); if (p == null) return; switch (fType) { case "warn": // 警告不处罚,只提示 break; case "mute": p.sendMessage( msg("chat_muted")); break; case "kick": p.kickPlayer( msg("chat_muted")); break; case "ban": Bukkit.dispatchCommand( Bukkit.getConsoleSender(), "ban " + fName + " §c多次发送违规链接"); break; case "banip": InetSocketAddress addr = p.getAddress(); if (addr != null) { String ip = addr .getAddress() .getHostAddress(); Bukkit.dispatchCommand( Bukkit.getConsoleSender(), "ban-ip " + ip + " §c发送违规链接"); } break; } }); } public void unmutePlayer(String name) { mutedPlayers.remove(name); } public void resetPlayer(String name) { mutedPlayers.remove(name); violationCount.remove(name); } public boolean isPlayerWhitelisted( String name) { return whitelistPlayers.contains(name); } public boolean isNotifyAdmin() { return notifyAdmin; } public boolean isNotifyAll() { return notifyAll; } public int getViolationCount(String name) { return violationCount .getOrDefault(name, 0); } public void incrementViolation( String name) { violationCount.put(name, getViolationCount(name) + 1); } public List getWhitelistUrls() { return whitelistUrls; } public Set getWhitelistPlayers() { return whitelistPlayers; } public Map getPunishmentRules() { return punishmentRules; } public Map getPunishmentDurations() { return punishmentDurations; } public Map getMutedPlayers() { return mutedPlayers; } public void setNotifyAdmin(boolean v) { notifyAdmin = v; } public void setNotifyAll(boolean v) { notifyAll = v; } public void setMuteDuration(int v) { muteDuration = v; } public void addUrl(String url) { whitelistUrls.add( url.toLowerCase().trim()); saveConfig(); } public boolean removeUrl(String url) { boolean r = whitelistUrls.remove( url.toLowerCase().trim()); saveConfig(); return r; } public void addWhitelistPlayer( String name) { whitelistPlayers.add(name); saveConfig(); } public boolean removeWhitelistPlayer( String name) { boolean r = whitelistPlayers.remove(name); saveConfig(); return r; } public void cleanExpired() { long now = System.currentTimeMillis(); mutedPlayers.entrySet().removeIf( e -> e.getValue() <= now); } private boolean parseBool(String v) { String s = v.toLowerCase().trim(); return s.equals("true") || s.equals("on") || s.equals("yes") || s.equals("1") || s.contains("开启") || s.contains("启用") || s.contains("是"); } }