Compare commits
30 commits
multigroup
...
master
Author | SHA1 | Date | |
---|---|---|---|
85e1bf1ca4 | |||
d647b99465 | |||
115364f202 | |||
5a975a1195 | |||
513ba7ad8c | |||
7581ef4595 | |||
cba3248484 | |||
1a3c8872d1 | |||
f36607df8d | |||
49a5dd76e7 | |||
c178415969 | |||
564e1eb5fa | |||
0c17f2cd80 | |||
301931866a | |||
afae76d9dd | |||
780834a922 | |||
6fa5ab6824 | |||
b7e4b668fe | |||
425210963f | |||
e4c91adac0 | |||
c2ee4fca4f | |||
52b3a5016d | |||
6b3509b4a2 | |||
d1f9dc803d | |||
706c5fade5 | |||
aebff1a12a | |||
a57cd87fff | |||
d1955da813 | |||
7cfb423c70 | |||
19f86511e0 |
16 changed files with 1312 additions and 129 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
out/
|
out/
|
||||||
target/
|
target/
|
||||||
*.iml
|
*.iml
|
||||||
|
/dependency-reduced-pom.xml
|
|
@ -1,6 +1,8 @@
|
||||||
# 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|
|
||||||
|-------|-------------|----------|
|
|-------|-------------|----------|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
210
src/main/java/com/alpt/vtools/listeners/ServerCloser.java
Normal file
210
src/main/java/com/alpt/vtools/listeners/ServerCloser.java
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
241
src/main/java/com/alpt/vtools/utils/MarkdownString.java
Normal file
241
src/main/java/com/alpt/vtools/utils/MarkdownString.java
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
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,5 +1,7 @@
|
||||||
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;
|
||||||
|
@ -9,11 +11,17 @@ 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 {
|
||||||
private final ProxyServer server;
|
private final ProxyServer server;
|
||||||
public final Logger logger;
|
public final Logger logger;
|
||||||
|
@ -40,10 +48,48 @@ 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,6 +5,8 @@ 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;
|
||||||
|
@ -29,7 +31,12 @@ 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()) {
|
||||||
commandSource.sendMessage(Component.text("Player " + strings[0] + " is on " + player.get().getCurrentServer().get().getServerInfo().getName() + "!").color(COLOR_YELLOW));
|
String serverName = player.get().getCurrentServer().get().getServerInfo().getName();
|
||||||
|
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<String>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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;
|
||||||
|
@ -19,7 +18,6 @@ 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) {
|
||||||
|
@ -33,7 +31,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<String>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -9,10 +9,7 @@ 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.ArrayList;
|
import java.util.*;
|
||||||
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;
|
||||||
|
@ -39,23 +36,20 @@ 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();
|
||||||
if (conn.isPresent()) {
|
if (conn.isPresent()) {
|
||||||
for (Player player : conn.get().getServer().getPlayersConnected()) {
|
for (Player player : conn.get().getServer().getPlayersConnected()) {
|
||||||
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());
|
||||||
|
@ -96,7 +90,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(String.format("Send Results:\n%s", sendResults)).color(COLOR_YELLOW));
|
commandSource.sendMessage(Component.text("Send Results:%n%s".formatted(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));
|
||||||
|
@ -108,21 +102,26 @@ 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) {
|
||||||
List<String> arg = new ArrayList<String>();
|
case 0, 1 -> {
|
||||||
if (currentArgs.length <= 1) {
|
List<String> args = new ArrayList<>(server.getPlayerCount() + 2);
|
||||||
arg.add("all");
|
args.add("all");
|
||||||
arg.add("current");
|
args.add("current");
|
||||||
for (Player player : server.getAllPlayers()) {
|
for (Player player : server.getAllPlayers()) {
|
||||||
arg.add(player.getUsername());
|
args.add(player.getUsername());
|
||||||
}
|
}
|
||||||
return arg;
|
return currentArgs.length==0?args:
|
||||||
} else if (currentArgs.length == 2) {
|
args.stream().filter(arg->arg.regionMatches(true,0,currentArgs[0],0,currentArgs[0].length())).toList();
|
||||||
for (RegisteredServer server : server.getAllServers()) {
|
}
|
||||||
arg.add(server.getServerInfo().getName());
|
case 2 -> {
|
||||||
|
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) {
|
||||||
|
|
41
src/main/java/de/strifel/VTools/commands/CommandServer.java
Normal file
41
src/main/java/de/strifel/VTools/commands/CommandServer.java
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
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<String>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@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) {
|
if (commandSource instanceof Player playerCommandSource) {
|
||||||
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 -> ((Player) commandSource).createConnectionRequest(serverConnection.getServer()).fireAndForget());
|
player.get().getCurrentServer().ifPresent(serverConnection -> playerCommandSource.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");
|
return commandInvocation.source().hasPermission("VTools.tps") || commandInvocation.source().hasPermission("vtools.server.auto");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
176
src/main/java/de/strifel/VTools/commands/utils/ServerUtil.java
Normal file
176
src/main/java/de/strifel/VTools/commands/utils/ServerUtil.java
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
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,47 +1,67 @@
|
||||||
package de.strifel.VTools.listeners;
|
package de.strifel.VTools.listeners;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
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;
|
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.yaml.snakeyaml.Yaml;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.File;
|
import java.util.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.nio.file.Files;
|
import java.util.function.BiConsumer;
|
||||||
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 HashSet<Long> CHAT_IDS = new HashSet<>();
|
private long CHAT_ID = 0L;
|
||||||
|
|
||||||
|
private int ONLINE_STATUS_MESSAGE_ID = -1;
|
||||||
|
|
||||||
private long backoffSec = 1L;
|
private long backoffSec = 1L;
|
||||||
|
|
||||||
public TGBridge(VTools plugin) {
|
private static final String DIVIDER = "————————\n";
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
@ -50,85 +70,141 @@ 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) {
|
||||||
((List<String>)config.getOrDefault("chat_id", List.of())).stream().forEach(s -> this.CHAT_IDS.add(Long.parseLong(s)));
|
CHAT_ID = Long.parseLong(plugin.getConfigOrDefault("chat_id", "0"));
|
||||||
this.CHAT_IDS.removeIf(id -> id == 0L);
|
TOKEN = plugin.getConfigOrDefault("token", "");
|
||||||
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_IDS.isEmpty()) return;
|
if (TOKEN.isEmpty() || CHAT_ID == 0L) 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 {
|
|
||||||
if (update != null &&
|
|
||||||
update.message() != null &&
|
|
||||||
update.message().chat() != null &&
|
|
||||||
CHAT_IDS.contains(update.message().chat().id()) &&
|
|
||||||
update.message().from() != null
|
|
||||||
) {
|
|
||||||
if (update.message().text() != null && !update.message().text().isEmpty()) {
|
|
||||||
String msg = update.message().text();
|
|
||||||
if (msg.equals("/list")) {
|
|
||||||
ArrayList<String> out = new ArrayList<>();
|
|
||||||
String fmt = server.getAllPlayers().size() > 1 ? "%d players are currently connected to the proxy." : "%d player is currently connected to the proxy.";
|
|
||||||
out.add(String.format(fmt, server.getAllPlayers().size()));
|
|
||||||
List<RegisteredServer> servers = new ArrayList<>(server.getAllServers());
|
|
||||||
for (RegisteredServer server : servers) {
|
|
||||||
List<Player> onServer = ImmutableList.copyOf(server.getPlayersConnected());
|
|
||||||
if (!onServer.isEmpty()) {
|
|
||||||
out.add(String.format("[%s] (%d): %s",
|
|
||||||
server.getServerInfo().getName(),
|
|
||||||
onServer.size(),
|
|
||||||
onServer.stream().map(Player::getUsername).collect(Collectors.joining(", ")))
|
|
||||||
);
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (update == null || update.message() == null) continue;
|
||||||
|
Message message = update.message();
|
||||||
|
if (message.chat() == null || message.chat().id() != CHAT_ID || message.from() == null) continue;
|
||||||
|
|
||||||
|
mergeMessage.abort();
|
||||||
|
|
||||||
|
String text = "";
|
||||||
|
Message replyTo = message.replyToMessage();
|
||||||
|
if (replyTo != null) {
|
||||||
|
text += "[reply > ";
|
||||||
|
String replyText = "";
|
||||||
|
String replyType = getMessageType(replyTo);
|
||||||
|
if (replyTo.text() != null) {
|
||||||
|
replyText += replyTo.text();
|
||||||
|
}
|
||||||
|
if (replyType != null) {
|
||||||
|
replyText += replyType;
|
||||||
|
if (replyTo.caption() != null) {
|
||||||
|
replyText += " \"" + replyTo.caption() + "\"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outbound(String.join("\n", out));
|
if (replyText.equals("")) {
|
||||||
|
replyText = "?";
|
||||||
}
|
}
|
||||||
tgInbound(update.message().from(), msg);
|
text += replyText + "] ";
|
||||||
}
|
}
|
||||||
else if (update.message().sticker() != null) {
|
|
||||||
tgInbound(update.message().from(), "[sticker]");
|
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;
|
||||||
}
|
}
|
||||||
else if (update.message().photo() != null) {
|
ONLINE_STATUS_MESSAGE_ID = replyTo.messageId();
|
||||||
tgInbound(update.message().from(), "[photo]");
|
updateOnlineStatus();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
else if (update.message().audio() != null) {
|
if (replyTo != null && (replyTo.from() == null || replyTo.messageId() <= 0)) {
|
||||||
tgInbound(update.message().from(), "[audio]");
|
outbound("must reply a message that from the bot (or reply nothing).");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
else if (update.message().voice() != null) {
|
|
||||||
tgInbound(update.message().from(), "[voice]");
|
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;
|
||||||
}
|
}
|
||||||
else if (update.message().document() != null) {
|
|
||||||
tgInbound(update.message().from(), "[document]");
|
|
||||||
|
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" -> {
|
||||||
|
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.");
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
} else {
|
||||||
|
outbound("still player online, will not shutdown.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,7 +212,7 @@ public class TGBridge {
|
||||||
}, (e) -> {
|
}, (e) -> {
|
||||||
plugin.logger.error("getting update", e);
|
plugin.logger.error("getting update", e);
|
||||||
plugin.logger.error(String.format("waiting %ds before getting another update", backoffSec));
|
plugin.logger.error(String.format("waiting %ds before getting another update", backoffSec));
|
||||||
try { Thread.sleep(backoffSec * 1000); } catch (InterruptedException ignored) {}
|
try {Thread.sleep(backoffSec * 1000);} catch (InterruptedException ignored) {}
|
||||||
backoffSec *= 2L;
|
backoffSec *= 2L;
|
||||||
if (backoffSec > 3600) {
|
if (backoffSec > 3600) {
|
||||||
backoffSec = 3600;
|
backoffSec = 3600;
|
||||||
|
@ -144,6 +220,101 @@ 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));
|
||||||
}
|
}
|
||||||
|
@ -156,18 +327,43 @@ 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) {
|
|
||||||
bot.execute(new SendMessage(CHAT_ID, content), new Callback<SendMessage, SendResponse>() {
|
SendMessage sendMessage = new SendMessage(CHAT_ID, content);
|
||||||
|
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) {
|
||||||
if (!sendResponse.isOk()) {
|
onResponse.accept(sendMessage, sendResponse);
|
||||||
plugin.logger.error(String.format("sendMessage error %d: %s", sendResponse.errorCode(), sendResponse.description()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -176,28 +372,225 @@ 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()) {
|
if (event.getPreviousServer().isEmpty() && !event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) {
|
||||||
if (!event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) {
|
String username = event.getPlayer().getUsername();
|
||||||
outbound(String.format("%s joined the proxy", event.getPlayer().getUsername()));
|
if (lastDisconnect.equals(username)) {
|
||||||
|
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")) {
|
||||||
outbound(String.format("%s left the proxy", event.getPlayer().getUsername()));
|
String username = 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,6 +7,7 @@ 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;
|
||||||
|
@ -53,7 +54,10 @@ 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