impl server watcher for minecraft servers
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 1m27s

This commit is contained in:
Lee 2024-07-06 06:21:05 +01:00
parent ee7e8b64c5
commit 514f4757a9
11 changed files with 482 additions and 12 deletions

@ -2,11 +2,17 @@ package cc.fascinated.bat.features.minecraft;
import cc.fascinated.bat.features.Feature; import cc.fascinated.bat.features.Feature;
import cc.fascinated.bat.features.FeatureProfile; import cc.fascinated.bat.features.FeatureProfile;
import cc.fascinated.bat.features.minecraft.command.MinecraftCommand; import cc.fascinated.bat.features.minecraft.command.minecraft.MinecraftCommand;
import cc.fascinated.bat.features.minecraft.command.serverwatcher.ServerWatcherCommand;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.service.CommandService; import cc.fascinated.bat.service.CommandService;
import cc.fascinated.bat.service.DiscordService;
import cc.fascinated.bat.service.GuildService;
import lombok.NonNull; import lombok.NonNull;
import net.dv8tion.jda.api.entities.Guild;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@ -14,10 +20,25 @@ import org.springframework.stereotype.Component;
*/ */
@Component @Component
public class MinecraftFeature extends Feature { public class MinecraftFeature extends Feature {
private final GuildService guildService;
@Autowired @Autowired
public MinecraftFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService) { public MinecraftFeature(@NonNull ApplicationContext context, @NonNull CommandService commandService, @NonNull GuildService guildService) {
super("Minecraft", FeatureProfile.FeatureState.DISABLED, true); super("Minecraft", FeatureProfile.FeatureState.DISABLED, true);
this.guildService = guildService;
super.registerCommand(commandService, context.getBean(MinecraftCommand.class)); super.registerCommand(commandService, context.getBean(MinecraftCommand.class));
super.registerCommand(commandService, context.getBean(ServerWatcherCommand.class));
}
/**
* Check servers every minute
*/
@Scheduled(cron = "0 * * * * *")
public void checkServers() {
for (Guild guild : DiscordService.JDA.getGuilds()) {
BatGuild batGuild = guildService.getGuild(guild.getId());
batGuild.getMinecraftProfile().checkServers();
}
} }
} }

