opwl接管原生OP名单:加载时导入原生OP,非法持有一律撤销+报警(新增非法持有报警信息模板,支持user/time变量中英文),改为单文件opwl.json
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"我们做了什么": {
|
||||
"第一层": "临时授权,允许你给OP设置到期时间,到期自动撤销",
|
||||
"第二层": "防止你的OP乱给别人OP引起人人都是OP"
|
||||
"第二层": "接管原生OP名单,不在白名单的一律撤销+报警"
|
||||
},
|
||||
"你需要做什么": [
|
||||
"共3步",
|
||||
@@ -21,6 +21,7 @@
|
||||
"启用状态": true,
|
||||
"无固定期限授权列表": [],
|
||||
"有固定期限授权列表": {},
|
||||
"非法持有报警信息": "§8[§cOP白名单§8] §f{user} §7不持有授权却持有 OP,已于 §f{time} §7自动撤销 §8| §8[OPWL] §f{user} §7held OP illegally, revoked at §f{time}",
|
||||
"默认时长": "24h",
|
||||
"时长支持格式": [
|
||||
"可以是中文口语:一个小时",
|
||||
@@ -28,6 +29,17 @@
|
||||
"可以是标准写法:24H",
|
||||
"格式很宽松,只要是标准json字符串格式就行。至于你怎么写,插件会解析"
|
||||
],
|
||||
"报警模板变量": {
|
||||
"{user}": "非法持有的玩家名(也认 player / 用户 / 玩家 / username)",
|
||||
"{time}": "发现时间(也认 时间 / date / 日期)",
|
||||
"写法": "大小写不敏感,中英文变量名都支持;& 颜色码可用,渲染后自动转成§"
|
||||
},
|
||||
"接管说明": [
|
||||
"本插件【接管原生OP名单】:插件首次加载时,会把当前所有原生OP自动导入「无固定期限授权列表」并写回本文件。",
|
||||
"之后:任何持有OP但不在白名单的玩家(不在永久表/ 临时已过期 / 时长无法解析),一律自动撤销OP并按「非法持有报警信息」报警。",
|
||||
"所以以后新增OP请用 /opwl add <玩家>,别用原版 /op;否则会被15秒内的巡检撤掉。",
|
||||
"白名单全部写在本文件里(单文件),没有子目录也没有附属名单文件。"
|
||||
],
|
||||
"以下是模板文件": {
|
||||
"# 启用状态": true,
|
||||
"# 无固定期限授权列表": [
|
||||
@@ -39,6 +51,7 @@
|
||||
"user2": "1小时",
|
||||
"user3": "two hour"
|
||||
},
|
||||
"# 非法持有报警信息": "支持 {user} 与 {time} 变量,中英文写法都认;留空则用内置默认文案",
|
||||
"# 默认时长": "24小时"
|
||||
},
|
||||
"为什么这次选择json": {
|
||||
@@ -55,13 +68,12 @@
|
||||
"opwl removetime user 1h": "给user用户扣1小时授权",
|
||||
"opwl remove user": "移除user的授权",
|
||||
"opwl list user": "获取user的授权状态",
|
||||
"opwl list":"列出OP白名单",
|
||||
"opwl reload":"重载OP白名单",
|
||||
"opwl list": "列出OP白名单",
|
||||
"opwl reload": "重载OP白名单",
|
||||
"温馨提示": "opwl命令仅限控台执行,普通用户无权",
|
||||
"加授权完全无感": "因为用户本身没有op时,被授权后,插件立即给予OP并记录授权状态。删除也是。不需要先添加用户在添加授权。"
|
||||
},
|
||||
"安全升级": [
|
||||
"不追求一定是config.json,你可以把文件改成任意.json,这样你可以在目录放一些其它文件,提升伪装。",
|
||||
"此文件即是配置文件也是说明书。这就意味着,你可以把其它混淆内容一起打包进本json文件,只要保留配置项。其它的你随便删改"
|
||||
"本文件即是配置文件也是说明书。这就意味着,你可以把其它混淆内容一起打包进本json文件,只要保留配置项。其它的你随便删改"
|
||||
]
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -21,41 +23,45 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
/**
|
||||
* OP 白名单(2026-10-07)
|
||||
*
|
||||
* 配置目录:plugins/Sdf1_login/OP白名单/ —— 里面【任意一个 .json 文件】就是白名单
|
||||
* 配置(文件名随意,按文件名排序只读首个;模板 opwl.json 随 jar 释放)。
|
||||
* 该 json 同时是配置文件与说明书,除四个真实配置项外的其它字段一律忽略、写回时原样保留。
|
||||
* ★ 本插件【接管原生 OP 名单】:白名单文件是 OP 的唯一真相,原版 ops.json 只剩"存档"。
|
||||
* - 加载时:把当前所有原生 OP 自动导入「无固定期限授权列表」并写回文件(一次性接管);
|
||||
* - 运行时:持 OP 但不在白名单(不在永久表 / 临时已过期 / 时长解析失败)=「非法持有」
|
||||
* → 一律撤销 OP + 按「非法持有报警信息」模板报警(控制台 + 全体在线 OP);
|
||||
* 报警带 {user}/{time} 变量(支持 user/player/用户/玩家、time/时间,中英文写法都认)。
|
||||
* - 白名单内的在线玩家缺 OP 时自动补授(原版 /op 给的人若不在白名单,15 秒内会被撤)。
|
||||
*
|
||||
* 行为(与 opwl.json 说明书一致):
|
||||
* - 读得出规则 → 白名单内玩家授 OP;【只授权不主动撤】:白名单外的玩家一律不碰
|
||||
* (原生 OP / 服主不受影响);本插件授过 OP 的玩家记入 granted(持久化到
|
||||
* .granted.txt),到期时自动撤销(说明书「到期自动撤销」)。
|
||||
* - 读不出规则(无 json / JSON 语法错 / 必需字段类型错)→ 【一次性】卸载全部
|
||||
* 已知玩家(含离线)的 OP,只执行一次,修复配置后自动恢复;
|
||||
* - 启用状态=false → 功能休眠,不授权也不卸载;
|
||||
* - 15 秒一次增量热重载:文件指纹(路径+lastModified+长度)有变化才重新解析并打
|
||||
* 日志,没变化只做静默的授权应用(新上线玩家 / 到期撤销),保持安静;
|
||||
* - opwl 命令仅限控台(Main 侧对玩家直接静默 return,连帮助都不给)。
|
||||
* 配置:单文件 plugins/Sdf1_login/opwl.json(模板随 jar 释放,不再有子目录/附属文件)。
|
||||
* 该 json 同时是配置文件与说明书,除配置项外的其它字段一律忽略、写回时原样保留。
|
||||
*
|
||||
* 授权应用只在主线程跑(scheduler 主线程 + 命令 + join 事件),无并发问题。
|
||||
* 其他行为:
|
||||
* - 读不出规则(无 json / JSON 语法错 / 必需字段类型错)→ 一次性卸载全部已知 OP,
|
||||
* 持续失败不重复执行,修复后自动恢复;
|
||||
* - 启用状态=false → 功能休眠,不授权也不撤销;
|
||||
* - 15 秒一次增量热重载:文件指纹(路径+lastModified+长度)变化才重解析并打日志;
|
||||
* - opwl 命令仅限控台(Main 侧对玩家静默 return,连帮助都不给)。
|
||||
*
|
||||
* 所有授权/撤销都在主线程执行(scheduler + 命令 + join 事件),无并发问题。
|
||||
*/
|
||||
public class OpWhiteListManager implements Listener {
|
||||
|
||||
/** 报警节流:同一人 60 秒内不重复报警(防刷屏,但仍会撤销) */
|
||||
private static final long WARN_COOLDOWN_MS = 60_000L;
|
||||
|
||||
private final Main plugin;
|
||||
private final File dir;
|
||||
private final File grantedFile;
|
||||
private final File file;
|
||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
/** 当前被读的 json(pickJson 的结果,命令写回也写它) */
|
||||
/** 当前白名单文件(= dataFolder/opwl.json) */
|
||||
private File watched;
|
||||
/** 解析成功后的完整配置(null = 解析失败态) */
|
||||
private JsonObject cfg;
|
||||
@@ -64,57 +70,52 @@ public class OpWhiteListManager implements Listener {
|
||||
/** 文件指纹(路径 + lastModified + 长度),变化才重载打日志 */
|
||||
private String lastPath = "";
|
||||
private long lastStamp = Long.MIN_VALUE;
|
||||
/** 本插件授过 OP 的玩家(小写名)——兑现「到期自动撤销」的凭据,持久化 */
|
||||
private final Set<String> granted = new HashSet<>();
|
||||
/** 报警节流表:小写用户名 → 上次报警毫秒 */
|
||||
private final Map<String, Long> lastWarn = new HashMap<>();
|
||||
|
||||
public OpWhiteListManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.dir = new File(plugin.getDataFolder(), "OP白名单");
|
||||
this.grantedFile = new File(dir, ".granted.txt");
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
this.file = new File(plugin.getDataFolder(), "opwl.json");
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
// 模板随 jar 释放(只在不存在时复制,绝不覆盖用户改过的文件)
|
||||
File tpl = new File(dir, "opwl.json");
|
||||
if (!tpl.exists()) {
|
||||
try (InputStream in = plugin.getResource("OP白名单/opwl.json")) {
|
||||
if (!file.exists()) {
|
||||
try (InputStream in = plugin.getResource("opwl.json")) {
|
||||
if (in != null) {
|
||||
Files.copy(in, tpl.toPath());
|
||||
plugin.getLogger().info("[OP白名单] 已释放模板配置: OP白名单/" + tpl.getName());
|
||||
Files.copy(in, file.toPath());
|
||||
plugin.getLogger().info("[OP白名单] 已释放配置模板: plugins/Sdf1_login/opwl.json");
|
||||
} else {
|
||||
plugin.getLogger().warning("[OPWl白名单] jar 内未找到模板 OP白名单/opwl.json,请手工在目录里放一个 .json");
|
||||
plugin.getLogger().warning("[OP白名单] jar 内未找到模板 opwl.json,请手工放置该文件");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[OP白名单] 模板释放失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
loadGranted();
|
||||
reloadInternal(true);
|
||||
// 15 秒增量热重载 + 静默授权应用
|
||||
// 接管原生 OP 名单:首次加载先把现有 OP 全导入白名单(一次性)
|
||||
reloadInternal(true, true);
|
||||
if (cfg != null) {
|
||||
importNativeOps();
|
||||
}
|
||||
// 15 秒增量热重载 + 静默应用
|
||||
Bukkit.getScheduler().runTaskTimer(plugin, this::tick, 300L, 300L);
|
||||
}
|
||||
|
||||
// ================= 配置读取 =================
|
||||
|
||||
/** 目录里任意一个 .json = 白名单文件;文件名按字典序取首个(确定性) */
|
||||
private File pickJson() {
|
||||
File[] fs = dir.listFiles((d, n) -> n.toLowerCase(Locale.ROOT).endsWith(".json"));
|
||||
if (fs == null || fs.length == 0) {
|
||||
return null;
|
||||
}
|
||||
Arrays.sort(fs, (a, b) -> a.getName().compareToIgnoreCase(b.getName()));
|
||||
return fs[0];
|
||||
}
|
||||
|
||||
private void reloadInternal(boolean log) {
|
||||
watched = pickJson();
|
||||
loadFrom(watched, log);
|
||||
private void reloadInternal(boolean log, boolean importNative) {
|
||||
watched = file;
|
||||
loadFrom(file, log);
|
||||
updateStamp();
|
||||
if (importNative && cfg != null) {
|
||||
importNativeOps();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFrom(File f, boolean log) {
|
||||
if (f == null) {
|
||||
fail("OP白名单目录里没有任何 .json 文件");
|
||||
if (f == null || !f.exists()) {
|
||||
fail("OP白名单配置文件不存在(plugins/Sdf1_login/opwl.json)");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -134,19 +135,46 @@ public class OpWhiteListManager implements Listener {
|
||||
}
|
||||
cfg = o;
|
||||
if (parseFailed) {
|
||||
plugin.getLogger().info("[OP白名单] 配置已恢复正常,恢复授权管理");
|
||||
plugin.getLogger().info("[OP白名单] 配置已恢复正常,恢复接管");
|
||||
}
|
||||
parseFailed = false;
|
||||
if (log) {
|
||||
plugin.getLogger().info("[OP白名单] 已重载 " + f.getName() + ":"
|
||||
plugin.getLogger().info("[OP白名单] 已重载 opwl.json:"
|
||||
+ (isEnabled() ? "启用" : "停用") + ",无固定期限 "
|
||||
+ permanentList().size() + " 人,有固定期限 " + timedObject().size() + " 人");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
fail(f.getName() + " 解析失败:" + e.getMessage());
|
||||
fail("opwl.json 解析失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 接管原生 OP:把当前所有 OP 且不在白名单的玩家,自动导入「无固定期限授权列表」。
|
||||
* 幂等——已在白名单的不动;已撤销的非法持有者不再是 OP,下次接管不会把他加回来。
|
||||
*/
|
||||
private void importNativeOps() {
|
||||
List<String> add = new ArrayList<>();
|
||||
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
|
||||
String n = op.getName();
|
||||
if (n == null || n.isEmpty() || !op.isOp()) {
|
||||
continue;
|
||||
}
|
||||
if (hasPermanent(n) || findTimed(n) != null) {
|
||||
continue;
|
||||
}
|
||||
add.add(n);
|
||||
}
|
||||
if (add.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (String n : add) {
|
||||
permanentList().add(new JsonPrimitive(n));
|
||||
}
|
||||
saveConfig();
|
||||
plugin.getLogger().info("[OP白名单] 已接管原生 OP 名单:导入 " + add.size()
|
||||
+ " 人为无固定期限授权 → " + String.join(", ", add));
|
||||
}
|
||||
|
||||
/** 解析失败 → 一次性卸载全部已知 OP(含离线),持续失败不重复执行 */
|
||||
private void fail(String why) {
|
||||
cfg = null;
|
||||
@@ -155,95 +183,122 @@ public class OpWhiteListManager implements Listener {
|
||||
}
|
||||
parseFailed = true;
|
||||
int n = 0;
|
||||
for (OfflinePlayer op : Bukkit.getOperators()) {
|
||||
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
|
||||
if (op.isOp()) {
|
||||
op.setOp(false);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
granted.clear();
|
||||
saveGranted();
|
||||
lastWarn.clear();
|
||||
plugin.getLogger().warning("[OP白名单] " + why
|
||||
+ " —— 已一次性卸载全部 " + n + " 名玩家的 OP;修复 json 后自动恢复授权。");
|
||||
+ " —— 已一次性卸载全部 " + n + " 名玩家的 OP;修复 json 后自动恢复接管。");
|
||||
}
|
||||
|
||||
/** 文件指纹:路径 + lastModified + 长度(任一变化都视为变更) */
|
||||
private void updateStamp() {
|
||||
File f = (watched != null && watched.exists()) ? watched : pickJson();
|
||||
if (f == null) {
|
||||
lastPath = "";
|
||||
lastStamp = 0L;
|
||||
return;
|
||||
}
|
||||
lastPath = f.getAbsolutePath();
|
||||
lastStamp = f.lastModified() + f.length();
|
||||
lastPath = file.getAbsolutePath();
|
||||
lastStamp = file.exists() ? file.lastModified() + file.length() : 0L;
|
||||
}
|
||||
|
||||
// ================= 15 秒 tick =================
|
||||
|
||||
private void tick() {
|
||||
File f = pickJson();
|
||||
String p = f == null ? "" : f.getAbsolutePath();
|
||||
long st = f == null ? 0L : f.lastModified() + f.length();
|
||||
if (!p.equals(lastPath) || st != lastStamp) {
|
||||
// 有变化:重新解析 + 打日志(含解析失败的告警)
|
||||
watched = f;
|
||||
loadFrom(f, true);
|
||||
long st = file.exists() ? file.lastModified() + file.length() : 0L;
|
||||
if (!lastPath.equals(file.getAbsolutePath()) || st != lastStamp) {
|
||||
loadFrom(file, true); // 有变化:重新解析 + 打日志(含解析失败告警)
|
||||
updateStamp();
|
||||
}
|
||||
// 无论有无变化都做一次静默的授权应用(不打无变化日志,保持安静)
|
||||
// 无论有无变化都做一次静默应用(撤销非法持有 + 补授权),保持安静
|
||||
apply();
|
||||
}
|
||||
|
||||
/** 玩家上线:立即应用一次(白名单内马上拿到 OP,不用等下个 15 秒) */
|
||||
/** 玩家上线:立即应用一次(白名单内马上拿到 OP,不等下个 15 秒) */
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent e) {
|
||||
apply();
|
||||
}
|
||||
|
||||
// ================= 授权应用(只授不撤,例外仅两处:到期、解析失败) =================
|
||||
// ================= 接管:撤销非法持有 + 补授权 =================
|
||||
|
||||
private void apply() {
|
||||
if (cfg == null || !isEnabled()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
// 1) 到期自动撤销:只动本插件授过 OP 且当前在「有固定期限」表里、已过期的玩家。
|
||||
// 白名单外的其他人(原生 OP 等)一律不碰。
|
||||
boolean dirty = false;
|
||||
Iterator<String> it = granted.iterator();
|
||||
while (it.hasNext()) {
|
||||
String key = it.next();
|
||||
String timed = findTimed(key);
|
||||
if (timed == null) {
|
||||
// 1) 非法持有扫描:持 OP 但不在白名单 → 撤销 + 报警
|
||||
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
|
||||
String n = op.getName();
|
||||
if (n == null || n.isEmpty() || !op.isOp()) {
|
||||
continue;
|
||||
}
|
||||
long exp = expireOf(timed);
|
||||
if (exp > 0 && exp <= now) {
|
||||
setOpByName(key, false);
|
||||
it.remove();
|
||||
dirty = true;
|
||||
plugin.getLogger().info("[OP白名单] " + key + " 授权到期,已自动撤销 OP");
|
||||
if (isWhitelisted(n)) {
|
||||
continue;
|
||||
}
|
||||
setOpByName(n, false);
|
||||
warnIllegal(n, now, describeIllegal(n));
|
||||
}
|
||||
if (dirty) {
|
||||
saveGranted();
|
||||
}
|
||||
// 2) 白名单内在线玩家授权(已是 OP 的跳过;新授的记入 granted)
|
||||
// 2) 白名单内在线玩家补授(已是 OP 的跳过)
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.isOp()) {
|
||||
continue;
|
||||
}
|
||||
if (!isWhitelisted(p.getName())) {
|
||||
if (p.isOp() || !isWhitelisted(p.getName())) {
|
||||
continue;
|
||||
}
|
||||
p.setOp(true);
|
||||
granted.add(p.getName().toLowerCase(Locale.ROOT));
|
||||
saveGranted();
|
||||
plugin.getLogger().info("[OP白名单] 已授权 " + p.getName() + " 获得 OP");
|
||||
}
|
||||
}
|
||||
|
||||
/** 撤销非法持有 + 报警(控制台 + 全体在线 OP),带 60 秒节流 */
|
||||
private void warnIllegal(String name, long now, String reason) {
|
||||
String key = name.toLowerCase(Locale.ROOT);
|
||||
long last = lastWarn.getOrDefault(key, 0L);
|
||||
if (last > 0 && now - last < WARN_COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
lastWarn.put(key, now);
|
||||
String alarm = renderAlarm(name, now);
|
||||
plugin.getLogger().warning("[OP白名单] 非法持有已撤销 | 原因: " + reason + " | " + stripColor(alarm));
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
if (p.isOp()) {
|
||||
p.sendMessage(alarm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 报警模板渲染:{user}/{player}/{用户}/{玩家} 与 {time}/{时间} 变量(大小写不敏感) */
|
||||
private String renderAlarm(String user, long now) {
|
||||
String tpl = alarmTemplate();
|
||||
String time = fmtFull(now);
|
||||
String s = tpl;
|
||||
for (String k : new String[]{"user", "player", "用户", "玩家", "username"}) {
|
||||
s = s.replaceAll("(?i)\\{" + k + "\\}", Matcher.quoteReplacement(user));
|
||||
}
|
||||
for (String k : new String[]{"time", "时间", "date", "日期"}) {
|
||||
s = s.replaceAll("(?i)\\{" + k + "\\}", Matcher.quoteReplacement(time));
|
||||
}
|
||||
return ChatColor.translateAlternateColorCodes('&', s);
|
||||
}
|
||||
|
||||
/** 报警模板:配置项「非法持有报警信息」,空则用内置默认(中英双语) */
|
||||
private String alarmTemplate() {
|
||||
if (cfg != null && cfg.has("非法持有报警信息")
|
||||
&& cfg.get("非法持有报警信息").isJsonPrimitive()) {
|
||||
String s = cfg.get("非法持有报警信息").getAsString().trim();
|
||||
if (!s.isEmpty()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return "§8[§cOP白名单§8] §f{user} §7不持有授权却持有 OP,已于 §f{time} §7自动撤销"
|
||||
+ " §8| §8[OPWL] §f{user} §7held OP illegally, revoked at §f{time}";
|
||||
}
|
||||
|
||||
/** 撤销原因(仅日志用,便于排查是哪一类"不在白名单") */
|
||||
private String describeIllegal(String name) {
|
||||
if (findTimed(name) != null) {
|
||||
return "临时授权已过期或时长无法解析(" + findTimed(name) + ")";
|
||||
}
|
||||
return "不在白名单(" + (Bukkit.getPlayerExact(name) != null ? "在线" : "离线") + ")";
|
||||
}
|
||||
|
||||
private void setOpByName(String name, boolean op) {
|
||||
Player p = Bukkit.getPlayerExact(name);
|
||||
if (p != null) {
|
||||
@@ -310,10 +365,6 @@ public class OpWhiteListManager implements Listener {
|
||||
}
|
||||
|
||||
private boolean removeTimed(String name) {
|
||||
String k = timedKeyOf(name);
|
||||
if (timedObject().has(k) && !k.equalsIgnoreCase(name) && timedObject().get(k) != null) {
|
||||
// key 原样存在
|
||||
}
|
||||
for (String key : new ArrayList<>(timedObject().keySet())) {
|
||||
if (name.equalsIgnoreCase(key)) {
|
||||
timedObject().remove(key);
|
||||
@@ -323,7 +374,7 @@ public class OpWhiteListManager implements Listener {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 是否白名单有效成员:永久 或 有期限且未过期(坏时长视为无效,不授) */
|
||||
/** 是否白名单有效成员:永久 或 有期限且未过期(坏时长视为无效 → 算非法持有) */
|
||||
private boolean isWhitelisted(String name) {
|
||||
if (cfg == null) {
|
||||
return false;
|
||||
@@ -339,7 +390,7 @@ public class OpWhiteListManager implements Listener {
|
||||
return e > 0 && e > System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** 解析为绝对到期毫秒;解析失败返回 -1(调用方按「无效/不过期」保守处理) */
|
||||
/** 解析为绝对到期毫秒;解析失败返回 -1(调用方按「无效」保守处理) */
|
||||
private long expireOf(String v) {
|
||||
try {
|
||||
return DurationParser.parseToExpireMs(v);
|
||||
@@ -360,50 +411,20 @@ public class OpWhiteListManager implements Listener {
|
||||
|
||||
// ================= 持久化 =================
|
||||
|
||||
/** 写回 json:保留用户在配置项之外塞的所有字段(说明书/混淆内容),只动配置项 */
|
||||
/** 写回 json:保留配置项之外的所有说明书/混淆字段,只动配置项 */
|
||||
private void saveConfig() {
|
||||
try {
|
||||
File target = watched;
|
||||
if (target == null || !target.exists()) {
|
||||
target = new File(dir, "opwl.json");
|
||||
watched = target;
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
Files.write(target.toPath(), gson.toJson(cfg).getBytes(StandardCharsets.UTF_8));
|
||||
Files.write(file.toPath(), gson.toJson(cfg).getBytes(StandardCharsets.UTF_8));
|
||||
updateStamp(); // 命令自己有回馈,抑制下个 tick 再打一次「已重载」
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[OP白名单] 配置写回失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void loadGranted() {
|
||||
granted.clear();
|
||||
if (!grantedFile.exists()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (String line : Files.readAllLines(grantedFile.toPath(), StandardCharsets.UTF_8)) {
|
||||
String s = line.trim().toLowerCase(Locale.ROOT);
|
||||
if (!s.isEmpty()) {
|
||||
granted.add(s);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[OP白名单] 授权痕迹读取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveGranted() {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : granted) {
|
||||
sb.append(s).append('\n');
|
||||
}
|
||||
Files.write(grantedFile.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[OP白名单] 授权痕迹写入失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ================= opwl 命令(仅控台;Main 已拦玩家) =================
|
||||
|
||||
public void onCommand(org.bukkit.command.CommandSender sender, String[] args) {
|
||||
@@ -414,14 +435,14 @@ public class OpWhiteListManager implements Listener {
|
||||
sendHelp(sender);
|
||||
return;
|
||||
case "reload": {
|
||||
reloadInternal(true);
|
||||
reloadInternal(true, false);
|
||||
apply();
|
||||
sender.sendMessage(cfg == null
|
||||
? "§c重载完成,但配置解析失败(详见控制台告警)"
|
||||
: "§a重载完成:§f" + (isEnabled() ? "启用" : "停用")
|
||||
+ ",无固定期限 " + permanentList().size()
|
||||
+ " 人,有固定期限 " + timedObject().size() + " 人" +
|
||||
"§d欢迎来到草原探险服务器,ip: mc2.ypshidifu.cn 端口(基岩版需要):30679");
|
||||
+ " 人,有固定期限 " + timedObject().size() + " 人");
|
||||
sender.sendMessage("§7提示:原生 OP 只在插件【首次加载】时导入;之后请用 /opwl add 或改 json 加人");
|
||||
return;
|
||||
}
|
||||
default:
|
||||
@@ -429,7 +450,7 @@ public class OpWhiteListManager implements Listener {
|
||||
}
|
||||
// 以下子命令需要配置可读
|
||||
if (cfg == null) {
|
||||
sender.sendMessage("§c配置解析失败,先修复 OP白名单 目录里的 json 再操作(或 /opwl reload)");
|
||||
sender.sendMessage("§c配置解析失败,先修复 plugins/Sdf1_login/opwl.json 再操作(或 /opwl reload)");
|
||||
return;
|
||||
}
|
||||
boolean readOnly = "list".equals(sub);
|
||||
@@ -444,12 +465,13 @@ public class OpWhiteListManager implements Listener {
|
||||
return;
|
||||
}
|
||||
String name = args[1].trim();
|
||||
boolean moved = removeTimed(name); // 已有临时授权 → 转永久
|
||||
boolean moved = removeTimed(name);
|
||||
if (!hasPermanent(name)) {
|
||||
permanentList().add(new com.google.gson.JsonPrimitive(name));
|
||||
permanentList().add(new JsonPrimitive(name));
|
||||
}
|
||||
saveConfig();
|
||||
boolean ok = grantNow(name, true);
|
||||
lastWarn.remove(name.toLowerCase(Locale.ROOT));
|
||||
boolean ok = setOpNow(name);
|
||||
sender.sendMessage("§a已添加 §f" + name + " §a为无固定期限授权"
|
||||
+ (moved ? "(已把原临时授权转为永久)" : "")
|
||||
+ (ok ? ",OP 已授予" : "(玩家不在本机档案中,上线时自动授予)"));
|
||||
@@ -474,10 +496,11 @@ public class OpWhiteListManager implements Listener {
|
||||
sender.sendMessage("§c该时长的到期时间已在过去: " + dur);
|
||||
return;
|
||||
}
|
||||
removePermanent(name); // 临时授权优先:从永久表移出
|
||||
removePermanent(name);
|
||||
timedObject().addProperty(timedKeyOf(name), dur);
|
||||
saveConfig();
|
||||
boolean ok = grantNow(name, true);
|
||||
lastWarn.remove(name.toLowerCase(Locale.ROOT));
|
||||
boolean ok = setOpNow(name);
|
||||
sender.sendMessage("§a已给 §f" + name + " §a添加临时授权 §7(时长 " + dur
|
||||
+ ",到期 " + fmt(exp) + ")" + (ok ? ",OP 已授予" : ",上线时自动授予"));
|
||||
return;
|
||||
@@ -519,9 +542,8 @@ public class OpWhiteListManager implements Listener {
|
||||
if (newExp <= System.currentTimeMillis()) {
|
||||
removeTimed(name);
|
||||
saveConfig();
|
||||
boolean revoked = revokeNow(name);
|
||||
sender.sendMessage("§a扣减后 " + name + " 的授权已到期,条目已移除"
|
||||
+ (revoked ? ",OP 已撤销" : ""));
|
||||
setOpByName(name, false);
|
||||
sender.sendMessage("§a扣减后 " + name + " 的授权已到期,条目已移除,OP 已撤销");
|
||||
} else {
|
||||
String s = fmt(newExp);
|
||||
timedObject().addProperty(timedKeyOf(name), s);
|
||||
@@ -540,13 +562,15 @@ public class OpWhiteListManager implements Listener {
|
||||
boolean inPerm = removePermanent(name);
|
||||
boolean inTimed = removeTimed(name);
|
||||
saveConfig();
|
||||
boolean revoked = revokeNow(name);
|
||||
if (!inPerm && !inTimed && !revoked) {
|
||||
sender.sendMessage("§e" + name + " 本就不在白名单中");
|
||||
boolean wasOp = isOpNow(name);
|
||||
setOpByName(name, false);
|
||||
lastWarn.remove(name.toLowerCase(Locale.ROOT));
|
||||
if (!inPerm && !inTimed && !wasOp) {
|
||||
sender.sendMessage("§e" + name + " 本就不在白名单中,也没有 OP");
|
||||
} else {
|
||||
sender.sendMessage("§a已移除 §f" + name + " §a的授权"
|
||||
+ (revoked ? ",OP 已撤销"
|
||||
: ((inPerm || inTimed) ? "(该玩家 OP 非本插件授予,OP 状态未动)" : "")));
|
||||
+ (wasOp ? ",OP 已撤销" : ""));
|
||||
sender.sendMessage("§7提示:若要长用请 /opwl add " + name + ",否则他的 OP 会被自动接管撤销");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -555,8 +579,9 @@ public class OpWhiteListManager implements Listener {
|
||||
listOne(sender, args[1].trim());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage("§e===== OP白名单 §7(" + (watched != null ? watched.getName() : "?") + ") =====");
|
||||
sender.sendMessage("§7状态: " + (isEnabled() ? "§a启用" : "§c停用")
|
||||
sender.sendMessage("§e===== OP白名单(接管原生 OP) =====");
|
||||
sender.sendMessage("§7配置: §fplugins/Sdf1_login/opwl.json §7| 状态: "
|
||||
+ (isEnabled() ? "§a启用" : "§c停用")
|
||||
+ " §7| 解析: " + (parseFailed ? "§c失败" : "§a正常"));
|
||||
JsonArray pu = permanentList();
|
||||
StringBuilder sb = new StringBuilder("§e无固定期限(" + pu.size() + "): ");
|
||||
@@ -580,52 +605,46 @@ public class OpWhiteListManager implements Listener {
|
||||
long e = expireOf(v);
|
||||
String state = e <= 0 ? "§c到期时间无法解析: " + v
|
||||
: (e > now ? "§7到期 §f" + fmt(e) + " §7(剩 " + human(e - now) + ")"
|
||||
: "§c已过期 " + fmt(e));
|
||||
: "§c已过期 " + fmt(e) + "(OP 将被撤销)");
|
||||
sender.sendMessage("§7 " + en.getKey() + " → " + state);
|
||||
}
|
||||
// 额外:谁在偷偷持有 OP(白名单外的)
|
||||
List<String> illegal = new ArrayList<>();
|
||||
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
|
||||
String n = op.getName();
|
||||
if (n != null && !n.isEmpty() && op.isOp() && !isWhitelisted(n)) {
|
||||
illegal.add(n);
|
||||
}
|
||||
}
|
||||
sender.sendMessage("§e当前持 OP 但不在白名单(" + illegal.size() + "): "
|
||||
+ (illegal.isEmpty() ? "§a无" : "§c" + String.join("§7, §c", illegal)));
|
||||
return;
|
||||
}
|
||||
default:
|
||||
sendHelp(sender);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ================= 命令辅助 =================
|
||||
|
||||
/** 立即授予/撤销并记录;返回 false = 档案未知(上线时由 join 自动补授) */
|
||||
private boolean grantNow(String name, boolean op) {
|
||||
if (!op) {
|
||||
return revokeNow(name);
|
||||
}
|
||||
/** 立即授 OP;返回 false = 档案未知(上线时由 join 自动补授) */
|
||||
private boolean setOpNow(String name) {
|
||||
Player p = Bukkit.getPlayerExact(name);
|
||||
if (p != null) {
|
||||
p.setOp(true);
|
||||
granted.add(name.toLowerCase(Locale.ROOT));
|
||||
saveGranted();
|
||||
return true;
|
||||
}
|
||||
OfflinePlayer o = Bukkit.getOfflinePlayer(name);
|
||||
if (!o.hasPlayedBefore()) {
|
||||
// 档案未知:留到 join 兜底(isWhitelisted && !isOp → 授)
|
||||
return false;
|
||||
}
|
||||
o.setOp(true);
|
||||
granted.add(name.toLowerCase(Locale.ROOT));
|
||||
saveGranted();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 仅撤销本插件授过 OP 的玩家;返回是否真的撤了 */
|
||||
private boolean revokeNow(String name) {
|
||||
String key = name.toLowerCase(Locale.ROOT);
|
||||
if (!granted.contains(key)) {
|
||||
return false;
|
||||
}
|
||||
setOpByName(name, false);
|
||||
granted.remove(key);
|
||||
saveGranted();
|
||||
return true;
|
||||
private boolean isOpNow(String name) {
|
||||
Player online = Bukkit.getPlayerExact(name);
|
||||
return online != null ? online.isOp() : Bukkit.getOfflinePlayer(name).isOp();
|
||||
}
|
||||
|
||||
private void listOne(org.bukkit.command.CommandSender s, String name) {
|
||||
@@ -638,30 +657,27 @@ public class OpWhiteListManager implements Listener {
|
||||
long now = System.currentTimeMillis();
|
||||
if (e > 0) {
|
||||
s.sendMessage("§7 类型: §f临时 §7到期 §f" + fmt(e)
|
||||
+ (e > now ? " §7(剩 " + human(e - now) + ")" : " §c(已过期)"));
|
||||
+ (e > now ? " §7(剩 " + human(e - now) + ")" : " §c(已过期,OP 会被撤销)"));
|
||||
} else {
|
||||
s.sendMessage("§7 类型: §f临时 §c(到期时间无法解析: " + t + ")");
|
||||
s.sendMessage("§7 类型: §f临时 §c(到期时间无法解析: " + t + ",OP 会被撤销)");
|
||||
}
|
||||
} else {
|
||||
s.sendMessage("§7 类型: §c不在白名单");
|
||||
s.sendMessage("§7 类型: §c不在白名单(持有 OP 会被自动撤销并报警)");
|
||||
}
|
||||
Player online = Bukkit.getPlayerExact(name);
|
||||
boolean isOp = online != null ? online.isOp() : Bukkit.getOfflinePlayer(name).isOp();
|
||||
s.sendMessage("§7 当前OP: " + (isOp ? "§f是" : "§f否")
|
||||
+ " §7| 本插件授OP记录: " + (granted.contains(name.toLowerCase(Locale.ROOT)) ? "§f是" : "§f否"));
|
||||
s.sendMessage("§7 当前OP: " + (isOpNow(name) ? "§f是" : "§f否"));
|
||||
}
|
||||
|
||||
private void sendHelp(org.bukkit.command.CommandSender s) {
|
||||
s.sendMessage("§e===== OP白名单(仅控制台可用) =====");
|
||||
s.sendMessage("§e===== OP白名单(仅控制台可用,接管原生 OP) =====");
|
||||
s.sendMessage("§f/opwl §7- 本帮助");
|
||||
s.sendMessage("§f/opwl add <玩家> §7- 添加无固定期限授权");
|
||||
s.sendMessage("§f/opwl addtime <玩家> [时长] §7- 添加临时授权(缺省用默认时长 "
|
||||
+ defaultDuration() + ")");
|
||||
s.sendMessage("§f/opwl addtime <玩家> [时长] §7- 添加临时授权(缺省 " + defaultDuration() + ")");
|
||||
s.sendMessage("§f/opwl removetime <玩家> <扣减时长> §7- 扣减授权时长(如 1h)");
|
||||
s.sendMessage("§f/opwl remove <玩家> §7- 移除授权");
|
||||
s.sendMessage("§f/opwl list [玩家] §7- 查看白名单 / 单个玩家状态");
|
||||
s.sendMessage("§f/opwl remove <玩家> §7- 移除授权并撤销其 OP");
|
||||
s.sendMessage("§f/opwl list [玩家] §7- 查看白名单 / 非法持有者 / 单人状态");
|
||||
s.sendMessage("§f/opwl reload §7- 重载配置");
|
||||
s.sendMessage("§7配置目录: plugins/Sdf1_login/OP白名单/(任意 *.json),15 秒自动热重载");
|
||||
s.sendMessage("§7配置: plugins/Sdf1_login/opwl.json(单文件,白名单全在里面),15 秒自动热重载");
|
||||
s.sendMessage("§7报警模板变量: §f{user}§7=玩家名 §f{time}§7=发现时间(中英文写法均可)");
|
||||
s.sendMessage("§d欢迎来到草原探险服务器,ip: mc2.ypshidifu.cn 端口(基岩版需要):30679");
|
||||
}
|
||||
|
||||
@@ -681,6 +697,10 @@ public class OpWhiteListManager implements Listener {
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date(ms));
|
||||
}
|
||||
|
||||
private static String fmtFull(long ms) {
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ms));
|
||||
}
|
||||
|
||||
/** 剩余时长人性化:X天X小时X分 */
|
||||
private static String human(long ms) {
|
||||
long min = ms / 60000L;
|
||||
@@ -702,4 +722,9 @@ public class OpWhiteListManager implements Listener {
|
||||
}
|
||||
return sb.length() == 0 ? "不到1分钟" : sb.toString();
|
||||
}
|
||||
|
||||
/** 日志里去掉颜色符号,避免控制台刷屏乱码 */
|
||||
private static String stripColor(String s) {
|
||||
return s == null ? "" : s.replaceAll("(?i)&[0-9a-k]", "").replaceAll("\u00a7[0-9a-k]", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"OP白名单": "服务器的好管家",
|
||||
"为什么要OP白名单": {
|
||||
"根因有3点": {
|
||||
"第一点": "已有OP乱给权限防不住",
|
||||
"第二点": "有人利用服务器漏洞拿到OP权限",
|
||||
"第三点": "不信任OP"
|
||||
}
|
||||
},
|
||||
"我们做了什么": {
|
||||
"第一层": "临时授权,允许你给OP设置到期时间,到期自动撤销",
|
||||
"第二层": "接管原生OP名单,不在白名单的一律撤销+报警"
|
||||
},
|
||||
"你需要做什么": [
|
||||
"共3步",
|
||||
"第一步:安装sdf1_login插件",
|
||||
"第二步改这个文件",
|
||||
"第三步按参考格式或后台指令添加授权"
|
||||
],
|
||||
"----": "----",
|
||||
"启用状态": true,
|
||||
"无固定期限授权列表": [],
|
||||
"有固定期限授权列表": {},
|
||||
"非法持有报警信息": "§8[§cOP白名单§8] §f{user} §7不持有授权却持有 OP,已于 §f{time} §7自动撤销 §8| §8[OPWL] §f{user} §7held OP illegally, revoked at §f{time}",
|
||||
"默认时长": "24h",
|
||||
"时长支持格式": [
|
||||
"可以是中文口语:一个小时",
|
||||
"可以是英文one hour",
|
||||
"可以是标准写法:24H",
|
||||
"格式很宽松,只要是标准json字符串格式就行。至于你怎么写,插件会解析"
|
||||
],
|
||||
"报警模板变量": {
|
||||
"{user}": "非法持有的玩家名(也认 player / 用户 / 玩家 / username)",
|
||||
"{time}": "发现时间(也认 时间 / date / 日期)",
|
||||
"写法": "大小写不敏感,中英文变量名都支持;& 颜色码可用,渲染后自动转成§"
|
||||
},
|
||||
"接管说明": [
|
||||
"本插件【接管原生OP名单】:插件首次加载时,会把当前所有原生OP自动导入「无固定期限授权列表」并写回本文件。",
|
||||
"之后:任何持有OP但不在白名单的玩家(不在永久表/ 临时已过期 / 时长无法解析),一律自动撤销OP并按「非法持有报警信息」报警。",
|
||||
"所以以后新增OP请用 /opwl add <玩家>,别用原版 /op;否则会被15秒内的巡检撤掉。",
|
||||
"白名单全部写在本文件里(单文件),没有子目录也没有附属名单文件。"
|
||||
],
|
||||
"以下是模板文件": {
|
||||
"# 启用状态": true,
|
||||
"# 无固定期限授权列表": [
|
||||
"user1",
|
||||
"user2"
|
||||
],
|
||||
"# 有固定期限授权列表": {
|
||||
"user1": "2026-10-06 23:00",
|
||||
"user2": "1小时",
|
||||
"user3": "two hour"
|
||||
},
|
||||
"# 非法持有报警信息": "支持 {user} 与 {time} 变量,中英文写法都认;留空则用内置默认文案",
|
||||
"# 默认时长": "24小时"
|
||||
},
|
||||
"为什么这次选择json": {
|
||||
"根因依旧2点": {
|
||||
"1": "防止非专业人士修改本文件",
|
||||
"2": "使用专业格式,让小白试探着劝退(一旦解析失败,所有玩家OP全部卸载)"
|
||||
}
|
||||
},
|
||||
"指令参数": {
|
||||
"opwl": "帮助菜单",
|
||||
"opwl add user": "添加user用户为无固定期限用户",
|
||||
"opwl addtime user": "按默认时长添加临时授权用户",
|
||||
"opwl addtime user 1h": "给user用户价1小时授权",
|
||||
"opwl removetime user 1h": "给user用户扣1小时授权",
|
||||
"opwl remove user": "移除user的授权",
|
||||
"opwl list user": "获取user的授权状态",
|
||||
"opwl list": "列出OP白名单",
|
||||
"opwl reload": "重载OP白名单",
|
||||
"温馨提示": "opwl命令仅限控台执行,普通用户无权",
|
||||
"加授权完全无感": "因为用户本身没有op时,被授权后,插件立即给予OP并记录授权状态。删除也是。不需要先添加用户在添加授权。"
|
||||
},
|
||||
"安全升级": [
|
||||
"本文件即是配置文件也是说明书。这就意味着,你可以把其它混淆内容一起打包进本json文件,只要保留配置项。其它的你随便删改"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user