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.OnlineStatus;
|
||||
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 java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
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 {
|
||||
private final JDA jda;
|
||||
private final String serverIp;
|
||||
private final long channelId;
|
||||
private final short serverPort;
|
||||
private final ServerStatusFetcher serverStatusFetcher;
|
||||
|
||||
private int lastPlayerCount = -1;
|
||||
private final Set<String> lastPlayerNames = new HashSet<>();
|
||||
|
||||
public ServerBot(JDA jda, String serverIp, long channelId, ServerStatusFetcher serverStatusFetcher) {
|
||||
public ServerBot(JDA jda, String serverIp, short serverPort, ServerStatusFetcher serverStatusFetcher) {
|
||||
this.jda = jda;
|
||||
this.serverIp = serverIp;
|
||||
this.channelId = channelId;
|
||||
this.serverPort = serverPort;
|
||||
this.serverStatusFetcher = serverStatusFetcher;
|
||||
}
|
||||
|
||||
|
@ -47,14 +48,22 @@ public class ServerBot implements Runnable {
|
|||
ArrayNode serversArray = configData.withArray("servers");
|
||||
List<ServerBot> bots = new ArrayList<>(serversArray.size());
|
||||
ServerStatusFetcher serverStatusFetcher = new ServerStatusFetcher();
|
||||
var virtualPool = Executors.newVirtualThreadPerTaskExecutor();
|
||||
for (JsonNode node : serversArray) {
|
||||
String token = node.get("discord-token").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());
|
||||
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();
|
||||
bots.add(new ServerBot(jda, serverIp, channelId, serverStatusFetcher));
|
||||
bots.add(new ServerBot(jda, serverIp, serverPort, serverStatusFetcher));
|
||||
}
|
||||
return bots;
|
||||
}
|
||||
|
@ -68,13 +77,13 @@ public class ServerBot implements Runnable {
|
|||
}
|
||||
while (true) {
|
||||
try {
|
||||
ServerStatus status = serverStatusFetcher.fetch(serverIp);
|
||||
ServerStatus status = serverStatusFetcher.fetchViaSocket(serverIp, serverPort);
|
||||
displayStatus(status);
|
||||
} catch (IOException e) {
|
||||
handleError(e);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(10000);
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace(System.err);
|
||||
Thread.currentThread().interrupt();
|
||||
|
@ -83,11 +92,10 @@ public class ServerBot implements Runnable {
|
|||
}
|
||||
|
||||
private void displayStatus(ServerStatus status) {
|
||||
String text = String.format("%d/%d players", status.playersOnline(), status.maxPlayers());
|
||||
OnlineStatus onlineStatus = jda.getPresence().getStatus();
|
||||
OnlineStatus newOnlineStatus = status.playersOnline() > 0 ? OnlineStatus.ONLINE : OnlineStatus.IDLE;
|
||||
Activity activity = jda.getPresence().getActivity();
|
||||
Activity newActivity = Activity.customStatus(text);
|
||||
Activity newActivity = Activity.customStatus(formatStatusText(status));
|
||||
|
||||
boolean shouldUpdate = onlineStatus != newOnlineStatus ||
|
||||
activity == null ||
|
||||
|
@ -95,31 +103,17 @@ public class ServerBot implements Runnable {
|
|||
if (shouldUpdate) {
|
||||
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) {
|
||||
Set<String> set = new HashSet<>(current);
|
||||
set.removeAll(lastPlayerNames);
|
||||
return set;
|
||||
private String formatStatusText(ServerStatus status) {
|
||||
if (status.playerNames().isEmpty()) {
|
||||
return "0 players online.";
|
||||
}
|
||||
|
||||
private Set<String> getPlayersLeft(Set<String> current) {
|
||||
Set<String> set = new HashSet<>(lastPlayerNames);
|
||||
set.removeAll(current);
|
||||
return set;
|
||||
String playerNames = "\uD83C\uDFAE " + status.playerNames().stream().sorted().collect(Collectors.joining(", "));
|
||||
if (playerNames.length() > Activity.MAX_ACTIVITY_NAME_LENGTH) {
|
||||
return playerNames.substring(0, Activity.MAX_ACTIVITY_NAME_LENGTH - 3) + "...";
|
||||
}
|
||||
return playerNames;
|
||||
}
|
||||
|
||||
private void handleError(IOException e) {
|
||||
|
@ -128,7 +122,5 @@ public class ServerBot implements Runnable {
|
|||
jda.getPresence().setActivity(Activity.customStatus("Error."));
|
||||
}
|
||||
e.printStackTrace(System.err);
|
||||
lastPlayerCount = -1;
|
||||
lastPlayerNames.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,68 @@
|
|||
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.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public class ServerStatusFetcher {
|
||||
private final HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(3))
|
||||
.executor(Executors.newVirtualThreadPerTaskExecutor())
|
||||
.build();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public ServerStatus fetch(String ip) throws IOException {
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.mcsrvstat.us/3/" + ip))
|
||||
.GET()
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
try {
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) throw new IOException("Non-200 status code: " + response.statusCode());
|
||||
ObjectNode data = objectMapper.readValue(response.body(), ObjectNode.class);
|
||||
Set<String> playerNames = new HashSet<>();
|
||||
ArrayNode playersArray = data.get("players").withArray("list");
|
||||
for (JsonNode node : playersArray) {
|
||||
playerNames.add(node.get("name").asText());
|
||||
public ServerStatus fetchViaSocket(String ip, short port) throws IOException {
|
||||
// long start = System.currentTimeMillis();
|
||||
try (var socket = new Socket(ip, port)) {
|
||||
OutputStream sOut = socket.getOutputStream();
|
||||
InputStream sIn = socket.getInputStream();
|
||||
|
||||
// Send the handshake request.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ServerProtocol.writeVarInt(out, 0x00); // Handshake packet id.
|
||||
ServerProtocol.writeVarInt(out, 764); // Protocol version for 1.20.2.
|
||||
ServerProtocol.writeString(out, ip);
|
||||
new DataOutputStream(out).writeShort(25565);
|
||||
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(
|
||||
data.get("players").get("online").asInt(),
|
||||
data.get("players").get("max").asInt(),
|
||||
playerNames
|
||||
obj.get("players").get("online").asInt(),
|
||||
obj.get("players").get("max").asInt(),
|
||||
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