Added starter implementation.

This commit is contained in:
Andrew Lalis 2023-11-28 13:39:12 -05:00
parent cec1853d73
commit 4efc1002a9
6 changed files with 312 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
config.json

69
pom.xml Normal file
View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.andrewlalis</groupId>
<artifactId>mc-status-bot</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0-beta.18</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.16.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.andrewlalis.mc_status_bot.McStatusBot</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package com.andrewlalis.mc_status_bot;
import java.nio.file.Path;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class McStatusBot {
public static void main(String[] args) throws Exception {
Executor executor = Executors.newVirtualThreadPerTaskExecutor();
for (ServerBot bot : ServerBot.read(Path.of("config.json"))) {
System.out.println("Starting server status bot for " + bot.getServerIp());
executor.execute(bot);
}
}
}

View File

@ -0,0 +1,134 @@
package com.andrewlalis.mc_status_bot;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.*;
public class ServerBot implements Runnable {
private final JDA jda;
private final String serverIp;
private final long channelId;
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) {
this.jda = jda;
this.serverIp = serverIp;
this.channelId = channelId;
this.serverStatusFetcher = serverStatusFetcher;
}
public String getServerIp() {
return serverIp;
}
public static Collection<ServerBot> read(Path jsonConfigFile) throws Exception {
if (Files.notExists(jsonConfigFile)) throw new IOException("File " + jsonConfigFile + " doesn't exist.");
ObjectMapper mapper = new ObjectMapper();
ObjectNode configData;
try (var in = Files.newInputStream(jsonConfigFile)) {
configData = mapper.readValue(in, ObjectNode.class);
}
ArrayNode serversArray = configData.withArray("servers");
List<ServerBot> bots = new ArrayList<>(serversArray.size());
ServerStatusFetcher serverStatusFetcher = new ServerStatusFetcher();
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();
JDABuilder builder = JDABuilder.create(token, Collections.emptyList());
builder.disableCache(CacheFlag.ACTIVITY, CacheFlag.VOICE_STATE);
JDA jda = builder.build();
bots.add(new ServerBot(jda, serverIp, channelId, serverStatusFetcher));
}
return bots;
}
@Override
public void run() {
try {
jda.awaitReady();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while awaiting ready status.", e);
}
while (true) {
try {
ServerStatus status = serverStatusFetcher.fetch(serverIp);
displayStatus(status);
} catch (IOException e) {
handleError(e);
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace(System.err);
Thread.currentThread().interrupt();
}
}
}
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);
boolean shouldUpdate = onlineStatus != newOnlineStatus ||
activity == null ||
!activity.getName().equals(newActivity.getName());
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 Set<String> getPlayersLeft(Set<String> current) {
Set<String> set = new HashSet<>(lastPlayerNames);
set.removeAll(current);
return set;
}
private void handleError(IOException e) {
if (jda.getPresence().getStatus() != OnlineStatus.DO_NOT_DISTURB) {
jda.getPresence().setStatus(OnlineStatus.DO_NOT_DISTURB);
jda.getPresence().setActivity(Activity.customStatus("Error."));
}
e.printStackTrace(System.err);
lastPlayerCount = -1;
lastPlayerNames.clear();
}
}

View File

@ -0,0 +1,9 @@
package com.andrewlalis.mc_status_bot;
import java.util.Set;
public record ServerStatus(
int playersOnline,
int maxPlayers,
Set<String> playerNames
) {}

View File

@ -0,0 +1,48 @@
package com.andrewlalis.mc_status_bot;
import com.fasterxml.jackson.databind.JsonNode;
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;
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());
}
return new ServerStatus(
data.get("players").get("online").asInt(),
data.get("players").get("max").asInt(),
playerNames
);
} catch (IOException | InterruptedException e) {
throw new IOException("Failed to get server status.", e);
}
}
}