diff --git a/README.md b/README.md index c5d19a3..8716e13 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,22 @@ This bot was created for the Minecraft Server ShantyTown (shantytown.eu) to prov Features are focused on the Minecraft server, so this bot won't work properly on other servers. For this reason the sourcecode is public, so you can steal the code and modify it for your own server ;) . + +* [ShantyBot](#shantybot) + * [Permissions](#permissions) + * [General Permissions](#general-permissions) + * [Activities regarding permissions](#activities-regarding-permissions) + * [Commands](#commands) + + ## Permissions The bot needs the following permissions to work properly: +### General Permissions + +The bot will ask for these permissions: + | Permission | Usage | |----------------------|-----------------------------------------------------------------------| | Add Reactions | Currently no usages - potential for upcoming features | @@ -23,3 +35,18 @@ The bot needs the following permissions to work properly: | Speak | Required for the music bot | | Use Slash Commands | Required for the music bot and other features | +### Activities regarding permissions + +With these permissions the bot has access to following activities: + +![](https://private-user-images.githubusercontent.com/97811064/355839024-aa4e9f31-4852-4d71-b57c-fc3d3a416472.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjMwMzgxNTMsIm5iZiI6MTcyMzAzNzg1MywicGF0aCI6Ii85NzgxMTA2NC8zNTU4MzkwMjQtYWE0ZTlmMzEtNDg1Mi00ZDcxLWI1N2MtZmMzZDNhNDE2NDcyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODA3VDEzMzczM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWUwMmFiMTU5ODdlOThhYTFlY2U3YjgzOGM1ZTJkZWQxMWMxNDZiNjk0YjBiNDIzYjgyNzNiNDA4ZGMzZWNhOGMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.QGpqYJL29ol7zK5hJ_FRYgvAkj1T7mwmYmmNUiGxFo4) + +## Commands + +The bot provides the following slash commands: + +| Command | Description | +|----------------------------------|-----------------------------------------------------------------------| +| `/löschen` `amount` | Deletes the last \ messages in the current channel | +| `/play` `search query` or `link` | Searches for a song on YouTube or via link (YouTube, Soundcloud, ...) | +| `/version` | Shows the current version of the bot | diff --git a/pom.xml b/pom.xml index 855cd3e..c26e251 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ de.rettichlp shantybot - 1.1.4 + 1.2.0 jar ShantyBot @@ -40,6 +40,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-webflux + @@ -49,6 +53,13 @@ provided + + + com.google.code.gson + gson + 2.11.0 + + net.dv8tion diff --git a/src/main/java/de/rettichlp/shantybot/ShantyBot.java b/src/main/java/de/rettichlp/shantybot/ShantyBot.java index 85d3f71..d4bbb8e 100644 --- a/src/main/java/de/rettichlp/shantybot/ShantyBot.java +++ b/src/main/java/de/rettichlp/shantybot/ShantyBot.java @@ -5,9 +5,13 @@ import de.rettichlp.shantybot.buttons.ResumeButton; import de.rettichlp.shantybot.buttons.SkipButton; import de.rettichlp.shantybot.buttons.StopButton; +import de.rettichlp.shantybot.commands.CommandsCommand; import de.rettichlp.shantybot.commands.DeleteMessageCommand; -import de.rettichlp.shantybot.commands.MusicPlayCommand; +import de.rettichlp.shantybot.commands.IpCommand; +import de.rettichlp.shantybot.commands.MusicCommand; +import de.rettichlp.shantybot.commands.PlayersCommand; import de.rettichlp.shantybot.commands.VersionCommand; +import de.rettichlp.shantybot.common.api.API; import de.rettichlp.shantybot.common.configuration.DiscordBotProperties; import de.rettichlp.shantybot.common.lavaplayer.AudioPlayerManager; import de.rettichlp.shantybot.listeners.GuildMemberListener; @@ -41,6 +45,7 @@ public class ShantyBot implements WebMvcConfigurer { public static JDA discordBot; public static DiscordBotProperties discordBotProperties; public static AudioPlayerManager audioPlayerManager; + public static API api; public static void main(String[] args) throws InterruptedException { ConfigurableApplicationContext context = run(ShantyBot.class, args); @@ -53,6 +58,8 @@ public static void main(String[] args) throws InterruptedException { getRuntime().addShutdownHook(new Thread(() -> ofNullable(discordBot).ifPresent(JDA::shutdown))); log.info("Discord bot started in {}ms", currentTimeMillis() - discordBotStartTime); + + api = new API(); } private static void startDiscordBot() throws InterruptedException { @@ -61,8 +68,11 @@ private static void startDiscordBot() throws InterruptedException { .disableCache(MEMBER_OVERRIDES) // Disable parts of the cache .enableIntents(MESSAGE_CONTENT, GUILD_MEMBERS, GUILD_MESSAGES, GUILD_VOICE_STATES) .addEventListeners( + new CommandsCommand("befehle"), new DeleteMessageCommand("löschen"), - new MusicPlayCommand("play"), + new IpCommand("ip"), + new MusicCommand("musik"), + new PlayersCommand("spieler"), new VersionCommand("version") ) .addEventListeners( @@ -79,12 +89,16 @@ private static void startDiscordBot() throws InterruptedException { .build().awaitReady(); discordBot.getGuilds().forEach(guild -> guild.updateCommands().addCommands( - slash("play", "Lässt den Bot Deinen Channel betreten und die angegebene Musik spielen") - .addOption(STRING, "link", "Link oder Name des Songs", true), - slash("version", "Zeigt die aktuelle Version des ShantyBots"), slash("löschen", "Löscht die angegebene Menge an Nachrichten (optional eines bestimmten Nutzers)") .addOption(INTEGER, "anzahl", "Anzahl der Nachrichten, die gelöscht werden sollen", true) - .setDefaultPermissions(enabledFor(MESSAGE_MANAGE)) + .setDefaultPermissions(enabledFor(MESSAGE_MANAGE)), + + slash("befehle", "Zeigt alle verfügbaren Befehle des ShantyBots"), + slash("ip", "Zeigt die IP, Version und zusätzliche Informationen über den Minecraft Server"), + slash("musik", "Lässt den Bot Deinen Channel betreten und die angegebene Musik spielen") + .addOption(STRING, "link", "Link oder Name des Songs", true), + slash("spieler", "Zeigt die Anzahl der Spieler die gerade auf dem Minecraft Server sind"), + slash("version", "Zeigt die aktuelle Version des ShantyBots") ).queue()); audioPlayerManager = new AudioPlayerManager(); diff --git a/src/main/java/de/rettichlp/shantybot/commands/CommandsCommand.java b/src/main/java/de/rettichlp/shantybot/commands/CommandsCommand.java new file mode 100644 index 0000000..c7eca28 --- /dev/null +++ b/src/main/java/de/rettichlp/shantybot/commands/CommandsCommand.java @@ -0,0 +1,31 @@ +package de.rettichlp.shantybot.commands; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.SelfUser; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import static de.rettichlp.shantybot.ShantyBot.discordBot; +import static java.util.Objects.requireNonNull; + +public class CommandsCommand extends CommandBase { + + public CommandsCommand(String name) { + super(name); + } + + @Override + public void onCommand(SlashCommandInteractionEvent event) { + SelfUser botUser = discordBot.getSelfUser(); + EmbedBuilder embedBuilder = new EmbedBuilder() + .setTitle("ShantyBot Befehle") + .setAuthor(botUser.getName(), null, botUser.getAvatarUrl()); + + requireNonNull(event.getGuild()).retrieveCommands().queue(commands -> { + commands.stream() + .filter(command -> command.getApplicationId().equals(botUser.getId())) + .forEach(command -> embedBuilder.addField("/" + command.getName(), command.getDescription(), false)); + + event.replyEmbeds(embedBuilder.build()).setEphemeral(true).queue(); + }); + } +} diff --git a/src/main/java/de/rettichlp/shantybot/commands/IpCommand.java b/src/main/java/de/rettichlp/shantybot/commands/IpCommand.java new file mode 100644 index 0000000..f06dd24 --- /dev/null +++ b/src/main/java/de/rettichlp/shantybot/commands/IpCommand.java @@ -0,0 +1,32 @@ +package de.rettichlp.shantybot.commands; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import static de.rettichlp.shantybot.ShantyBot.api; +import static de.rettichlp.shantybot.common.services.UtilService.sendSelfDeletingMessage; +import static java.util.Optional.ofNullable; + +public class IpCommand extends CommandBase { + + public IpCommand(String name) { + super(name); + } + + @Override + public void onCommand(SlashCommandInteractionEvent event) { + if (api.apiNotReachable()) { + sendSelfDeletingMessage(event, "Die [API](https://api.mcsrvstat.us/3/shantytown.eu) ist aktuell nicht erreichbar. Bitte versuche es später erneut."); + return; + } + + String version = ofNullable(api.getVersion()) + .map(s -> " und ist aktuell auf der Version **" + s + "**.") + .orElse("."); + + String maintenance = api.isMaintenance() ? "\n⚠️ Aktuell sind Wartungsarbeiten!" : ""; + + String offline = ofNullable(api.isOffline()).map(b -> b ? "\n⛔ Der Server ist aktuell offline!" : "").orElse(""); + + event.reply("ShantyTown hat die IP **[shantytown.eu](https://shantytown.eu/)**%s%s%s" .formatted(version, maintenance, offline)).queue(); + } +} diff --git a/src/main/java/de/rettichlp/shantybot/commands/MusicPlayCommand.java b/src/main/java/de/rettichlp/shantybot/commands/MusicCommand.java similarity index 95% rename from src/main/java/de/rettichlp/shantybot/commands/MusicPlayCommand.java rename to src/main/java/de/rettichlp/shantybot/commands/MusicCommand.java index 08ea015..c817591 100644 --- a/src/main/java/de/rettichlp/shantybot/commands/MusicPlayCommand.java +++ b/src/main/java/de/rettichlp/shantybot/commands/MusicCommand.java @@ -14,9 +14,9 @@ import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; -public class MusicPlayCommand extends CommandBase { +public class MusicCommand extends CommandBase { - public MusicPlayCommand(String name) { + public MusicCommand(String name) { super(name); } diff --git a/src/main/java/de/rettichlp/shantybot/commands/PlayersCommand.java b/src/main/java/de/rettichlp/shantybot/commands/PlayersCommand.java new file mode 100644 index 0000000..c82dd6d --- /dev/null +++ b/src/main/java/de/rettichlp/shantybot/commands/PlayersCommand.java @@ -0,0 +1,32 @@ +package de.rettichlp.shantybot.commands; + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import static de.rettichlp.shantybot.ShantyBot.api; +import static de.rettichlp.shantybot.common.services.UtilService.sendSelfDeletingMessage; +import static java.util.Objects.nonNull; +import static java.util.Optional.ofNullable; + +public class PlayersCommand extends CommandBase { + + public PlayersCommand(String name) { + super(name); + } + + @Override + public void onCommand(SlashCommandInteractionEvent event) { + if (api.apiNotReachable()) { + sendSelfDeletingMessage(event, "Die [API](https://api.mcsrvstat.us/3/shantytown.eu) ist aktuell nicht erreichbar. Bitte versuche es später erneut."); + return; + } + + String maxPlayers = ofNullable(api.getMaxPlayers()) + .map(s -> " von **" + s + "**") + .orElse(""); + + String areString = api.getOnlinePlayers() == 1 ? "ist" : "sind"; + String playerString = (nonNull(api.getMaxPlayers()) && api.getMaxPlayers() != 1) ? "Spielern" : "Spieler"; + + event.reply("Aktuell %s **%d**%s %s online." .formatted(areString, api.getOnlinePlayers(), maxPlayers, playerString)).queue(); + } +} diff --git a/src/main/java/de/rettichlp/shantybot/common/api/API.java b/src/main/java/de/rettichlp/shantybot/common/api/API.java new file mode 100644 index 0000000..d17afbf --- /dev/null +++ b/src/main/java/de/rettichlp/shantybot/common/api/API.java @@ -0,0 +1,156 @@ +package de.rettichlp.shantybot.common.api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import static com.google.gson.JsonParser.parseString; +import static java.util.Objects.isNull; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static reactor.core.publisher.Mono.just; + +@Log4j2 +public class API { + + /** + * Checks if the API is online + * + * @return true if the API is online, false otherwise + * + * @see #getJsonObject() + */ + public boolean apiNotReachable() { + return isNull(getJsonObject()); + } + + /** + * Returns the version of the server + * + * @return the version of the server + * + * @see #getJsonObject() + */ + @Nullable + public String getVersion() { + return ofNullable(getJsonObject()) + .map(jsonObject -> jsonObject.get("version")) + .map(JsonElement::getAsString) + .orElse(null); + } + + /** + * Checks if the server is in maintenance mode by checking if the MOTD contains the word "Wartungsarbeiten" + * + * @return true if the server is in maintenance mode, false otherwise + * + * @see #getJsonObject() + */ + public boolean isMaintenance() { + return ofNullable(getJsonObject()) + .map(jsonObject -> jsonObject.get("motd")) + .map(JsonElement::getAsJsonObject) + .map(jsonObject -> jsonObject.get("clean")) + .map(jsonElement -> jsonElement.getAsString().toLowerCase().contains("wartungsarbeiten")) + .orElse(false); + } + + /** + * Checks if the server is offline + * + * @return true if the server is offline, false otherwise + * + * @see #getJsonObject() + */ + @Nullable + public Boolean isOffline() { + return ofNullable(getJsonObject()) + .map(jsonObject -> !jsonObject.get("online").getAsBoolean()) + .orElse(null); + } + + /** + * Returns the amount of online players + * + * @return the amount of online players + * + * @see #getPlayerJsonObject() + */ + public int getOnlinePlayers() { + return ofNullable(getPlayerJsonObject()) + .map(jsonObject -> jsonObject.get("online").getAsInt()) + .orElse(0); + } + + /** + * Returns the maximum amount of players + * + * @return the maximum amount of players or null if the player JSON object is null + * + * @see #getPlayerJsonObject() + */ + @Nullable + public Integer getMaxPlayers() { + JsonObject playerJsonObject = getPlayerJsonObject(); + return isNull(playerJsonObject) ? null : playerJsonObject.get("max").getAsInt(); + } + + /** + * Returns the player object from the JSON response + * + * @return the player object from the JSON response or null if the JSON object is null + * + * @see #getJsonObject() + */ + @Nullable + private JsonObject getPlayerJsonObject() { + return ofNullable(getJsonObject()) + .map(jsonObject -> jsonObject.get("players")) + .map(JsonElement::getAsJsonObject) + .orElse(null); + } + + /** + * Returns the JSON object from the API + * + * @return the JSON object from the API or null if the response is not successful + */ + @Nullable + private JsonObject getJsonObject() { + ResponseEntity responseEntity = sendRequest(); + return responseEntity.getStatusCode().is2xxSuccessful() + ? parseString(requireNonNull(responseEntity.getBody())).getAsJsonObject() + : null; + } + + /** + * Sends a request to the API + * + * @return the response entity + */ + private ResponseEntity sendRequest() { + return WebClient.builder() + .baseUrl("https://api.mcsrvstat.us/3/shantytown.eu") + .build() + .get() + .retrieve() + .bodyToMono(String.class) + .map(ResponseEntity::ok) + .onErrorResume(WebClientResponseException.class, ex -> { + String responseBodyAsString = ex.getResponseBodyAsString(); + + HttpStatusCode statusCode = ex.getStatusCode(); + if (!statusCode.is4xxClientError()) { + log.error("Request failed with code {}: {}", statusCode, responseBodyAsString); + } + + return just(ResponseEntity.status(statusCode).body(responseBodyAsString)); + }) + .block(); + } +} diff --git a/src/main/java/de/rettichlp/shantybot/common/lavaplayer/GuildMusicManager.java b/src/main/java/de/rettichlp/shantybot/common/lavaplayer/GuildMusicManager.java index 11a2ce4..f1cdf57 100644 --- a/src/main/java/de/rettichlp/shantybot/common/lavaplayer/GuildMusicManager.java +++ b/src/main/java/de/rettichlp/shantybot/common/lavaplayer/GuildMusicManager.java @@ -22,6 +22,7 @@ import static de.rettichlp.shantybot.common.services.UtilService.millisecondsToMMSS; import static java.nio.ByteBuffer.wrap; +import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import static net.dv8tion.jda.api.interactions.components.buttons.Button.danger; import static net.dv8tion.jda.api.interactions.components.buttons.Button.primary; @@ -47,7 +48,9 @@ public GuildMusicManager(AudioPlayerManager manager) { public void queue(Channel channel, AudioTrack audioTrack) { this.musicTextChannel = (TextChannel) channel; this.queue.add(audioTrack); - this.audioPlayer.startTrack(this.queue.poll(), true); + if (isNull(this.audioPlayer.getPlayingTrack())) { + this.audioPlayer.startTrack(this.queue.poll(), false); + } } public void nextTrack() { diff --git a/src/main/java/de/rettichlp/shantybot/common/services/RoleSyncService.java b/src/main/java/de/rettichlp/shantybot/common/services/RoleSyncService.java index f73df6c..537e464 100644 --- a/src/main/java/de/rettichlp/shantybot/common/services/RoleSyncService.java +++ b/src/main/java/de/rettichlp/shantybot/common/services/RoleSyncService.java @@ -36,7 +36,7 @@ public void deleteOldLogEntries() { guild.addRoleToMember(member, memberRole).queue(success -> { log.info("Discord role synchronising: Add role {} to member {}", memberRole.getName(), member.getEffectiveName()); future.complete(null); - }, future::completeExceptionally); + }, future::completeExceptionally); return future; }) .toList();