Added starter implementation.
This commit is contained in:
parent
cec1853d73
commit
4efc1002a9
|
@ -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
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.andrewlalis.mc_status_bot;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record ServerStatus(
|
||||||
|
int playersOnline,
|
||||||
|
int maxPlayers,
|
||||||
|
Set<String> playerNames
|
||||||
|
) {}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue