diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..428853a --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Compiled class file +*.class +.gradle +build/ +bin/ +target/ +.idea/ +.vscode/ + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..185ae83 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Luminia Discord Bot + +This Discord bot written for Minecraft server Luminia + +> [!IMPORTANT] +> This bot is written for the Luminia server Discord in Java + Kotlin and is currently no longer supported + +## 🛠 Building the JAR File +To build the project from source: +1. Clone the repository: +```bash +git clone https://github.com/MEFRREEX/Luminia-Discord-Bot.git +``` +2. Navigate to the project directory: +```bash +cd Luminia-Discord-Bot +``` +3. Build the JAR file using Gradle: +```bash +gradle build +``` +The compiled artifact will be located in the `bot/build/libs` directory \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000..84ee264 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") version "1.9.21" +} + +kotlin { + jvmToolchain(21) +} + +tasks.withType { + archiveFileName.set("Luminia-Discord-Bot-API-${project.version}.jar") +} + +sourceSets { + main { + java { + srcDirs("src/main/kotlin") + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/Command.kt b/api/src/main/kotlin/com/luminia/discord/api/command/Command.kt new file mode 100644 index 0000000..c3dd3dc --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/Command.kt @@ -0,0 +1,13 @@ +package com.luminia.discord.api.command + +import com.luminia.discord.api.command.data.CommandData +import net.dv8tion.jda.api.events.Event + +abstract class Command { + + abstract fun getCommandData(): CommandData + + abstract fun getCommandType(): CommandType + + abstract fun execute(event: T) +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/CommandManager.java b/api/src/main/kotlin/com/luminia/discord/api/command/CommandManager.java new file mode 100644 index 0000000..3dd6e01 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/CommandManager.java @@ -0,0 +1,22 @@ +package com.luminia.discord.api.command; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent; + +import java.util.Collections; +import java.util.Map; + +public interface CommandManager { + + JDA getJDA(); + + Map>> getCommands(); + + default Command getCommand(CommandType type, String name) { + return this.getCommands().getOrDefault(type, Collections.emptyMap()).get(name); + } + + void register(Command... command); + + void execute(Command command, GenericCommandInteractionEvent event); +} diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/CommandType.java b/api/src/main/kotlin/com/luminia/discord/api/command/CommandType.java new file mode 100644 index 0000000..970424b --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/CommandType.java @@ -0,0 +1,7 @@ +package com.luminia.discord.api.command; + +public enum CommandType { + SLASH, + MESSAGE, + USER +} diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/MessageCommand.kt b/api/src/main/kotlin/com/luminia/discord/api/command/MessageCommand.kt new file mode 100644 index 0000000..867ae51 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/MessageCommand.kt @@ -0,0 +1,10 @@ +package com.luminia.discord.api.command + +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent + +abstract class MessageCommand : Command() { + + override fun getCommandType(): CommandType { + return CommandType.MESSAGE + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/SlashCommand.kt b/api/src/main/kotlin/com/luminia/discord/api/command/SlashCommand.kt new file mode 100644 index 0000000..6e4003d --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/SlashCommand.kt @@ -0,0 +1,10 @@ +package com.luminia.discord.api.command + +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent + +abstract class SlashCommand : Command() { + + override fun getCommandType(): CommandType { + return CommandType.SLASH + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/UserCommand.kt b/api/src/main/kotlin/com/luminia/discord/api/command/UserCommand.kt new file mode 100644 index 0000000..17cf36f --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/UserCommand.kt @@ -0,0 +1,10 @@ +package com.luminia.discord.api.command + +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent + +abstract class UserCommand : Command() { + + override fun getCommandType(): CommandType { + return CommandType.USER + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/data/CommandData.kt b/api/src/main/kotlin/com/luminia/discord/api/command/data/CommandData.kt new file mode 100644 index 0000000..12e5c32 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/data/CommandData.kt @@ -0,0 +1,32 @@ +package com.luminia.discord.api.command.data + +import com.luminia.discord.api.command.permission.CommandPermission +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions +import net.dv8tion.jda.api.interactions.commands.build.Commands +import net.dv8tion.jda.api.interactions.commands.build.CommandData as JDACommandData + +interface CommandData { + + fun getJDACommandData(): JDACommandData + + fun getPermission(): CommandPermission + + fun setName(name: String): CommandData + + fun setDefaultPermissions(permission: DefaultMemberPermissions): CommandData + + fun setPermission(permission: CommandPermission): CommandData + + fun setGuildOnly(guildOnly: Boolean): CommandData + + fun setNSFW(nsfw: Boolean): CommandData + + companion object { + + fun slash(name: String, description: String): SlashCommandDataBuilder = SlashCommandDataBuilder(Commands.slash(name, description)) + + fun message(name: String): CommandDataBuilder = CommandDataBuilder(Commands.message(name)) + + fun context(name: String): CommandDataBuilder = CommandDataBuilder(Commands.user(name)) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/data/CommandDataBuilder.kt b/api/src/main/kotlin/com/luminia/discord/api/command/data/CommandDataBuilder.kt new file mode 100644 index 0000000..042a0f6 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/data/CommandDataBuilder.kt @@ -0,0 +1,39 @@ +package com.luminia.discord.api.command.data + +import com.luminia.discord.api.command.permission.CommandPermission +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions +import net.dv8tion.jda.api.interactions.commands.build.CommandData as JDACommandData + +open class CommandDataBuilder(private val jdaCommandData: JDACommandData) : CommandData { + + private var permission: CommandPermission = CommandPermission.DEFAULT + + override fun getJDACommandData(): JDACommandData = jdaCommandData + + override fun getPermission(): CommandPermission = permission + + override fun setName(name: String): CommandDataBuilder { + jdaCommandData.setName(name) + return this + } + + override fun setDefaultPermissions(permission: DefaultMemberPermissions): CommandDataBuilder { + jdaCommandData.setDefaultPermissions(permission) + return this + } + + override fun setPermission(permission: CommandPermission): CommandDataBuilder { + this.permission = permission + return this + } + + override fun setGuildOnly(guildOnly: Boolean): CommandDataBuilder { + jdaCommandData.setGuildOnly(guildOnly) + return this + } + + override fun setNSFW(nsfw: Boolean): CommandDataBuilder { + jdaCommandData.setNSFW(nsfw) + return this + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/data/SlashCommandData.kt b/api/src/main/kotlin/com/luminia/discord/api/command/data/SlashCommandData.kt new file mode 100644 index 0000000..98a5c6c --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/data/SlashCommandData.kt @@ -0,0 +1,29 @@ +package com.luminia.discord.api.command.data + +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData + +interface SlashCommandData : CommandData { + + fun setDescription(description: String): SlashCommandData + + fun addOptions(vararg options: OptionData): SlashCommandData + + fun addOptions(options: Collection): SlashCommandData + + fun addOption(type: OptionType, name: String, description: String, required: Boolean, autoComplete: Boolean): SlashCommandData + + fun addOption(type: OptionType, name: String, description: String, required: Boolean): SlashCommandData + + fun addOption(type: OptionType, name: String, description: String): SlashCommandData + + fun addSubcommands(vararg subcommands: SubcommandData): SlashCommandData + + fun addSubcommands(subcommands: Collection): SlashCommandData + + fun addSubcommandGroups(vararg groups: SubcommandGroupData): SlashCommandData + + fun addSubcommandGroups(groups: Collection): SlashCommandData +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/data/SlashCommandDataBuilder.kt b/api/src/main/kotlin/com/luminia/discord/api/command/data/SlashCommandDataBuilder.kt new file mode 100644 index 0000000..ed6f06a --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/data/SlashCommandDataBuilder.kt @@ -0,0 +1,66 @@ +package com.luminia.discord.api.command.data + +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData as JDASlashCommandData + +class SlashCommandDataBuilder(private val jdaCommandData: JDASlashCommandData) : CommandDataBuilder(jdaCommandData), SlashCommandData { + + override fun setDescription(description: String): SlashCommandDataBuilder { + jdaCommandData.setDescription(description) + return this + } + + override fun addOptions(vararg options: OptionData): SlashCommandDataBuilder { + jdaCommandData.addOptions(*options) + return this + } + + override fun addOptions(options: Collection): SlashCommandDataBuilder { + jdaCommandData.addOptions(options) + return this + } + + override fun addOption( + type: OptionType, + name: String, + description: String, + required: Boolean, + autoComplete: Boolean + ): SlashCommandDataBuilder { + jdaCommandData.addOption(type, name, description, required, autoComplete) + return this + } + + override fun addOption(type: OptionType, name: String, description: String, required: Boolean): SlashCommandDataBuilder { + jdaCommandData.addOption(type, name, description, required) + return this + } + + override fun addOption(type: OptionType, name: String, description: String): SlashCommandDataBuilder { + jdaCommandData.addOption(type, name, description) + return this + } + + override fun addSubcommands(vararg subcommands: SubcommandData): SlashCommandDataBuilder { + jdaCommandData.addSubcommands(*subcommands) + return this + } + + override fun addSubcommands(subcommands: Collection): SlashCommandDataBuilder { + jdaCommandData.addSubcommands(subcommands) + return this + } + + override fun addSubcommandGroups(vararg groups: SubcommandGroupData): SlashCommandDataBuilder { + jdaCommandData.addSubcommandGroups(*groups) + return this + } + + override fun addSubcommandGroups(groups: Collection): SlashCommandDataBuilder { + jdaCommandData.addSubcommandGroups(groups) + return this + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/command/permission/CommandPermission.kt b/api/src/main/kotlin/com/luminia/discord/api/command/permission/CommandPermission.kt new file mode 100644 index 0000000..275f118 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/command/permission/CommandPermission.kt @@ -0,0 +1,44 @@ +package com.luminia.discord.api.command.permission + +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.entities.Member +import net.dv8tion.jda.api.entities.Role + +interface CommandPermission { + + fun defaultValue(): Boolean { + return false + } + + fun test(member: Member): Boolean + + companion object { + + fun role(roleId: Long): CommandPermission { + return object : CommandPermission { + override fun test(member: Member): Boolean { + return member.roles.stream().anyMatch { role: Role -> role.idLong == roleId } + } + } + } + + fun permission(permission: Permission?): CommandPermission { + return object : CommandPermission { + override fun test(member: Member): Boolean { + return member.hasPermission(permission) + } + } + } + + val DEFAULT: CommandPermission = object : CommandPermission { + + override fun defaultValue(): Boolean { + return true; + } + + override fun test(member: Member): Boolean { + return true + } + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/handler/EventManager.java b/api/src/main/kotlin/com/luminia/discord/api/handler/EventManager.java new file mode 100644 index 0000000..4fa283c --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/handler/EventManager.java @@ -0,0 +1,16 @@ +package com.luminia.discord.api.handler; + +import net.dv8tion.jda.api.events.GenericEvent; + +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public interface EventManager { + + Map, Set>> getHandlers(); + + void register(Object... listeners); + + void handle(GenericEvent event); +} diff --git a/api/src/main/kotlin/com/luminia/discord/api/handler/SimpleHandler.kt b/api/src/main/kotlin/com/luminia/discord/api/handler/SimpleHandler.kt new file mode 100644 index 0000000..c95e7a9 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/handler/SimpleHandler.kt @@ -0,0 +1,28 @@ +package com.luminia.discord.api.handler + +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.hooks.EventListener +import java.util.function.Consumer + +class SimpleHandler : EventListener { + + companion object { + private val handlers: MutableMap, Consumer> = HashMap() + + @Suppress("unchecked_cast") + @JvmStatic + fun subscribe(eventType: Class, callback: Consumer) { + handlers[eventType] = callback as Consumer + } + + inline fun subscribe(crossinline action: (T) -> Unit) { + subscribe(T::class.java) { event -> action(event) } + } + } + + override fun onEvent(event: GenericEvent) { + val clazz = event.javaClass + val callback = handlers[clazz] + callback?.accept(event) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/handler/Subscribe.java b/api/src/main/kotlin/com/luminia/discord/api/handler/Subscribe.java new file mode 100644 index 0000000..bf5ee43 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/handler/Subscribe.java @@ -0,0 +1,12 @@ +package com.luminia.discord.api.handler; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Subscribe { + +} diff --git a/api/src/main/kotlin/com/luminia/discord/api/logger/BaseLogger.java b/api/src/main/kotlin/com/luminia/discord/api/logger/BaseLogger.java new file mode 100644 index 0000000..f7a49c1 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/logger/BaseLogger.java @@ -0,0 +1,43 @@ +package com.luminia.discord.api.logger; + +import com.luminia.discord.api.utils.TextFormat; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public abstract class BaseLogger { + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS"); + + public void info(String message) { + log(LogLevel.INFO, message); + } + + public void warn(String message) { + log(LogLevel.WARN, message); + } + + public void error(String message) { + error(message, null); + } + + public void error(String message, Throwable throwable) { + log(LogLevel.ERROR, message, throwable); + } + + public void debug(String message) { + log(LogLevel.DEBUG, message); + } + + private void log(LogLevel level, String message) { + log(level, message, null); + } + + private void log(LogLevel level, String message, Throwable throwable) { + String date = DATE_FORMAT.format(new Date()); + System.out.println(TextFormat.MAGENTA + date + TextFormat.RESET + " [" + level + "]" + " " + message); + if (throwable != null) { + throwable.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/luminia/discord/api/logger/LogLevel.java b/api/src/main/kotlin/com/luminia/discord/api/logger/LogLevel.java new file mode 100644 index 0000000..b9befad --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/logger/LogLevel.java @@ -0,0 +1,25 @@ +package com.luminia.discord.api.logger; + +import com.luminia.discord.api.utils.TextFormat; +import lombok.Getter; + +@Getter +public enum LogLevel { + INFO("INFO ", TextFormat.YELLOW), + WARN("WARN ", TextFormat.RED), + ERROR("ERROR", TextFormat.RED), + DEBUG("DEBUG", TextFormat.CYAN); + + private final String name; + private final String color; + + LogLevel(String name, String color) { + this.name = name; + this.color = color; + } + + @Override + public String toString() { + return color + name + TextFormat.RESET; + } +} diff --git a/api/src/main/kotlin/com/luminia/discord/api/utils/TextFormat.java b/api/src/main/kotlin/com/luminia/discord/api/utils/TextFormat.java new file mode 100644 index 0000000..8ecd2e4 --- /dev/null +++ b/api/src/main/kotlin/com/luminia/discord/api/utils/TextFormat.java @@ -0,0 +1,26 @@ +package com.luminia.discord.api.utils; + +public interface TextFormat { + String BLACK = "\u001b[0;30m"; + String RED = "\u001b[0;31m"; + String GREEN = "\u001b[0;92m"; + String YELLOW = "\u001b[0;33m"; + String BLUE = "\u001b[0;34m"; + String MAGENTA = "\u001b[0;35m"; + String CYAN = "\u001b[0;36m"; + String WHITE = "\u001b[0;37m"; + + String BLACK_BACKGROUND = "\u001b[40m"; + String RED_BACKGROUND = "\u001b[41m"; + String GREEN_BACKGROUND = "\u001b[42m"; + String YELLOW_BACKGROUND = "\u001b[43m"; + String BLUE_BACKGROUND = "\u001b[44m"; + String MAGENTA_BACKGROUND = "\u001b[45m"; + String CYAN_BACKGROUND = "\u001b[46m"; + String WHITE_BACKGROUND = "\u001b[47m"; + + String RESET = "\u001b[0m"; + String BOLD = "\u001b[1m"; + String UNDERLINE = "\u001b[4m"; + String REVERSED = "\u001b[7m"; +} \ No newline at end of file diff --git a/bot/build.gradle.kts b/bot/build.gradle.kts new file mode 100644 index 0000000..a414d03 --- /dev/null +++ b/bot/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + kotlin("jvm") version "1.9.21" + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +repositories { + maven("https://jitpack.io") +} + +dependencies { + api(project(":api")) + api(project(":config")) + api("org.yaml:snakeyaml:2.3") + api("com.github.MEFRREEX:JOOQConnector:1.0.1") + api("org.jooq:jooq:3.19.7") +} + +kotlin { + jvmToolchain(21) +} + +tasks.withType { + archiveFileName.set("Luminia-Discord-Bot-${project.version}.jar") + manifest { + attributes["Main-Class"] = "com.luminia.discord.bot.Bootstrap" + } +} + +tasks.build { + dependsOn(tasks.shadowJar) +} + +sourceSets { + main { + java { + srcDirs("src/main/kotlin") + } + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/Bootstrap.java b/bot/src/main/kotlin/com/luminia/discord/bot/Bootstrap.java new file mode 100644 index 0000000..ed99a22 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/Bootstrap.java @@ -0,0 +1,46 @@ +package com.luminia.discord.bot; + +import com.luminia.config.Config; +import com.luminia.discord.bot.logger.Logger; +import com.luminia.discord.bot.utils.ConfigHelper; + +import java.io.File; +import java.util.List; + +public class Bootstrap { + + private static final Logger logger = Logger.getInstance(); + + public static void main(String[] args) { + logger.info("Starting the bot..."); + + List resources = List.of( + ConfigHelper.CONFIG + ); + + for (String name : resources) { + File file = new File(name); + if (!file.exists()) { + ConfigHelper.saveResource(name, false); + logger.info("Resource \"" + name + "\" saved."); + } + ConfigHelper.loadConfig(name); + } + + Config config = ConfigHelper.getConfig(ConfigHelper.CONFIG); + if (config == null) { + logger.error("Failed to start bot. File config.yml not found."); + return; + } + + try { + new DiscordBot(config.nodes("discord.token").asString(), config); + } catch (Exception e) { + logger.error("Failed to start the bot", e); + return; + } + + logger.info("Bot is started!"); + } +} + diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/DiscordBot.java b/bot/src/main/kotlin/com/luminia/discord/bot/DiscordBot.java new file mode 100644 index 0000000..45e21b8 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/DiscordBot.java @@ -0,0 +1,92 @@ +package com.luminia.discord.bot; + +import com.luminia.config.Config; +import com.luminia.discord.api.command.CommandManager; +import com.luminia.discord.api.handler.EventManager; +import com.luminia.discord.api.handler.SimpleHandler; +import com.luminia.discord.bot.command.CommandManagerImpl; +import com.luminia.discord.bot.command.impl.*; +import com.luminia.discord.bot.handler.*; +import com.luminia.discord.bot.handler.impl.*; +import com.luminia.discord.bot.service.translation.TranslationService; +import com.luminia.discord.bot.service.translation.TranslationServiceImpl; +import com.luminia.discord.bot.settings.repository.SettingsRepository; +import com.luminia.discord.bot.settings.repository.SettingsRepositoryImpl; +import com.mefrreex.jooq.database.IDatabase; +import com.mefrreex.jooq.database.SQLiteDatabase; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.MemberCachePolicy; +import net.dv8tion.jda.api.utils.cache.CacheFlag; +import lombok.Getter; + +import java.io.File; + +@Getter +public class DiscordBot { + + @Getter + private static DiscordBot instance; + + private final String token; + private final JDA jda; + + private final EventManager eventManager; + private final CommandManager commandManager; + private final TranslationService translationService; + + private final SettingsRepository settingsRepository; + + private final long startTime = System.currentTimeMillis(); + + public DiscordBot(String token, Config config) { + DiscordBot.instance = this; + + this.token = token; + this.jda = JDABuilder.createDefault(token) + .setMemberCachePolicy(MemberCachePolicy.ALL) + .disableCache(CacheFlag.MEMBER_OVERRIDES, CacheFlag.VOICE_STATE) + .enableIntents(GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT) + .setBulkDeleteSplittingEnabled(false) + .setActivity(Activity.listening("luminia.fun")) + .build(); + + IDatabase database = new SQLiteDatabase(new File("database.db")); + this.settingsRepository = new SettingsRepositoryImpl(database); + + this.translationService = new TranslationServiceImpl(config); + + this.jda.addEventListener(new EventHandler()); + this.eventManager = new EventManagerImpl(jda); + this.eventManager.register( + new SimpleHandler(), + new CommandInteractionHandler(), + new ModalInteractionHandler(), + new ButtonInteractionHandler(), + new MemberTimeoutHandler(), + new GuildMemberJoinHandler() + ); + + this.commandManager = new CommandManagerImpl(jda); + this.commandManager.register( + new AvatarCommand(), + new ProfileCommand(), + new MemberCountCommand(), + new MembersCommand(), + new MessageEmbedCommand(), + new MessageCommand(), + new MinecraftStatusCommand(), + new MinecraftServerCommand(), + new SiteCommand(), + new RconCommand(), + new TimeoutCommand(), + new SetNameCommand() + ); + } + + public void shutdown() { + jda.shutdown(); + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/CommandManagerImpl.java b/bot/src/main/kotlin/com/luminia/discord/bot/command/CommandManagerImpl.java new file mode 100644 index 0000000..7688484 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/CommandManagerImpl.java @@ -0,0 +1,89 @@ +package com.luminia.discord.bot.command; + +import com.luminia.discord.api.command.*; +import com.luminia.discord.api.command.permission.CommandPermission; +import com.luminia.discord.bot.logger.Logger; +import com.luminia.discord.bot.service.translation.TranslationService; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; + +import java.util.*; + +public class CommandManagerImpl implements CommandManager { + + private final JDA jda; + private final Map>> commands; + private final TranslationService translationService; + + public CommandManagerImpl(JDA jda) { + this.jda = jda; + this.commands = new HashMap<>(); + this.translationService = TranslationService.getInstance(); + } + + @Override + public JDA getJDA() { + return jda; + } + + @Override + public Map>> getCommands() { + return commands; + } + + @Override + public void register(Command... commands) { + Arrays.stream(commands) + .forEach(command -> { + this.commands.computeIfAbsent(command.getCommandType(), c -> new HashMap<>()) + .put(command.getCommandData().getJDACommandData().getName(), command); + }); + + jda.updateCommands() + .addCommands(Arrays.stream(commands) + .map(command -> command.getCommandData().getJDACommandData()) + .toList()) + .queue(); + } + + @Override + public void execute(Command command, GenericCommandInteractionEvent event) { + CommandPermission permission = command.getCommandData().getPermission(); + Member member = event.getMember(); + + if (member != null) { + if (!permission.test(member)) { + event.reply(translationService.translate("generic-no-permission")).setEphemeral(true).queue(); + return; + } + } else { + if (!permission.defaultValue()) { + event.reply(translationService.translate("generic-no-permission")).setEphemeral(true).queue(); + return; + } + } + + try { + if (command instanceof SlashCommand slashCommand && event instanceof SlashCommandInteractionEvent slashEvent) { + slashCommand.execute(slashEvent); + return; + } + if (command instanceof MessageCommand messageCommand && event instanceof MessageContextInteractionEvent messageEvent) { + messageCommand.execute(messageEvent); + return; + } + if (command instanceof UserCommand messageCommand && event instanceof UserContextInteractionEvent userEvent) { + messageCommand.execute(userEvent); + } + } catch (Exception exception) { + Logger.getInstance().error("An error occurred while executing the command", exception); + event.reply(translationService.translate("generic-command-error", exception.getMessage())) + .setEphemeral(true) + .queue(); + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/AvatarCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/AvatarCommand.kt new file mode 100644 index 0000000..39d8658 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/AvatarCommand.kt @@ -0,0 +1,48 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.BotColors +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData + +class AvatarCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + return CommandData.slash("avatar", TranslationFormat.commandDescription("avatar")) + .addSubcommands(SubcommandData("guild", translationService.translate("command-avatar-subcommand-guild-name"))) + .addSubcommands(SubcommandData("user", translationService.translate("command-avatar-subcommand-user-name")) + .addOptions(OptionData(OptionType.USER, "user", translationService.translate("command-avatar-option-user-name")))) + } + + override fun execute(event: SlashCommandInteractionEvent) { + when(event.subcommandName) { + "guild" -> { + event.guild?.let { guild -> + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-avatar-message-guild-avatar")) + .setImage(guild.iconUrl) + .setColor(BotColors.PRIMARY) + .build() + event.replyEmbeds(embed).queue() + } + } + "user" -> { + val user = event.getOption("user")?.asUser + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-avatar-message-user-avatar", user?.name)) + .setImage(user?.avatarUrl) + .setColor(BotColors.PRIMARY) + .build() + event.replyEmbeds(embed).queue() + } + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MemberCountCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MemberCountCommand.kt new file mode 100644 index 0000000..f2aba50 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MemberCountCommand.kt @@ -0,0 +1,29 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.BotColors +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent + +class MemberCountCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + return CommandData.slash("membercount", TranslationFormat.commandDescription("member-count")) + } + + override fun execute(event: SlashCommandInteractionEvent) { + event.guild?.let { guild -> + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-member-count-success-title")) + .setDescription(translationService.translate("command-member-count-success-description", guild.memberCount)) + .setColor(BotColors.PRIMARY) + .build() + event.replyEmbeds(embed).queue() + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MembersCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MembersCommand.kt new file mode 100644 index 0000000..7401329 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MembersCommand.kt @@ -0,0 +1,40 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.BotColors +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent + +class MembersCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + return CommandData.slash("members", TranslationFormat.commandDescription("members")) + } + + override fun execute(event: SlashCommandInteractionEvent) { + event.guild?.let { guild -> + val roles = guild.roles.filter { role -> + guild.getMembersWithRoles(role).isNotEmpty() && !role.tags.isBot + } + + val builder = StringBuilder().append("```kt\n") + roles.forEach { role -> + builder.append(translationService.translate("command-members-success-members-with-role", role.name, guild.getMembersWithRoles(role).count()) + "\n") + } + builder.append("```") + + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-members-success-title")) + .setDescription(builder.toString()) + .setFooter(translationService.translate("command-members-success-footer", guild.memberCount)) + .setColor(BotColors.PRIMARY) + .build() + event.replyEmbeds(embed).queue() + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MessageCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MessageCommand.kt new file mode 100644 index 0000000..2420395 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MessageCommand.kt @@ -0,0 +1,43 @@ +package com.luminia.discord.bot.command.impl + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.BotUtils +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData + +class MessageCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + return CommandData.slash("message", TranslationFormat.commandDescription("message")) + .addOptions( + OptionData(OptionType.STRING, "message", translationService.translate("command-message-option-message-name")), + OptionData(OptionType.ATTACHMENT, "attachment", translationService.translate("command-message-option-attachment-name")) + .setRequired(false) + ) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MESSAGE_MANAGE)) + } + + override fun execute(event: SlashCommandInteractionEvent) { + val message = event.getOption("message")?.asString + val attachment = event.getOption("attachment")?.asAttachment + + event.channel.sendMessage(message ?: "").apply { + if (attachment != null) { + addFiles(BotUtils.getFileUploadFromAttachment(attachment)) + } + }.queue { + event.reply(translationService.translate("command-message-message-success")) + .setEphemeral(true) + .queue() + } + } + +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MessageEmbedCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MessageEmbedCommand.kt new file mode 100644 index 0000000..8aea786 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MessageEmbedCommand.kt @@ -0,0 +1,101 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.extension.setHandler +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData +import net.dv8tion.jda.api.interactions.components.text.TextInput +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle +import net.dv8tion.jda.api.interactions.modals.Modal +import java.awt.Color + +class MessageEmbedCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + val optionRole = OptionData(OptionType.STRING, "message", translationService.translate("command-embed-option-message-name")).setRequired(false) + return CommandData.slash("embed", TranslationFormat.commandDescription("embed")) + .addSubcommands( + SubcommandData("guild", translationService.translate("command-embed-subcommand-guild-name")).addOptions(optionRole), + SubcommandData("user", translationService.translate("command-embed-subcommand-user-name")).addOptions(optionRole), + SubcommandData("none", translationService.translate("command-embed-subcommand-none-name")).addOptions(optionRole) + ) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MESSAGE_MANAGE)) + } + + override fun execute(event: SlashCommandInteractionEvent) { + val subcommandName = event.subcommandName + val message = event.getOption("message")?.asString + + val components = listOf( + TextInput.create("title", translationService.translate("command-embed-modal-input-title-name"), TextInputStyle.SHORT) + .setPlaceholder(translationService.translate("command-embed-modal-input-title-placeholder")) + .setMaxLength(256) + .build(), + TextInput.create("description", translationService.translate("command-embed-modal-input-description-name"), TextInputStyle.PARAGRAPH) + .setPlaceholder(translationService.translate("command-embed-modal-input-description-placeholder")) + .setMaxLength(4000) + .build(), + TextInput.create("color", translationService.translate("command-embed-modal-input-color-name"), TextInputStyle.SHORT) + .setPlaceholder(translationService.translate("command-embed-modal-input-color-placeholder")) + .setRequired(false) + .setMinLength(6) + .setMaxLength(7) + .build(), + TextInput.create("image", translationService.translate("command-embed-modal-input-image-name"), TextInputStyle.SHORT) + .setPlaceholder(translationService.translate("command-embed-modal-input-image-placeholder")) + .setRequired(false) + .build(), + TextInput.create("footer", translationService.translate("command-embed-modal-input-footer-name"), TextInputStyle.SHORT) + .setPlaceholder(translationService.translate("command-embed-modal-input-footer-placeholder")) + .setMaxLength(512) + .setRequired(false) + .build() + ) + + val modal = Modal.create("embed", translationService.translate("command-embed-modal-title-name")) + .apply { + components.forEach { + addActionRow(it) + } + } + .build() + .setHandler { event -> + val embed = EmbedBuilder() + .setTitle(event.getValue("title")?.asString) + .setDescription(event.getValue("description")?.asString) + .setFooter(event.getValue("footer")?.asString) + .setThumbnail(when(subcommandName) { + "guild" -> event.member!!.guild.iconUrl + "user" -> event.member!!.user.avatarUrl + else -> null + }) + + event.getValue("color")?.asString.let { + embed.setColor(Color.decode(if (it!!.length < 6) "#9f70fd" else it)) + } + event.getValue("image")?.asString.let { + if (!it.isNullOrEmpty()) embed.setImage(it) + } + + event.channel.sendMessage(message ?: "") + .addEmbeds(embed.build()) + .queue() + + event.reply(translationService.translate("command-embed-message-success")) + .setEphemeral(true) + .queue() + } + + event.replyModal(modal).queue() + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MinecraftServerCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MinecraftServerCommand.kt new file mode 100644 index 0000000..c8ae816 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MinecraftServerCommand.kt @@ -0,0 +1,57 @@ +package com.luminia.discord.bot.command.impl + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.BotColors +import com.luminia.discord.bot.utils.ConfigHelper +import com.luminia.discord.bot.utils.query.BedrockQuery +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent + +class MinecraftServerCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + private val bedrockQuery = BedrockQuery() + + override fun getCommandData(): CommandData { + return CommandData.slash("server", TranslationFormat.commandDescription("server")) + } + + override fun execute(event: SlashCommandInteractionEvent) { + val config = ConfigHelper.getConfig(ConfigHelper.CONFIG) ?: return + + val serverNode = config.node("minecraft-server-command") + val address = serverNode.node("address").asString() + val port = serverNode.node("port").asInt() + + bedrockQuery.query(address, port).thenAcceptAsync { response -> + + val builder = StringBuilder() + .append(translationService.translate("command-server-embed-server-information") + "\n") + .append(translationService.translate("command-server-embed-address", address) + "\n") + .append(translationService.translate("command-server-embed-port", port) + "\n\n") + + if (response.online) { + builder + .append(translationService.translate("command-server-embed-current-information") + "\n") + .append(translationService.translate("command-server-embed-player-count", response.playerCount, response.maxPlayers) + "\n") + .append(translationService.translate("command-server-embed-minecraft-version", response.minecraftVersion) + "\n") + } else { + builder.append(translationService.translate("command-server-embed-server-offline") + "\n") + } + + builder.append("\n" + translationService.translate("command-server-embed-site")) + + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-server-embed-title")) + .setDescription(builder.toString()) + .setImage(serverNode.node("image").asString()) + .setColor(BotColors.PRIMARY) + + event.replyEmbeds(embed.build()).queue() + } + + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MinecraftStatusCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MinecraftStatusCommand.kt new file mode 100644 index 0000000..d67455c --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/MinecraftStatusCommand.kt @@ -0,0 +1,64 @@ +package com.luminia.discord.bot.command.impl + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.BotColors +import com.luminia.discord.bot.utils.query.BedrockQuery +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import java.net.InetAddress + +class MinecraftStatusCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + private val bedrockQuery = BedrockQuery() + + override fun getCommandData(): CommandData { + return CommandData.slash("status", TranslationFormat.commandDescription("status")) + .addOptions( + OptionData(OptionType.STRING, "address", translationService.translate("command-status-option-address-name")), + OptionData(OptionType.INTEGER, "port", translationService.translate("command-status-option-port-name")) + ) + } + + override fun execute(event: SlashCommandInteractionEvent) { + val options = event.options + + if (options.isEmpty()) { + event.reply(translationService.translate("command-status-message-no-address-given")).setEphemeral(true).queue() + return + } + + val address = options[0].asString + val port = options.getOrNull(1)?.asInt ?: 19132 + + bedrockQuery.query(address, port).thenAcceptAsync { response -> + + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-status-embed-title", address, port)) + .setColor(BotColors.PRIMARY) + + println(response.minecraftVersion) + val builder = StringBuilder() + .append(translationService.translate("command-status-embed-motd", response.motd) + "\n\n") + .append(translationService.translate("command-status-embed-player-count", response.playerCount, response.maxPlayers) + "\n") + .append(translationService.translate("command-status-embed-minecraft-version", response.minecraftVersion, response.protocolVersion) + "\n") + .append(translationService.translate("command-status-embed-gamemode", response.gamemode) + "\n\n") + .append(translationService.translate("command-status-embed-software", response.software) + "\n\n") + .append(translationService.translate("command-status-embed-additional") + "\n") + .append(translationService.translate("command-status-embed-address", InetAddress.getByName(address).hostAddress)) + + if (response.online) { + embed.setDescription(builder.toString()) + } else { + embed.setDescription(translationService.translate("command-status-embed-server-offline")) + } + + event.replyEmbeds(embed.build()).setEphemeral(true).queue() + } + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/ProfileCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/ProfileCommand.kt new file mode 100644 index 0000000..c2964d6 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/ProfileCommand.kt @@ -0,0 +1,88 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.BotColors +import com.luminia.discord.bot.utils.BotUtils +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData + +class ProfileCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + return CommandData.slash("profile", TranslationFormat.commandDescription("profile")) + .addOptions(OptionData(OptionType.USER, "user", translationService.translate("command-profile-option-user-name"))) + } + + override fun execute(event: SlashCommandInteractionEvent) { + event.getOption("user")?.asMember?.let { member -> + val builder = StringBuilder() + .append(translationService.translate("command-profile-embed-user-name", member.effectiveName) + "\n") + .append(translationService.translate("command-profile-embed-user-id", member.user.name) + "\n") + .append(translationService.translate("command-profile-embed-user-id-long", member.idLong) + "\n") + .append(translationService.translate("command-profile-embed-user-online-status", member.onlineStatus.name, BotUtils.getEmojiFromStatus(member.onlineStatus).formatted)) + + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-profile-embed-title")) + .setDescription(builder.toString()) + .setThumbnail(member.user.avatarUrl) + .setColor(BotColors.PRIMARY) + .build() + +// TODO buttons +// val canInteract = member.canInteract(event.guild!!.selfMember) +// val canPunish = !canInteract && member != event.guild!!.selfMember +// +// val buttons = listOf( +// ActionRow.of( +// Button.danger("ban", "Забанить").withDisabled(!canPunish), +// Button.danger("kick", "Кикнуть").withDisabled(!canPunish) +// ), +// ActionRow.of( +// Button.primary("change_name", "Изменить имя") +// .withDisabled(canInteract) +// .setHandler { changeName(member, it) } +// ) +// ) + + event.replyEmbeds(embed) + .queue() + } + } + +// private fun changeName(member: Member, event: ButtonInteractionEvent) { +// if (!BotUtils.canInteractWithPermission(event.member, member, Permission.NICKNAME_CHANGE)) { +// event.reply("Вы не можете изменять имя").setEphemeral(true).queue() +// return +// } +// +// val nameInput = TextInput.create("name", "Имя", TextInputStyle.SHORT) +// .setPlaceholder(member.effectiveName) +// .setMinLength(1) +// .setMaxLength(32) +// .build() +// +// val modal = Modal.create("change_name", "Изменить имя") +// .addActionRow(nameInput) +// .build() +// +// modal.setHandler { interaction -> +// try { +// val name = interaction.getValue("name")?.asString +// member.modifyNickname(name).queue { +// event.reply("Имя изменено на $name").setEphemeral(true).queue() +// } +// } catch (e: HierarchyException) { +// event.reply("Бот не может изменить имя этого пользователя").setEphemeral(true).queue() +// } +// } +// +// event.replyModal(modal).queue() +// } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/RconCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/RconCommand.kt new file mode 100644 index 0000000..85384ec --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/RconCommand.kt @@ -0,0 +1,171 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.settings.option.OptionTypes +import com.luminia.discord.bot.settings.repository.SettingsRepository +import com.luminia.discord.bot.utils.BotColors +import com.luminia.discord.bot.utils.extension.setHandler +import com.luminia.discord.bot.utils.rcon.RconClient +import com.luminia.discord.bot.utils.rcon.exception.AuthenticationException +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.entities.Guild +import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData +import net.dv8tion.jda.api.interactions.components.buttons.Button +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle +import kotlin.concurrent.thread + +class RconCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + private val settingsRepository = SettingsRepository.getInstance(); + + override fun getCommandData(): CommandData { + return CommandData.slash("rcon", TranslationFormat.commandDescription("rcon")) + .addSubcommands( + SubcommandData("command", TranslationFormat.commandSubcommand("rcon", "command")) + .addOptions( + OptionData(OptionType.STRING, "command", TranslationFormat.commandOption("rcon", "command")) + ), + SubcommandData("setrole", TranslationFormat.commandSubcommand("rcon", "setrole")) + .addOptions( + OptionData(OptionType.ROLE, "role", TranslationFormat.commandOption("rcon", "role")) + ), + SubcommandData("setrcon", TranslationFormat.commandSubcommand("rcon", "setrcon")) + .addOptions( + OptionData(OptionType.STRING, "address", TranslationFormat.commandOption("rcon", "address")), + OptionData(OptionType.INTEGER, "port", TranslationFormat.commandOption("rcon", "port")), + OptionData(OptionType.STRING, "password", TranslationFormat.commandOption("rcon", "password")) + ) + ) + .setGuildOnly(true) + } + + override fun execute(event: SlashCommandInteractionEvent) { + val guild = event.guild ?: return + when(event.subcommandName) { + "setrole" -> handleSetRole(event, guild) + "setrcon" -> handleSetRcon(event, guild) + "command" -> handleCommand(event, guild) + } + } + + private fun handleSetRole(event: SlashCommandInteractionEvent, guild: Guild) { + if (!event.member!!.hasPermission(Permission.ADMINISTRATOR)) { + event.reply(translationService.translate("generic-no-permission")).setEphemeral(true).queue() + return + } + + val role = event.getOption("role")?.asRole ?: return + + settingsRepository.setOption(guild.idLong, OptionTypes.RCON_ROLE, role.idLong).join() + + event.reply(translationService.translate("command-rcon-message-role-set", role.name)) + .setEphemeral(true) + .queue() + } + + private fun handleSetRcon(event: SlashCommandInteractionEvent, guild: Guild) { + if (!event.member!!.hasPermission(Permission.ADMINISTRATOR)) { + event.reply(translationService.translate("generic-no-permission")).setEphemeral(true).queue() + return + } + + val address = event.getOption("address")?.asString ?: return + val port = event.getOption("port")?.asInt ?: return + val password = event.getOption("password")?.asString ?: return + + settingsRepository.setOption(guild.idLong, OptionTypes.RCON_ADDRESS, address) + settingsRepository.setOption(guild.idLong, OptionTypes.RCON_PORT, port) + settingsRepository.setOption(guild.idLong, OptionTypes.RCON_PASSWORD, password) + + event.reply(translationService.translate("command-rcon-message-rcon-set")) + .setEphemeral(true) + .queue() + } + + private fun handleCommand(event: SlashCommandInteractionEvent, guild: Guild) { + val role = guild.getRoleById(settingsRepository.getOption(guild.idLong, OptionTypes.RCON_ROLE).join()) + if (role == null) { + event.reply(translationService.translate("command-rcon-message-role-not-found")).setEphemeral(true).queue() + return + } + + if (!event.member!!.roles.contains(role)) { + event.reply(translationService.translate("generic-no-permission")).setEphemeral(true).queue() + return + } + + val command = event.getOption("command")?.asString + + val address = settingsRepository.getOption(guild.idLong, OptionTypes.RCON_ADDRESS).join() + val port = settingsRepository.getOption(guild.idLong, OptionTypes.RCON_PORT).join() + val password = settingsRepository.getOption(guild.idLong, OptionTypes.RCON_PASSWORD).join() + + if (address == null || port == null || password == null) { + event.reply(translationService.translate("command-rcon-message-rcon-not-set")).setEphemeral(true).queue() + return + } + + thread(start = true) { + val rcon = RconClient(address, port) + + try { + rcon.connect(password) + } catch (exception: AuthenticationException) { + if (exception.message == "Server is offline") { + event.reply(translationService.translate("command-rcon-message-server-offline")).queue() + } else { + event.reply(translationService.translate("command-rcon-message-auth-error", exception.message)).queue() + exception.printStackTrace() + } + return@thread + } + + sendCommand(event, rcon, command) + } + } + + private fun sendCommand(genericEvent: GenericInteractionCreateEvent, rcon: RconClient, command: String?) { + val response = rcon.sendCommand(command) + val embed = EmbedBuilder() + .setTitle(translationService.translate("command-rcon-embed-title")) + .setDescription(response.ifBlank { translationService.translate("command-rcon-embed-response-empty") }) + .setColor(BotColors.PRIMARY) + .build() + + val button = Button.primary("resend", translationService.translate("command-rcon-button-resend")) + .withStyle(ButtonStyle.SECONDARY) + .setHandler { + val guild = it.guild ?: return@setHandler + + val role = guild.getRoleById(settingsRepository.getOption(guild.idLong, OptionTypes.RCON_ROLE).join()) + if (role == null) { + it.reply(translationService.translate("command-rcon-message-role-not-found")).setEphemeral(true).queue() + return@setHandler + } + + if (!it.member!!.roles.contains(role)) { + it.reply(translationService.translate("generic-no-permission")).setEphemeral(true).queue() + return@setHandler + } + + sendCommand(it, rcon, command) + } + + val event = when(genericEvent) { + is SlashCommandInteractionEvent -> genericEvent + is ButtonInteractionEvent -> genericEvent + else -> return + } + event.replyEmbeds(embed).addActionRow(button).queue() + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/SetNameCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/SetNameCommand.kt new file mode 100644 index 0000000..755a872 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/SetNameCommand.kt @@ -0,0 +1,37 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData + +class SetNameCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + return CommandData.slash("setname", TranslationFormat.commandDescription("setname")) + .addOptions( + OptionData(OptionType.USER, "user", translationService.translate("command-setname-option-user-name")), + OptionData(OptionType.STRING, "name", translationService.translate("command-setname-option-name-name")) + ) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.NICKNAME_CHANGE)) + } + + override fun execute(event: SlashCommandInteractionEvent) { + val user = event.getOption("user")?.asMember?: return + val name = event.getOption("name")?.asString + + if (user.canInteract(user)) { + user.modifyNickname(name).queue() + event.reply(translationService.translate("command-setname-message-changed", user.effectiveName, name)).queue() + } else { + event.reply(translationService.translate("generic-can-not-interact")).setEphemeral(true).queue() + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/SiteCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/SiteCommand.kt new file mode 100644 index 0000000..8f418e6 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/SiteCommand.kt @@ -0,0 +1,20 @@ +package com.luminia.discord.bot.command.impl + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent + +class SiteCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + return CommandData.slash("site", TranslationFormat.commandDescription("site")) + } + + override fun execute(event: SlashCommandInteractionEvent) { + event.reply(translationService.translate("command-site-message-success")).queue() + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/TimeoutCommand.kt b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/TimeoutCommand.kt new file mode 100644 index 0000000..0add90d --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/command/impl/TimeoutCommand.kt @@ -0,0 +1,58 @@ +package com.luminia.discord.bot.command.impl; + +import com.luminia.discord.api.command.SlashCommand +import com.luminia.discord.api.command.data.CommandData +import com.luminia.discord.bot.service.translation.TranslationFormat +import com.luminia.discord.bot.service.translation.TranslationService +import com.luminia.discord.bot.utils.TimeFormatter +import com.luminia.discord.bot.utils.TimeParser +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions +import net.dv8tion.jda.api.interactions.commands.OptionType +import net.dv8tion.jda.api.interactions.commands.build.OptionData +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData +import java.time.Duration +import java.util.concurrent.TimeUnit + +class TimeoutCommand : SlashCommand() { + + private val translationService = TranslationService.getInstance() + + override fun getCommandData(): CommandData { + val options = listOf( + OptionData(OptionType.USER, "user", translationService.translate("command-timeout-option-user-name")), + OptionData(OptionType.STRING, "duration", translationService.translate("command-timeout-option-duration-name")) + ) + return CommandData.slash("timeout", TranslationFormat.commandDescription("timeout")) + .addSubcommands(SubcommandData("add", translationService.translate("command-timeout-subcommand-add-name")).addOptions(options)) + .addSubcommands(SubcommandData("remove", translationService.translate("command-timeout-subcommand-remove-name")).addOptions(options[0])) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MODERATE_MEMBERS)) + } + + override fun execute(event: SlashCommandInteractionEvent) { + val user = event.getOption("user")?.asMember ?: return + + when(event.subcommandName) { + "add" -> { + val duration = TimeParser.parse(event.getOption("duration")?.asString) + if (duration == null) { + event.reply(translationService.translate("command-timeout-message-invalid-duration")).setEphemeral(true).queue() + return + } + + if (duration > TimeUnit.DAYS.toMillis(28)) { + event.reply(translationService.translate("command-timeout-message-duration-limit")).setEphemeral(true).queue() + return + } + + user.timeoutFor(Duration.ofMillis(duration)).queue() + event.reply(translationService.translate("command-timeout-message-timeout-added", user.effectiveName, TimeFormatter.format(duration))).queue() + } + "remove" -> { + user.removeTimeout().queue() + event.reply(translationService.translate("command-timeout-message-timeout-removed", user.effectiveName)).queue() + } + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/handler/EventHandler.java b/bot/src/main/kotlin/com/luminia/discord/bot/handler/EventHandler.java new file mode 100644 index 0000000..a06882c --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/handler/EventHandler.java @@ -0,0 +1,16 @@ +package com.luminia.discord.bot.handler; + +import com.luminia.discord.bot.DiscordBot; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.hooks.EventListener; +import org.jetbrains.annotations.NotNull; + +public class EventHandler implements EventListener { + + private final DiscordBot discordBot = DiscordBot.getInstance(); + + @Override + public void onEvent(@NotNull GenericEvent event) { + discordBot.getEventManager().handle(event); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/handler/EventManagerImpl.java b/bot/src/main/kotlin/com/luminia/discord/bot/handler/EventManagerImpl.java new file mode 100644 index 0000000..f8f38be --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/handler/EventManagerImpl.java @@ -0,0 +1,73 @@ +package com.luminia.discord.bot.handler; + +import com.luminia.discord.api.handler.EventManager; +import com.luminia.discord.api.handler.Subscribe; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.hooks.EventListener; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class EventManagerImpl implements EventManager { + + private final JDA jda; + private final Map, Set>> handlers; + + public EventManagerImpl(JDA jda) { + this.jda = jda; + this.handlers = new HashMap<>(); + } + + @Override + public Map, Set>> getHandlers() { + return handlers; + } + + @Override + public void register(Object... listeners) { + for (Object listener : listeners) { + + if (listener instanceof EventListener) { + jda.addEventListener(listener); + continue; + } + + for (Method method : listener.getClass().getDeclaredMethods()) { + Subscribe subscribe = method.getAnnotation(Subscribe.class); + if (subscribe == null || method.getParameterCount() != 1) { + continue; + } + + Class[] parameterTypes = method.getParameterTypes(); + if (!GenericEvent.class.isAssignableFrom(parameterTypes[0])) { + continue; + } + + @SuppressWarnings("unchecked") + Class eventClass = (Class) parameterTypes[0]; + + Consumer callback = event -> { + try { + method.invoke(listener, event); + } catch (Exception e) { + e.printStackTrace(); + } + }; + + handlers.computeIfAbsent(eventClass, e -> new HashSet<>()).add(callback); + } + } + } + + @Override + public void handle(GenericEvent event) { + if (handlers.containsKey(event.getClass())) { + handlers.get(event.getClass()).forEach(handler -> handler.accept(event)); + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/ButtonInteractionHandler.java b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/ButtonInteractionHandler.java new file mode 100644 index 0000000..ca068dc --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/ButtonInteractionHandler.java @@ -0,0 +1,27 @@ +package com.luminia.discord.bot.handler.impl; + +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class ButtonInteractionHandler extends ListenerAdapter { + + private static final Map> handlers = new HashMap<>(); + + @Override + public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { + String buttonId = event.getButton().getId(); + Consumer handler = handlers.get(buttonId); + if (handler != null) { + handler.accept(event); + } + } + + public static void addHandler(String buttonId, Consumer handler) { + handlers.put(buttonId, handler); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/CommandInteractionHandler.java b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/CommandInteractionHandler.java new file mode 100644 index 0000000..cd0dcfd --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/CommandInteractionHandler.java @@ -0,0 +1,38 @@ +package com.luminia.discord.bot.handler.impl; + +import com.luminia.discord.api.command.*; +import com.luminia.discord.bot.DiscordBot; +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +public class CommandInteractionHandler extends ListenerAdapter { + + private final DiscordBot discordBot = DiscordBot.getInstance(); + + @Override + public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { + CommandManager commandManager = discordBot.getCommandManager(); + Command command = commandManager.getCommand(CommandType.SLASH, event.getName()); + + commandManager.execute(command, event); + } + + @Override + public void onMessageContextInteraction(@NotNull MessageContextInteractionEvent event) { + CommandManager commandManager = discordBot.getCommandManager(); + Command command = commandManager.getCommand(CommandType.MESSAGE, event.getName()); + + commandManager.execute(command, event); + } + + @Override + public void onUserContextInteraction(@NotNull UserContextInteractionEvent event) { + CommandManager commandManager = discordBot.getCommandManager(); + Command command = commandManager.getCommand(CommandType.USER, event.getName()); + + commandManager.execute(command, event); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/GuildMemberJoinHandler.java b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/GuildMemberJoinHandler.java new file mode 100644 index 0000000..d6fbc75 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/GuildMemberJoinHandler.java @@ -0,0 +1,54 @@ +package com.luminia.discord.bot.handler.impl; + +import com.luminia.config.Config; +import com.luminia.discord.bot.service.translation.TranslationService; +import com.luminia.discord.bot.utils.BotEmoji; +import com.luminia.discord.bot.utils.ConfigHelper; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +public class GuildMemberJoinHandler extends ListenerAdapter { + + private final Config config = ConfigHelper.getConfig(ConfigHelper.CONFIG); + + private boolean isEnabled(String type) { + return config.nodes("welcome-messages-channel." + type).asBoolean(); + } + + private long getChannelId() { + return config.nodes("welcome-messages-channel.channel-id").asLong(); + } + + @Override + public void onGuildMemberJoin(@NotNull GuildMemberJoinEvent event) { + if (!this.isEnabled("enable-join")) { + return; + } + + TextChannel channel = event.getGuild().getTextChannelById(this.getChannelId()); + if (channel != null) { + channel.sendMessage(TranslationService.getInstance().translate("generic-join-message", + BotEmoji.ARROW_RIGHT_GREEN.getFormatted(), + event.getMember().getEffectiveName(), + event.getMember().getAsMention())).queue(); + } + } + + @Override + public void onGuildMemberRemove(@NotNull GuildMemberRemoveEvent event) { + if (!this.isEnabled("enable-leave")) { + return; + } + + TextChannel channel = event.getGuild().getTextChannelById(this.getChannelId()); + if (channel != null) { + channel.sendMessage(TranslationService.getInstance().translate("generic-leave-message", + BotEmoji.ARROW_LEFT_RED.getFormatted(), + event.getMember().getEffectiveName(), + event.getMember().getAsMention())).queue(); + } + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/MemberTimeoutHandler.java b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/MemberTimeoutHandler.java new file mode 100644 index 0000000..d0df1e3 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/MemberTimeoutHandler.java @@ -0,0 +1,47 @@ +package com.luminia.discord.bot.handler.impl; + +import com.luminia.discord.bot.service.translation.TranslationService; +import com.luminia.discord.bot.utils.BotColors; +import com.luminia.discord.bot.utils.TimeFormatter; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateTimeOutEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +public class MemberTimeoutHandler extends ListenerAdapter { + + @Override + public void onGuildMemberUpdateTimeOut(@NotNull GuildMemberUpdateTimeOutEvent event) { + TranslationService translationService = TranslationService.getInstance(); + + Guild guild = event.getGuild(); + User user = event.getMember().getUser(); + + if (event.getGuild().getSelfMember().getUser().equals(user)) { + return; + } + + user.openPrivateChannel().queue(channel -> { + if (!channel.canTalk()) { + return; + } + + EmbedBuilder embed = new EmbedBuilder().setTitle(translationService.translate("timeout-embed-title")); + + if (event.getNewTimeOutEnd() == null) { + embed + .setDescription(translationService.translate("timeout-embed-description-remove", guild.getName())) + .setColor(BotColors.SUCCESS); + } else { + long duration = event.getNewTimeOutEnd().toInstant().toEpochMilli() - System.currentTimeMillis(); + embed + .setDescription(translationService.translate("timeout-embed-description-add", TimeFormatter.format(duration), guild.getName())) + .setColor(BotColors.WARNING); + } + + channel.sendMessageEmbeds(embed.build()).queue(); + }); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/ModalInteractionHandler.java b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/ModalInteractionHandler.java new file mode 100644 index 0000000..935234f --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/handler/impl/ModalInteractionHandler.java @@ -0,0 +1,27 @@ +package com.luminia.discord.bot.handler.impl; + +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class ModalInteractionHandler extends ListenerAdapter { + + private static final Map> handlers = new HashMap<>(); + + @Override + public void onModalInteraction(@NotNull ModalInteractionEvent event) { + Consumer handler = handlers.get(event.getModalId()); + if (handler != null) { + handler.accept(event); + handlers.remove(event.getModalId()); + } + } + + public static void addHandler(String modalId, Consumer handler) { + handlers.put(modalId, handler); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/logger/Logger.java b/bot/src/main/kotlin/com/luminia/discord/bot/logger/Logger.java new file mode 100644 index 0000000..70635d8 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/logger/Logger.java @@ -0,0 +1,10 @@ +package com.luminia.discord.bot.logger; + +import com.luminia.discord.api.logger.BaseLogger; +import lombok.Getter; + +public class Logger extends BaseLogger { + + @Getter + private static final Logger instance = new Logger(); +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationFormat.kt b/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationFormat.kt new file mode 100644 index 0000000..946da7b --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationFormat.kt @@ -0,0 +1,18 @@ +package com.luminia.discord.bot.service.translation + +import com.luminia.discord.bot.translation.LanguageCode + +object TranslationFormat { + + fun commandDescription(name: String, code: LanguageCode = LanguageCode.RUS): String { + return TranslationService.getInstance().translate("command-$name-description", code) + } + + fun commandSubcommand(name: String, subcommand: String, code: LanguageCode = LanguageCode.RUS): String { + return TranslationService.getInstance().translate("command-$name-subcommand-$subcommand-name", code) + } + + fun commandOption(name: String, option: String, code: LanguageCode = LanguageCode.RUS): String { + return TranslationService.getInstance().translate("command-$name-option-$option-name", code) + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationService.java b/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationService.java new file mode 100644 index 0000000..e5a0e6b --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationService.java @@ -0,0 +1,17 @@ +package com.luminia.discord.bot.service.translation; + +import com.luminia.discord.bot.DiscordBot; +import com.luminia.discord.bot.translation.LanguageCode; + +public interface TranslationService { + + LanguageCode getLanguage(); + + String translate(String key, Object... replacements); + + String translate(String key, LanguageCode code, Object... replacements); + + static TranslationService getInstance() { + return DiscordBot.getInstance().getTranslationService(); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationServiceImpl.java b/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationServiceImpl.java new file mode 100644 index 0000000..a9a9578 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/service/translation/TranslationServiceImpl.java @@ -0,0 +1,57 @@ +package com.luminia.discord.bot.service.translation; + +import com.luminia.config.Config; +import com.luminia.discord.bot.translation.TranslationContainer; +import com.luminia.discord.bot.translation.TranslationProvider; +import com.luminia.discord.bot.utils.ConfigHelper; +import com.luminia.discord.bot.translation.LanguageCode; + +import java.io.File; + +public class TranslationServiceImpl implements TranslationService { + + private final LanguageCode fallbackLanguage; + private final TranslationProvider provider; + + public TranslationServiceImpl(Config config) { + this.fallbackLanguage = LanguageCode.getByName(config.node("language").asString()); + + File translationFolder = new File("lang"); + translationFolder.mkdirs(); + + for (LanguageCode code : LanguageCode.values()) { + ConfigHelper.saveResource("lang/" + code.getName() + ".yml", false); + } + + this.provider = new TranslationProvider(); + this.provider.init(translationFolder); + } + + @Override + public LanguageCode getLanguage() { + return fallbackLanguage; + } + + @Override + public String translate(String key, Object... replacements) { + return translate(key, fallbackLanguage, replacements); + } + + @Override + public String translate(String key, LanguageCode code, Object... replacements) { + TranslationContainer container = provider.getContainer(key); + if (container == null) return "null"; + + String message = container.hasTranslation(code) ? + container.getTranslation(code, "null") : + container.getTranslation(fallbackLanguage, "null"); + + int i = 0; + for (Object replacement : replacements) { + message = message.replace("[" + i + "]", String.valueOf(replacement)); + i++; + } + + return message; + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/Option.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/Option.java new file mode 100644 index 0000000..5b1f072 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/Option.java @@ -0,0 +1,34 @@ +package com.luminia.discord.bot.settings.option; + +import lombok.Getter; + +@Getter +public abstract class Option { + + private final String name; + private final String displayName; + private final T defaultValue; + + public Option(String name, T defaultValue) { + this(name, name, defaultValue); + } + + public Option(String name, String displayName, T defaultValue) { + this.name = name; + this.displayName = displayName; + this.defaultValue = defaultValue; + } + + /** + * Get option type + */ + public abstract OptionType getType(); + + /** + * Get object as option type + */ + @SuppressWarnings("unchecked") + public T asType(String value) { + return (T) this.getType().parse(value); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionBoolean.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionBoolean.java new file mode 100644 index 0000000..c5cffbb --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionBoolean.java @@ -0,0 +1,21 @@ +package com.luminia.discord.bot.settings.option; + +public class OptionBoolean extends Option { + + public OptionBoolean(String name) { + super(name, false); + } + + public OptionBoolean(String name, boolean defaultValue) { + super(name, defaultValue); + } + + public OptionBoolean(String name, String displayName, boolean defaultValue) { + super(name, displayName, defaultValue); + } + + @Override + public OptionType getType() { + return OptionType.BOOLEAN; + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionDouble.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionDouble.java new file mode 100644 index 0000000..7fb3cd2 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionDouble.java @@ -0,0 +1,21 @@ +package com.luminia.discord.bot.settings.option; + +public class OptionDouble extends Option { + + public OptionDouble(String name) { + super(name, 0D); + } + + public OptionDouble(String name, double defaultValue) { + super(name, defaultValue); + } + + public OptionDouble(String name, String displayName, double defaultValue) { + super(name, displayName, defaultValue); + } + + @Override + public OptionType getType() { + return OptionType.DOUBLE; + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionInteger.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionInteger.java new file mode 100644 index 0000000..8183cd2 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionInteger.java @@ -0,0 +1,21 @@ +package com.luminia.discord.bot.settings.option; + +public class OptionInteger extends Option { + + public OptionInteger(String name) { + super(name, 0); + } + + public OptionInteger(String name, int defaultValue) { + super(name, defaultValue); + } + + public OptionInteger(String name, String displayName, int defaultValue) { + super(name, displayName, defaultValue); + } + + @Override + public OptionType getType() { + return OptionType.INTEGER; + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionLong.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionLong.java new file mode 100644 index 0000000..8b96e73 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionLong.java @@ -0,0 +1,21 @@ +package com.luminia.discord.bot.settings.option; + +public class OptionLong extends Option { + + public OptionLong(String name) { + super(name, 0L); + } + + public OptionLong(String name, long defaultValue) { + super(name, defaultValue); + } + + public OptionLong(String name, String displayName, long defaultValue) { + super(name, displayName, defaultValue); + } + + @Override + public OptionType getType() { + return OptionType.LONG; + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionString.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionString.java new file mode 100644 index 0000000..f86927c --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionString.java @@ -0,0 +1,21 @@ +package com.luminia.discord.bot.settings.option; + +public class OptionString extends Option{ + + public OptionString(String name) { + super(name, ""); + } + + public OptionString(String name, String defaultValue) { + super(name, defaultValue); + } + + public OptionString(String name, String displayName, String defaultValue) { + super(name, displayName, defaultValue); + } + + @Override + public OptionType getType() { + return OptionType.STRING; + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionType.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionType.java new file mode 100644 index 0000000..4fc784d --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionType.java @@ -0,0 +1,21 @@ +package com.luminia.discord.bot.settings.option; + +import java.util.function.Function; + +public enum OptionType { + BOOLEAN(Boolean::parseBoolean), + STRING(value -> value), + DOUBLE(Double::parseDouble), + INTEGER(Integer::parseInt), + LONG(Long::parseLong); + + private final Function function; + + OptionType(Function function) { + this.function = function; + } + + public Object parse(String value) { + return function.apply(value); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionTypes.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionTypes.java new file mode 100644 index 0000000..a68f0e3 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/option/OptionTypes.java @@ -0,0 +1,9 @@ +package com.luminia.discord.bot.settings.option; + +public interface OptionTypes { + + OptionLong RCON_ROLE = new OptionLong("rcon_role", "Rcon Role ID", 0L); + OptionString RCON_ADDRESS = new OptionString("rcon_address", "Rcon Role ID", null); + OptionInteger RCON_PORT = new OptionInteger("rcon_port", "Rcon Role ID", 19132); + OptionString RCON_PASSWORD = new OptionString("rcon_password", "Rcon Role ID", null); +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/repository/SettingsRepository.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/repository/SettingsRepository.java new file mode 100644 index 0000000..96dbb1c --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/repository/SettingsRepository.java @@ -0,0 +1,19 @@ +package com.luminia.discord.bot.settings.repository; + +import com.luminia.discord.bot.DiscordBot; +import com.luminia.discord.bot.settings.option.Option; + +import java.util.concurrent.CompletableFuture; + +public interface SettingsRepository { + + CompletableFuture setOption(long id, Option option, Object value); + + CompletableFuture getOption(long id, Option option); + + CompletableFuture getOptionOrDefault(long id, Option option, T defaultValue); + + static SettingsRepository getInstance() { + return DiscordBot.getInstance().getSettingsRepository(); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/settings/repository/SettingsRepositoryImpl.java b/bot/src/main/kotlin/com/luminia/discord/bot/settings/repository/SettingsRepositoryImpl.java new file mode 100644 index 0000000..7492c64 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/settings/repository/SettingsRepositoryImpl.java @@ -0,0 +1,72 @@ +package com.luminia.discord.bot.settings.repository; + +import com.luminia.discord.bot.settings.option.Option; +import com.mefrreex.jooq.database.IDatabase; +import org.jooq.Record; +import org.jooq.Result; +import org.jooq.Table; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; + +import java.util.concurrent.CompletableFuture; + +public class SettingsRepositoryImpl implements SettingsRepository { + + private final IDatabase database; + private final Table table; + + public SettingsRepositoryImpl(IDatabase database) { + this.database = database; + this.table = DSL.table("settings"); + + database.getConnection().thenAcceptAsync(connection -> { + DSL.using(connection) + .createTableIfNotExists(table) + .column("id", SQLDataType.BIGINT) + .column("name", SQLDataType.VARCHAR) + .column("value", SQLDataType.VARCHAR) + .execute(); + }).join(); + } + + @Override + public CompletableFuture setOption(long id, Option option, Object value) { + return database.getConnection().thenAcceptAsync(connection -> { + int updatedRows = DSL.using(connection).update(table) + .set(DSL.field("value"), value.toString()) + .where(DSL.field("id").eq(id)) + .and(DSL.field("name").eq(option.getName())) + .execute(); + if (updatedRows == 0) { + DSL.using(connection).insertInto(table) + .set(DSL.field("id"), id) + .set(DSL.field("name"), option.getName()) + .set(DSL.field("value"), value.toString()) + .execute(); + } + }); + } + + @Override + public CompletableFuture getOption(long id, Option option) { + return this.getOptionOrDefault(id, option, option.getDefaultValue()); + } + + @Override + public CompletableFuture getOptionOrDefault(long id, Option option, T defaultValue) { + return database.getConnection().thenApplyAsync(connection -> { + Result result = DSL.using(connection) + .select() + .from(table) + .where(DSL.field("id").eq(id)) + .and(DSL.field("name").eq(option.getName())) + .fetch(); + + if (result.isNotEmpty()) { + return option.asType(result.getFirst().get(DSL.field("value", String.class))); + } else { + return defaultValue; + } + }); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/translation/LanguageCode.java b/bot/src/main/kotlin/com/luminia/discord/bot/translation/LanguageCode.java new file mode 100644 index 0000000..5630d48 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/translation/LanguageCode.java @@ -0,0 +1,69 @@ +package com.luminia.discord.bot.translation; + +import java.util.HashMap; +import java.util.Map; + +public enum LanguageCode { + ENG("eng", "en_GB", "en_US"), + DEU("deu", "de_DE"), + ESP("esp", "es_ES", "es_MX"), + FRA("fra", "fr_FR", "fr_CA"), + ITA("ita", "it_IT"), + JPN("jpn", "ja_JP"), + KOR("kor", "ko_KR"), + POR("por", "pt_BR", "pt_PT"), + RUS("rus", "ru_RU"), + ZH_CN("zh_cn", "zh_CN"), + ZH_TW("zh_tw", "zh_TW"), + NLD("nld", "nl_NL"), + BGR("bgr", "bg_BG"), + CES("ces", "cs_CZ"), + DAN("dan", "da_DK"), + ELL("ell", "el_GR"), + FIN("fin", "fi_FI"), + HUN("hun", "hu_HU"), + IND("ind", "id_ID"), + NOR("nor", "nb_NO"), + POL("pol", "pl_PL"), + SLK("slk", "sk_SK"), + SWE("swe", "sv_SE"), + TRK("trk", "tr_TR"), + UKR("ukr", "uk_UA"); + + private final String name; + private final String[] codes; + + private static final Map BY_NAME = new HashMap<>(); + private static final Map BY_CODE = new HashMap<>(); + + private LanguageCode(String name, String... codes) { + this.name = name; + this.codes = codes; + } + + public String getName() { + return name; + } + + public String[] getLangCodes() { + return codes; + } + + public static LanguageCode getByName(String name) { + return BY_NAME.get(name); + } + + public static LanguageCode getByCode(String code) { + return BY_CODE.get(code); + } + + static { + for (LanguageCode languageCode : values()) { + BY_NAME.put(languageCode.getName(), languageCode); + + for (String langCode : languageCode.getLangCodes()) { + BY_CODE.put(langCode, languageCode); + } + } + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/translation/TranslationContainer.java b/bot/src/main/kotlin/com/luminia/discord/bot/translation/TranslationContainer.java new file mode 100644 index 0000000..787bf5a --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/translation/TranslationContainer.java @@ -0,0 +1,49 @@ +package com.luminia.discord.bot.translation; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class TranslationContainer { + + private final Map translations = new HashMap<>(); + + public static final TranslationContainer EMPTY = new TranslationContainer() { + + @Override + public Map getTranslations() { + return Collections.emptyMap(); + } + + @Override + public String getTranslation(LanguageCode code, String defaultValue) { + return null; + } + + @Override + public void addTranslation(LanguageCode code, String message) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasTranslation(LanguageCode code) { + return false; + } + }; + + public Map getTranslations() { + return translations; + } + + public String getTranslation(LanguageCode code, String defaultValue) { + return translations.getOrDefault(code, defaultValue); + } + + public void addTranslation(LanguageCode code, String message) { + translations.put(code, message); + } + + public boolean hasTranslation(LanguageCode code) { + return translations.containsKey(code); + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/translation/TranslationProvider.java b/bot/src/main/kotlin/com/luminia/discord/bot/translation/TranslationProvider.java new file mode 100644 index 0000000..3f46326 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/translation/TranslationProvider.java @@ -0,0 +1,43 @@ +package com.luminia.discord.bot.translation; + +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class TranslationProvider { + + private final Map containers = new HashMap<>(); + + private final static Yaml yaml = new Yaml(); + + public void init(File folder) { + for (LanguageCode code : LanguageCode.values()) { + + File file = new File(folder, code.getName() + ".yml"); + if (file.exists()) { + try (FileInputStream fileInputStream = new FileInputStream(file)) { + + Map strings = yaml.loadAs(fileInputStream, Map.class); + strings.forEach((key, message) -> containers + .computeIfAbsent(key, k -> new TranslationContainer()) + .addTranslation(code, message)); + + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public TranslationContainer getContainer(String key) { + return containers.getOrDefault(key, TranslationContainer.EMPTY); + } + + public Map getContainers() { + return containers; + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotColors.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotColors.java new file mode 100644 index 0000000..370dbaf --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotColors.java @@ -0,0 +1,12 @@ +package com.luminia.discord.bot.utils; + +import java.awt.*; + +public interface BotColors { + + Color PRIMARY = Color.decode("#9f70fd"); + + Color SUCCESS = Color.decode("#7ED7C1"); + + Color WARNING = Color.decode("#EE4266"); +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotEmoji.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotEmoji.java new file mode 100644 index 0000000..6c11e68 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotEmoji.java @@ -0,0 +1,10 @@ +package com.luminia.discord.bot.utils; + +import net.dv8tion.jda.api.entities.emoji.Emoji; + +public interface BotEmoji { + + Emoji ARROW_RIGHT_GREEN = Emoji.fromCustom("arrow_right_green", 1235329507284488265L, false); + + Emoji ARROW_LEFT_RED = Emoji.fromCustom("arrow_left_red", 1235329917156196473L, false); +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotUtils.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotUtils.java new file mode 100644 index 0000000..230d950 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/BotUtils.java @@ -0,0 +1,34 @@ +package com.luminia.discord.bot.utils; + +import net.dv8tion.jda.api.OnlineStatus; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.utils.FileUpload; + +public class BotUtils { + + public static Emoji getEmojiFromStatus(OnlineStatus status) { + return Emoji.fromUnicode( + switch (status) { + case ONLINE -> ":green_circle:"; + case IDLE -> ":yellow_circle:"; + case DO_NOT_DISTURB -> ":red_circle:"; + default -> ":black_circle:"; + } + ); + } + + public static FileUpload getFileUploadFromAttachment(Message.Attachment attachment) { + return FileUpload.fromData(attachment.getProxy().download().join(), attachment.getFileName()); + } + + public static boolean canInteract(Member member, Member interactable) { + return canInteractWithPermission(member, interactable, null); + } + + public static boolean canInteractWithPermission(Member member, Member interactable, Permission permission) { + return interactable.canInteract(member) && (permission == null || member.hasPermission(permission)); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/ConfigHelper.kt b/bot/src/main/kotlin/com/luminia/discord/bot/utils/ConfigHelper.kt new file mode 100644 index 0000000..8ba6dc0 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/ConfigHelper.kt @@ -0,0 +1,42 @@ +package com.luminia.discord.bot.utils + +import com.luminia.config.Config +import com.luminia.config.ConfigType +import java.io.File + +object ConfigHelper { + + const val CONFIG = "config.yml" + + private val configs: MutableMap = mutableMapOf() + + @JvmStatic + fun getConfig(name: String): Config? { + return configs[name] + } + + @JvmStatic + fun getConfigNotNull(name: String): Config { + return configs[name]!! + } + + @JvmStatic + fun loadConfig(name: String) { + configs[name] = ConfigType.DETECT.createOf(name); + } + + @JvmStatic + fun saveResource(target: String, replace: Boolean = false) { + saveResource(target, target, replace) + } + + @JvmStatic + fun saveResource(target: String, output: String, replace: Boolean = false) { + val resourceStream = javaClass.classLoader.getResourceAsStream(target) + val outputFile = File(output) + if (resourceStream != null && (replace || !outputFile.exists())) { + outputFile.createNewFile() + outputFile.outputStream().use { resourceStream.copyTo(it) } + } + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/TimeFormatter.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/TimeFormatter.java new file mode 100644 index 0000000..c36c965 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/TimeFormatter.java @@ -0,0 +1,74 @@ +package com.luminia.discord.bot.utils; + +import com.luminia.discord.bot.service.translation.TranslationService; + +import java.util.concurrent.TimeUnit; + +public class TimeFormatter { + + private static final String[] TIME_FORMS = { + "time-form-day", + "time-form-days", + "time-form-many-days", + "time-form-hour", + "time-form-hours", + "time-form-many-hours", + "time-form-minute", + "time-form-minutes", + "time-form-many-minutes", + "time-form-second", + "time-form-seconds", + "time-form-many-seconds" + }; + + /** + * Get time form + */ + private static String form(int form) { + return TranslationService.getInstance().translate(TIME_FORMS[form]); + } + + /** + * Formats the time in milliseconds into a string representation. + * @param millis Time in milliseconds + * @return String representation of the formatted time + */ + public static String format(long millis) { + if (millis < 1000) { + return 0 + " " + form(11); + } + + long days = TimeUnit.MILLISECONDS.toDays(millis); + long hours = TimeUnit.MILLISECONDS.toHours(millis) % 24; + long minutes = TimeUnit.MILLISECONDS.toMinutes(millis) % 60; + long seconds = TimeUnit.MILLISECONDS.toSeconds(millis) % 60; + + StringBuilder formatted = new StringBuilder(); + appendTimeUnit(formatted, days, form(0), form(1), form(2)); + appendTimeUnit(formatted, hours, form(3), form(4), form(5)); + appendTimeUnit(formatted, minutes, form(6), form(7), form(8)); + appendTimeUnit(formatted, seconds, form(9), form(10), form(11)); + + return formatted.toString().trim(); + } + + private static void appendTimeUnit(StringBuilder builder, long value, String form1, String form2, String form5) { + if (value > 0) { + builder.append(value).append(" ").append(getNounForm(value, form1, form2, form5)).append(" "); + } + } + + public static String getNounForm(long number, String form1, String form2, String form5) { + long absNumber = Math.abs(number); + if (absNumber % 100 >= 11 && absNumber % 100 <= 19) { + return form5; + } + if (absNumber % 10 == 1) { + return form1; + } + if (absNumber % 10 >= 2 && absNumber % 10 <= 4) { + return form2; + } + return form5; + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/TimeParser.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/TimeParser.java new file mode 100644 index 0000000..3a625a0 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/TimeParser.java @@ -0,0 +1,37 @@ +package com.luminia.discord.bot.utils; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TimeParser { + + private static final Pattern TIME_PATTERN = Pattern.compile("(\\d+)(mo|[smhdwy])"); + private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS; + + public static Long parse(String timeString) { + Matcher matcher = TIME_PATTERN.matcher(timeString.trim().toLowerCase()); + + if (!matcher.find()) { + return null; + } + + Long number = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2); + + return switch(unit) { + case "s" -> TimeUnit.SECONDS.toMillis(number); + case "m" -> TimeUnit.MINUTES.toMillis(number); + case "h" -> TimeUnit.HOURS.toMillis(number); + case "d" -> TimeUnit.DAYS.toMillis(number); + case "w" -> TimeUnit.DAYS.toMillis(7 * number); + case "mo" -> TimeUnit.DAYS.toMillis(30 * number); + case "y" -> TimeUnit.DAYS.toMillis(365 * number); + default -> DEFAULT_TIME_UNIT.toMillis(number); + }; + } + + public static boolean canParse(String timeString) { + return TIME_PATTERN.matcher(timeString.trim().toLowerCase()).matches(); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/extension/ButtonExtensions.kt b/bot/src/main/kotlin/com/luminia/discord/bot/utils/extension/ButtonExtensions.kt new file mode 100644 index 0000000..7ecd7fc --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/extension/ButtonExtensions.kt @@ -0,0 +1,12 @@ +package com.luminia.discord.bot.utils.extension + +import com.luminia.discord.bot.handler.impl.ButtonInteractionHandler +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent +import net.dv8tion.jda.api.interactions.components.buttons.Button + +import java.util.function.Consumer + +fun Button.setHandler(handler: Consumer): Button { + ButtonInteractionHandler.addHandler(this.id, handler) + return this +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/extension/ModalExtensions.kt b/bot/src/main/kotlin/com/luminia/discord/bot/utils/extension/ModalExtensions.kt new file mode 100644 index 0000000..dcc471a --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/extension/ModalExtensions.kt @@ -0,0 +1,12 @@ +package com.luminia.discord.bot.utils.extension + +import com.luminia.discord.bot.handler.impl.ModalInteractionHandler +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent +import net.dv8tion.jda.api.interactions.modals.Modal + +import java.util.function.Consumer + +fun Modal.setHandler(handler: Consumer): Modal { + ModalInteractionHandler.addHandler(this.id, handler) + return this +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/query/BedrockQuery.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/query/BedrockQuery.java new file mode 100644 index 0000000..e0a6014 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/query/BedrockQuery.java @@ -0,0 +1,71 @@ +package com.luminia.discord.bot.utils.query; + +import lombok.NonNull; + +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class BedrockQuery { + + private static final byte UNCONNECTED_PING = 0x01; + private static final byte[] UNCONNECTED_MESSAGE_SEQUENCE = {0x00, (byte) 0xff, (byte) 0xff, 0x00, (byte) 0xfe, (byte) 0xfe, (byte) 0xfe, (byte) 0xfe, (byte) 0xfd, (byte) 0xfd, (byte) 0xfd, (byte) 0xfd, 0x12, 0x34, 0x56, 0x78}; + + private static final Random random = new Random(); + private static long dialerId = random.nextLong(); + + private final int timeout; + + public BedrockQuery() { + this(2000); + } + + public BedrockQuery(int timeout) { + this.timeout = timeout; + } + + public CompletableFuture query(String address, int port) { + return query(new InetSocketAddress(address, port)); + } + + public CompletableFuture query(InetSocketAddress address) { + return CompletableFuture.supplyAsync(() -> { + try { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + + dataOutputStream.writeByte(UNCONNECTED_PING); + dataOutputStream.writeLong(System.currentTimeMillis() / 1000); + dataOutputStream.write(UNCONNECTED_MESSAGE_SEQUENCE); + dataOutputStream.writeLong(dialerId++); + + byte[] requestData = outputStream.toByteArray(); + byte[] responseData = new byte[1024 * 1024 * 4]; + + try (DatagramSocket socket = new DatagramSocket()) { + DatagramPacket requestPacket = new DatagramPacket(requestData, requestData.length, address.getAddress(), address.getPort()); + socket.send(requestPacket); + + DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length); + socket.setSoTimeout(timeout); + socket.receive(responsePacket); + + // MCPE;;;;;;;;;;; + String[] splittedData = new String(responsePacket.getData(), 35, responsePacket.getLength(), StandardCharsets.UTF_8).split(";"); + + int protocol = Integer.parseInt(splittedData[2]); + int playerCount = Integer.parseInt(splittedData[4]); + int maxPlayers = Integer.parseInt(splittedData[5]); + + return new BedrockQueryResponse(true, splittedData[1], protocol, splittedData[3], playerCount, maxPlayers, splittedData[7], splittedData[8]); + } + } catch (Exception e) { + return BedrockQueryResponse.empty(); + } + }); + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/query/BedrockQueryResponse.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/query/BedrockQueryResponse.java new file mode 100644 index 0000000..8710f23 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/query/BedrockQueryResponse.java @@ -0,0 +1,19 @@ +package com.luminia.discord.bot.utils.query; + +public record BedrockQueryResponse( + boolean online, + String motd, + int protocolVersion, + String minecraftVersion, + int playerCount, + int maxPlayers, + String software, + String gamemode +) { + + private static final BedrockQueryResponse EMPTY = new BedrockQueryResponse(false, "", -1, "", 0, 0, "", ""); + + public static BedrockQueryResponse empty() { + return EMPTY; + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/RconClient.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/RconClient.java new file mode 100644 index 0000000..af10c2d --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/RconClient.java @@ -0,0 +1,106 @@ +package com.luminia.discord.bot.utils.rcon; + +import com.luminia.discord.bot.utils.rcon.exception.AuthenticationException; +import com.luminia.discord.bot.utils.rcon.packet.RconPacket; +import com.luminia.discord.bot.utils.rcon.packet.RconPacketType; +import lombok.Getter; +import lombok.Setter; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + +@Getter +@Setter +public class RconClient { + + private final InetSocketAddress address; + private final Charset charset; + private SocketChannel socketChannel; + private String password; + + public RconClient(String host, int port) { + this(host, port, StandardCharsets.UTF_8); + } + + public RconClient(String host, int port, Charset charset) { + this.address = new InetSocketAddress(host, port); + this.charset = charset; + } + + /** + * Connect to the server using the provided password + * @param password Password for authentication + */ + public void connect(String password) { + this.setPassword(password); + this.connect(); + } + + /** + * Connect to the server using the stored password + * @throws AuthenticationException If authentication fails + */ + public void connect() throws AuthenticationException { + try { + socketChannel = SocketChannel.open(); + socketChannel.socket().setSoTimeout(1500); + socketChannel.connect(address); + } catch (IOException e) { + throw new AuthenticationException("Authorization error", e); + } + + RconPacket packet = new RconPacket(RconPacketType.AUTH_REQUEST); + packet.setPayload(password.getBytes()); + + RconPacket response = this.sendPacket(packet); + + if (response == null) { + throw new AuthenticationException("Server is offline"); + } + + if (response.getRequestId() == -1) { + throw new AuthenticationException("Wrong password"); + } + } + + /** + * Send a command to the server + * @param command Command to send + * @return Server response + */ + public String sendCommand(String command) { + RconPacket packet = new RconPacket(RconPacketType.COMMAND_EXECUTE); + packet.setPayload(command.getBytes(charset)); + + RconPacket response = sendPacket(packet); + return new String(response.getPayload(), charset); + } + + /** + * Send the packet to the server + * @param packet RconPacket to send + * @param socketChannel SocketChannel to use for sending + * @return A packet sent by the server in response + */ + private RconPacket sendPacket(RconPacket packet, SocketChannel socketChannel) { + try { + return packet.send(socketChannel); + } catch (IOException e) { + return null; + } + } + + /** + * Send the packet to the socketChannel + * @param packet RconPacket to send + * @return A packet sent by the server in response + */ + public RconPacket sendPacket(RconPacket packet) { + return this.sendPacket(packet, socketChannel); + } +} + diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/exception/AuthenticationException.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/exception/AuthenticationException.java new file mode 100644 index 0000000..79ca5e5 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/exception/AuthenticationException.java @@ -0,0 +1,12 @@ +package com.luminia.discord.bot.utils.rcon.exception; + +public class AuthenticationException extends RuntimeException { + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/packet/RconPacket.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/packet/RconPacket.java new file mode 100644 index 0000000..ee17096 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/packet/RconPacket.java @@ -0,0 +1,78 @@ +package com.luminia.discord.bot.utils.rcon.packet; + +import lombok.Getter; +import lombok.Setter; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; +import java.util.Random; + +@Getter +@Setter +public class RconPacket { + + private final int packetType; + private final int requestId; + private byte[] payload = new byte[1]; + + public RconPacket(int packetType) { + this(packetType, new Random().nextInt()); + } + + public RconPacket(int packetType, int requestId) { + this.packetType = packetType; + this.requestId = requestId; + } + + public RconPacket send(SocketChannel socketChannel) throws IOException { + try { + this.write(socketChannel); + } catch(IOException e) { + e.printStackTrace(); + } + + return read(socketChannel); + } + + protected void write(SocketChannel socketChannel) throws IOException { + int length = payload.length + 14; + + ByteBuffer buffer = ByteBuffer.allocate(length); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.putInt(length - 4); + buffer.putInt(requestId); + buffer.putInt(packetType); + buffer.put(payload); + + buffer.put((byte) 0); + buffer.put((byte) 0); + + socketChannel.write(ByteBuffer.wrap(buffer.array())); + } + + protected RconPacket read(SocketChannel socketChannel) throws IOException { + InputStream stream = socketChannel.socket().getInputStream(); + + ByteBuffer buffer = ByteBuffer.allocate(4 * 3); + stream.read(buffer.array()); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int length = buffer.getInt(); + int requestId = buffer.getInt(); + int type = buffer.getInt(); + + byte[] payload = new byte[length - 10]; + DataInputStream dataStream = new DataInputStream(stream); + dataStream.readFully(payload); + dataStream.read(new byte[2]); + + RconPacket packet = new RconPacket(type, requestId); + packet.setPayload(payload); + return packet; + } +} \ No newline at end of file diff --git a/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/packet/RconPacketType.java b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/packet/RconPacketType.java new file mode 100644 index 0000000..13b7161 --- /dev/null +++ b/bot/src/main/kotlin/com/luminia/discord/bot/utils/rcon/packet/RconPacketType.java @@ -0,0 +1,8 @@ +package com.luminia.discord.bot.utils.rcon.packet; + +public interface RconPacketType { + int AUTH_REQUEST = 3; + int AUTH_RESPONSE = 2; + int COMMAND_EXECUTE =2; + int COMMAND_RESPONSE = 0; +} diff --git a/bot/src/main/resources/config.yml b/bot/src/main/resources/config.yml new file mode 100644 index 0000000..51c2649 --- /dev/null +++ b/bot/src/main/resources/config.yml @@ -0,0 +1,19 @@ +# Available languages: rus (Русский) +language: "rus" + +# Discord settings +discord: + # Bot token + token: "your token here" + +# MinecraftServer command settings +minecraft-server-command: + address: "luminia.fun" + port: "19132" + image: "https://cdn.discordapp.com/attachments/1013476536923471892/1192067181601632296/portal.png?ex=662ce193&is=662b9013&hm=c3b956ec9e8b4f250a95190bd4f453b84fb521be92aeec7954e4475a2261a585&" + +# Channel with join and leave messages +welcome-messages-channel: + enable-join: true # Enable join messages + enable-leave: true # Enable leave messages + channel-id: 1013476536923471892 # Channel id \ No newline at end of file diff --git a/bot/src/main/resources/lang/rus.yml b/bot/src/main/resources/lang/rus.yml new file mode 100644 index 0000000..012d5e9 --- /dev/null +++ b/bot/src/main/resources/lang/rus.yml @@ -0,0 +1,148 @@ +# Общие сообщения +generic-command-error: "При выполнении команды произошла ошибка. Подробности см. в консоли" +generic-no-permission: "У вас нет разрешения на использование этой команды" +generic-can-not-interact: "Бот не может взаимодействовать с этим пользователем" +generic-join-message: "[0] [2] присоединился(-лась) к серверу" +generic-leave-message: "[0] [2] покинул(-а) сервер" + +# Форматтер времени +time-form-day: "день" +time-form-days: "дня" +time-form-many-days: "дней" +time-form-hour: "час" +time-form-hours: "часа" +time-form-many-hours: "часов" +time-form-minute: "минута" +time-form-minutes: "минуты" +time-form-many-minutes: "минут" +time-form-second: "секунда" +time-form-seconds: "секунды" +time-form-many-seconds: "секунд" + +# Команды + +# Команда Avatar +command-avatar-description: "Получить аватар сервера или пользователя" +command-avatar-subcommand-guild-name: "Получить аватар сервера" +command-avatar-subcommand-user-name: "Получить аватар пользователя" +command-avatar-option-user-name: "Пользователь" +command-avatar-message-guild-avatar: "Аватар сервера" +command-avatar-message-user-avatar: "Аватар пользователя [0]" + +# Команда Profile +command-profile-description: "Информация о пользователе" +command-profile-option-user-name: "Пользователь" +command-profile-embed-title: "Информация" +command-profile-embed-user-name: "Имя: `[0]`" +command-profile-embed-user-id: "Идентификатор: `[0]`" +command-profile-embed-user-id-long: "Цифровой идентификатор: `[0]`" +command-profile-embed-user-online-status: "Онлайн статус: `[0]` [1]" + +# Комнада MemberCount +command-member-count-description: "Количество участников сервера" +command-member-count-success-title: "Количество участников" +command-member-count-success-description: "На сервере находится [0] участника(ов)" + +# Комнада Members +command-members-description: "Информация о участниках сервера" +command-members-success-title: "Участники сервера" +command-members-success-members-with-role: "Участники c ролью [0]: [1]" +command-members-success-footer: "Всего участников: [0]" + +# Комнада MessageEmbed +command-embed-description: "Отправить Embed" +command-embed-subcommand-guild-name: "Аватар сервера" +command-embed-subcommand-user-name: "Аватар пользователя" +command-embed-subcommand-none-name: "Без аватара" +command-embed-option-message-name: "Сообщение" +command-embed-option-role-name: "Пинг роли" +command-embed-modal-title-name: "Embed" +command-embed-modal-input-title-name: "Заголовок" +command-embed-modal-input-title-placeholder: "Заголовок" +command-embed-modal-input-description-name: "Описание" +command-embed-modal-input-description-placeholder: "Описание" +command-embed-modal-input-color-name: "Цвет (hex)" +command-embed-modal-input-color-placeholder: "#9f70fd" +command-embed-modal-input-image-name: "Ссылка на изображение" +command-embed-modal-input-image-placeholder: "Ссылка" +command-embed-modal-input-footer-name: "Нижний текст" +command-embed-modal-input-footer-placeholder: "Нижний текст" +command-embed-message-success: "Embed отправлен!" + +# Комнада Message +command-message-description: "Отправить сообщение" +command-message-option-message-name: "Сообщение" +command-message-option-attachment-name: "Прикрепление" +command-message-message-success: "Сообщение отправлено!" + +# Команда MinecraftStatus +command-status-description: "Получить Ping-информацию сервера" +command-status-option-address-name: "Адрес сервера" +command-status-option-port-name: "Порт сервера" +command-status-embed-title: "Информация о сервере [0]:[1]" +command-status-embed-motd: "📝 Motd: `[0]`" +command-status-embed-player-count: "Количество игроков: [0]/[1]" +command-status-embed-minecraft-version: "Версия Minecraft: [0] [Версия протокола: [1]]" +command-status-embed-gamemode: "Режим игры [0]" +command-status-embed-software: "💻 Программное обеспечение: [0]" +command-status-embed-additional: "**Дополнительно**" +command-status-embed-address: "🌐 IP-адрес сервера: [0]" +command-status-embed-server-offline: "Сервер не вернул ответ" +command-status-message-no-address-given: "Вы не указали адрес сервера" + +# Команда MinecraftServer +command-server-description: "Майнкрафт сервер" +command-server-embed-title: "Информация о сервере" +command-server-embed-server-information: "💻 **Данные сервера**" +command-server-embed-address: "Адрес сервера: `[0]`" +command-server-embed-port: "Порт сервера `[0]`" +command-server-embed-current-information: "📈 **Текущая информация**" +command-server-embed-player-count: "Онлайн сервера: [0] из [1]" +command-server-embed-minecraft-version: "Версия Minecraft: [0]" +command-server-embed-server-offline: "Сервер в данный момент оффлайн" +command-server-embed-site: "Также не забывайте, что у сервера есть свой сайт: https://luminia.fun" + +# Команда Site +command-site-description: "Сайт сервера" +command-site-message-success: "🔎 Сайт сервера https://luminia.fun" + +command-rcon-description: "Отправить команду через RCON на сервер" +command-rcon-subcommand-command-name: "Отправить команду" +command-rcon-subcommand-setrole-name: "Установить роль для доступа к RCON" +command-rcon-subcommand-setrcon-name: "Установить данные для RCON" +command-rcon-option-command-name: "Команда" +command-rcon-option-role-name: "Роль" +command-rcon-option-address-name: "Адрес" +command-rcon-option-port-name: "Порт" +command-rcon-option-password-name: "Пароль" +command-rcon-message-role-set: "Роль для RCON установлена" +command-rcon-message-rcon-set: "Данные для RCON установлены" +command-rcon-message-role-not-found: "Роль для доступа к RCON не найдена" +command-rcon-message-rcon-not-set: "Данные для RCON не установлены" +command-rcon-message-server-offline: "Сервер оффлайн" +command-rcon-message-auth-error: "Ошибка авторизации ([0]). Подробности см. в консоли" +command-rcon-embed-title: "Ответ сервера" +command-rcon-embed-response-empty: "Сервер не вернул ответа" +command-rcon-button-resend: "Отправить повторно" + +# Команда Timeout +command-timeout-description: "Выдать тайм-аут" +command-timeout-subcommand-add-name: "Выдать тайм-аут" +command-timeout-subcommand-remove-name: "Снять тайм-аут" +command-timeout-option-user-name: "Пользователь" +command-timeout-option-duration-name: "Продолжительность" +command-timeout-message-invalid-duration: "Продолжительность должна быть в таком формате: 15m (15 минут), 1d (1 день), 1w (1 неделя)" +command-timeout-message-duration-limit: "Максимальная продолжительность тайм-аута 28 дней" +command-timeout-message-timeout-added: "Пользователю [0] выдан тайм-аут на [1]" +command-timeout-message-timeout-removed: "С пользователя [0] снят тайм-аут" + +# Команда SetName +command-setname-description: "Установить имя пользователю" +command-setname-option-user-name: "Пользователь" +command-setname-option-name-name: "Имя" +command-setname-message-changed: "Имя пользователя [0] изменено на [1]" + +# Другое +timeout-embed-title: "Тайм-аут" +timeout-embed-description-add: "Вам был выдан тайм-аут на **[0]** на сервере **[1]**" +timeout-embed-description-remove: "Вам был снят тайм-аут на сервере **[0]**" \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..a80c10c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + `java-library` + `maven-publish` + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +java.sourceCompatibility = JavaVersion.VERSION_21 + +tasks.build { + dependsOn(tasks.shadowJar) +} + +allprojects { + group = "com.mefrreex" + description = "discordbot" + version = "2.2.3" +} + +subprojects { + + apply { + plugin("java-library") + } + + repositories { + mavenLocal() + maven("https://repo.maven.apache.org/maven2/") + } + + dependencies { + api("net.dv8tion:JDA:5.0.0-beta.23") + compileOnlyApi("org.projectlombok:lombok:1.18.30") + annotationProcessor("org.projectlombok:lombok:1.18.30") + } + + tasks.withType { + options.encoding = "UTF-8" + } + + tasks.withType { + options.encoding = "UTF-8" + } +} \ No newline at end of file diff --git a/config/build.gradle.kts b/config/build.gradle.kts new file mode 100644 index 0000000..1f1a6a1 --- /dev/null +++ b/config/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("java") +} + +dependencies { + api("org.spongepowered:configurate-yaml:4.1.2") + api("org.spongepowered:configurate-gson:4.1.2") + api("org.spongepowered:configurate-hocon:4.1.2") + api("com.google.guava:guava:32.1.2-jre") +} + +tasks.withType { + archiveFileName.set("Luminia-Discord-Bot-Config-${project.version}.jar") +} \ No newline at end of file diff --git a/config/src/main/java/com/luminia/config/Config.java b/config/src/main/java/com/luminia/config/Config.java new file mode 100644 index 0000000..cd769da --- /dev/null +++ b/config/src/main/java/com/luminia/config/Config.java @@ -0,0 +1,64 @@ +package com.luminia.config; + +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.loader.ConfigurationLoader; +import lombok.Getter; + +import java.io.File; +import java.util.LinkedHashMap; + +public abstract class Config { + + private final @Getter File file; + private final @Getter ConfigurationLoader loader; + + private ConfigurationNode root; + + public Config(File file, ConfigurationLoader loader) { + this.file = file; + this.loader = loader; + this.root = this.load(); + } + + public ConfigurationNode load() { + try { + return loader.load(); + } catch (ConfigurateException e) { + throw new RuntimeException("Failed to load Config " + file, e); + } + } + + public void save() { + try { + loader.save(root); + } catch (ConfigurateException e) { + throw new RuntimeException("Failed to save Config " + file, e); + } + } + + public ConfigNode root() { + return new ConfigNode(root); + } + + public void setRoot(ConfigurationNode node) { + this.root = node; + } + + public void setRoot(ConfigNode node) { + this.setRoot(node.getConfigurationNode()); + } + + public void setRoot(LinkedHashMap values) { + this.root = loader.createNode(); + values.forEach((key, value) -> this.root().nodes(key).setValue(value)); + } + + public ConfigNode node(Object... path) { + return this.root().node(path); + } + + public ConfigNode nodes(String fullPath) { + return this.root().nodes(fullPath); + } +} diff --git a/config/src/main/java/com/luminia/config/ConfigNode.java b/config/src/main/java/com/luminia/config/ConfigNode.java new file mode 100644 index 0000000..b6390f6 --- /dev/null +++ b/config/src/main/java/com/luminia/config/ConfigNode.java @@ -0,0 +1,182 @@ +package com.luminia.config; + +import com.luminia.config.utils.NumberParser; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class ConfigNode { + + private final ConfigurationNode node; + + public ConfigNode(ConfigurationNode node) { + this.node = node; + } + + public ConfigurationNode getConfigurationNode() { + return node; + } + + public ConfigNode node(Object... path) { + return new ConfigNode(node.node(path)); + } + + public ConfigNode nodes(String fullPath) { + String[] pathArray = fullPath.split("\\."); + ConfigurationNode currentNode = node; + + for (String path : pathArray) { + currentNode = currentNode.node(path); + } + + return new ConfigNode(currentNode); + } + + public Object getValue() { + return getValue(null); + } + + @SuppressWarnings("unchecked") + public T getValue(T defaultValue) { + Object rawValue = this.raw(); + return (rawValue != null) ? (T) rawValue : defaultValue; + } + + public T getValue(Class classOfT) { + try { + return node.get(classOfT); + } catch (Exception e) { + return null; + } + } + + public void setValue(Object value) { + try { + node.set(value); + } catch (SerializationException e) { + throw new RuntimeException("Failed to set node value " + this); + } + } + + public Object raw() { + return node.raw(); + } + + public boolean isNull() { + return this.raw() == null; + } + + public String asString() { + return asString(null); + } + + public String asString(String defaultValue) { + return String.valueOf(getValue(defaultValue)); + } + + public boolean isString() { + return this.raw() instanceof String; + } + + public Integer asInt() { + return asInt(0); + } + + public Integer asInt(Integer defaultValue) { + return NumberParser.parseInteger(String.valueOf(getValue(defaultValue))); + } + + public boolean isInt() { + return this.raw() instanceof Integer; + } + + public Long asLong() { + return asLong(0L); + } + + public Long asLong(Long defaultValue) { + return NumberParser.parseLong(String.valueOf(getValue(defaultValue))); + } + + public boolean isLong() { + return this.raw() instanceof Long; + } + + public Double asDouble() { + return asDouble(0D); + } + + public Double asDouble(Double defaultValue) { + return NumberParser.parseDouble(String.valueOf(getValue(defaultValue))); + } + + public boolean isDouble() { + return this.raw() instanceof Double; + } + + public Float asFloat() { + return asFloat(0F); + } + + public Float asFloat(Float defaultValue) { + return NumberParser.parseFloat(String.valueOf(getValue(defaultValue))); + } + + public boolean isFloat() { + return this.raw() instanceof Float; + } + + public Boolean asBoolean() { + return asBoolean(false); + } + + public Boolean asBoolean(Boolean defaultValue) { + return getValue(defaultValue); + } + + public boolean isBoolean() { + return this.raw() instanceof Boolean; + } + + public List asList() { + return asList(Collections.emptyList()); + } + + public List asList(List defaultValue) { + return getValue(defaultValue); + } + + public boolean isList() { + return node.isList(); + } + + public List asStringList() { + return asStringList(Collections.emptyList()); + } + + public List asStringList(List defaultValue) { + return getValue(defaultValue); + } + + public Map asMap() { + return asMap(Collections.emptyMap()); + } + + @SuppressWarnings("unchecked") + public Map asMap(Map defaultValue) { + Object value = this.raw(); + return value instanceof Map ? (Map) value : defaultValue; + } + + public boolean isMap() { + return node.isMap(); + } + + @Override + public String toString() { + return node.toString(); + } +} \ No newline at end of file diff --git a/config/src/main/java/com/luminia/config/ConfigType.java b/config/src/main/java/com/luminia/config/ConfigType.java new file mode 100644 index 0000000..f5ba777 --- /dev/null +++ b/config/src/main/java/com/luminia/config/ConfigType.java @@ -0,0 +1,41 @@ +package com.luminia.config; + +import com.luminia.config.utils.FileUtils; +import com.google.common.collect.ImmutableMap; + +import java.io.File; +import java.util.Map; + +public enum ConfigType { + DETECT, + YAML, + JSON, + HOCON; + + public static final Map EXTENSIONS = ImmutableMap.builder() + .put("yaml", YAML) + .put("yml", YAML) + .put("json", JSON) + .put("conf", HOCON) + .build(); + + public Config createOf(String fileName) { + return createOf(new File(fileName)); + } + + public Config createOf(File file) { + String extension = FileUtils.getExtension(file).toLowerCase(); + if (this == DETECT && EXTENSIONS.containsKey(extension)) { + return switch (EXTENSIONS.get(extension)) { + case YAML -> new YamlConfig(file); + case JSON -> new JsonConfig(file); + default -> null; + }; + } else if (this == YAML) { + return new YamlConfig(file); + } else if (this == JSON) { + return new JsonConfig(file); + } + return null; + } +} diff --git a/config/src/main/java/com/luminia/config/HoconConfig.java b/config/src/main/java/com/luminia/config/HoconConfig.java new file mode 100644 index 0000000..4ba46b3 --- /dev/null +++ b/config/src/main/java/com/luminia/config/HoconConfig.java @@ -0,0 +1,18 @@ +package com.luminia.config; + +import org.spongepowered.configurate.hocon.HoconConfigurationLoader; + +import java.io.File; + +public class HoconConfig extends Config { + + public HoconConfig(File file) { + super(file, HoconConfigurationLoader.builder() + .file(file) + .build()); + } + + public HoconConfig(String fileName) { + this(new File(fileName)); + } +} \ No newline at end of file diff --git a/config/src/main/java/com/luminia/config/JsonConfig.java b/config/src/main/java/com/luminia/config/JsonConfig.java new file mode 100644 index 0000000..4ddcd54 --- /dev/null +++ b/config/src/main/java/com/luminia/config/JsonConfig.java @@ -0,0 +1,18 @@ +package com.luminia.config; + +import org.spongepowered.configurate.gson.GsonConfigurationLoader; + +import java.io.File; + +public class JsonConfig extends Config { + + public JsonConfig(File file) { + super(file, GsonConfigurationLoader.builder() + .file(file) + .build()); + } + + public JsonConfig(String fileName) { + this(new File(fileName)); + } +} diff --git a/config/src/main/java/com/luminia/config/YamlConfig.java b/config/src/main/java/com/luminia/config/YamlConfig.java new file mode 100644 index 0000000..e9098c0 --- /dev/null +++ b/config/src/main/java/com/luminia/config/YamlConfig.java @@ -0,0 +1,21 @@ +package com.luminia.config; + +import org.spongepowered.configurate.yaml.NodeStyle; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.File; + +public class YamlConfig extends Config { + + public YamlConfig(File file) { + super(file, YamlConfigurationLoader.builder() + .file(file) + .nodeStyle(NodeStyle.BLOCK) + .indent(2) + .build()); + } + + public YamlConfig(String fileName) { + this(new File(fileName)); + } +} diff --git a/config/src/main/java/com/luminia/config/utils/FileUtils.java b/config/src/main/java/com/luminia/config/utils/FileUtils.java new file mode 100644 index 0000000..4057070 --- /dev/null +++ b/config/src/main/java/com/luminia/config/utils/FileUtils.java @@ -0,0 +1,15 @@ +package com.luminia.config.utils; + +import java.io.File; + +public class FileUtils { + + public static String getExtension(File file) { + String fileName = file.getName(); + int lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex != -1 && lastDotIndex != 0) { + return fileName.substring(lastDotIndex + 1); + } + return ""; + } +} \ No newline at end of file diff --git a/config/src/main/java/com/luminia/config/utils/NumberParser.java b/config/src/main/java/com/luminia/config/utils/NumberParser.java new file mode 100644 index 0000000..1f38953 --- /dev/null +++ b/config/src/main/java/com/luminia/config/utils/NumberParser.java @@ -0,0 +1,37 @@ +package com.luminia.config.utils; + +public class NumberParser { + + public static Integer parseInteger(String string) { + try { + return Integer.parseInt(string); + } catch (NumberFormatException e) { + return null; + } + } + + public static Long parseLong(String string) { + try { + return Long.parseLong(string); + } catch (NumberFormatException e) { + return null; + } + } + + public static Double parseDouble(String string) { + try { + return Double.parseDouble(string); + } catch (NumberFormatException e) { + return null; + } + } + + public static Float parseFloat(String string) { + try { + return Float.parseFloat(string); + } catch (NumberFormatException e) { + return null; + } + } +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0c01479 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 25 21:07:29 EEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2930ebf --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" +} + +rootProject.name = "Luminia-Discord-Bot" + +include("api") +include("config") +include("bot")