Compare commits
1 commit
master
...
multigroup
Author | SHA1 | Date | |
---|---|---|---|
9cb090ce2d |
16 changed files with 128 additions and 1311 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,4 +2,3 @@
|
||||||
out/
|
out/
|
||||||
target/
|
target/
|
||||||
*.iml
|
*.iml
|
||||||
/dependency-reduced-pom.xml
|
|
|
@ -1,8 +1,6 @@
|
||||||
# VTools
|
# VTools
|
||||||
Tools for Velocity proxy server.
|
Tools for Velocity proxy server.
|
||||||
|
|
||||||
add telegram/auto shutdown support for spec server.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|Command|What is does?|Permission|
|
|Command|What is does?|Permission|
|
||||||
|-------|-------------|----------|
|
|-------|-------------|----------|
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
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;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public class OnlinePlayerQueryService {
|
|
||||||
|
|
||||||
public static OnlinePlayerQueryService INSTANCE;
|
|
||||||
private final VTools plugin;
|
|
||||||
|
|
||||||
private OnlinePlayerQueryService(VTools plugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Gson gson = new Gson();
|
|
||||||
|
|
||||||
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", port), 0);
|
|
||||||
|
|
||||||
server.createContext("/api/getOnlinePlayers", exchange -> {
|
|
||||||
Collection<Player> players = plugin.getServer().getAllPlayers();
|
|
||||||
ArrayList<String> playerNames = new ArrayList<>(players.size());
|
|
||||||
for (Player player : players) {
|
|
||||||
playerNames.add(player.getUsername());
|
|
||||||
}
|
|
||||||
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
|
||||||
exchange.sendResponseHeaders(200, 0);
|
|
||||||
|
|
||||||
try (OutputStream os = exchange.getResponseBody()) {
|
|
||||||
String response = gson.toJson(playerNames);
|
|
||||||
os.write(response.getBytes());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.start();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@TestOnly
|
|
||||||
public static void main(String[] args) {
|
|
||||||
ArrayList<String> arrayList = new ArrayList<>();
|
|
||||||
arrayList.add("jerry");
|
|
||||||
System.out.println(gson.toJson(arrayList));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,210 +0,0 @@
|
||||||
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 java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
|
|
||||||
public class ServerCloser {
|
|
||||||
private static final long MINUTE = 60L * 1000;
|
|
||||||
|
|
||||||
private final VTools plugin;
|
|
||||||
private final ProxyServer server;
|
|
||||||
|
|
||||||
private final LinkedBlockingQueue<Counter> lock = new LinkedBlockingQueue<>();
|
|
||||||
|
|
||||||
public static ServerCloser INSTANCE;
|
|
||||||
|
|
||||||
private ServerCloser(VTools plugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
this.server = plugin.getServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void close() {
|
|
||||||
if (!INSTANCE.server.getAllPlayers().isEmpty()) {
|
|
||||||
plugin.logger.error("ServerCloser: 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。");
|
|
||||||
TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String apiUrl = plugin.getConfigOrDefault("azure_api_url", "https://example.com/");
|
|
||||||
try {
|
|
||||||
Runtime.getRuntime().exec(new String[]{"systemd-run", "--description=pymcd_stop", "--quiet", "--user", "--collect", "-p", "StandardInput=data", "-p", "StandardInputData=c2V0IC1lCnA9IiQocGdyZXAgLWYgcHltY2QucHkgLUFvKSIKa2lsbCAtSU5UICIkcCIKdGFpbCAtLXBpZD0iJHAiIC1mIC9kZXYvbnVsbApjdXJsIC1zIC1IICJDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24iIC1kICJ7XCJhY3Rpb25cIjpcInN0b3BcIn0iICIkMSIK", "bash", "-s", apiUrl});
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
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).reason("收到关机命令已经过了 %s 分钟,即将关机。").noLastWarning().start());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void slowShutdown() {
|
|
||||||
synchronized (lock) {
|
|
||||||
while (!lock.isEmpty()) {
|
|
||||||
lock.poll().cancel();
|
|
||||||
}
|
|
||||||
lock.add(new Counter(INSTANCE, 60).reason("虽然关机被取消,但 %s 分钟内依旧没有玩家上线,即将关机。").start());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void noShutdown() {
|
|
||||||
synchronized (lock) {
|
|
||||||
while (!lock.isEmpty()) {
|
|
||||||
lock.poll().cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Counter {
|
|
||||||
private final ServerCloser instance;
|
|
||||||
private final int totalMin;
|
|
||||||
private int minLeft;
|
|
||||||
private boolean canceled = false;
|
|
||||||
private String reason;
|
|
||||||
private static final String DEFAULT_REASON = "距离上一个玩家离开已经过了 %s 分钟,即将关机。";
|
|
||||||
private boolean lastWarning = true;
|
|
||||||
|
|
||||||
protected Counter(ServerCloser instance, int minute) {
|
|
||||||
this.instance = instance;
|
|
||||||
totalMin = minute;
|
|
||||||
minLeft = minute;
|
|
||||||
this.reason = DEFAULT_REASON;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Counter reason(String reason) {
|
|
||||||
this.reason = reason;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Counter noLastWarning() {
|
|
||||||
lastWarning = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
switch (minLeft) {
|
|
||||||
case 1 -> {
|
|
||||||
String msg = "服务器即将在一分钟后关机,使用 /fuck 以取消。";
|
|
||||||
instance.plugin.logger.info(msg);
|
|
||||||
if (lastWarning) {
|
|
||||||
TGBridge.log(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 0 -> {
|
|
||||||
String msg = "ServerCloser: " + reason.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,241 +0,0 @@
|
||||||
package com.alpt.vtools.utils;
|
|
||||||
|
|
||||||
import com.pengrad.telegrambot.model.Message;
|
|
||||||
import com.pengrad.telegrambot.model.MessageEntity;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
|
|
||||||
public class MarkdownString {
|
|
||||||
|
|
||||||
private MarkdownString() {
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 这里的 format 假设 BiFunction 的输入都是转义过的。
|
|
||||||
*/
|
|
||||||
private static final Map<MessageEntity.Type, BiFunction<StringBuilder, MessageEntity, StringBuilder>> formats;
|
|
||||||
|
|
||||||
static {
|
|
||||||
Map<MessageEntity.Type, BiFunction<StringBuilder, MessageEntity, StringBuilder>> map = new EnumMap<>(MessageEntity.Type.class);
|
|
||||||
|
|
||||||
map.put(MessageEntity.Type.bold, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.bold;
|
|
||||||
return s.insert(0, "*").append("*");
|
|
||||||
});
|
|
||||||
map.put(MessageEntity.Type.strikethrough, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.strikethrough;
|
|
||||||
return s.insert(0, "~").append("~");
|
|
||||||
});
|
|
||||||
map.put(MessageEntity.Type.spoiler, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.spoiler;
|
|
||||||
return s.insert(0, "||").append("||");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
map.put(MessageEntity.Type.italic, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.italic;
|
|
||||||
return s.insert(0, "\r_\r").append("\r_\r"); // todo: 精简一下 到底哪里需要加\r
|
|
||||||
});
|
|
||||||
map.put(MessageEntity.Type.underline, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.underline;
|
|
||||||
return s.insert(0, "\r__\r").append("\r__\r");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://core.telegram.org/bots/api#formatting-options
|
|
||||||
// 上面这几个套娃友好,自身随意套娃,但不能和等宽组合
|
|
||||||
|
|
||||||
|
|
||||||
map.put(MessageEntity.Type.code, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.code;
|
|
||||||
return s.insert(0, "`").append("`");
|
|
||||||
});
|
|
||||||
map.put(MessageEntity.Type.pre, (rawStr, messageEntity) -> {
|
|
||||||
assert messageEntity.type() == MessageEntity.Type.pre;
|
|
||||||
rawStr.insert(0, "\n");
|
|
||||||
String language = messageEntity.language();
|
|
||||||
if (language != null && language.length() > 0) {
|
|
||||||
rawStr.insert(0, language);
|
|
||||||
}
|
|
||||||
return rawStr.insert(0, "```").append("\n```");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 等宽(pre 和 code)完全禁止套娃
|
|
||||||
|
|
||||||
map.put(MessageEntity.Type.text_link, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.text_link;
|
|
||||||
return s.insert(0, "[").append("](").append(escapeStr(e.url())).append(")");
|
|
||||||
});
|
|
||||||
map.put(MessageEntity.Type.text_mention, (s, e) -> {
|
|
||||||
assert e.type() == MessageEntity.Type.text_mention;
|
|
||||||
if (e.user() == null || e.user().id() == null) {
|
|
||||||
return s; // 没有id时爆炸(
|
|
||||||
}
|
|
||||||
return s.insert(0, "[").append("](").append("tg://user?id=").append(e.user().id()).append(")");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 链接类不能和链接类套娃,但是可以和加粗等套娃友好的组合
|
|
||||||
|
|
||||||
formats = Collections.unmodifiableMap(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static class StringBlock {
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "StringBlock{" +
|
|
||||||
"text='" + text + '\'' +
|
|
||||||
", entities=" + entities +
|
|
||||||
", offset=" + offset +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
|
|
||||||
private final String text;
|
|
||||||
private Map<MessageEntity.Type, MessageEntity> entities;
|
|
||||||
private final int offset;
|
|
||||||
|
|
||||||
StringBlock(String text, int offset) {
|
|
||||||
this.text = text;
|
|
||||||
this.offset = offset;
|
|
||||||
this.entities = new EnumMap<>(MessageEntity.Type.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getMarkdownTextWithoutLink() { // todo: 压缩 *text1**_text2_* 为 *text1_text2_*
|
|
||||||
// 链接类不能和链接类套娃
|
|
||||||
assert (!entities.containsKey(MessageEntity.Type.text_link)) || (!entities.containsKey(MessageEntity.Type.text_mention));
|
|
||||||
// 等宽(pre 和 code)完全禁止套娃
|
|
||||||
assert (!entities.containsKey(MessageEntity.Type.pre) && !entities.containsKey(MessageEntity.Type.code)) || (entities.size() <= 1);
|
|
||||||
|
|
||||||
StringBuilder s = new StringBuilder(escapeStr(text));
|
|
||||||
for (var entry : entities.entrySet()) {
|
|
||||||
BiFunction<StringBuilder, MessageEntity, StringBuilder> repeater = (sb, e) -> sb;
|
|
||||||
s = formats.getOrDefault(entry.getKey(), repeater).apply(s, entry.getValue()); // 无用赋值...理论上。
|
|
||||||
}
|
|
||||||
return s.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String markdownString(Message message) {
|
|
||||||
if (message.entities() == null || message.entities().length == 0) return message.text();
|
|
||||||
|
|
||||||
List<StringBlock> stringBlocks = createStringBlocks(message);
|
|
||||||
|
|
||||||
|
|
||||||
StringBuilder s = new StringBuilder();
|
|
||||||
for (int i = 0; i < stringBlocks.size(); i++) {
|
|
||||||
|
|
||||||
|
|
||||||
var stringBlock = stringBlocks.get(i);
|
|
||||||
var entities = stringBlock.entities;
|
|
||||||
String withoutLink = stringBlock.getMarkdownTextWithoutLink();
|
|
||||||
if (entities.containsKey(MessageEntity.Type.text_mention) ||
|
|
||||||
entities.containsKey(MessageEntity.Type.text_link)) {
|
|
||||||
assert (!entities.containsKey(MessageEntity.Type.text_mention) ||
|
|
||||||
!entities.containsKey(MessageEntity.Type.text_link));
|
|
||||||
StringBuilder linkText = new StringBuilder();
|
|
||||||
MessageEntity entity = entities.containsKey(MessageEntity.Type.text_mention) ?
|
|
||||||
stringBlock.entities.get(MessageEntity.Type.text_mention) :
|
|
||||||
stringBlock.entities.get(MessageEntity.Type.text_link);
|
|
||||||
assert entity.offset() == stringBlock.offset;
|
|
||||||
int end = entity.length() + entity.offset();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
linkText.append(stringBlock.getMarkdownTextWithoutLink());
|
|
||||||
if (stringBlock.text.length() + stringBlock.offset == end) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (stringBlock.text.length() + stringBlock.offset > end) {
|
|
||||||
throw new IllegalStateException("奶冰可爱捏");
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
stringBlock = stringBlocks.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
s.append(formats.get(entity.type()).apply(linkText, entity));
|
|
||||||
} else {
|
|
||||||
s.append(withoutLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return 一组 StringBlock,被完全切开的文本,每部分都和相邻部分格式不一样,顺序的。
|
|
||||||
*/
|
|
||||||
private static List<StringBlock> createStringBlocks(Message message) {
|
|
||||||
List<StringBlock> stringBlocks = new ArrayList<>();
|
|
||||||
Set<Integer> splitPoints = new HashSet<>();
|
|
||||||
|
|
||||||
// 定位切割点
|
|
||||||
for (MessageEntity entity : message.entities()) {
|
|
||||||
if (formats.containsKey(entity.type())) {
|
|
||||||
Integer offset = entity.offset();
|
|
||||||
Integer length = entity.length();
|
|
||||||
splitPoints.add(offset);
|
|
||||||
splitPoints.add(offset + length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切割文本
|
|
||||||
String text = message.text();
|
|
||||||
List<Integer> sortedSplitPoints = new ArrayList<>(splitPoints);
|
|
||||||
Collections.sort(sortedSplitPoints);
|
|
||||||
|
|
||||||
int start = 0;
|
|
||||||
for (int splitPoint : sortedSplitPoints) {
|
|
||||||
if (splitPoint == 0) continue;// 跳过第一个空的
|
|
||||||
stringBlocks.add(new StringBlock(text.substring(start, splitPoint), start));
|
|
||||||
start = splitPoint;
|
|
||||||
}
|
|
||||||
if (start < text.length()) {
|
|
||||||
stringBlocks.add(new StringBlock(text.substring(start), start));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 给文本重新赋格式(MessageEntity)
|
|
||||||
for (MessageEntity entity : message.entities()) {
|
|
||||||
if (!formats.containsKey(entity.type())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (StringBlock stringBlock : stringBlocks) {
|
|
||||||
int blockStart = stringBlock.offset;
|
|
||||||
int blockEnd = blockStart + stringBlock.text.length();
|
|
||||||
|
|
||||||
int entityStart = entity.offset();
|
|
||||||
int entityEnd = entityStart + entity.length();
|
|
||||||
|
|
||||||
assert (blockStart < blockEnd) && (entityStart < entityEnd);
|
|
||||||
if (blockStart >= entityEnd || blockEnd <= entityStart) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
assert (blockStart >= entityStart) && (blockEnd <= entityEnd) : String.format("%s,%s,%s,%s", blockStart, blockEnd, entityStart, entityEnd);
|
|
||||||
MessageEntity old = stringBlock.entities.put(entity.type(), entity);
|
|
||||||
assert (old == null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stringBlocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final boolean[] SHOULD_ESCAPE = new boolean[128];
|
|
||||||
|
|
||||||
static {
|
|
||||||
// In all other places characters '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' must be escaped with the preceding character '\'.
|
|
||||||
char[] chars = {'_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'};
|
|
||||||
for (char c : chars) {
|
|
||||||
SHOULD_ESCAPE[c] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String escapeStr(String s) {
|
|
||||||
StringBuilder r = new StringBuilder();
|
|
||||||
for (String character : s.split("")) {
|
|
||||||
int codePoint = character.codePointAt(0);
|
|
||||||
if (codePoint < 128 && SHOULD_ESCAPE[codePoint]) {
|
|
||||||
r.append('\\');
|
|
||||||
}
|
|
||||||
r.append(character);
|
|
||||||
}
|
|
||||||
return r.toString();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,5 @@
|
||||||
package de.strifel.VTools;
|
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.Subscribe;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||||
import com.velocitypowered.api.plugin.Plugin;
|
import com.velocitypowered.api.plugin.Plugin;
|
||||||
|
@ -11,15 +9,9 @@ import de.strifel.VTools.commands.*;
|
||||||
import de.strifel.VTools.listeners.*;
|
import de.strifel.VTools.listeners.*;
|
||||||
import net.kyori.adventure.text.format.TextColor;
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.yaml.snakeyaml.Yaml;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
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.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 {
|
public class VTools {
|
||||||
|
@ -48,48 +40,10 @@ public class VTools {
|
||||||
server.getCommandManager().register("staffchat", new CommandStaffChat(server), "sc");
|
server.getCommandManager().register("staffchat", new CommandStaffChat(server), "sc");
|
||||||
server.getCommandManager().register("restart", new CommandRestart(server));
|
server.getCommandManager().register("restart", new CommandRestart(server));
|
||||||
server.getCommandManager().register("tps", new CommandTp(server), "jump");
|
server.getCommandManager().register("tps", new CommandTp(server), "jump");
|
||||||
server.getCommandManager().register("server", new CommandServer(server), "serverv");
|
|
||||||
server.getCommandManager().register("servers", new CommandServers(server), "allservers");
|
server.getCommandManager().register("servers", new CommandServers(server), "allservers");
|
||||||
loadConfig();
|
|
||||||
new TGBridge(this).register();
|
new TGBridge(this).register();
|
||||||
new PlayerStatus(this).register();
|
new PlayerStatus(this).register();
|
||||||
new GlobalChat(this).register();
|
new GlobalChat(this).register();
|
||||||
ServerCloser.createInstance(this);
|
|
||||||
OnlinePlayerQueryService.createInstance(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> 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() {
|
public ProxyServer getServer() {
|
||||||
|
|
|
@ -5,8 +5,6 @@ import com.velocitypowered.api.command.SimpleCommand;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.api.proxy.ProxyServer;
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.event.ClickEvent;
|
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -31,12 +29,7 @@ public class CommandFind implements SimpleCommand {
|
||||||
if (strings.length == 1) {
|
if (strings.length == 1) {
|
||||||
Optional<Player> player = server.getPlayer(strings[0]);
|
Optional<Player> player = server.getPlayer(strings[0]);
|
||||||
if (player.isPresent() && player.get().getCurrentServer().isPresent()) {
|
if (player.isPresent() && player.get().getCurrentServer().isPresent()) {
|
||||||
String serverName = player.get().getCurrentServer().get().getServerInfo().getName();
|
commandSource.sendMessage(Component.text("Player " + strings[0] + " is on " + player.get().getCurrentServer().get().getServerInfo().getName() + "!").color(COLOR_YELLOW));
|
||||||
commandSource.sendMessage(Component.empty()
|
|
||||||
.append(Component.text("Player " + strings[0] + " is on ").color(COLOR_YELLOW))
|
|
||||||
.append(Component.text(serverName).clickEvent(ClickEvent.runCommand("/server " + serverName)).color(NamedTextColor.GRAY))
|
|
||||||
.append(Component.text("!").color(COLOR_YELLOW))
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
commandSource.sendMessage(Component.text("The player is not online!").color(COLOR_YELLOW));
|
commandSource.sendMessage(Component.text("The player is not online!").color(COLOR_YELLOW));
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ public class CommandGlobalChat implements SimpleCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> suggest(Invocation invocation) {
|
public List<String> suggest(Invocation invocation) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package de.strifel.VTools.commands;
|
package de.strifel.VTools.commands;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
import com.velocitypowered.api.command.SimpleCommand;
|
import com.velocitypowered.api.command.SimpleCommand;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.api.proxy.ProxyServer;
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
|
@ -18,6 +19,7 @@ public class CommandRestart implements SimpleCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(SimpleCommand.Invocation invocation) {
|
public void execute(SimpleCommand.Invocation invocation) {
|
||||||
|
CommandSource commandSource = invocation.source();
|
||||||
String[] strings = invocation.arguments();
|
String[] strings = invocation.arguments();
|
||||||
|
|
||||||
if (strings.length > 0) {
|
if (strings.length > 0) {
|
||||||
|
@ -31,7 +33,7 @@ public class CommandRestart implements SimpleCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> suggest(SimpleCommand.Invocation invocation) {
|
public List<String> suggest(SimpleCommand.Invocation invocation) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -9,7 +9,10 @@ import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -36,7 +39,8 @@ public class CommandSend implements SimpleCommand {
|
||||||
for (Player player : server.getAllPlayers()) {
|
for (Player player : server.getAllPlayers()) {
|
||||||
oPlayer.add(player);
|
oPlayer.add(player);
|
||||||
}
|
}
|
||||||
} else if (strings[0].equals("current")) {
|
}
|
||||||
|
else if (strings[0].equals("current")) {
|
||||||
if (commandSource instanceof Player) {
|
if (commandSource instanceof Player) {
|
||||||
Player playerSource = (Player)commandSource;
|
Player playerSource = (Player)commandSource;
|
||||||
Optional<ServerConnection> conn = playerSource.getCurrentServer();
|
Optional<ServerConnection> conn = playerSource.getCurrentServer();
|
||||||
|
@ -45,11 +49,13 @@ public class CommandSend implements SimpleCommand {
|
||||||
oPlayer.add(player);
|
oPlayer.add(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
commandSource.sendMessage(Component.text("Command is only for players.").color(COLOR_RED));
|
commandSource.sendMessage(Component.text("Command is only for players.").color(COLOR_RED));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Optional<Player> p = server.getPlayer(strings[0]);
|
Optional<Player> p = server.getPlayer(strings[0]);
|
||||||
if (p.isPresent()) {
|
if (p.isPresent()) {
|
||||||
oPlayer.add(p.get());
|
oPlayer.add(p.get());
|
||||||
|
@ -90,7 +96,7 @@ public class CommandSend implements SimpleCommand {
|
||||||
String sendResults = results.isEmpty() ? "nothing" : results.entrySet().stream().map(
|
String sendResults = results.isEmpty() ? "nothing" : results.entrySet().stream().map(
|
||||||
entry -> String.format("%s : %d", entry.getKey(), entry.getValue())
|
entry -> String.format("%s : %d", entry.getKey(), entry.getValue())
|
||||||
).collect(Collectors.joining("\n"));
|
).collect(Collectors.joining("\n"));
|
||||||
commandSource.sendMessage(Component.text("Send Results:%n%s".formatted(sendResults)).color(COLOR_YELLOW));
|
commandSource.sendMessage(Component.text(String.format("Send Results:\n%s", sendResults)).color(COLOR_YELLOW));
|
||||||
}).start();
|
}).start();
|
||||||
} else {
|
} else {
|
||||||
commandSource.sendMessage(Component.text("The server or user does not exist!").color(COLOR_RED));
|
commandSource.sendMessage(Component.text("The server or user does not exist!").color(COLOR_RED));
|
||||||
|
@ -102,26 +108,21 @@ public class CommandSend implements SimpleCommand {
|
||||||
|
|
||||||
public List<String> suggest(SimpleCommand.Invocation invocation) {
|
public List<String> suggest(SimpleCommand.Invocation invocation) {
|
||||||
String[] currentArgs = invocation.arguments();
|
String[] currentArgs = invocation.arguments();
|
||||||
switch (currentArgs.length) {
|
|
||||||
case 0, 1 -> {
|
List<String> arg = new ArrayList<String>();
|
||||||
List<String> args = new ArrayList<>(server.getPlayerCount() + 2);
|
if (currentArgs.length <= 1) {
|
||||||
args.add("all");
|
arg.add("all");
|
||||||
args.add("current");
|
arg.add("current");
|
||||||
for (Player player : server.getAllPlayers()) {
|
for (Player player : server.getAllPlayers()) {
|
||||||
args.add(player.getUsername());
|
arg.add(player.getUsername());
|
||||||
}
|
}
|
||||||
return currentArgs.length==0?args:
|
return arg;
|
||||||
args.stream().filter(arg->arg.regionMatches(true,0,currentArgs[0],0,currentArgs[0].length())).toList();
|
} else if (currentArgs.length == 2) {
|
||||||
}
|
for (RegisteredServer server : server.getAllServers()) {
|
||||||
case 2 -> {
|
arg.add(server.getServerInfo().getName());
|
||||||
return server.getAllServers().stream().map(s -> s.getServerInfo().getName())
|
|
||||||
.filter(name -> name.regionMatches(true, 0, currentArgs[1], 0, currentArgs[1].length()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
return new ArrayList<>(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return arg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasPermission(SimpleCommand.Invocation invocation) {
|
public boolean hasPermission(SimpleCommand.Invocation invocation) {
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
package de.strifel.VTools.commands;
|
|
||||||
|
|
||||||
import com.velocitypowered.api.command.SimpleCommand;
|
|
||||||
import com.velocitypowered.api.proxy.Player;
|
|
||||||
import com.velocitypowered.api.proxy.ProxyServer;
|
|
||||||
import de.strifel.VTools.commands.utils.ServerUtil;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
|
|
||||||
import static de.strifel.VTools.VTools.COLOR_RED;
|
|
||||||
|
|
||||||
public class CommandServer extends ServerUtil implements SimpleCommand {
|
|
||||||
|
|
||||||
private final ProxyServer server;
|
|
||||||
|
|
||||||
public CommandServer(ProxyServer server) {
|
|
||||||
super(server);
|
|
||||||
this.server = server;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(final SimpleCommand.Invocation invocation) {
|
|
||||||
if (invocation.source() instanceof Player) {
|
|
||||||
if (
|
|
||||||
invocation.source().hasPermission("velocity.command.server") ||
|
|
||||||
server.getAllPlayers().stream().anyMatch(p -> p.hasPermission("vtools.send") && p.hasPermission("vtools.send.recvmsg"))) {
|
|
||||||
super.execute(invocation);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
invocation.source().sendMessage(Component.text("Supervisor offline.").color(COLOR_RED));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
invocation.source().sendMessage(Component.text("Command is only for players.").color(COLOR_RED));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(Invocation commandInvocation) {
|
|
||||||
return commandInvocation.source().hasPermission("vtools.server.auto");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -34,7 +34,7 @@ public class CommandServers implements SimpleCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> suggest(SimpleCommand.Invocation invocation) {
|
public List<String> suggest(SimpleCommand.Invocation invocation) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -26,11 +26,11 @@ public class CommandTp implements SimpleCommand {
|
||||||
CommandSource commandSource = commandInvocation.source();
|
CommandSource commandSource = commandInvocation.source();
|
||||||
String[] strings = commandInvocation.arguments();
|
String[] strings = commandInvocation.arguments();
|
||||||
|
|
||||||
if (commandSource instanceof Player playerCommandSource) {
|
if (commandSource instanceof Player) {
|
||||||
if (strings.length == 1) {
|
if (strings.length == 1) {
|
||||||
Optional<Player> player = server.getPlayer(strings[0]);
|
Optional<Player> player = server.getPlayer(strings[0]);
|
||||||
if (player.isPresent()) {
|
if (player.isPresent()) {
|
||||||
player.get().getCurrentServer().ifPresent(serverConnection -> playerCommandSource.createConnectionRequest(serverConnection.getServer()).fireAndForget());
|
player.get().getCurrentServer().ifPresent(serverConnection -> ((Player) commandSource).createConnectionRequest(serverConnection.getServer()).fireAndForget());
|
||||||
commandSource.sendMessage(Component.text("Connecting to the server of " + strings[0]).color(COLOR_YELLOW));
|
commandSource.sendMessage(Component.text("Connecting to the server of " + strings[0]).color(COLOR_YELLOW));
|
||||||
} else {
|
} else {
|
||||||
commandSource.sendMessage(Component.text("Player does not exists.").color(COLOR_RED));
|
commandSource.sendMessage(Component.text("Player does not exists.").color(COLOR_RED));
|
||||||
|
@ -56,6 +56,6 @@ public class CommandTp implements SimpleCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPermission(Invocation commandInvocation) {
|
public boolean hasPermission(Invocation commandInvocation) {
|
||||||
return commandInvocation.source().hasPermission("VTools.tps") || commandInvocation.source().hasPermission("vtools.server.auto");
|
return commandInvocation.source().hasPermission("VTools.tps");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,176 +0,0 @@
|
||||||
package de.strifel.VTools.commands.utils;
|
|
||||||
|
|
||||||
import static net.kyori.adventure.text.event.HoverEvent.showText;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.velocitypowered.api.command.CommandSource;
|
|
||||||
import com.velocitypowered.api.command.SimpleCommand;
|
|
||||||
import com.velocitypowered.api.permission.Tristate;
|
|
||||||
import com.velocitypowered.api.proxy.Player;
|
|
||||||
import com.velocitypowered.api.proxy.ProxyServer;
|
|
||||||
import com.velocitypowered.api.proxy.ServerConnection;
|
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
|
||||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import net.kyori.adventure.text.TextComponent;
|
|
||||||
import net.kyori.adventure.text.TranslatableComponent;
|
|
||||||
import net.kyori.adventure.text.event.ClickEvent;
|
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
|
||||||
|
|
||||||
// https://github.com/PaperMC/Velocity/blob/40b76c633276fcd6aea165baeae74039b2d059c4/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements Velocity's {@code /server} command.
|
|
||||||
*/
|
|
||||||
public abstract class ServerUtil implements SimpleCommand {
|
|
||||||
|
|
||||||
public static final int MAX_SERVERS_TO_LIST = 50;
|
|
||||||
private final ProxyServer server;
|
|
||||||
|
|
||||||
public ServerUtil(ProxyServer server) {
|
|
||||||
this.server = server;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(final SimpleCommand.Invocation invocation) {
|
|
||||||
final CommandSource source = invocation.source();
|
|
||||||
final String[] args = invocation.arguments();
|
|
||||||
|
|
||||||
if (!(source instanceof Player)) {
|
|
||||||
source.sendMessage(CommandMessages.PLAYERS_ONLY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Player player = (Player) source;
|
|
||||||
if (args.length == 1) {
|
|
||||||
// Trying to connect to a server.
|
|
||||||
String serverName = args[0];
|
|
||||||
Optional<RegisteredServer> toConnect = server.getServer(serverName);
|
|
||||||
if (toConnect.isEmpty()) {
|
|
||||||
player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST.args(Component.text(serverName)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
player.createConnectionRequest(toConnect.get()).fireAndForget();
|
|
||||||
} else {
|
|
||||||
outputServerInformation(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void outputServerInformation(Player executor) {
|
|
||||||
String currentServer = executor.getCurrentServer().map(ServerConnection::getServerInfo)
|
|
||||||
.map(ServerInfo::getName).orElse("<unknown>");
|
|
||||||
executor.sendMessage(Component.translatable(
|
|
||||||
"velocity.command.server-current-server",
|
|
||||||
NamedTextColor.YELLOW,
|
|
||||||
Component.text(currentServer)));
|
|
||||||
|
|
||||||
List<RegisteredServer> servers = BuiltinCommandUtil.sortedServerList(server);
|
|
||||||
if (servers.size() > MAX_SERVERS_TO_LIST) {
|
|
||||||
executor.sendMessage(Component.translatable(
|
|
||||||
"velocity.command.server-too-many", NamedTextColor.RED));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assemble the list of servers as components
|
|
||||||
TextComponent.Builder serverListBuilder = Component.text()
|
|
||||||
.append(Component.translatable("velocity.command.server-available",
|
|
||||||
NamedTextColor.YELLOW))
|
|
||||||
.append(Component.space());
|
|
||||||
for (int i = 0; i < servers.size(); i++) {
|
|
||||||
RegisteredServer rs = servers.get(i);
|
|
||||||
serverListBuilder.append(formatServerComponent(currentServer, rs));
|
|
||||||
if (i != servers.size() - 1) {
|
|
||||||
serverListBuilder.append(Component.text(", ", NamedTextColor.GRAY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executor.sendMessage(serverListBuilder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextComponent formatServerComponent(String currentPlayerServer, RegisteredServer server) {
|
|
||||||
ServerInfo serverInfo = server.getServerInfo();
|
|
||||||
TextComponent serverTextComponent = Component.text(serverInfo.getName());
|
|
||||||
|
|
||||||
int connectedPlayers = server.getPlayersConnected().size();
|
|
||||||
TranslatableComponent playersTextComponent;
|
|
||||||
if (connectedPlayers == 1) {
|
|
||||||
playersTextComponent = Component.translatable(
|
|
||||||
"velocity.command.server-tooltip-player-online");
|
|
||||||
} else {
|
|
||||||
playersTextComponent = Component.translatable(
|
|
||||||
"velocity.command.server-tooltip-players-online");
|
|
||||||
}
|
|
||||||
playersTextComponent = playersTextComponent.args(Component.text(connectedPlayers));
|
|
||||||
if (serverInfo.getName().equals(currentPlayerServer)) {
|
|
||||||
serverTextComponent = serverTextComponent.color(NamedTextColor.GREEN)
|
|
||||||
.hoverEvent(
|
|
||||||
showText(
|
|
||||||
Component.translatable("velocity.command.server-tooltip-current-server")
|
|
||||||
.append(Component.newline())
|
|
||||||
.append(playersTextComponent))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
serverTextComponent = serverTextComponent.color(NamedTextColor.GRAY)
|
|
||||||
.clickEvent(ClickEvent.runCommand("/server " + serverInfo.getName()))
|
|
||||||
.hoverEvent(
|
|
||||||
showText(
|
|
||||||
Component.translatable("velocity.command.server-tooltip-offer-connect-server")
|
|
||||||
.append(Component.newline())
|
|
||||||
.append(playersTextComponent))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return serverTextComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> suggest(final SimpleCommand.Invocation invocation) {
|
|
||||||
final String[] currentArgs = invocation.arguments();
|
|
||||||
Stream<String> possibilities = server.getAllServers().stream()
|
|
||||||
.map(rs -> rs.getServerInfo().getName());
|
|
||||||
|
|
||||||
if (currentArgs.length == 0) {
|
|
||||||
return possibilities.collect(Collectors.toList());
|
|
||||||
} else if (currentArgs.length == 1) {
|
|
||||||
return possibilities
|
|
||||||
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
} else {
|
|
||||||
return ImmutableList.of();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(final SimpleCommand.Invocation invocation) {
|
|
||||||
return invocation.source().getPermissionValue("velocity.command.server") != Tristate.FALSE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/PaperMC/Velocity/blob/b0862d2d16c4ba7560d3f24c824d78793ac3d9e0/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/CommandMessages.java
|
|
||||||
|
|
||||||
class CommandMessages {
|
|
||||||
|
|
||||||
public static final TranslatableComponent PLAYERS_ONLY = Component.translatable(
|
|
||||||
"velocity.command.players-only", NamedTextColor.RED);
|
|
||||||
public static final TranslatableComponent SERVER_DOES_NOT_EXIST = Component.translatable(
|
|
||||||
"velocity.command.server-does-not-exist", NamedTextColor.RED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/PaperMC/Velocity/blob/b0862d2d16c4ba7560d3f24c824d78793ac3d9e0/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/BuiltinCommandUtil.java
|
|
||||||
|
|
||||||
class BuiltinCommandUtil {
|
|
||||||
|
|
||||||
private BuiltinCommandUtil() {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<RegisteredServer> sortedServerList(ProxyServer proxy) {
|
|
||||||
List<RegisteredServer> servers = new ArrayList<>(proxy.getAllServers());
|
|
||||||
servers.sort(Comparator.comparing(RegisteredServer::getServerInfo));
|
|
||||||
return Collections.unmodifiableList(servers);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +1,47 @@
|
||||||
package de.strifel.VTools.listeners;
|
package de.strifel.VTools.listeners;
|
||||||
|
|
||||||
import com.alpt.vtools.listeners.ServerCloser;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.alpt.vtools.utils.MarkdownString;
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.pengrad.telegrambot.Callback;
|
import com.pengrad.telegrambot.Callback;
|
||||||
import com.pengrad.telegrambot.TelegramBot;
|
import com.pengrad.telegrambot.TelegramBot;
|
||||||
import com.pengrad.telegrambot.UpdatesListener;
|
import com.pengrad.telegrambot.UpdatesListener;
|
||||||
import com.pengrad.telegrambot.model.Message;
|
|
||||||
import com.pengrad.telegrambot.model.Update;
|
import com.pengrad.telegrambot.model.Update;
|
||||||
import com.pengrad.telegrambot.model.User;
|
import com.pengrad.telegrambot.model.User;
|
||||||
import com.pengrad.telegrambot.model.request.ParseMode;
|
|
||||||
import com.pengrad.telegrambot.request.EditMessageText;
|
|
||||||
import com.pengrad.telegrambot.request.GetChat;
|
|
||||||
import com.pengrad.telegrambot.request.SendMessage;
|
import com.pengrad.telegrambot.request.SendMessage;
|
||||||
import com.pengrad.telegrambot.response.BaseResponse;
|
|
||||||
import com.pengrad.telegrambot.response.GetChatResponse;
|
|
||||||
import com.pengrad.telegrambot.response.SendResponse;
|
import com.pengrad.telegrambot.response.SendResponse;
|
||||||
import com.velocitypowered.api.event.Subscribe;
|
import com.velocitypowered.api.event.Subscribe;
|
||||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||||
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
|
|
||||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.api.proxy.ProxyServer;
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import de.strifel.VTools.VTools;
|
import de.strifel.VTools.VTools;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.yaml.snakeyaml.Yaml;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.io.File;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.function.BiConsumer;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class TGBridge {
|
public class TGBridge {
|
||||||
private static final long MINUTE = 60_000;
|
|
||||||
private final VTools plugin;
|
private final VTools plugin;
|
||||||
private final ProxyServer server;
|
private final ProxyServer server;
|
||||||
@SuppressWarnings("java:S3008")
|
|
||||||
protected static TGBridge INSTANCE = null;
|
protected static TGBridge INSTANCE = null;
|
||||||
|
|
||||||
private TelegramBot bot;
|
private TelegramBot bot;
|
||||||
private String TOKEN = "";
|
private String TOKEN = "";
|
||||||
private long CHAT_ID = 0L;
|
private HashSet<Long> CHAT_IDS = new HashSet<>();
|
||||||
|
|
||||||
private int ONLINE_STATUS_MESSAGE_ID = -1;
|
|
||||||
|
|
||||||
private long backoffSec = 1L;
|
private long backoffSec = 1L;
|
||||||
|
|
||||||
private static final String DIVIDER = "————————\n";
|
public TGBridge(VTools plugin) {
|
||||||
/**
|
|
||||||
* markdown escaped
|
|
||||||
*/
|
|
||||||
private String pinNote;
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S3010")
|
|
||||||
public TGBridge(@NotNull VTools plugin) {
|
|
||||||
if (INSTANCE != null) {
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
INSTANCE = this;
|
INSTANCE = this;
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.server = plugin.getServer();
|
this.server = plugin.getServer();
|
||||||
|
@ -70,141 +50,85 @@ public class TGBridge {
|
||||||
public void register() {
|
public void register() {
|
||||||
server.getEventManager().register(plugin, this);
|
server.getEventManager().register(plugin, this);
|
||||||
botInit();
|
botInit();
|
||||||
initUpdateThread();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void 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:\n- \"0\"\ntoken: \"\"\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
String configStr = Files.readString(Path.of(configFile.toURI()), StandardCharsets.UTF_8);
|
||||||
|
Yaml yaml = new Yaml();
|
||||||
|
Map<String, Object> config = yaml.load(configStr);
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
CHAT_ID = Long.parseLong(plugin.getConfigOrDefault("chat_id", "0"));
|
((List<String>)config.getOrDefault("chat_id", List.of())).stream().forEach(s -> this.CHAT_IDS.add(Long.parseLong(s)));
|
||||||
TOKEN = plugin.getConfigOrDefault("token", "");
|
this.CHAT_IDS.removeIf(id -> id == 0L);
|
||||||
|
this.TOKEN = (String)config.getOrDefault("token", "");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.logger.error("parsing config", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void botInit() {
|
private void botInit() {
|
||||||
loadConfig();
|
loadConfig();
|
||||||
if (TOKEN.isEmpty() || CHAT_ID == 0L) return;
|
if (TOKEN.isEmpty() || CHAT_IDS.isEmpty()) return;
|
||||||
bot = new TelegramBot(TOKEN);
|
bot = new TelegramBot(TOKEN);
|
||||||
getPinnedMessage();
|
|
||||||
bot.setUpdatesListener(updates -> {
|
bot.setUpdatesListener(updates -> {
|
||||||
backoffSec = 1L;
|
backoffSec = 1L;
|
||||||
for (Update update : updates) {
|
for (Update update : updates) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (update == null || update.message() == null) continue;
|
if (update != null &&
|
||||||
Message message = update.message();
|
update.message() != null &&
|
||||||
if (message.chat() == null || message.chat().id() != CHAT_ID || message.from() == null) continue;
|
update.message().chat() != null &&
|
||||||
|
CHAT_IDS.contains(update.message().chat().id()) &&
|
||||||
mergeMessage.abort();
|
update.message().from() != null
|
||||||
|
) {
|
||||||
String text = "";
|
if (update.message().text() != null && !update.message().text().isEmpty()) {
|
||||||
Message replyTo = message.replyToMessage();
|
String msg = update.message().text();
|
||||||
if (replyTo != null) {
|
if (msg.equals("/list")) {
|
||||||
text += "[reply > ";
|
ArrayList<String> out = new ArrayList<>();
|
||||||
String replyText = "";
|
String fmt = server.getAllPlayers().size() > 1 ? "%d players are currently connected to the proxy." : "%d player is currently connected to the proxy.";
|
||||||
String replyType = getMessageType(replyTo);
|
out.add(String.format(fmt, server.getAllPlayers().size()));
|
||||||
if (replyTo.text() != null) {
|
List<RegisteredServer> servers = new ArrayList<>(server.getAllServers());
|
||||||
replyText += replyTo.text();
|
for (RegisteredServer server : servers) {
|
||||||
}
|
List<Player> onServer = ImmutableList.copyOf(server.getPlayersConnected());
|
||||||
if (replyType != null) {
|
if (!onServer.isEmpty()) {
|
||||||
replyText += replyType;
|
out.add(String.format("[%s] (%d): %s",
|
||||||
if (replyTo.caption() != null) {
|
server.getServerInfo().getName(),
|
||||||
replyText += " \"" + replyTo.caption() + "\"";
|
onServer.size(),
|
||||||
}
|
onServer.stream().map(Player::getUsername).collect(Collectors.joining(", ")))
|
||||||
}
|
|
||||||
if (replyText.equals("")) {
|
|
||||||
replyText = "?";
|
|
||||||
}
|
|
||||||
text += replyText + "] ";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.text() != null && !message.text().isEmpty()) {
|
|
||||||
String msgText = message.text();
|
|
||||||
if (msgText.startsWith("/")) {
|
|
||||||
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" -> {
|
|
||||||
if (arg == null || arg.length() == 0) {
|
|
||||||
if (replyTo == null) {
|
|
||||||
outbound("""
|
|
||||||
usage:
|
|
||||||
use "/setpin" reply a message that from the bot to set that message to pin-message,
|
|
||||||
or use "/setpin <note>" to update current pinned message.""");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ONLINE_STATUS_MESSAGE_ID = replyTo.messageId();
|
|
||||||
updateOnlineStatus();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (replyTo != null && (replyTo.from() == null || replyTo.messageId() <= 0)) {
|
|
||||||
outbound("must reply a message that from the bot (or reply nothing).");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String markdownString = MarkdownString.markdownString(message);
|
|
||||||
|
|
||||||
String shouldBeCommand = markdownString.substring(0, "/setpin ".length());
|
|
||||||
if (!shouldBeCommand.matches("/setpin[\t \n]")) {
|
|
||||||
outbound("\"/setpin\" must be plain text.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
outbound("old pinned note: \n" + pinNote, ParseMode.MarkdownV2);
|
|
||||||
|
|
||||||
pinNote = markdownString.substring("/setpin ".length());
|
|
||||||
if (replyTo != null) {
|
|
||||||
ONLINE_STATUS_MESSAGE_ID = replyTo.messageId();
|
|
||||||
}
|
|
||||||
updateOnlineStatus();
|
|
||||||
}
|
|
||||||
case "/genpin" -> outbound(genPinMessage(),
|
|
||||||
(sendMessage, sendResponse) -> {
|
|
||||||
if (!sendResponse.isOk()) {
|
|
||||||
plugin.logger.error(String.format("sendMessage error %d: %s", sendResponse.errorCode(), sendResponse.description()));
|
|
||||||
} else {
|
|
||||||
int messageId = sendResponse.message().messageId();
|
|
||||||
ONLINE_STATUS_MESSAGE_ID = messageId > 0 ? messageId : ONLINE_STATUS_MESSAGE_ID;
|
|
||||||
}
|
|
||||||
}, ParseMode.MarkdownV2
|
|
||||||
);
|
);
|
||||||
case "/shutdown" -> {
|
|
||||||
if (server.getAllPlayers().isEmpty()) {
|
|
||||||
ServerCloser.INSTANCE.fastShutdown();
|
|
||||||
outbound("服务器即将在一分钟后关机,使用 /fuck 以取消。");
|
|
||||||
} else {
|
|
||||||
outbound("still player online, can't shutdown.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "/fuck" -> {
|
outbound(String.join("\n", out));
|
||||||
if (server.getAllPlayers().isEmpty()) {
|
|
||||||
if("fuck".equalsIgnoreCase(arg)){
|
|
||||||
ServerCloser.INSTANCE.noShutdown();
|
|
||||||
outbound("shutdown timer disabled until next player join & left.");
|
|
||||||
}else {
|
|
||||||
ServerCloser.INSTANCE.slowShutdown();
|
|
||||||
outbound("shutdown timer has been set to 60 minutes.");
|
|
||||||
}
|
}
|
||||||
} else {
|
tgInbound(update.message().from(), msg);
|
||||||
outbound("still player online, will not shutdown.");
|
}
|
||||||
|
else if (update.message().sticker() != null) {
|
||||||
|
tgInbound(update.message().from(), "[sticker]");
|
||||||
|
}
|
||||||
|
else if (update.message().photo() != null) {
|
||||||
|
tgInbound(update.message().from(), "[photo]");
|
||||||
|
}
|
||||||
|
else if (update.message().audio() != null) {
|
||||||
|
tgInbound(update.message().from(), "[audio]");
|
||||||
|
}
|
||||||
|
else if (update.message().voice() != null) {
|
||||||
|
tgInbound(update.message().from(), "[voice]");
|
||||||
|
}
|
||||||
|
else if (update.message().document() != null) {
|
||||||
|
tgInbound(update.message().from(), "[document]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default -> {}
|
|
||||||
}
|
}
|
||||||
}
|
catch (Exception e) {
|
||||||
text += msgText;
|
|
||||||
}
|
|
||||||
String messageType = getMessageType(message);
|
|
||||||
if (messageType != null) {
|
|
||||||
text += "[" + messageType + (message.caption() != null ? " \"" + message.caption() + "\"]" : "]");
|
|
||||||
}
|
|
||||||
if (!text.equals("")) {
|
|
||||||
tgInbound(message.from(), text);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
plugin.logger.error("handling update", e);
|
plugin.logger.error("handling update", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,101 +144,6 @@ public class TGBridge {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getMessageType(Message message) {
|
|
||||||
if (message.sticker() != null) {
|
|
||||||
String emoji = message.sticker().emoji();
|
|
||||||
return emoji != null ? "sticker " + emoji : "sticker";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.photo() != null) {
|
|
||||||
return "photo";
|
|
||||||
}
|
|
||||||
if (message.audio() != null) {
|
|
||||||
return "audio";
|
|
||||||
}
|
|
||||||
if (message.voice() != null) {
|
|
||||||
return "voice";
|
|
||||||
}
|
|
||||||
if (message.document() != null) {
|
|
||||||
return "document";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean getPinnedMessage() {
|
|
||||||
try {
|
|
||||||
GetChatResponse response = bot.execute(new GetChat(CHAT_ID));
|
|
||||||
Message pinnedMessage = response.chat().pinnedMessage();
|
|
||||||
readOldPinnedMessage(pinnedMessage);
|
|
||||||
updateOnlineStatus();
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
plugin.logger.error("get group info failed.");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readOldPinnedMessage(Message message) {
|
|
||||||
ONLINE_STATUS_MESSAGE_ID = message.messageId();
|
|
||||||
String markdownText = MarkdownString.markdownString(message);
|
|
||||||
|
|
||||||
String[] s = markdownText.split(DIVIDER, 2);
|
|
||||||
pinNote = (s.length == 2) ?
|
|
||||||
s[1] :
|
|
||||||
"\r_\r" + MarkdownString.escapeStr("(use \"/setpin\" <note> to set note here)") + "\r_\r";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return markdown escaped str
|
|
||||||
*/
|
|
||||||
private String genPinMessage() {
|
|
||||||
return (pinNote != null && pinNote.length() != 0) ?
|
|
||||||
genOnlineStatus() + "\n\n" + DIVIDER + pinNote :
|
|
||||||
genOnlineStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return markdown escaped str
|
|
||||||
*/
|
|
||||||
|
|
||||||
private int shutdownCountMinutes = -1;
|
|
||||||
|
|
||||||
public static void setShuttingDown(int minute) {
|
|
||||||
INSTANCE.shutdownCountMinutes = minute;
|
|
||||||
INSTANCE.updateOnlineStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String genOnlineStatus() {
|
|
||||||
ArrayList<String> out = new ArrayList<>();
|
|
||||||
int playerCount = server.getAllPlayers().size();
|
|
||||||
out.add(switch (playerCount) {
|
|
||||||
case 0 -> "nobody here\\.";
|
|
||||||
case 1 -> "only one player online\\.";
|
|
||||||
default -> playerCount + " players online\\.";
|
|
||||||
});
|
|
||||||
List<RegisteredServer> registeredServers = new ArrayList<>(server.getAllServers());
|
|
||||||
for (RegisteredServer registeredServer : registeredServers) {
|
|
||||||
LinkedList<Player> onServer = new LinkedList<>();
|
|
||||||
for (Player player : registeredServer.getPlayersConnected()) {
|
|
||||||
if (!lastDisconnect.equals(player.getUsername())) {
|
|
||||||
onServer.add(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!onServer.isEmpty()) {
|
|
||||||
out.add(
|
|
||||||
String.format("\\[%s\\] \\(%d\\): %s",
|
|
||||||
"`" + MarkdownString.escapeStr(registeredServer.getServerInfo().getName()) + "`",
|
|
||||||
onServer.size(),
|
|
||||||
onServer.stream().map(player -> "`" + MarkdownString.escapeStr(player.getUsername()) + "`").collect(Collectors.joining(", ")))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String result = String.join("\n", out);
|
|
||||||
if (shutdownCountMinutes < 0) return 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void tgInbound(User user, String content) {
|
protected void tgInbound(User user, String content) {
|
||||||
inbound(String.format("[tg] <%s> %s", user.lastName() == null ? user.firstName() : String.format("%s %s", user.firstName(), user.lastName()), content));
|
inbound(String.format("[tg] <%s> %s", user.lastName() == null ? user.firstName() : String.format("%s %s", user.firstName(), user.lastName()), content));
|
||||||
}
|
}
|
||||||
|
@ -327,43 +156,18 @@ public class TGBridge {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void outbound(String content) {
|
||||||
protected void outbound(String content, ParseMode parseMode) {
|
|
||||||
outbound(content, (sendMessage, sendResponse) -> {
|
|
||||||
if (!sendResponse.isOk()) {
|
|
||||||
plugin.logger.error(String.format("sendMessage error %d: %s", sendResponse.errorCode(), sendResponse.description()));
|
|
||||||
}
|
|
||||||
}, parseMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void error(String context) {
|
|
||||||
INSTANCE.appendMessage("*" + MarkdownString.escapeStr(context) + "*");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void log(String context) {
|
|
||||||
INSTANCE.appendMessage("_" + MarkdownString.escapeStr(context) + "_");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void outbound(String content) {outbound(content, (ParseMode) null);}
|
|
||||||
|
|
||||||
|
|
||||||
protected void outbound(String content, @NotNull BiConsumer<SendMessage, SendResponse> onResponse) {outbound(content, onResponse, null);}
|
|
||||||
|
|
||||||
protected void outbound(String content, @NotNull BiConsumer<SendMessage, SendResponse> onResponse, ParseMode parseMode) {
|
|
||||||
if (bot == null) return;
|
if (bot == null) return;
|
||||||
if (content.length() > 4000) {
|
if (content.length() > 4000) {
|
||||||
content = content.substring(0, 4000);
|
content = content.substring(0, 4000);
|
||||||
}
|
}
|
||||||
|
for (long CHAT_ID : CHAT_IDS) {
|
||||||
SendMessage sendMessage = new SendMessage(CHAT_ID, content);
|
bot.execute(new SendMessage(CHAT_ID, content), new Callback<SendMessage, SendResponse>() {
|
||||||
if (parseMode != null) {
|
|
||||||
boolean a = sendMessage == sendMessage.parseMode(parseMode);
|
|
||||||
assert a;
|
|
||||||
}
|
|
||||||
bot.execute(sendMessage, new Callback<SendMessage, SendResponse>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(SendMessage sendMessage, SendResponse sendResponse) {
|
public void onResponse(SendMessage sendMessage, SendResponse sendResponse) {
|
||||||
onResponse.accept(sendMessage, sendResponse);
|
if (!sendResponse.isOk()) {
|
||||||
|
plugin.logger.error(String.format("sendMessage error %d: %s", sendResponse.errorCode(), sendResponse.description()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -372,225 +176,28 @@ public class TGBridge {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onProxyShutdown(ProxyShutdownEvent event) {
|
public void onProxyShutdown(ProxyShutdownEvent event) {
|
||||||
if (bot == null) return;
|
if (bot == null) return;
|
||||||
bot.removeGetUpdatesListener();
|
bot.removeGetUpdatesListener();
|
||||||
bot.shutdown();
|
bot.shutdown();
|
||||||
PROXY_SHUT_DOWN = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onServerConnected(ServerConnectedEvent event) {
|
public void onServerConnected(ServerConnectedEvent event) {
|
||||||
if (event.getPreviousServer().isEmpty() && !event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) {
|
if (event.getPreviousServer().isEmpty()) {
|
||||||
String username = event.getPlayer().getUsername();
|
if (!event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) {
|
||||||
if (lastDisconnect.equals(username)) {
|
outbound(String.format("%s joined the proxy", event.getPlayer().getUsername()));
|
||||||
lastDisconnect = "";
|
|
||||||
}
|
}
|
||||||
joinLeftAnnounce(String.format("`%s` joined the server\\.", MarkdownString.escapeStr(username)));
|
|
||||||
}
|
}
|
||||||
updateRequests.add(new UpdateRequest());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String lastDisconnect = "";
|
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onDisconnect(DisconnectEvent event) {
|
public void onDisconnect(DisconnectEvent event) {
|
||||||
if (!event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) {
|
if (!event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) {
|
||||||
String username = event.getPlayer().getUsername();
|
outbound(String.format("%s left the proxy", event.getPlayer().getUsername()));
|
||||||
if (username != null) {
|
|
||||||
lastDisconnect = username;
|
|
||||||
joinLeftAnnounce(String.format("`%s` left the server\\.", MarkdownString.escapeStr(username)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateRequests.add(new UpdateRequest());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void onServerPostConnect(ServerPostConnectEvent event) {
|
|
||||||
updateRequests.add(new UpdateRequest());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean PROXY_SHUT_DOWN = false;
|
|
||||||
|
|
||||||
private static class UpdateRequest {}
|
|
||||||
|
|
||||||
private LinkedBlockingQueue<UpdateRequest> updateRequests = new LinkedBlockingQueue<>();
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S3776")
|
|
||||||
private void initUpdateThread() {
|
|
||||||
new Thread(() -> {
|
|
||||||
while (true) {
|
|
||||||
if (PROXY_SHUT_DOWN) {
|
|
||||||
setOnlineStatusNotAvailable();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
UpdateRequest oldestRequest = null;
|
|
||||||
try {
|
|
||||||
oldestRequest = updateRequests.take();
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
if (oldestRequest == null) {
|
|
||||||
plugin.logger.warn("updateRequests.take() return a null value, why?");
|
|
||||||
sleep(10000);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
updateRequests.clear();
|
|
||||||
|
|
||||||
if (!updateOnlineStatus()) {
|
|
||||||
updateRequests.add(oldestRequest); // 更新失败 回去吧您内
|
|
||||||
sleep(10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
new Thread(() -> {
|
|
||||||
while (true) {
|
|
||||||
if (PROXY_SHUT_DOWN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
String oldestMessage = announceQueue.take();
|
|
||||||
appendMessage(oldestMessage);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Object mergeMessageLock = new Object();
|
|
||||||
|
|
||||||
private void appendMessage(String message) {
|
|
||||||
synchronized (mergeMessageLock) {
|
|
||||||
if (!mergeMessage.isValid()) {
|
|
||||||
mergeMessage = new MergeMessage(message);
|
|
||||||
} else {
|
|
||||||
mergeMessage.addLines(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sleep(int millis) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(millis);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean updateOnlineStatus() {
|
|
||||||
return editOnlineStatusMessage(genPinMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean setOnlineStatusNotAvailable() {
|
|
||||||
return updateOnlineStatus();
|
|
||||||
// return editOnlineStatusMessage("(proxy already shutdown)");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
|
|
||||||
|
|
||||||
protected boolean editOnlineStatusMessage(String markdownText) {
|
|
||||||
if (ONLINE_STATUS_MESSAGE_ID < 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
BaseResponse response;
|
|
||||||
try {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return response.isOk();
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MergeMessage {
|
|
||||||
private int messageId;
|
|
||||||
private long time;
|
|
||||||
private long timeMinute;
|
|
||||||
|
|
||||||
private StringBuilder text;
|
|
||||||
private boolean abort = false;
|
|
||||||
|
|
||||||
boolean isValid() {
|
|
||||||
if (abort || messageId < 1) return false;
|
|
||||||
long current = System.currentTimeMillis();
|
|
||||||
if (current / MINUTE != timeMinute) return false;
|
|
||||||
long dt = current - time;
|
|
||||||
return dt <= 30_000 && dt >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void abort() {
|
|
||||||
abort = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MergeMessage(String firstMessage) {
|
|
||||||
text = new StringBuilder(firstMessage);
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
timeMinute = time / MINUTE;
|
|
||||||
send();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void send() {
|
|
||||||
if (bot == null) {
|
|
||||||
messageId = -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SendResponse response;
|
|
||||||
try {
|
|
||||||
response = bot.execute(new SendMessage(CHAT_ID, text.toString()).parseMode(ParseMode.MarkdownV2));
|
|
||||||
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
messageId = -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!response.isOk()) {
|
|
||||||
messageId = -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
messageId = response.message().messageId();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MergeMessage() {
|
|
||||||
messageId = 0;
|
|
||||||
time = 0;
|
|
||||||
timeMinute = 0;
|
|
||||||
text = new StringBuilder();
|
|
||||||
abort = false;
|
|
||||||
} //dummy
|
|
||||||
|
|
||||||
private void addLines(String... messages) {
|
|
||||||
for (String message : messages) {
|
|
||||||
text.append('\n').append(message);
|
|
||||||
}
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void update() {
|
|
||||||
if (!isValid()) {
|
|
||||||
plugin.logger.error("message should only push to a valid object");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (bot == null) {
|
|
||||||
messageId = -1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
bot.execute(new EditMessageText(CHAT_ID, messageId, text.toString()).parseMode(ParseMode.MarkdownV2));
|
|
||||||
} catch (RuntimeException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MergeMessage mergeMessage = new MergeMessage();
|
|
||||||
private LinkedBlockingQueue<String> announceQueue = new LinkedBlockingQueue<>();
|
|
||||||
|
|
||||||
private void joinLeftAnnounce(String message) {
|
|
||||||
announceQueue.add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.TextComponent;
|
import net.kyori.adventure.text.TextComponent;
|
||||||
import net.kyori.adventure.text.TranslatableComponent;
|
import net.kyori.adventure.text.TranslatableComponent;
|
||||||
import net.kyori.adventure.text.event.ClickEvent;
|
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -54,10 +53,7 @@ public class GlistUtil {
|
||||||
|
|
||||||
TextComponent.Builder builder = Component.text()
|
TextComponent.Builder builder = Component.text()
|
||||||
.append(Component.text("[" + server.getServerInfo().getName() + "] ",
|
.append(Component.text("[" + server.getServerInfo().getName() + "] ",
|
||||||
NamedTextColor.DARK_AQUA)
|
NamedTextColor.DARK_AQUA))
|
||||||
.clickEvent(ClickEvent.runCommand("/server " + server.getServerInfo().getName()))
|
|
||||||
|
|
||||||
)
|
|
||||||
.append(Component.text("(" + onServer.size() + ")", NamedTextColor.GRAY))
|
.append(Component.text("(" + onServer.size() + ")", NamedTextColor.GRAY))
|
||||||
.append(Component.text(": "))
|
.append(Component.text(": "))
|
||||||
.resetStyle();
|
.resetStyle();
|
||||||
|
|
Loading…
Reference in a new issue