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