@ -0,0 +1,143 @@
package cc.fascinated.bat.features.minecraft;
import cc.fascinated.bat.Emojis;
import cc.fascinated.bat.common.*;
import com.google.gson.Gson;
import lombok.Getter;
import net.dv8tion.jda.api.EmbedBuilder;
import org.bson.Document;
import xyz.mcutils.McUtilsAPI;
import xyz.mcutils.models.server.MinecraftServer;
import xyz.mcutils.models.server.ServerPlatform;
import java.util.ArrayList;
import java.util.List;
/**
* @author Fascinated (fascinated7)
*/
@Getter
public class MinecraftProfile extends Serializable {
/**
* The servers that are getting their status watched
*/
private final List<ServerWatcher> serverWatchers = new ArrayList<>();
/**
* Adds a server watcher
*
* @param serverWatcher - The server watcher to add
*/
public void addServerWatcher(ServerWatcher serverWatcher) {
serverWatchers.add(serverWatcher);
}
/**
* Removes a server watcher
*
* @param serverWatcher - The server watcher to remove
*/
public void removeServerWatcher(ServerWatcher serverWatcher) {
serverWatchers.remove(serverWatcher);
}
/**
* Gets a server watcher by hostname
*
* @param hostname the hostname of the server
* @param platform the platform of the server
* @return the server watcher
*/
public ServerWatcher getServerWatcher(String hostname, ServerPlatform platform) {
for (ServerWatcher serverWatcher : serverWatchers) {
if (serverWatcher.getHostname().equalsIgnoreCase(hostname) && serverWatcher.getPlatform() == platform) {
return serverWatcher;
}
}
return null;
}
public void checkServers() {
for (ServerWatcher server : serverWatchers) {
int platformDefaultPort = server.getPlatform() == ServerPlatform.JAVA ? 25565 : 19132;
String hostname = server.getHostname() + (server.getPort() != platformDefaultPort ? ":" + server.getPort() : "");
boolean isOnline = true;
MinecraftServer minecraftServer = null;
switch (server.getPlatform()) {
case JAVA -> {
try {
minecraftServer = McUtilsAPI.getJavaServer(hostname);
} catch (Exception e) {
isOnline = false;
}
}
case BEDROCK -> {
try {
minecraftServer = McUtilsAPI.getBedrockServer(hostname);
} catch (Exception e) {
isOnline = false;
}
}
}
if (isOnline == server.isLastState()) {
return;
}
server.setLastState(isOnline);
EmbedBuilder embedBuilder = isOnline ? EmbedUtils.successEmbed() : EmbedUtils.errorEmbed();
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Server Watcher");
description.appendLine("%s %s server `%s` is now **%s**".formatted(
isOnline ? Emojis.CHECK_MARK_EMOJI : Emojis.CROSS_MARK_EMOJI,
EnumUtils.getEnumName(server.getPlatform()),
hostname,
isOnline ? "online" : "offline"
), false);
if (minecraftServer != null) {
description.appendLine("Players: `%s/%s`".formatted(
NumberFormatter.simpleFormat(minecraftServer.getPlayers().getOnline()),
NumberFormatter.simpleFormat(minecraftServer.getPlayers().getMax())
), true);
}
server.getChannel().sendMessageEmbeds(embedBuilder
.setDescription(description.build())
.setThumbnail("https://api.mcutils.xyz/server/icon/%s".formatted(hostname))
.build()).queue();
}
}
@Override
public void load(Document document, Gson gson) {
for (Document watcherDocument : document.getList("serverWatchers", Document.class, new ArrayList<>())) {
serverWatchers.add(new ServerWatcher(
watcherDocument.getString("hostname"),
watcherDocument.getInteger("port"),
ServerPlatform.valueOf(watcherDocument.getString("platform")),
watcherDocument.getString("channelId"),
watcherDocument.getBoolean("lastState", false)
));
}
}
@Override
public Document serialize(Gson gson) {
Document document = new Document();
List<Document> watcherDocuments = new ArrayList<>();
for (ServerWatcher serverWatcher : serverWatchers) {
Document watcherDocument = new Document();
watcherDocument.append("hostname", serverWatcher.getHostname());
watcherDocument.append("port", serverWatcher.getPort());
watcherDocument.append("platform", serverWatcher.getPlatform().name());
watcherDocument.append("channelId", serverWatcher.getChannelId());
watcherDocument.append("lastState", serverWatcher.isLastState());
watcherDocuments.add(watcherDocument);
}
document.append("serverWatchers", watcherDocuments);
return document;
}
@Override
public void reset() {
serverWatchers.clear();
}
}

@ -0,0 +1,54 @@
package cc.fascinated.bat.features.minecraft;
import cc.fascinated.bat.common.ChannelUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import xyz.mcutils.models.server.ServerPlatform;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@Setter
public class ServerWatcher {
/**
* The hostname of the server
*/
private final String hostname;
/**
* The port of the server
*/
private final int port;
/**
* The platform of the server
*/
private final ServerPlatform platform;
/**
* The channel id to send notifications in
*/
private final String channelId;
/**
* The last state of the server
* <p>
* true = online
* false = offline
* </p>
*/
private boolean lastState;
/**
* Gets the channel
*
* @return - The channel
*/
public TextChannel getChannel() {
return ChannelUtils.getTextChannel(channelId);
}
}

