diff --git a/src/main/java/de/strifel/VTools/OnlinePlayerQueryService.java b/src/main/java/com/alpt/vtools/listeners/OnlinePlayerQueryService.java similarity index 73% rename from src/main/java/de/strifel/VTools/OnlinePlayerQueryService.java rename to src/main/java/com/alpt/vtools/listeners/OnlinePlayerQueryService.java index 3422a3f..62b7efc 100644 --- a/src/main/java/de/strifel/VTools/OnlinePlayerQueryService.java +++ b/src/main/java/com/alpt/vtools/listeners/OnlinePlayerQueryService.java @@ -1,8 +1,9 @@ -package de.strifel.VTools; +package com.alpt.vtools.listeners; import com.google.gson.Gson; import com.sun.net.httpserver.HttpServer; import com.velocitypowered.api.proxy.Player; +import de.strifel.VTools.VTools; import org.jetbrains.annotations.TestOnly; import java.io.IOException; @@ -13,18 +14,26 @@ import java.util.Collection; public class OnlinePlayerQueryService { - + public static OnlinePlayerQueryService INSTANCE; private final VTools plugin; - public OnlinePlayerQueryService(VTools plugin) { + private OnlinePlayerQueryService(VTools plugin) { this.plugin = plugin; } private static final Gson gson = new Gson(); - public void register() { + public static OnlinePlayerQueryService createInstance(VTools plugin) { + if (INSTANCE != null) return INSTANCE; + INSTANCE = new OnlinePlayerQueryService(plugin); + INSTANCE.register(); + return INSTANCE; + } + + private void register() { + int port = Integer.parseInt(plugin.getConfigOrDefault("http_service_port", "17611")); try { - HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 17611), 0); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0); server.createContext("/api/getOnlinePlayers", exchange -> { Collection players = plugin.getServer().getAllPlayers(); diff --git a/src/main/java/com/alpt/vtools/listeners/ServerCloser.java b/src/main/java/com/alpt/vtools/listeners/ServerCloser.java new file mode 100644 index 0000000..5d64e71 --- /dev/null +++ b/src/main/java/com/alpt/vtools/listeners/ServerCloser.java @@ -0,0 +1,223 @@ +package com.alpt.vtools.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.ProxyServer; +import de.strifel.VTools.VTools; +import de.strifel.VTools.listeners.TGBridge; +import okhttp3.*; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingQueue; + +public class ServerCloser { + private static final long MINUTE = 60L * 1000; + private static final OkHttpClient CLIENT = new OkHttpClient(); + + private static final String ACTION_STOP_JSON = "{\"action\":\"stop\"}"; + @SuppressWarnings("java:S1068") + private static final String ACTION_START_JSON = "{\"action\":\"start\"}"; + private final VTools plugin; + private final ProxyServer server; + + private final LinkedBlockingQueue lock = new LinkedBlockingQueue<>(); + + public static ServerCloser INSTANCE; + + private ServerCloser(VTools plugin) { + this.plugin = plugin; + this.server = plugin.getServer(); + } + + private boolean executeAzure() { + return executeAzure(ACTION_STOP_JSON); + } + + private boolean executeAzure(String action) { + RequestBody requestBody = RequestBody.create(action, MediaType.parse("application/json")); + Request request = new Request.Builder() + .url(plugin.getConfigOrDefault("azure_api_url", "https://example.com/")) + .post(requestBody) + .addHeader("Content-Type", "application/json") + .build(); + + try (Response response = CLIENT.newCall(request).execute()) { + String body = response.body() == null ? "null" : response.body().string(); + plugin.logger.info("ServerCloser: http request response: {} ({}).", body, response.code()); + return (response.code() >= 200 && response.code() < 300); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + + private void close() { + if (!INSTANCE.server.getAllPlayers().isEmpty()) { + plugin.logger.error("ServerCloser: 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。"); + TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。"); + } + boolean httpSuccess = false; + for (int i = 0; i < 3; i++) { + httpSuccess = executeAzure(); + if (httpSuccess) { + TGBridge.log("ServerCloser: 向azure发送关机命令成功,正在关闭 pymcd."); + TGBridge.setShuttingDown(0); + break; + } + } + if (!httpSuccess) { + TGBridge.error("服务器关机 http 请求失效,服务器可能没有正常关闭。"); + } else { + try { + Runtime.getRuntime().exec(new String[]{"pkill", "pymcd"}); + } catch (IOException e) { + TGBridge.error("关闭 pymcd 时出现问题:" + e.getMessage()); + } + } + } + + private boolean firstInit; + + public static ServerCloser createInstance(VTools plugin) { + if (INSTANCE != null) return INSTANCE; + INSTANCE = new ServerCloser(plugin); + INSTANCE.server.getEventManager().register(plugin, INSTANCE); + INSTANCE.firstInit = true; + INSTANCE.update(); + return INSTANCE; + } + + + @SuppressWarnings("java:S106") + @Subscribe + public void onDisconnect(DisconnectEvent event) { + update(); + } + + @Subscribe + public void onServerConnected(ServerConnectedEvent event) { + update(); + } + + private void update() { + if (server.getAllPlayers().isEmpty()) { + initCountdown(); + return; + } + boolean canceledSomething = false; + synchronized (lock) { + while (!lock.isEmpty()) { + lock.poll().cancel(); + canceledSomething = true; + } + } + if (canceledSomething) { + plugin.logger.info("ServerCloser: 有玩家在线,干掉任何可能的关机计时器。"); + } + + TGBridge.setShuttingDown(-1); + } + + private void initCountdown() { + startCountdown(firstInit); + firstInit = false; + } + + private void startCountdown(boolean isFirstInit) { + synchronized (lock) { + while (!lock.isEmpty()) { + lock.poll().cancel(); + } + lock.add(new Counter(INSTANCE, isFirstInit ? 60 : 15).start()); + } + } + + + public void fastShutdown() { + synchronized (lock) { + while (!lock.isEmpty()) { + lock.poll().cancel(); + } + lock.add(new Counter(INSTANCE, 1).start()); + } + } + + public void slowShutdown() { + synchronized (lock) { + while (!lock.isEmpty()) { + lock.poll().cancel(); + } + lock.add(new Counter(INSTANCE, 60).start()); + } + } + + private static class Counter { + private final ServerCloser instance; + private final int totalMin; + private int minLeft; + private boolean canceled = false; + + protected Counter(ServerCloser instance, int minute) { + this.instance = instance; + totalMin = minute; + minLeft = minute; + } + + protected synchronized void cancel() { + canceled = true; + this.notifyAll(); + } + + private boolean started = false; + private final Object startLock = new Object(); + + protected Counter start() { + synchronized (startLock) { + if (started) return this; + started = true; + } + new Thread(this::run).start(); + return this; + } + + private synchronized void run() { + while (true) { + if (canceled) return; + TGBridge.setShuttingDown(minLeft); + try { + this.wait(MINUTE); + } catch (InterruptedException ignored) {} + if (canceled) return; + if (!instance.server.getAllPlayers().isEmpty()) { + instance.plugin.logger.error("ServerCloser: 定时器发现服务器有人。这不应发生,因为定时器本应该被直接打断。"); + TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器发现服务器有人。这不应发生,因为定时器本应该被直接打断。"); + canceled = true; + } + if (canceled) return; + minLeft--; + switch (minLeft) { + case 1 -> { + String msg = "服务器即将在一分钟后关机"; + instance.plugin.logger.info(msg); + TGBridge.log(msg); + } + case 0 -> { + String msg = "ServerCloser: 距离上一个玩家离开已经过了 %s 分钟,即将关机。".formatted(totalMin); + instance.plugin.logger.info(msg); + TGBridge.log(msg); + instance.close(); + canceled = true; + } + case -1 -> { + instance.plugin.logger.error("ServerCloser: 定时器写炸了。"); + TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器写炸了。"); + canceled = true; + } + default -> instance.plugin.logger.info("服务器即将在 {} 分钟后关机", minLeft); + } + } + } + } +} diff --git a/src/main/java/de/strifel/VTools/listeners/MarkdownString.java b/src/main/java/com/alpt/vtools/utils/MarkdownString.java similarity index 99% rename from src/main/java/de/strifel/VTools/listeners/MarkdownString.java rename to src/main/java/com/alpt/vtools/utils/MarkdownString.java index fdcc9fb..49b9c87 100644 --- a/src/main/java/de/strifel/VTools/listeners/MarkdownString.java +++ b/src/main/java/com/alpt/vtools/utils/MarkdownString.java @@ -1,4 +1,4 @@ -package de.strifel.VTools.listeners; +package com.alpt.vtools.utils; import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.MessageEntity; diff --git a/src/main/java/de/strifel/VTools/ServerCloser.java b/src/main/java/de/strifel/VTools/ServerCloser.java deleted file mode 100644 index 574bcdf..0000000 --- a/src/main/java/de/strifel/VTools/ServerCloser.java +++ /dev/null @@ -1,191 +0,0 @@ -package de.strifel.VTools; - -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.DisconnectEvent; -import com.velocitypowered.api.event.player.ServerConnectedEvent; -import com.velocitypowered.api.proxy.ProxyServer; -import de.strifel.VTools.listeners.TGBridge; -import okhttp3.*; -import org.yaml.snakeyaml.Yaml; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; - -public class ServerCloser { - private static final long INACTIVITY_TIMEOUT_MILLISECOND = 15L * 60 * 1000; // 15 minutes - private static final long MINUTE = 60L * 1000; // 15 minutes - private static final OkHttpClient CLIENT = new OkHttpClient(); - private String apiUrl; - - private static final String ACTION_STOP_JSON = "{\"action\":\"stop\"}"; - @SuppressWarnings("java:S1068") - private static final String ACTION_START_JSON = "{\"action\":\"start\"}"; - private final VTools plugin; - private final ProxyServer server; - - private Timer closeServerTimer = new Timer(); - - private final Object lock = new Object(); - - public ServerCloser(VTools plugin) { - this.plugin = plugin; - this.server = plugin.getServer(); - } - - private boolean executeAzure() { - return executeAzure(ACTION_STOP_JSON); - } - - private boolean executeAzure(String action) { - RequestBody requestBody = RequestBody.create(action, MediaType.parse("application/json")); - Request request = new Request.Builder() - .url(apiUrl) - .post(requestBody) - .addHeader("Content-Type", "application/json") - .build(); - - try { - Response response = CLIENT.newCall(request).execute(); - plugin.logger.info("ServerCloser: http request response: {} ({}).", response.body().string(), response.code()); - return (response.code() >= 200 && response.code() < 300); - } catch (Exception e) { - e.printStackTrace(); - } - return false; - } - - - private void close() { - boolean httpSuccess = false; - for (int i = 0; i < 3; i++) { - httpSuccess = executeAzure(); - if (httpSuccess) { - TGBridge.log("ServerCloser: 向azure发送关机命令成功,正在关闭 pymcd."); - TGBridge.setShuttingDown(0); - break; - } - } - if (!httpSuccess) { - TGBridge.error("服务器关机 http 请求失效,服务器可能没有正常关闭。"); - } else { - try { - Runtime.getRuntime().exec("pkill pymcd"); - } catch (IOException e) { - TGBridge.error("关闭 pymcd 时出现问题:" + e.getMessage()); - } - } - } - - private boolean firstInit; - private int countDown = -1; - - public void register() { - server.getEventManager().register(plugin, this); - firstInit = true; - update(); - loadConfig(); - } - - - private void loadConfig() { - try { - File configDir = plugin.dataDirectory.toFile(); - if (!configDir.exists()) { - configDir.mkdir(); - } - File configFile = new File(configDir, "config.yaml"); - if (!configFile.exists()) { - Files.write(Path.of(configFile.toURI()), "chat_id: \"0\"\ntoken: \"\"\n".getBytes(StandardCharsets.UTF_8)); - } - String configStr = Files.readString(Path.of(configFile.toURI()), StandardCharsets.UTF_8); - Yaml yaml = new Yaml(); - Map config = yaml.load(configStr); - apiUrl = config.getOrDefault("azure_api_url", "https://example.com/"); - } catch (Exception e) { - plugin.logger.error("parsing config", e); - } - } - - @SuppressWarnings("java:S106") - @Subscribe - public void onDisconnect(DisconnectEvent event) { - update(); - } - - @Subscribe - public void onServerConnected(ServerConnectedEvent event) { - update(); - } - - private void update() { - var hasPlayer = !server.getAllPlayers().isEmpty(); - - if (hasPlayer) { - plugin.logger.info("ServerCloser: 有玩家在线,干掉任何可能的关机计时器。"); - closeServerTimer.cancel(); - countDown = -1; - TGBridge.setShuttingDown(-1); - return; - } - initCountdown(); - } - - private void initCountdown() { - countDown = firstInit ? 60 : 15; - firstInit = false; - startCountdown(); - } - - private void startCountdown() { - TGBridge.setShuttingDown(countDown); - Timer timer = new Timer(); - timer.schedule(new TimerTask() { - @Override - @SuppressWarnings("java:S1199") - public void run() { - { - countDown--; - if (countDown < 0) { - TGBridge.error("ServerCloser: #bug @NaAlOH4 计时器写炸了"); - closeServerTimer.cancel(); - return; - } - - if (server.getAllPlayers().isEmpty()) { - switch (countDown) { - case 1 -> { - TGBridge.log("服务器即将在一分钟后关机"); - startCountdown(); - } - case 0 -> { - plugin.logger.info("ServerCloser: 距离上一个玩家离开已经过了%s,即将关机。".formatted(firstInit ? "一小时" : "15分钟")); - TGBridge.log("ServerCloser: 距离上一个玩家离开已经过了%s,即将关机。".formatted(firstInit ? "一小时" : "15分钟")); - close(); - closeServerTimer.cancel(); - } - default -> startCountdown(); - } - } else { - plugin.logger.error("ServerCloser: 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。"); - TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。"); - closeServerTimer.cancel(); - } - } - } - }, MINUTE); - plugin.logger.info("ServerCloser: 将在 {} 分钟后自动关机。", countDown); - - synchronized (lock) { - closeServerTimer.cancel(); - closeServerTimer = timer; - } - } - - -} diff --git a/src/main/java/de/strifel/VTools/VTools.java b/src/main/java/de/strifel/VTools/VTools.java index 7a14367..f595c3c 100644 --- a/src/main/java/de/strifel/VTools/VTools.java +++ b/src/main/java/de/strifel/VTools/VTools.java @@ -1,5 +1,7 @@ package de.strifel.VTools; +import com.alpt.vtools.listeners.OnlinePlayerQueryService; +import com.alpt.vtools.listeners.ServerCloser; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.plugin.Plugin; @@ -9,11 +11,17 @@ import de.strifel.VTools.commands.*; import de.strifel.VTools.listeners.*; import net.kyori.adventure.text.format.TextColor; import org.slf4j.Logger; +import org.yaml.snakeyaml.Yaml; import javax.inject.Inject; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; -@Plugin(id = "vtools", name="VTools", version="1.0-SNAPSHOT", description="Some commands!", authors="unnamed") +@Plugin(id = "vtools", name = "VTools", version = "1.0-SNAPSHOT", description = "Some commands!", authors = "unnamed") public class VTools { private final ProxyServer server; public final Logger logger; @@ -42,11 +50,46 @@ public class VTools { server.getCommandManager().register("tps", new CommandTp(server), "jump"); server.getCommandManager().register("server", new CommandServer(server), "serverv"); server.getCommandManager().register("servers", new CommandServers(server), "allservers"); + loadConfig(); new TGBridge(this).register(); new PlayerStatus(this).register(); new GlobalChat(this).register(); - new ServerCloser(this).register(); - new OnlinePlayerQueryService(this).register(); + ServerCloser.createInstance(this); + OnlinePlayerQueryService.createInstance(this); + } + + private Map config = new HashMap<>(); + + public String getConfig(String k) { + return config.get(k); + } + + public String getConfigOrDefault(String k, String v) { + return config.getOrDefault(k, v); + } + + private void loadConfig() { + try { + File configDir = dataDirectory.toFile(); + if (!configDir.exists()) { + configDir.mkdir(); + } + File configFile = new File(configDir, "config.yaml"); + if (!configFile.exists()) { + String defVal = """ + chat_id: "0" + token: "" + azure_api_url: "https://example.com/" + http_service_port: 17611 + """; + Files.write(Path.of(configFile.toURI()), defVal.getBytes(StandardCharsets.UTF_8)); + } + String configStr = Files.readString(Path.of(configFile.toURI()), StandardCharsets.UTF_8); + Yaml yaml = new Yaml(); + config = yaml.load(configStr); + } catch (Exception e) { + logger.error("parsing config", e); + } } public ProxyServer getServer() { diff --git a/src/main/java/de/strifel/VTools/listeners/TGBridge.java b/src/main/java/de/strifel/VTools/listeners/TGBridge.java index 575c605..cc3b1a2 100644 --- a/src/main/java/de/strifel/VTools/listeners/TGBridge.java +++ b/src/main/java/de/strifel/VTools/listeners/TGBridge.java @@ -1,5 +1,7 @@ package de.strifel.VTools.listeners; +import com.alpt.vtools.listeners.ServerCloser; +import com.alpt.vtools.utils.MarkdownString; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.pengrad.telegrambot.Callback; @@ -27,13 +29,8 @@ import de.strifel.VTools.VTools; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.yaml.snakeyaml.Yaml; import java.io.IOException; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.*; import java.util.concurrent.LinkedBlockingQueue; import java.util.function.BiConsumer; @@ -77,24 +74,9 @@ public class TGBridge { } private void loadConfig() { - try { - File configDir = plugin.dataDirectory.toFile(); - if (!configDir.exists()) { - configDir.mkdir(); - } - File configFile = new File(configDir, "config.yaml"); - if (!configFile.exists()) { - Files.write(Path.of(configFile.toURI()), "chat_id: \"0\"\ntoken: \"\"\n".getBytes(StandardCharsets.UTF_8)); - } - String configStr = Files.readString(Path.of(configFile.toURI()), StandardCharsets.UTF_8); - Yaml yaml = new Yaml(); - Map config = yaml.load(configStr); - synchronized (this) { - this.CHAT_ID = Long.parseLong(config.getOrDefault("chat_id", "0")); - this.TOKEN = config.getOrDefault("token", ""); - } - } catch (Exception e) { - plugin.logger.error("parsing config", e); + synchronized (this) { + CHAT_ID = Long.parseLong(plugin.getConfigOrDefault("chat_id", "0")); + TOKEN = plugin.getConfigOrDefault("token", ""); } } @@ -138,9 +120,10 @@ public class TGBridge { if (message.text() != null && !message.text().isEmpty()) { String msgText = message.text(); if (msgText.startsWith("/")) { - String[] s = msgText.split("(@[A-Za-z0-9_]bot)?[\t \n]+", 2); + String[] s = msgText.split("((@\\w+bot)|(@\\w+bot)?[\t \n]+)", 2); String command = s[0]; @Nullable String arg = s.length == 2 ? s[1] : null; + System.out.println(command); switch (command) { case "/list" -> outbound(genOnlineStatus(), ParseMode.MarkdownV2); case "/setpin" -> { @@ -188,6 +171,22 @@ public class TGBridge { } }, ParseMode.MarkdownV2 ); + case "/shutdown" -> { + if (server.getAllPlayers().isEmpty()) { + ServerCloser.INSTANCE.fastShutdown(); + outbound("server will shutdown in 1 minute.\nuse /fuck to cancel."); + } else { + outbound("still player online, can't shutdown."); + } + } + case "/fuck" -> { + if (server.getAllPlayers().isEmpty()) { + ServerCloser.INSTANCE.slowShutdown(); + outbound("shutdown timer has been set to 60 minutes."); + } else { + outbound("still player online, can't shutdown."); + } + } default -> {} } } @@ -307,7 +306,7 @@ public class TGBridge { } String result = String.join("\n", out); if (shutdownCountMinutes < 0) return result; - if (shutdownCountMinutes == 0) return "server is shutdown\\.%n%s".formatted(result); + if (shutdownCountMinutes == 0 || PROXY_SHUT_DOWN) return "server already shutdown\\.%n%s".formatted(result); return "server will shutdown after %s minute\\.%n%s".formatted(shutdownCountMinutes, result); } @@ -431,14 +430,14 @@ public class TGBridge { } catch (InterruptedException ignored) {} if (oldestRequest == null) { plugin.logger.warn("updateRequests.take() return a null value, why?"); - sleep(1000); + sleep(10000); continue; } updateRequests.clear(); if (!updateOnlineStatus()) { updateRequests.add(oldestRequest); // 更新失败 回去吧您内 - sleep(1000); + sleep(10000); } } }).start(); @@ -479,7 +478,8 @@ public class TGBridge { } protected boolean setOnlineStatusNotAvailable() { - return editOnlineStatusMessage("(proxy already shutdown)"); + return updateOnlineStatus(); +// return editOnlineStatusMessage("(proxy already shutdown)"); } private static final Gson prettyGson = new GsonBuilder().setPrettyPrinting().create(); @@ -490,8 +490,11 @@ public class TGBridge { } BaseResponse response; try { - response = bot.execute(new EditMessageText(CHAT_ID, ONLINE_STATUS_MESSAGE_ID, markdownText).parseMode(ParseMode.MarkdownV2)); + response = bot.execute(new EditMessageText(CHAT_ID, ONLINE_STATUS_MESSAGE_ID, markdownText).parseMode(ParseMode.MarkdownV2).disableWebPagePreview(true)); if (!response.isOk()) { + if (response.description().equals("Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")) { + return true; + } String responseJSON = prettyGson.toJson(response); plugin.logger.warn("update failed: {}", responseJSON); } @@ -526,7 +529,7 @@ public class TGBridge { protected JoinLeftAnnounceMessage(String firstMessage) { text = new StringBuilder(firstMessage); time = System.currentTimeMillis(); - timeMinute = time/MINUTE; + timeMinute = time / MINUTE; sendAnnounceMessage(); }