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