@ -1,4 +1,4 @@
package cc.fascinated.bat.features.minecraft.command; package cc.fascinated.bat.features.minecraft.command.minecraft;
import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -26,8 +26,8 @@ import xyz.mcutils.models.player.Skin;
name = "lookup-player", name = "lookup-player",
description = "Lookup a Minecraft player" description = "Lookup a Minecraft player"
) )
public class LookupPlayerCommand extends BatCommand { public class LookupPlayerSubCommand extends BatCommand {
public LookupPlayerCommand() { public LookupPlayerSubCommand() {
super.addOptions( super.addOptions(
new OptionData(OptionType.STRING, "player", "The player to lookup", true) new OptionData(OptionType.STRING, "player", "The player to lookup", true)
); );

@ -1,4 +1,4 @@
package cc.fascinated.bat.features.minecraft.command; package cc.fascinated.bat.features.minecraft.command.minecraft;
import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -27,8 +27,8 @@ import xyz.mcutils.models.server.MinecraftServer;
name = "lookup-server", name = "lookup-server",
description = "Lookup a Minecraft server" description = "Lookup a Minecraft server"
) )
public class LookupServerCommand extends BatCommand { public class LookupServerSubCommand extends BatCommand {
public LookupServerCommand() { public LookupServerSubCommand() {
super.addOptions( super.addOptions(
new OptionData(OptionType.STRING, "platform", "The platform of the server to lookup", true) new OptionData(OptionType.STRING, "platform", "The platform of the server to lookup", true)
.addChoice("Java", "java") .addChoice("Java", "java")

@ -1,4 +1,4 @@
package cc.fascinated.bat.features.minecraft.command; package cc.fascinated.bat.features.minecraft.command.minecraft;
import cc.fascinated.bat.command.BatCommand; import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo; import cc.fascinated.bat.command.CommandInfo;
@ -13,14 +13,15 @@ import org.springframework.stereotype.Component;
@Component @Component
@CommandInfo( @CommandInfo(
name = "minecraft", name = "minecraft",
description = "Minecraft related commands" description = "Minecraft related commands",
userInstall = true
) )
public class MinecraftCommand extends BatCommand { public class MinecraftCommand extends BatCommand {
@Autowired @Autowired
public MinecraftCommand(@NonNull ApplicationContext context) { public MinecraftCommand(@NonNull ApplicationContext context) {
super.addSubCommands( super.addSubCommands(
context.getBean(LookupPlayerCommand.class), context.getBean(LookupPlayerSubCommand.class),
context.getBean(LookupServerCommand.class) context.getBean(LookupServerSubCommand.class)
); );
} }
} }

@ -0,0 +1,100 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.minecraft.ServerWatcher;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.stereotype.Component;
import xyz.mcutils.McUtilsAPI;
import xyz.mcutils.models.server.MinecraftServer;
import xyz.mcutils.models.server.ServerPlatform;
/**
* @author Fascinated (fascinated7)
*/
@Component("minecraft-server-watcher.add:sub")
@CommandInfo(
name = "add",
description = "Add a server to the server watcher"
)
public class AddSubCommand extends BatCommand {
public AddSubCommand() {
super.addOptions(
new OptionData(OptionType.CHANNEL, "channel", "The channel to send the server watcher notifications", true),
new OptionData(OptionType.STRING, "platform", "The platform of the server to lookup", true)
.addChoice("Java", "java")
.addChoice("Bedrock", "bedrock"),
new OptionData(OptionType.STRING, "host", "The host/ip of the server to add", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping platformOption = event.getOption("platform");
OptionMapping hostOption = event.getOption("host");
OptionMapping channelOption = event.getOption("channel");
if (platformOption == null || hostOption == null || channelOption == null) {
return;
}
String platform = platformOption.getAsString();
String host = hostOption.getAsString();
GuildChannelUnion channelUnion = channelOption.getAsChannel();
TextChannel textChannel = channelUnion.asTextChannel();
MinecraftServer server;
try {
if (platform.equalsIgnoreCase("java")) {
server = McUtilsAPI.getJavaServer(host);
} else {
server = McUtilsAPI.getBedrockServer(host);
}
} catch (Exception ex) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The server `%s` is invalid or offline".formatted(host))
.build()).queue();
return;
}
MinecraftProfile profile = guild.getMinecraftProfile();
if (profile.getServerWatchers().size() >= 10) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("You can only have a maximum of `10` server watchers")
.build()).queue();
return;
}
if (profile.getServerWatcher(host, ServerPlatform.valueOf(platform.toUpperCase())) != null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The server `%s` is already being watched".formatted(host))
.build()).queue();
return;
}
profile.addServerWatcher(new ServerWatcher(
server.getHostname(),
server.getPort(),
ServerPlatform.valueOf(platform.toUpperCase()),
textChannel.getId(),
false
));
profile.checkServers(); // Force check the servers
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("Setup the server watcher for `%s` in %s".formatted(
server.getHostname(),
textChannel.getAsMention()
)).build()).queue();
}
}

@ -0,0 +1,50 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedDescriptionBuilder;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.common.EnumUtils;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.minecraft.ServerWatcher;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import org.springframework.stereotype.Component;
import xyz.mcutils.models.server.ServerPlatform;
/**
* @author Fascinated (fascinated7)
*/
@Component("minecraft-server-watcher.list:sub")
@CommandInfo(
name = "list",
description = "Shows a list of all the servers being watched"
)
public class ListSubCommand extends BatCommand {
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
MinecraftProfile profile = guild.getMinecraftProfile();
EmbedDescriptionBuilder description = new EmbedDescriptionBuilder("Server Watcher");
description.appendLine("Here is a list of all the servers being watched", false);
description.emptyLine();
for (ServerWatcher server : profile.getServerWatchers()) {
int platformDefaultPort = server.getPlatform() == ServerPlatform.JAVA ? 25565 : 19132;
String hostname = server.getHostname() + (server.getPort() != platformDefaultPort ? ":" + server.getPort() : "");
description.appendLine("`%s` (%s) - %s".formatted(
hostname,
EnumUtils.getEnumName(server.getPlatform()),
server.getChannel().getAsMention()
), true);
}
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription(description.build())
.build()).queue();
}
}

@ -0,0 +1,62 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import cc.fascinated.bat.common.EmbedUtils;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.minecraft.ServerWatcher;
import cc.fascinated.bat.model.BatGuild;
import cc.fascinated.bat.model.BatUser;
import lombok.NonNull;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.stereotype.Component;
import xyz.mcutils.models.server.ServerPlatform;
/**
* @author Fascinated (fascinated7)
*/
@Component("minecraft-server-watcher.remove:sub")
@CommandInfo(
name = "remove",
description = "Remove a server from the server watcher"
)
public class RemoveSubCommand extends BatCommand {
public RemoveSubCommand() {
super.addOptions(
new OptionData(OptionType.STRING, "platform", "The platform of the server to lookup", true)
.addChoice("Java", "java")
.addChoice("Bedrock", "bedrock"),
new OptionData(OptionType.STRING, "host", "The host/ip of the server to remove", true)
);
}
@Override
public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction event) {
OptionMapping platformOption = event.getOption("platform");
OptionMapping hostOption = event.getOption("host");
if (platformOption == null || hostOption == null) {
return;
}
String platform = platformOption.getAsString();
String host = hostOption.getAsString();
MinecraftProfile profile = guild.getMinecraftProfile();
ServerWatcher serverWatcher = profile.getServerWatcher(host, ServerPlatform.valueOf(platform.toUpperCase()));
if (serverWatcher == null) {
event.replyEmbeds(EmbedUtils.errorEmbed()
.setDescription("The server `%s` is not being watched".formatted(host))
.build()).queue();
return;
}
profile.removeServerWatcher(serverWatcher);
event.replyEmbeds(EmbedUtils.successEmbed()
.setDescription("The server `%s` has been removed from the server watcher".formatted(host))
.build()).queue();
}
}

@ -0,0 +1,29 @@
package cc.fascinated.bat.features.minecraft.command.serverwatcher;
import cc.fascinated.bat.command.BatCommand;
import cc.fascinated.bat.command.CommandInfo;
import lombok.NonNull;
import net.dv8tion.jda.api.Permission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author Fascinated (fascinated7)
*/
@Component
@CommandInfo(
name = "minecraft-server-watcher",
description = "Configure the server watcher for Minecraft servers",
requiredPermissions = Permission.MANAGE_SERVER
)
public class ServerWatcherCommand extends BatCommand {
@Autowired
public ServerWatcherCommand(@NonNull ApplicationContext context) {
super.addSubCommands(
context.getBean(AddSubCommand.class),
context.getBean(RemoveSubCommand.class),
context.getBean(ListSubCommand.class)
);
}
}

@ -9,6 +9,7 @@ import cc.fascinated.bat.features.counter.CounterProfile;
import cc.fascinated.bat.features.leveling.LevelingFeature; import cc.fascinated.bat.features.leveling.LevelingFeature;
import cc.fascinated.bat.features.leveling.LevelingProfile; import cc.fascinated.bat.features.leveling.LevelingProfile;
import cc.fascinated.bat.features.logging.LogProfile; import cc.fascinated.bat.features.logging.LogProfile;
import cc.fascinated.bat.features.minecraft.MinecraftProfile;
import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile; import cc.fascinated.bat.features.namehistory.profile.guild.NameHistoryProfile;
import cc.fascinated.bat.features.reminder.ReminderProfile; import cc.fascinated.bat.features.reminder.ReminderProfile;
import cc.fascinated.bat.features.welcomer.WelcomerProfile; import cc.fascinated.bat.features.welcomer.WelcomerProfile;
@ -171,6 +172,15 @@ public class BatGuild extends ProfileHolder {
return getProfile(LevelingProfile.class); return getProfile(LevelingProfile.class);
} }
/**
* Gets the minecraft profile
*
* @return the minecraft profile
*/
public MinecraftProfile getMinecraftProfile() {
return getProfile(MinecraftProfile.class);
}
/** /**
* Saves the user * Saves the user
*/ */