Changed to use internal mc protocol
This commit is contained in:
parent
4efc1002a9
commit
c05594256b
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
mvn clean package
|
||||||
|
scp target/mc-status-bot-1.0-SNAPSHOT-shaded.jar root@andrewlalis.com:/opt/mc-status-bot/mc-status-bot.jar
|
|
@ -8,27 +8,28 @@ import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
import net.dv8tion.jda.api.OnlineStatus;
|
import net.dv8tion.jda.api.OnlineStatus;
|
||||||
import net.dv8tion.jda.api.entities.Activity;
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
|
||||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class ServerBot implements Runnable {
|
public class ServerBot implements Runnable {
|
||||||
private final JDA jda;
|
private final JDA jda;
|
||||||
private final String serverIp;
|
private final String serverIp;
|
||||||
private final long channelId;
|
private final short serverPort;
|
||||||
private final ServerStatusFetcher serverStatusFetcher;
|
private final ServerStatusFetcher serverStatusFetcher;
|
||||||
|
|
||||||
private int lastPlayerCount = -1;
|
public ServerBot(JDA jda, String serverIp, short serverPort, ServerStatusFetcher serverStatusFetcher) {
|
||||||
private final Set<String> lastPlayerNames = new HashSet<>();
|
|
||||||
|
|
||||||
public ServerBot(JDA jda, String serverIp, long channelId, ServerStatusFetcher serverStatusFetcher) {
|
|
||||||
this.jda = jda;
|
this.jda = jda;
|
||||||
this.serverIp = serverIp;
|
this.serverIp = serverIp;
|
||||||
this.channelId = channelId;
|
this.serverPort = serverPort;
|
||||||
this.serverStatusFetcher = serverStatusFetcher;
|
this.serverStatusFetcher = serverStatusFetcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,14 +48,22 @@ public class ServerBot implements Runnable {
|
||||||
ArrayNode serversArray = configData.withArray("servers");
|
ArrayNode serversArray = configData.withArray("servers");
|
||||||
List<ServerBot> bots = new ArrayList<>(serversArray.size());
|
List<ServerBot> bots = new ArrayList<>(serversArray.size());
|
||||||
ServerStatusFetcher serverStatusFetcher = new ServerStatusFetcher();
|
ServerStatusFetcher serverStatusFetcher = new ServerStatusFetcher();
|
||||||
|
var virtualPool = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
for (JsonNode node : serversArray) {
|
for (JsonNode node : serversArray) {
|
||||||
String token = node.get("discord-token").asText();
|
String token = node.get("discord-token").asText();
|
||||||
String serverIp = node.get("server-ip").asText();
|
String serverIp = node.get("server-ip").asText();
|
||||||
long channelId = node.get("channel-id").asLong();
|
short serverPort = node.get("server-port").shortValue();
|
||||||
JDABuilder builder = JDABuilder.create(token, Collections.emptyList());
|
JDABuilder builder = JDABuilder.create(token, Collections.emptyList());
|
||||||
builder.disableCache(CacheFlag.ACTIVITY, CacheFlag.VOICE_STATE);
|
builder.disableCache(
|
||||||
|
CacheFlag.ACTIVITY, CacheFlag.VOICE_STATE, CacheFlag.EMOJI,
|
||||||
|
CacheFlag.STICKER, CacheFlag.CLIENT_STATUS, CacheFlag.ONLINE_STATUS,
|
||||||
|
CacheFlag.SCHEDULED_EVENTS
|
||||||
|
);
|
||||||
|
builder.setCallbackPool(virtualPool);
|
||||||
|
builder.setEventPool(virtualPool);
|
||||||
|
builder.setRateLimitElastic(virtualPool, true);
|
||||||
JDA jda = builder.build();
|
JDA jda = builder.build();
|
||||||
bots.add(new ServerBot(jda, serverIp, channelId, serverStatusFetcher));
|
bots.add(new ServerBot(jda, serverIp, serverPort, serverStatusFetcher));
|
||||||
}
|
}
|
||||||
return bots;
|
return bots;
|
||||||
}
|
}
|
||||||
|
@ -68,13 +77,13 @@ public class ServerBot implements Runnable {
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
ServerStatus status = serverStatusFetcher.fetch(serverIp);
|
ServerStatus status = serverStatusFetcher.fetchViaSocket(serverIp, serverPort);
|
||||||
displayStatus(status);
|
displayStatus(status);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
handleError(e);
|
handleError(e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Thread.sleep(10000);
|
Thread.sleep(5000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
e.printStackTrace(System.err);
|
e.printStackTrace(System.err);
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
|
@ -83,11 +92,10 @@ public class ServerBot implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayStatus(ServerStatus status) {
|
private void displayStatus(ServerStatus status) {
|
||||||
String text = String.format("%d/%d players", status.playersOnline(), status.maxPlayers());
|
|
||||||
OnlineStatus onlineStatus = jda.getPresence().getStatus();
|
OnlineStatus onlineStatus = jda.getPresence().getStatus();
|
||||||
OnlineStatus newOnlineStatus = status.playersOnline() > 0 ? OnlineStatus.ONLINE : OnlineStatus.IDLE;
|
OnlineStatus newOnlineStatus = status.playersOnline() > 0 ? OnlineStatus.ONLINE : OnlineStatus.IDLE;
|
||||||
Activity activity = jda.getPresence().getActivity();
|
Activity activity = jda.getPresence().getActivity();
|
||||||
Activity newActivity = Activity.customStatus(text);
|
Activity newActivity = Activity.customStatus(formatStatusText(status));
|
||||||
|
|
||||||
boolean shouldUpdate = onlineStatus != newOnlineStatus ||
|
boolean shouldUpdate = onlineStatus != newOnlineStatus ||
|
||||||
activity == null ||
|
activity == null ||
|
||||||
|
@ -95,31 +103,17 @@ public class ServerBot implements Runnable {
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
jda.getPresence().setPresence(newOnlineStatus, newActivity);
|
jda.getPresence().setPresence(newOnlineStatus, newActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastPlayerCount != -1 && lastPlayerCount != status.playersOnline()) {
|
|
||||||
final TextChannel channel = jda.getTextChannelById(channelId);
|
|
||||||
for (String name : getPlayersJoined(status.playerNames())) {
|
|
||||||
channel.sendMessage(name + " joined the server.").queue();
|
|
||||||
}
|
|
||||||
for (String name : getPlayersLeft(status.playerNames())) {
|
|
||||||
channel.sendMessage(name + " left the server.").queue();
|
|
||||||
}
|
|
||||||
lastPlayerCount = status.playersOnline();
|
|
||||||
lastPlayerNames.clear();
|
|
||||||
lastPlayerNames.addAll(status.playerNames());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> getPlayersJoined(Set<String> current) {
|
private String formatStatusText(ServerStatus status) {
|
||||||
Set<String> set = new HashSet<>(current);
|
if (status.playerNames().isEmpty()) {
|
||||||
set.removeAll(lastPlayerNames);
|
return "0 players online.";
|
||||||
return set;
|
|
||||||
}
|
}
|
||||||
|
String playerNames = "\uD83C\uDFAE " + status.playerNames().stream().sorted().collect(Collectors.joining(", "));
|
||||||
private Set<String> getPlayersLeft(Set<String> current) {
|
if (playerNames.length() > Activity.MAX_ACTIVITY_NAME_LENGTH) {
|
||||||
Set<String> set = new HashSet<>(lastPlayerNames);
|
return playerNames.substring(0, Activity.MAX_ACTIVITY_NAME_LENGTH - 3) + "...";
|
||||||
set.removeAll(current);
|
}
|
||||||
return set;
|
return playerNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleError(IOException e) {
|
private void handleError(IOException e) {
|
||||||
|
@ -128,7 +122,5 @@ public class ServerBot implements Runnable {
|
||||||
jda.getPresence().setActivity(Activity.customStatus("Error."));
|
jda.getPresence().setActivity(Activity.customStatus("Error."));
|
||||||
}
|
}
|
||||||
e.printStackTrace(System.err);
|
e.printStackTrace(System.err);
|
||||||
lastPlayerCount = -1;
|
|
||||||
lastPlayerNames.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,68 @@
|
||||||
package com.andrewlalis.mc_status_bot;
|
package com.andrewlalis.mc_status_bot;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.andrewlalis.mc_status_bot.server_prot.ServerProtocol;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.*;
|
||||||
import java.net.URI;
|
import java.net.Socket;
|
||||||
import java.net.http.HttpClient;
|
import java.util.stream.Collectors;
|
||||||
import java.net.http.HttpRequest;
|
import java.util.stream.StreamSupport;
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
public class ServerStatusFetcher {
|
public class ServerStatusFetcher {
|
||||||
private final HttpClient client = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(3))
|
|
||||||
.executor(Executors.newVirtualThreadPerTaskExecutor())
|
|
||||||
.build();
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
public ServerStatus fetch(String ip) throws IOException {
|
public ServerStatus fetchViaSocket(String ip, short port) throws IOException {
|
||||||
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.mcsrvstat.us/3/" + ip))
|
// long start = System.currentTimeMillis();
|
||||||
.GET()
|
try (var socket = new Socket(ip, port)) {
|
||||||
.timeout(Duration.ofSeconds(5))
|
OutputStream sOut = socket.getOutputStream();
|
||||||
.build();
|
InputStream sIn = socket.getInputStream();
|
||||||
try {
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
// Send the handshake request.
|
||||||
if (response.statusCode() != 200) throw new IOException("Non-200 status code: " + response.statusCode());
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
ObjectNode data = objectMapper.readValue(response.body(), ObjectNode.class);
|
ServerProtocol.writeVarInt(out, 0x00); // Handshake packet id.
|
||||||
Set<String> playerNames = new HashSet<>();
|
ServerProtocol.writeVarInt(out, 764); // Protocol version for 1.20.2.
|
||||||
ArrayNode playersArray = data.get("players").withArray("list");
|
ServerProtocol.writeString(out, ip);
|
||||||
for (JsonNode node : playersArray) {
|
new DataOutputStream(out).writeShort(25565);
|
||||||
playerNames.add(node.get("name").asText());
|
ServerProtocol.writeVarInt(out, 1); // Next-state enum: 1 for Status.
|
||||||
|
|
||||||
|
ServerProtocol.writeVarInt(sOut, out.size());
|
||||||
|
sOut.write(out.toByteArray());
|
||||||
|
sOut.flush();
|
||||||
|
|
||||||
|
// Immediately send status request.
|
||||||
|
out.reset();
|
||||||
|
ServerProtocol.writeVarInt(out, 0x00);
|
||||||
|
ServerProtocol.writeVarInt(sOut, out.size());
|
||||||
|
sOut.write(out.toByteArray());
|
||||||
|
sOut.flush();
|
||||||
|
|
||||||
|
// Receive the status response.
|
||||||
|
int responsePacketSize = ServerProtocol.readVarInt(sIn);
|
||||||
|
byte[] packetIdAndData = new byte[responsePacketSize];
|
||||||
|
int bytesRead = 0;
|
||||||
|
int attempts = 0;
|
||||||
|
while (bytesRead < responsePacketSize) {
|
||||||
|
bytesRead += sIn.read(packetIdAndData, bytesRead, packetIdAndData.length - bytesRead);
|
||||||
|
attempts++;
|
||||||
|
if (attempts > 100) break;
|
||||||
}
|
}
|
||||||
|
if (bytesRead != responsePacketSize) throw new IOException("Couldn't read full packet. Read " + bytesRead + " instead of " + responsePacketSize);
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(packetIdAndData);
|
||||||
|
int packetId = ServerProtocol.readVarInt(in);
|
||||||
|
if (packetId != 0x00) throw new IOException("Received invalid packetId when receiving status response: " + packetId);
|
||||||
|
String jsonData = ServerProtocol.readString(in);
|
||||||
|
// long dur = System.currentTimeMillis() - start;
|
||||||
|
// System.out.println("Received server status in " + dur + " ms.");
|
||||||
|
ObjectNode obj = objectMapper.readValue(jsonData, ObjectNode.class);
|
||||||
|
|
||||||
return new ServerStatus(
|
return new ServerStatus(
|
||||||
data.get("players").get("online").asInt(),
|
obj.get("players").get("online").asInt(),
|
||||||
data.get("players").get("max").asInt(),
|
obj.get("players").get("max").asInt(),
|
||||||
playerNames
|
StreamSupport.stream(obj.get("players").withArray("sample").spliterator(), false)
|
||||||
|
.map(node -> node.get("name").asText())
|
||||||
|
.collect(Collectors.toSet())
|
||||||
);
|
);
|
||||||
} catch (IOException | InterruptedException e) {
|
|
||||||
throw new IOException("Failed to get server status.", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.andrewlalis.mc_status_bot.server_prot;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class ServerProtocol {
|
||||||
|
private static final int SEGMENT_BITS = 0x7F;
|
||||||
|
private static final int CONTINUE_BIT = 0x80;
|
||||||
|
|
||||||
|
public static int readVarInt(InputStream in) throws IOException {
|
||||||
|
int value = 0;
|
||||||
|
int position = 0;
|
||||||
|
byte currentByte;
|
||||||
|
while (true) {
|
||||||
|
currentByte = (byte) in.read();
|
||||||
|
value |= (currentByte & SEGMENT_BITS) << position;
|
||||||
|
if ((currentByte & CONTINUE_BIT) == 0) break;
|
||||||
|
position += 7;
|
||||||
|
if (position >= 32) throw new IOException("VarInt is too big.");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeVarInt(OutputStream out, int value) throws IOException {
|
||||||
|
while (true) {
|
||||||
|
if ((value & ~SEGMENT_BITS) == 0) {
|
||||||
|
out.write(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.write((value & SEGMENT_BITS) | CONTINUE_BIT);
|
||||||
|
value >>>= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readString(InputStream in) throws IOException {
|
||||||
|
int length = readVarInt(in);
|
||||||
|
byte[] data = new byte[length];
|
||||||
|
int bytesRead = in.read(data);
|
||||||
|
if (bytesRead != length) throw new IOException("Couldn't read full string of length " + length);
|
||||||
|
return new String(data, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeString(OutputStream out, String s) throws IOException {
|
||||||
|
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
|
||||||
|
writeVarInt(out, bytes.length);
|
||||||
|
out.write(bytes);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue