diff --git a/.gitignore b/.gitignore index 0591374..7e8f5e6 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,12 @@ fabric.properties # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -# End of https://www.toptal.com/developers/gitignore/api/intellij,java \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/intellij,java +/.idea/copyright/*.xml +/.idea/.gitignore +/.idea/compiler.xml +/.idea/gradle.xml +/.idea/jarRepositories.xml +/.idea/misc.xml +/.idea/uiDesigner.xml +/.idea/vcs.xml diff --git a/build.gradle b/build.gradle index 3671d80..f349a3a 100644 --- a/build.gradle +++ b/build.gradle @@ -17,14 +17,13 @@ */ plugins { - id 'java' - id 'application' - id 'com.github.johnrengelman.shadow' version '5.2.0' - id 'idea' + id "java" + id "application" + id "com.github.johnrengelman.shadow" version "8.1.1" } group 'io.codemc' -version '2.0.0' +version '2.1.0' compileJava.options.encoding('UTF-8') @@ -32,16 +31,17 @@ repositories { mavenCentral() maven { url = 'https://jitpack.io' } maven { url = 'https://repo.codemc.io/repository/maven-public' } - maven { url = 'https://m2.chew.pro/snapshots' } + maven { url = 'https://m2.chew.pro/releases' } } dependencies { implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14' - implementation(group: 'net.dv8tion', name: 'JDA', version:'5.0.0-beta.23'){ + implementation(group: 'net.dv8tion', name: 'JDA', version:'5.0.0'){ exclude(module: 'opus-java') } - implementation group: 'pw.chew', name: 'jda-chewtils-commons', version: '2.0-SNAPSHOT' - implementation group: 'pw.chew', name: 'jda-chewtils-command', version: '2.0-SNAPSHOT' + implementation group: 'pw.chew', name: 'jda-chewtils-commons', version: '2.0' + implementation group: 'pw.chew', name: 'jda-chewtils-command', version: '2.0' + implementation group: 'org.spongepowered', name: 'configurate-gson', version: '4.1.2' } artifacts { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a9715..b38ed1f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun Apr 28 16:10:49 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/io/codemc/bot/CodeMCBot.java b/src/main/java/io/codemc/bot/CodeMCBot.java index 6332c53..0bcdc0d 100644 --- a/src/main/java/io/codemc/bot/CodeMCBot.java +++ b/src/main/java/io/codemc/bot/CodeMCBot.java @@ -18,14 +18,11 @@ package io.codemc.bot; -import com.jagrosh.jdautilities.command.CommandClient; import com.jagrosh.jdautilities.command.CommandClientBuilder; -import io.codemc.bot.commands.CmdApplication; -import io.codemc.bot.commands.CmdDisable; -import io.codemc.bot.commands.CmdMsg; -import io.codemc.bot.commands.CmdSubmit; +import io.codemc.bot.commands.*; +import io.codemc.bot.config.ConfigHandler; +import io.codemc.bot.listeners.ButtonListener; import io.codemc.bot.listeners.ModalListener; -import io.codemc.bot.utils.Constants; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.requests.GatewayIntent; @@ -34,37 +31,76 @@ import org.slf4j.LoggerFactory; import javax.security.auth.login.LoginException; +import java.util.List; public class CodeMCBot{ - private final Logger LOG = LoggerFactory.getLogger(CodeMCBot.class); + private final Logger logger = LoggerFactory.getLogger(CodeMCBot.class); + private final ConfigHandler configHandler = new ConfigHandler(); public static void main(String[] args){ try{ - new CodeMCBot().start(args[0]); + new CodeMCBot().start(); }catch(LoginException ex){ - new CodeMCBot().LOG.error("Unable to login to Discord!", ex); + new CodeMCBot().logger.error("Unable to login to Discord!", ex); } } - private void start(String token) throws LoginException{ - CommandClient commandClient = new CommandClientBuilder() - .setOwnerId( - "204232208049766400" // Andre_601#0601 - ) - .setCoOwnerIds( - "143088571656437760", // sgdc3#0001 - "282975975954710528" // tr7zw#4005 - ) - .setActivity(null) - .addSlashCommands( - new CmdApplication(), - new CmdDisable(), - new CmdMsg(), - new CmdSubmit() - ).forceGuildOnly(Constants.SERVER) - .build(); + private void start() throws LoginException{ + if(!configHandler.loadConfig()){ + logger.warn("Unable to load config.json! See previous logs for any errors."); + System.exit(1); + return; + } + + String token = configHandler.getString("bot_token"); + if(token == null || token.isEmpty()){ + logger.warn("Received invalid Bot Token!"); + System.exit(1); + return; + } + + long owner = configHandler.getLong("users", "owner"); + if(owner == -1L){ + logger.warn("Unable to retrieve Owner ID. This value is required!"); + System.exit(1); + return; + } + + long guildId = configHandler.getLong("server"); + if(guildId == -1L){ + logger.warn("Unable to retrieve Server ID. This value is required!"); + System.exit(1); + return; + } + + CommandClientBuilder clientBuilder = new CommandClientBuilder().setActivity(null).forceGuildOnly(guildId); + + clientBuilder.setOwnerId(owner); + List coOwners = configHandler.getLongList("users", "co_owners"); + + if(coOwners != null && !coOwners.isEmpty()){ + logger.info("Adding {} Co-Owner(s) to the bot.", coOwners.size()); + // Annoying, but setCoOwnerIds has no overload with a Collection... + long[] coOwnerIds = new long[coOwners.size()]; + for(int i = 0; i < coOwnerIds.length; i++){ + coOwnerIds[i] = coOwners.get(i); + } + + clientBuilder.setCoOwnerIds(coOwnerIds); + } + + logger.info("Adding commands..."); + clientBuilder.addSlashCommands( + new CmdApplication(this), + new CmdDisable(this), + new CmdMsg(this), + new CmdReload(this), + new CmdSubmit(this) + ); + + logger.info("Starting bot..."); JDABuilder.createDefault(token) .enableIntents( GatewayIntent.GUILD_MEMBERS, @@ -77,9 +113,14 @@ private void start(String token) throws LoginException{ "Applications" )) .addEventListeners( - commandClient, - new ModalListener() + clientBuilder.build(), + new ButtonListener(this), + new ModalListener(this) ) .build(); } + + public ConfigHandler getConfigHandler(){ + return configHandler; + } } diff --git a/src/main/java/io/codemc/bot/commands/BotCommand.java b/src/main/java/io/codemc/bot/commands/BotCommand.java new file mode 100644 index 0000000..81a5609 --- /dev/null +++ b/src/main/java/io/codemc/bot/commands/BotCommand.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024 CodeMC.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.codemc.bot.commands; + +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.codemc.bot.CodeMCBot; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.interactions.InteractionHook; + +import java.util.ArrayList; +import java.util.List; + +public abstract class BotCommand extends SlashCommand{ + + protected List allowedRoles = new ArrayList<>(); + protected boolean hasModalReply = false; + + public final CodeMCBot bot; + + public BotCommand(CodeMCBot bot){ + this.bot = bot; + } + + @Override + public void execute(SlashCommandEvent event){ + Guild guild = event.getGuild(); + if(guild == null){ + CommandUtil.EmbedReply.from(event) + .error("Command can only be executed in a Server!") + .send(); + return; + } + + if(guild.getIdLong() != bot.getConfigHandler().getLong("server")){ + CommandUtil.EmbedReply.from(event) + .error("Unable to find CodeMC Server!") + .send(); + return; + } + + Member member = event.getMember(); + if(member == null){ + CommandUtil.EmbedReply.from(event) + .error("Unable to retrieve Member from Event!") + .send(); + return; + } + + if(!CommandUtil.hasRole(member, allowedRoles)){ + CommandUtil.EmbedReply.from(event) + .error("You lack the permissions required to use this command!") + .send(); + return; + } + + if(hasModalReply){ + withModalReply(event); + }else{ + event.deferReply(true).queue(hook -> withHookReply(hook, event, guild, member)); + } + } + + public abstract void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member); + + public abstract void withModalReply(SlashCommandEvent event); +} diff --git a/src/main/java/io/codemc/bot/commands/CmdApplication.java b/src/main/java/io/codemc/bot/commands/CmdApplication.java index 0971b85..301ff45 100644 --- a/src/main/java/io/codemc/bot/commands/CmdApplication.java +++ b/src/main/java/io/codemc/bot/commands/CmdApplication.java @@ -20,10 +20,9 @@ import com.jagrosh.jdautilities.command.SlashCommand; import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.codemc.bot.CodeMCBot; import io.codemc.bot.utils.CommandUtil; -import io.codemc.bot.utils.Constants; import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; @@ -43,60 +42,54 @@ import java.util.List; import java.util.regex.Pattern; -public class CmdApplication extends SlashCommand{ +public class CmdApplication extends BotCommand{ - public CmdApplication(){ + public CmdApplication(CodeMCBot bot){ + super(bot); + this.name = "application"; this.help = "Accept or deny applications."; - this.userPermissions = new Permission[]{ - Permission.MANAGE_SERVER - }; + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "application"); this.children = new SlashCommand[]{ - new Accept(), - new Deny() + new Accept(bot), + new Deny(bot) }; } @Override - protected void execute(SlashCommandEvent event){} + public void withModalReply(SlashCommandEvent event){} - private static void handle(InteractionHook hook, Guild guild, long messageId, String str, boolean accepted){ - if(guild == null || !guild.getId().equals(Constants.SERVER)){ - CommandUtil.EmbedReply.fromHook(hook).withError("Unable to retrieve Server!").send(); - return; - } - - TextChannel requestChannel = guild.getTextChannelById(Constants.REQUEST_ACCESS); + @Override + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){} + + public static void handle(CodeMCBot bot, InteractionHook hook, Guild guild, long messageId, String str, boolean accepted){ + TextChannel requestChannel = guild.getTextChannelById(bot.getConfigHandler().getLong("channel", "request_access")); if(requestChannel == null){ - CommandUtil.EmbedReply.fromHook(hook).withError("Unable to retrieve `request-access` channel.").send(); + CommandUtil.EmbedReply.from(hook).error("Unable to retrieve `request-access` channel.").send(); return; } requestChannel.retrieveMessageById(messageId).queue(message -> { List embeds = message.getEmbeds(); if(embeds.isEmpty()){ - CommandUtil.EmbedReply.fromHook(hook).withError("Provided message does not have any embeds.").send(); + CommandUtil.EmbedReply.from(hook).error("Provided message does not have any embeds.").send(); return; } MessageEmbed embed = embeds.get(0); if(embed.getFooter() == null || embed.getFields().isEmpty()){ - CommandUtil.EmbedReply.fromHook(hook).withError( - "Embed does not have a footer or any Embed Fields." - ).send(); + CommandUtil.EmbedReply.from(hook).error("Embed does not have a footer or any Embed Fields.").send(); return; } String userId = embed.getFooter().getText(); if(userId == null || userId.isEmpty()){ - CommandUtil.EmbedReply.fromHook(hook).withError("Embed does not have a valid footer.").send(); + CommandUtil.EmbedReply.from(hook).error("Embed does not have a valid footer.").send(); return; } - System.out.println("Embed Footer text: " + userId); - String userLink = null; String repoLink = null; for(MessageEmbed.Field field : embed.getFields()){ @@ -112,19 +105,22 @@ private static void handle(InteractionHook hook, Guild guild, long messageId, St } if(userLink == null || repoLink == null){ - CommandUtil.EmbedReply.fromHook(hook).withError("Embed does not have any valid Fields.").send(); + CommandUtil.EmbedReply.from(hook).error("Embed does not have any valid Fields.").send(); return; } - TextChannel channel = guild.getTextChannelById(accepted ? Constants.ACCEPTED_REQUESTS : Constants.REJECTED_REQUESTS); + TextChannel channel = guild.getTextChannelById(accepted + ? bot.getConfigHandler().getLong("channels", "accepted_requests") + : bot.getConfigHandler().getLong("channels", "rejected_requests") + ); if(channel == null){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Unable to retrieve `" + (accepted ? "accepted" : "rejected") + "-requests` channel.") + CommandUtil.EmbedReply.from(hook) + .error("Unable to retrieve `" + (accepted ? "accepted" : "rejected") + "-requests` channel.") .send(); return; } - channel.sendMessage(getMessage(userId, userLink, repoLink, str, accepted)).queue(m -> { + channel.sendMessage(getMessage(bot, userId, userLink, repoLink, str, accepted)).queue(m -> { ThreadChannel thread = message.getStartedThread(); if(thread != null && !thread.isArchived()){ thread.getManager().setArchived(true) @@ -137,24 +133,23 @@ private static void handle(InteractionHook hook, Guild guild, long messageId, St Member member = guild.getMemberById(userId); if(!accepted){ - CommandUtil.EmbedReply.fromHook(hook) - .withMessage("Denied Application of " + (member == null ? "Unknown" : member.getUser().getEffectiveName()) + "!") - .asSuccess() + CommandUtil.EmbedReply.from(hook) + .success("Denied Application of " + (member == null ? "Unknown" : member.getUser().getEffectiveName()) + "!") .send(); return; } - Role authorRole = guild.getRoleById(Constants.ROLE_AUTHOR); + Role authorRole = guild.getRoleById(bot.getConfigHandler().getLong("author_role")); if(authorRole == null){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Unable to retrieve Author Role!") + CommandUtil.EmbedReply.from(hook) + .error("Unable to retrieve Author Role!") .send(); return; } if(member == null){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Unable to apply Role. Member not found!") + CommandUtil.EmbedReply.from(hook) + .error("Unable to apply Role. Member not found!") .send(); return; } @@ -162,15 +157,14 @@ private static void handle(InteractionHook hook, Guild guild, long messageId, St guild.addRoleToMember(member, authorRole) .reason("[Access Request] Application accepted.") .queue( - v -> CommandUtil.EmbedReply.fromHook(hook) - .withMessage("Accepted application of " + member.getUser().getEffectiveName() + "!") - .asSuccess() + v -> CommandUtil.EmbedReply.from(hook) + .success("Accepted application of " + member.getUser().getEffectiveName() + "!") .send(), new ErrorHandler() .handle( ErrorResponse.MISSING_PERMISSIONS, - e -> CommandUtil.EmbedReply.fromHook(hook) - .withIssue("I lack the `Manage Roles` permission to apply the role.") + e -> CommandUtil.EmbedReply.from(hook) + .appendWarning("I lack the `Manage Roles` permission to apply the role.") .send() ) ); @@ -178,10 +172,13 @@ private static void handle(InteractionHook hook, Guild guild, long messageId, St }); } - private static MessageCreateData getMessage(String userId, String userLink, String repoLink, String str, boolean accepted){ + private static MessageCreateData getMessage(CodeMCBot bot, String userId, String userLink, String repoLink, String str, boolean accepted){ + + String msg = String.join("\n", bot.getConfigHandler().getStringList("messages", (accepted ? "accepted" : "denied"))); + MessageEmbed embed = new EmbedBuilder() .setColor(accepted ? 0x00FF00 : 0xFF0000) - .setDescription(accepted ? Constants.ACCEPTED_MSG : Constants.REJECTED_MSG) + .setDescription(msg) .addField("User/Organisation:", userLink, true) .addField("Repository:", repoLink, true) .addField(accepted ? "New Project:" : "Reason:", str, false) @@ -193,17 +190,17 @@ private static MessageCreateData getMessage(String userId, String userLink, Stri .build(); } - private static class Accept extends SlashCommand{ + private static class Accept extends BotCommand{ private final Pattern projectUrlPattern = Pattern.compile("^https://ci\\.codemc\\.io/job/[a-zA-Z0-9-]+/job/[a-zA-Z0-9-_.]+/?$"); - public Accept(){ + public Accept(CodeMCBot bot){ + super(bot); + this.name = "accept"; this.help = "Accept an application"; - this.userPermissions = new Permission[]{ - Permission.MANAGE_SERVER - }; + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "application"); this.options = Arrays.asList( new OptionData(OptionType.STRING, "id", "The message id of the application.").setRequired(true), @@ -212,7 +209,10 @@ public Accept(){ } @Override - protected void execute(SlashCommandEvent event){ + public void withModalReply(SlashCommandEvent event){} + + @Override + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){ long messageId = event.getOption("id", -1L, option -> { try{ return Long.parseLong(option.getAsString()); @@ -223,32 +223,30 @@ protected void execute(SlashCommandEvent event){ String projectUrl = event.getOption("project-url", null, OptionMapping::getAsString); if(messageId == -1L || projectUrl == null){ - CommandUtil.EmbedReply.fromCommandEvent(event).withError( - "Message ID or Project URL were not present!" - ).send(); + CommandUtil.EmbedReply.from(hook).error("Message ID or Project URL were not present!").send(); return; } if(!projectUrlPattern.matcher(projectUrl).matches()){ - CommandUtil.EmbedReply.fromCommandEvent(event).withError( - "The provided Project URL did not match the pattern `https://ci.codemc.io/job//job/`!" - ).send(); + CommandUtil.EmbedReply.from(hook).error( + "The provided Project URL did not match the pattern `https://ci.codemc.io/job//job/`!") + .send(); return; } - event.deferReply(true).queue(hook -> handle(hook, event.getGuild(), messageId, projectUrl, true)); + handle(bot, hook, guild, messageId, projectUrl, true); } } - private static class Deny extends SlashCommand{ + private static class Deny extends BotCommand{ - public Deny(){ + public Deny(CodeMCBot bot){ + super(bot); + this.name = "deny"; this.help = "Deny an application"; - this.userPermissions = new Permission[]{ - Permission.MANAGE_SERVER - }; + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "application"); this.options = Arrays.asList( new OptionData(OptionType.STRING, "id", "The message id of the application.").setRequired(true), @@ -257,24 +255,19 @@ public Deny(){ } @Override - protected void execute(SlashCommandEvent event){ - long messageId = event.getOption("id", -1L, option -> { - try{ - return Long.parseLong(option.getAsString()); - }catch(NumberFormatException ex){ - return -1L; - } - }); + public void withModalReply(SlashCommandEvent event){} + + @Override + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){ + long messageId = event.getOption("id", -1L, OptionMapping::getAsLong); String reason = event.getOption("reason", null, OptionMapping::getAsString); if(messageId == -1L || reason == null){ - CommandUtil.EmbedReply.fromCommandEvent(event).withError( - "Message ID or Reason were not present!" - ).send(); + CommandUtil.EmbedReply.from(hook).error("Message ID or Reason were not present!").send(); return; } - event.deferReply(true).queue(hook -> handle(hook, event.getGuild(), messageId, reason, false)); + handle(bot, hook, guild, messageId, reason, false); } } } diff --git a/src/main/java/io/codemc/bot/commands/CmdDisable.java b/src/main/java/io/codemc/bot/commands/CmdDisable.java index 27ee9f9..12121f3 100644 --- a/src/main/java/io/codemc/bot/commands/CmdDisable.java +++ b/src/main/java/io/codemc/bot/commands/CmdDisable.java @@ -18,31 +18,36 @@ package io.codemc.bot.commands; -import com.jagrosh.jdautilities.command.SlashCommand; import com.jagrosh.jdautilities.command.SlashCommandEvent; -import net.dv8tion.jda.api.Permission; +import io.codemc.bot.CodeMCBot; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.interactions.InteractionHook; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class CmdDisable extends SlashCommand{ +public class CmdDisable extends BotCommand{ private final Logger logger = LoggerFactory.getLogger("Shutdown"); - public CmdDisable(){ + public CmdDisable(CodeMCBot bot){ + super(bot); + this.name = "disable"; this.help = "Disables the bot."; - - this.userPermissions = new Permission[]{ - Permission.MANAGE_SERVER - }; + + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "disable"); } @Override - protected void execute(SlashCommandEvent event){ - event.reply("Disabling bot...").setEphemeral(true).queue(m -> { - logger.info("Received disable command by {}.", event.getUser().getEffectiveName()); - logger.info("Disabling bot..."); + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){ + hook.editOriginalEmbeds(CommandUtil.getEmbed().setColor(0x00FF00).setDescription("Bot disabled!").build()).queue(h -> { + logger.info("Bot disabled by {}", event.getUser().getEffectiveName()); System.exit(0); }); } + + @Override + public void withModalReply(SlashCommandEvent event){} } diff --git a/src/main/java/io/codemc/bot/commands/CmdMsg.java b/src/main/java/io/codemc/bot/commands/CmdMsg.java index 5574c3a..e99e35c 100644 --- a/src/main/java/io/codemc/bot/commands/CmdMsg.java +++ b/src/main/java/io/codemc/bot/commands/CmdMsg.java @@ -20,12 +20,15 @@ import com.jagrosh.jdautilities.command.SlashCommand; import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.codemc.bot.CodeMCBot; import io.codemc.bot.utils.CommandUtil; -import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.ChannelType; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; @@ -36,34 +39,38 @@ import java.util.Arrays; -public class CmdMsg extends SlashCommand{ +public class CmdMsg extends BotCommand{ - public CmdMsg(){ + public CmdMsg(CodeMCBot bot){ + super(bot); + this.name = "msg"; this.help = "Sends a message in a specified channel or edits one."; - - this.userPermissions = new Permission[]{ - Permission.MANAGE_SERVER - }; + + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "msg"); this.children = new SlashCommand[]{ - new Post(), - new Edit() + new Post(bot), + new Edit(bot) }; } @Override - protected void execute(SlashCommandEvent event){} + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){} - private static class Post extends SlashCommand{ + @Override + public void withModalReply(SlashCommandEvent event){} + + private static class Post extends BotCommand{ - public Post(){ + public Post(CodeMCBot bot){ + super(bot); + this.name = "send"; this.help = "Sends a message as the Bot."; - this.userPermissions = new Permission[]{ - Permission.MANAGE_SERVER - }; + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "msg"); + this.hasModalReply = true; this.options = Arrays.asList( new OptionData(OptionType.CHANNEL, "channel", "The channel to sent the message in.") @@ -74,14 +81,17 @@ public Post(){ } @Override - protected void execute(SlashCommandEvent event){ + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){ + + } + + @Override + public void withModalReply(SlashCommandEvent event){ TextChannel channel = event.getOption("channel", null, option -> option.getAsChannel().asTextChannel()); boolean asEmbed = event.getOption("embed", false, OptionMapping::getAsBoolean); if(channel == null){ - CommandUtil.EmbedReply.fromCommandEvent(event) - .withError("Received invalid Channel input.") - .send(); + CommandUtil.EmbedReply.from(event).error("Received invalid Channel input.").send(); return; } @@ -98,15 +108,16 @@ protected void execute(SlashCommandEvent event){ } } - private static class Edit extends SlashCommand{ + private static class Edit extends BotCommand{ - public Edit(){ + public Edit(CodeMCBot bot){ + super(bot); + this.name = "edit"; this.help = "Edit an existing message of the bot."; - this.userPermissions = new Permission[]{ - Permission.MANAGE_SERVER - }; + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "msg"); + this.hasModalReply = true; this.options = Arrays.asList( new OptionData(OptionType.CHANNEL, "channel", "The channel to edit the message in.") @@ -119,7 +130,10 @@ public Edit(){ } @Override - protected void execute(SlashCommandEvent event){ + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){} + + @Override + public void withModalReply(SlashCommandEvent event){ TextChannel channel = event.getOption("channel", null, option -> option.getAsChannel().asTextChannel()); long messageId = event.getOption("id", -1L, option -> { try{ @@ -131,9 +145,7 @@ protected void execute(SlashCommandEvent event){ boolean asEmbed = event.getOption("embed", false, OptionMapping::getAsBoolean); if(channel == null || messageId == -1L){ - CommandUtil.EmbedReply.fromCommandEvent(event) - .withError("Received invalid Channel or Message ID.") - .send(); + CommandUtil.EmbedReply.from(event).error("Received invalid Channel or Message ID.").send(); return; } diff --git a/src/main/java/io/codemc/bot/commands/CmdReload.java b/src/main/java/io/codemc/bot/commands/CmdReload.java new file mode 100644 index 0000000..cbf29ee --- /dev/null +++ b/src/main/java/io/codemc/bot/commands/CmdReload.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 CodeMC.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.codemc.bot.commands; + +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.codemc.bot.CodeMCBot; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.interactions.InteractionHook; + +public class CmdReload extends BotCommand{ + + public CmdReload(CodeMCBot bot){ + super(bot); + + this.name = "reload"; + this.help = "Reloads the configuration."; + + this.allowedRoles = bot.getConfigHandler().getLongList("allowed_roles", "commands", "reload"); + } + + @Override + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){ + boolean success = bot.getConfigHandler().reloadConfig(); + + if(success){ + CommandUtil.EmbedReply.from(hook).success("Reload success!").send(); + }else{ + CommandUtil.EmbedReply.from(hook).error( + "There was an issue while reloading the configuration! Check console.") + .send(); + } + } + + @Override + public void withModalReply(SlashCommandEvent event){} +} diff --git a/src/main/java/io/codemc/bot/commands/CmdSubmit.java b/src/main/java/io/codemc/bot/commands/CmdSubmit.java index 394012e..93afebc 100644 --- a/src/main/java/io/codemc/bot/commands/CmdSubmit.java +++ b/src/main/java/io/codemc/bot/commands/CmdSubmit.java @@ -18,40 +18,54 @@ package io.codemc.bot.commands; -import com.jagrosh.jdautilities.command.SlashCommand; import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.codemc.bot.CodeMCBot; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.components.ActionRow; 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; -public class CmdSubmit extends SlashCommand{ +public class CmdSubmit extends BotCommand{ - public CmdSubmit(){ + public CmdSubmit(CodeMCBot bot){ + super(bot); + this.name = "submit"; this.help = "Submit a request to join the CodeMC CI with a project."; + + this.hasModalReply = true; } @Override - protected void execute(SlashCommandEvent event){ - TextInput userLink = TextInput.create("userlink", "User Link", TextInputStyle.SHORT) - .setPlaceholder("https://github.com/CodeMC") + public void withHookReply(InteractionHook hook, SlashCommandEvent event, Guild guild, Member member){} + + @Override + public void withModalReply(SlashCommandEvent event){ + TextInput user = TextInput.create("user", "GitHub Username", TextInputStyle.SHORT) + .setPlaceholder("CodeMC") .setRequired(true) .build(); - TextInput repoLink = TextInput.create("repolink", "Repository Link", TextInputStyle.SHORT) - .setPlaceholder("https://github.com/CodeMC/Bot") + TextInput repo = TextInput.create("repo", "Repository Name", TextInputStyle.SHORT) + .setPlaceholder("Bot") .setRequired(true) .build(); + TextInput repoLink = TextInput.create("repoLink", "Repository Link (Leave blank if on GitHub)", TextInputStyle.SHORT) + .setPlaceholder("https://git.example.com/CodeMC/Bot") + .build(); TextInput description = TextInput.create("description", "Description", TextInputStyle.PARAGRAPH) - .setPlaceholder("Duscird Vit fir tge CideNC Sercver.") + .setPlaceholder("Discord Bot for the CodeMC Server.") .setRequired(true) .setMaxLength(MessageEmbed.VALUE_MAX_LENGTH) .build(); Modal modal = Modal.create("submit", "Join Request") .addComponents( - ActionRow.of(userLink), + ActionRow.of(user), + ActionRow.of(repo), ActionRow.of(repoLink), ActionRow.of(description) ) diff --git a/src/main/java/io/codemc/bot/config/ConfigHandler.java b/src/main/java/io/codemc/bot/config/ConfigHandler.java new file mode 100644 index 0000000..68f0f29 --- /dev/null +++ b/src/main/java/io/codemc/bot/config/ConfigHandler.java @@ -0,0 +1,101 @@ +/* + * Copyright 2024 CodeMC.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.codemc.bot.config; + +import io.codemc.bot.CodeMCBot; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.gson.GsonConfigurationLoader; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; + +public class ConfigHandler{ + + private final Logger logger = LoggerFactory.getLogger(ConfigHandler.class); + private final File file = new File("./config.json"); + + private ConfigurationNode node = null; + + public ConfigHandler(){} + + public boolean loadConfig(){ + logger.info("Loading config.json..."); + + if(!file.exists()){ + try(InputStream stream = CodeMCBot.class.getResourceAsStream("/config.json")){ + if(stream == null){ + logger.warn("Unable to create config.json! InputStream was null."); + return false; + } + + Files.copy(stream, file.toPath()); + logger.info("Successfully created config.json!"); + }catch(IOException ex){ + logger.warn("Encountered IOException while creating config.json!", ex); + return false; + } + } + + return reloadConfig(); + } + + public boolean reloadConfig(){ + GsonConfigurationLoader loader = GsonConfigurationLoader.builder() + .file(file) + .build(); + + try{ + return (node = loader.load()) != null; + }catch(IOException ex){ + logger.warn("Encountered IOException while loading Configuration!", ex); + return false; + } + } + + public String getString(Object... path){ + return node.node(path).getString(""); + } + + public long getLong(Object... path){ + return node.node(path).getLong(-1L); + } + + public List getLongList(Object... path){ + try{ + return node.node(path).getList(Long.class); + }catch(SerializationException ex){ + return Collections.emptyList(); + } + } + + public List getStringList(Object... path){ + try{ + return node.node(path).getList(String.class); + }catch(SerializationException ex){ + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/io/codemc/bot/listeners/ButtonListener.java b/src/main/java/io/codemc/bot/listeners/ButtonListener.java new file mode 100644 index 0000000..42cfe58 --- /dev/null +++ b/src/main/java/io/codemc/bot/listeners/ButtonListener.java @@ -0,0 +1,115 @@ +/* + * Copyright 2024 CodeMC.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.codemc.bot.listeners; + +import io.codemc.bot.CodeMCBot; +import io.codemc.bot.commands.CmdApplication; +import io.codemc.bot.utils.CommandUtil; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ButtonListener extends ListenerAdapter{ + + private final CodeMCBot bot; + + public ButtonListener(CodeMCBot bot){ + this.bot = bot; + } + + @Override + public void onButtonInteraction(@NotNull ButtonInteractionEvent event){ + Guild guild = event.getGuild(); + if(!event.isFromGuild() || guild == null){ + CommandUtil.EmbedReply.from(event).error("Buttons only work on the CodeMC Server!").send(); + return; + } + + if(event.getButton().getId() == null){ + event.deferReply().queue(); + return; + } + + List acceptApplicationRoles = bot.getConfigHandler().getLongList("allowed_roles", "applications", "accept"); + List denyApplicationRoles = bot.getConfigHandler().getLongList("allowed_roles", "applications", "deny"); + + if(acceptApplicationRoles.isEmpty() || denyApplicationRoles.isEmpty()){ + CommandUtil.EmbedReply.from(event).error("No roles for accepting or denying applications set!").send(); + return; + } + + Member member = event.getMember(); + if(member == null){ + CommandUtil.EmbedReply.from(event).error("Cannot get Member from Server!").send(); + return; + } + + String[] values = event.getButton().getId().split(":"); + if(values.length < 4 || !values[0].equals("application")){ + CommandUtil.EmbedReply.from(event).error("Received non-application button event!").send(); + return; + } + + if(!values[1].equals("accept") && !values[1].equals("deny")){ + CommandUtil.EmbedReply.from(event).error( + "Received unknown Button Application type.", + "Expected `accept` or `deny` but got " + values[1] + "." + ).send(); + return; + } + + List roleIds = member.getRoles().stream() + .map(Role::getIdLong) + .toList(); + + if(values[1].equals("accept")){ + if(lacksRole(roleIds, acceptApplicationRoles)){ + CommandUtil.EmbedReply.from(event).error("You lack permissions to perform this action.").send(); + return; + } + + event.deferReply(true).queue( + // TODO: Add project URL here (Maybe move application handling from CmdApplication) + hook -> CmdApplication.handle(bot, hook, guild, event.getMessageIdLong(), "", true) + ); + }else{ + if(lacksRole(roleIds, denyApplicationRoles)){ + CommandUtil.EmbedReply.from(event).error("You lack permissions to perform this action.").send(); + return; + } + + event.deferReply(true).queue( + // TODO: Add project URL here (Maybe move application handling from CmdApplication) + hook -> CmdApplication.handle(bot, hook, guild, event.getMessageIdLong(), "", false) + ); + } + } + + private boolean lacksRole(List roleIds, List allowedRoleIds){ + if(roleIds.isEmpty()) + return true; + + return roleIds.stream().anyMatch(allowedRoleIds::contains); + } +} diff --git a/src/main/java/io/codemc/bot/listeners/ModalListener.java b/src/main/java/io/codemc/bot/listeners/ModalListener.java index 1b9ca0c..e4b864c 100644 --- a/src/main/java/io/codemc/bot/listeners/ModalListener.java +++ b/src/main/java/io/codemc/bot/listeners/ModalListener.java @@ -18,8 +18,9 @@ package io.codemc.bot.listeners; +import io.codemc.bot.CodeMCBot; +import io.codemc.bot.commands.CmdApplication; import io.codemc.bot.utils.CommandUtil; -import io.codemc.bot.utils.Constants; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageEmbed; @@ -28,132 +29,123 @@ import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.modals.ModalMapping; import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.MarkdownUtil; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Instant; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class ModalListener extends ListenerAdapter{ - private final Pattern userLinkPattern = Pattern.compile("^https://github\\.com/(?[a-zA-Z0-9-]+)/?$"); - private final Pattern repoLinkPattern = Pattern.compile("^https://github\\.com/(?[a-zA-Z0-9-]+)/(?[a-zA-Z0-9-_.]+)/?$"); - private final Logger logger = LoggerFactory.getLogger(ModalListener.class); + private final CodeMCBot bot; + + public ModalListener(CodeMCBot bot){ + this.bot = bot; + } + @Override public void onModalInteraction(@NotNull ModalInteractionEvent event){ if(!event.isFromGuild()) return; Guild guild = event.getGuild(); - if(guild == null || !guild.getId().equals(Constants.SERVER)){ - CommandUtil.EmbedReply.fromModalEvent(event) - .withError("Unable to retrieve CodeMC Server!") - .send(); + if(guild == null || guild.getIdLong() != bot.getConfigHandler().getLong("server")){ + CommandUtil.EmbedReply.from(event).error("Unable to retrieve CodeMC Server!").send(); return; } - if(event.getModalId().equals("submit")){ - event.deferReply(true).queue(hook -> { - - String userLink = value(event, "userlink"); - String repoLink = value(event, "repolink"); + String[] args = event.getModalId().split(":"); + + switch(args[0]){ + case "submit" -> event.deferReply(true).queue(hook -> { + String user = value(event, "user"); + String repo = value(event, "repo"); String description = value(event, "description"); - if(nullOrEmpty(userLink, repoLink, description)){ - CommandUtil.EmbedReply.fromHook(hook).withError( - "User Link, Repository Link or Description was not present!" - ).send(); + if(user == null || user.isEmpty() || repo == null || repo.isEmpty() || description == null || description.isEmpty()){ + CommandUtil.EmbedReply.from(hook).error( + "The option User, Repo and/or Description was not set properly!") + .send(); return; } - Matcher userMatcher = userLinkPattern.matcher(userLink); - Matcher repoMatcher = repoLinkPattern.matcher(repoLink); - - if(!userMatcher.matches() || !repoMatcher.matches()){ - CommandUtil.EmbedReply.fromHook(hook).withError( - "The provided User or Repository link does not match a valid GitHub URL.", - "Make sure the patterns are `https://github.com/` and `https://github.com//` respectively." - ).send(); + TextChannel requestChannel = guild.getTextChannelById(bot.getConfigHandler().getLong("channels", "request_access")); + if(requestChannel == null){ + CommandUtil.EmbedReply.from(hook).error("Unable to retrieve `request-access` channel!").send(); return; } - String username = String.format("[`%s`](%s)", userMatcher.group("user"), userLink); - String repo = String.format("[`%s/%s`](%s)", repoMatcher.group("user"), repoMatcher.group("repo"), repoLink); - String submitter = String.format("`%s` (%s)", event.getUser().getEffectiveName(), event.getUser().getAsMention()); + String repoLinkValue = value(event, "repoLink"); + if(repoLinkValue == null || repoLinkValue.isEmpty()) + repoLinkValue = "https://github.com/" + user + "/" + repo; - TextChannel requestChannel = guild.getTextChannelById(Constants.REQUEST_ACCESS); - if(requestChannel == null){ - CommandUtil.EmbedReply.fromHook(hook).withError( - "Unable to retrieve `request-access` channel!" - ).send(); - return; - } + String userLink = MarkdownUtil.maskedLink(user, "https://github.com/" + user); + String repoLink = MarkdownUtil.maskedLink(repo, repoLinkValue); + String submitter = String.format("`%s` (%s)", event.getUser().getEffectiveName(), event.getUser().getAsMention()); MessageEmbed embed = CommandUtil.getEmbed() - .addField("User/Organisation:", username, true) - .addField("Repository:", repo, true) + .addField("User/Organisation:", userLink, true) + .addField("Repository:", repoLink, true) .addField("Submitted by:", submitter, true) .addField("Description", description, false) .setFooter(event.getUser().getId()) .setTimestamp(Instant.now()) .build(); - requestChannel.sendMessageEmbeds(embed).queue( - message -> { - CommandUtil.EmbedReply.fromHook(hook).withMessage( - "[Request sent!](" + message.getJumpUrl() + ")" - ).asSuccess().send(); - - RestAction.allOf( - message.createThreadChannel("Access Request - " + event.getUser().getName()), - message.addReaction(Emoji.fromCustom("like", 935126958193405962L, false)), - message.addReaction(Emoji.fromCustom("dislike", 935126958235344927L, false)) - ).queue(); - - logger.info("[Access Request] User {} requested access to the CI.", event.getUser().getEffectiveName()); - }, - e -> CommandUtil.EmbedReply.fromHook(hook).withError( - "Error while submitting request!", - "Reported Error: " + e.getMessage() - ).send() + requestChannel.sendMessageEmbeds(embed) + .setActionRow( + Button.success("application:accept:" + user + ":" + repo, "Accept"), + Button.danger("application:deny:" + user + ":" + repo, "Deny") + ).queue( + message -> { + CommandUtil.EmbedReply.from(hook).success( + "[Request sent!](" + message.getJumpUrl() + ")") + .send(); + + RestAction.allOf( + message.createThreadChannel("Access Request - " + event.getUser().getName()), + message.addReaction(Emoji.fromCustom("like", 935126958193405962L, false)), + message.addReaction(Emoji.fromCustom("dislike", 935126958235344927L, false)) + ).queue(); + + logger.info("[Access Request] User {} requested access to the CI.", event.getUser().getEffectiveName()); + }, + e -> CommandUtil.EmbedReply.from(hook).error( + "Error while submitting request!", + "Reported Error: " + e.getMessage() + ).send() ); }); - }else - if(event.getModalId().startsWith("message:")){ - event.deferReply(true).queue(hook -> { - String[] args = event.getModalId().split(":"); + + case "message" -> event.deferReply(true).queue(hook -> { if(args.length < 4){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Invalid Modal data. Expected `>=4` but received `" + args.length + "`!") + CommandUtil.EmbedReply.from(hook) + .error("Invalid Modal data. Expected `4+` arguments but received `" + args.length + "`!") .send(); return; } TextChannel channel = guild.getTextChannelById(args[2]); if(channel == null){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Received invalid Text Channel.") - .send(); + CommandUtil.EmbedReply.from(hook).error("Received invalid Text Channel.").send(); return; } String text = value(event, "message"); if(text == null || text.isEmpty()){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Received invalid Message to sent/edit.") - .send(); + CommandUtil.EmbedReply.from(hook).error("Received invalid Message to sent/edit.").send(); return; } if(!channel.canTalk()){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("I lack the permission to see and/or write in " + channel.getAsMention() + ".") + CommandUtil.EmbedReply.from(hook) + .error("I lack the permission to see and/or write in " + channel.getAsMention() + ".") .send(); return; } @@ -164,23 +156,22 @@ public void onModalInteraction(@NotNull ModalInteractionEvent event){ if(asEmbed){ channel.sendMessageEmbeds(CommandUtil.getEmbed().setDescription(text).build()).queue( message -> sendConfirmation(hook, message, false), - e -> CommandUtil.EmbedReply.fromHook(hook) - .withError("Unable to sent message. Reason: " + e.getMessage()) + e -> CommandUtil.EmbedReply.from(hook) + .error("Unable to sent message. Reason: " + e.getMessage()) .send() ); }else{ channel.sendMessage(text).queue( message -> sendConfirmation(hook, message, false), - e -> CommandUtil.EmbedReply.fromHook(hook) - .withError("Unable to sent message. Reason: " + e.getMessage()) + e -> CommandUtil.EmbedReply.from(hook) + .error("Unable to sent message. Reason: " + e.getMessage()) .send() ); } - }else - if(args[1].equals("edit")){ + }else if(args[1].equals("edit")){ if(args.length == 4){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Received invalid Modal data. Expected `>4` but got `=4`") + CommandUtil.EmbedReply.from(hook) + .error("Received invalid Modal data. Expected `>4` but got `=4`") .send(); return; } @@ -193,8 +184,8 @@ public void onModalInteraction(@NotNull ModalInteractionEvent event){ } if(messageId == -1L){ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Received invalid message ID `" + args[4] + "`.") + CommandUtil.EmbedReply.from(hook) + .error("Received invalid message ID `" + args[4] + "`.") .send(); return; } @@ -204,51 +195,83 @@ public void onModalInteraction(@NotNull ModalInteractionEvent event){ if(asEmbed){ message.editMessageEmbeds(CommandUtil.getEmbed().setDescription(text).build()).setReplace(true).queue( m -> sendConfirmation(hook, m, true), - e -> CommandUtil.EmbedReply.fromHook(hook) - .withError("Unable to edit message. Reason: " + e.getMessage()) + e -> CommandUtil.EmbedReply.from(hook) + .error("Unable to edit message. Reason: " + e.getMessage()) .send() ); }else{ message.editMessage(text).setReplace(true).queue( m -> sendConfirmation(hook, m, true), - e -> CommandUtil.EmbedReply.fromHook(hook) - .withError("Unable to edit message. Reason: " + e.getMessage()) + e -> CommandUtil.EmbedReply.from(hook) + .error("Unable to edit message. Reason: " + e.getMessage()) .send() ); } } ); }else{ - CommandUtil.EmbedReply.fromHook(hook) - .withError("Received Unknown Message type: `" + args[1] + "`.") + CommandUtil.EmbedReply.from(hook) + .error("Received Unknown Message type: `" + args[1] + "`.") + .send(); + } + }); + + case "application" -> event.deferReply(true).queue(hook -> { + if(args.length < 3){ + CommandUtil.EmbedReply.from(hook) + .error("Invalid Modal data. Expected `3` args but received `" + args.length + "`!") .send(); + return; + } + + if(!args[1].equals("accepted") && !args[1].equals("denied")){ + CommandUtil.EmbedReply.from(hook) + .error("Received unknown Application type. Expected `accepted` or `denied` but received `" + args[1] + "`.") + .send(); + return; + } + + long messageId; + try{ + messageId = Long.parseLong(args[2]); + }catch(NumberFormatException ex){ + messageId = -1L; + } + + if(messageId == -1L){ + CommandUtil.EmbedReply.from(hook) + .error("Received Invalid Message ID. Expected number but got `" + args[2] + "` instead!") + .send(); + return; } + + boolean accepted = args[1].equals("accepted"); + + String text = value(event, "text"); + if(text == null || text.isEmpty()){ + CommandUtil.EmbedReply.from(hook) + .error("Received invalid " + (accepted ? "Project URL" : "Reason") + ". Text was empty/null.") + .send(); + return; + } + + CmdApplication.handle(bot, hook, guild, messageId, text, accepted); }); - }else{ - CommandUtil.EmbedReply.fromModalEvent(event) - .withError("Received unknwon Modal Data: `"+ event.getModalId() +"`") + + default -> CommandUtil.EmbedReply.from(event) + .error("Received Modal with unknown ID `" + event.getModalId() + "`.") .send(); } } private void sendConfirmation(InteractionHook hook, Message message, boolean edit){ - CommandUtil.EmbedReply.fromHook(hook) - .withMessage(String.format("[%s](%s)", edit ? "Message edited!" : "Message sent!", message.getJumpUrl())) - .asSuccess() + CommandUtil.EmbedReply.from(hook) + .success(String.format("[%s](%s)", edit ? "Message edited!" : "Message sent!", message.getJumpUrl())) .send(); logger.info("[Message] User {} {} a Message as the Bot.", hook.getInteraction().getUser().getEffectiveName(), edit ? "edited" : "sent"); } - private boolean nullOrEmpty(String... values){ - for(String value : values){ - if(value == null || value.isEmpty()) - return true; - } - - return false; - } - private String value(ModalInteractionEvent event, String id){ ModalMapping value = event.getValue(id); if(value == null) diff --git a/src/main/java/io/codemc/bot/utils/CommandUtil.java b/src/main/java/io/codemc/bot/utils/CommandUtil.java index 6ee8e08..2107837 100644 --- a/src/main/java/io/codemc/bot/utils/CommandUtil.java +++ b/src/main/java/io/codemc/bot/utils/CommandUtil.java @@ -21,10 +21,14 @@ import ch.qos.logback.classic.Logger; import com.jagrosh.jdautilities.command.SlashCommandEvent; import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.InteractionHook; import org.slf4j.LoggerFactory; +import java.util.List; + public class CommandUtil{ private static final Logger LOG = (Logger)LoggerFactory.getLogger(CommandUtil.class); @@ -35,80 +39,65 @@ public static EmbedBuilder getEmbed(){ return new EmbedBuilder().setColor(0x0172BA); } - public static class EmbedReply { - - private final SlashCommandEvent commandEvent; - private final ModalInteractionEvent modalEvent; - private final InteractionHook hook; - private final EmbedBuilder builder = new EmbedBuilder(); - - private EmbedReply(SlashCommandEvent commandEvent){ - this.commandEvent = commandEvent; - this.modalEvent = null; - this.hook = null; - } - - private EmbedReply(InteractionHook hook){ - this.commandEvent = null; - this.modalEvent = null; - this.hook = hook; - } - - private EmbedReply(ModalInteractionEvent modalEvent){ - this.commandEvent = null; - this.modalEvent = modalEvent; - this.hook = null; - } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean hasRole(Member member, List roleIds){ + if(roleIds.isEmpty()) + return true; - public static EmbedReply fromCommandEvent(SlashCommandEvent event){ - return new EmbedReply(event); - } + return member.getRoles().stream() + .filter(role -> roleIds.contains(role.getIdLong())) + .findFirst() + .orElse(null) != null; + } + + public static class EmbedReply { - public static EmbedReply fromModalEvent(ModalInteractionEvent event){ - return new EmbedReply(event); - } + private final T type; + private final EmbedBuilder builder = new EmbedBuilder(); - public static EmbedReply fromHook(InteractionHook hook){ - return new EmbedReply(hook); + private EmbedReply(T type){ + this.type = type; } - public EmbedReply withMessage(String... lines){ - builder.setDescription(String.join("\n", lines)); - return this; + public static EmbedReply from(T type){ + return new EmbedReply<>(type); } - public EmbedReply asSuccess(){ - builder.setColor(0x00FF00); + public EmbedReply success(String... lines){ + builder.setDescription(String.join("\n", lines)) + .setColor(0x00FF00); return this; } - public EmbedReply withError(String... lines){ - builder.setColor(0xFF0000) - .setDescription( - "There was an error while trying to handle the command!\n" + - "If this error persists, report it to Andre_601#0601" - ) - .addField("Error:", String.join("\n", lines), false); + public EmbedReply appendWarning(String... lines){ + builder.addField("Warning:", String.join("\n", lines), false) + .setColor(0xFFC800); return this; } - public EmbedReply withIssue(String... lines){ - builder.setColor(0xFFC800) - .addField("Warning:", String.join("\n", lines), false); + public EmbedReply error(String... lines){ + builder.setDescription( + "There was an error while trying to handle an action!\n" + + "If this error persists, report it to the Bot owner!") + .addField("Error:", String.join("\n", lines), false) + .setColor(0xFF0000); return this; } public void send(){ - if(commandEvent != null){ - commandEvent.replyEmbeds(builder.build()).queue(); + if(type instanceof SlashCommandEvent commandEvent){ + commandEvent.replyEmbeds(builder.build()).setEphemeral(true).queue(); + }else + if(type instanceof ModalInteractionEvent modalEvent){ + modalEvent.replyEmbeds(builder.build()).setEphemeral(true).queue(); }else - if(modalEvent != null){ - modalEvent.replyEmbeds(builder.build()).queue(); + if(type instanceof ButtonInteractionEvent buttonEvent){ + buttonEvent.replyEmbeds(builder.build()).queue(); }else - if(hook != null){ - hook.editOriginalEmbeds(builder.build()).queue(); + if(type instanceof InteractionHook hook){ + hook.editOriginal(EmbedBuilder.ZERO_WIDTH_SPACE).setEmbeds(builder.build()).queue(); }else{ - LOG.error("Received EmbedReply class with neither SlashCommandEvent, ModalInteractionEvent nor InteractionHook set!"); + LOG.error("Received unknown Type {} for EmbedReply!", type); } } } diff --git a/src/main/java/io/codemc/bot/utils/Constants.java b/src/main/java/io/codemc/bot/utils/Constants.java deleted file mode 100644 index f1b0438..0000000 --- a/src/main/java/io/codemc/bot/utils/Constants.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2021 CodeMC.io - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE - * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package io.codemc.bot.utils; - -public class Constants{ - - // Server ID - public static final String SERVER = "405915656039694336"; - - // Channel IDs - public static final String REQUEST_ACCESS = "782998340559306792"; - public static final String ACCEPTED_REQUESTS = "784119059138478080"; - public static final String REJECTED_REQUESTS = "800423355551449098"; - - // Role IDs - public static final String ROLE_AUTHOR = "405918641859723294"; - - // Result messages for applications - public static final String ACCEPTED_MSG = - "Your request has been **accepted**!\n" + - "You will now be able to login with your GitHub Account and access the approved Repository on the CI.\n" + - "\n" + - "Remember to [visit our Documentation](https://docs.codemc.io) and [read our FAQ](https://docs.codemc.io/faq) " + - "to know how to setup automatic builds!"; - public static final String REJECTED_MSG = - "Your request has been **rejected**.\n" + - "The reason for this can be found below.\n" + - "\n" + - "You may re-apply for access unless mentioned so in the reason."; -} diff --git a/src/main/resources/config.json b/src/main/resources/config.json new file mode 100644 index 0000000..2208375 --- /dev/null +++ b/src/main/resources/config.json @@ -0,0 +1,30 @@ +{ + "bot_token": "TOKEN", + "server": 0, + "channels": { + "request_access": 0, + "accepted_requests": 0, + "rejected_requests": 0 + }, + "author_role": 0, + "allowed_roles": { + "applications": { + "accept": [], + "deny": [] + }, + "commands": { + "application": [], + "disable": [], + "msg": [], + "reload": [] + } + }, + "users": { + "owner": 0, + "co_owners": [] + }, + "messages": { + "accepted": [], + "denied": [] + } +} \ No newline at end of file diff --git a/src/main/resources/config.json.sample b/src/main/resources/config.json.sample new file mode 100644 index 0000000..70b2ba6 --- /dev/null +++ b/src/main/resources/config.json.sample @@ -0,0 +1,61 @@ +{ + "bot_token": "TOKEN", + "server": 405915656039694336, + "channels": { + "request_access": 1233971297185431582, + "accepted_requests": 784119059138478080, + "rejected_requests": 800423355551449098 + }, + "author_role": 405918641859723294, + "allowed_roles": { + "applications": { + "accept": [ + 405917902865170453, + 659568973079379971, + 1233971297185431582 + ], + "deny": [ + 405917902865170453, + 659568973079379971, + 1233971297185431582 + ] + } + "commands": { + "application": [ + 405917902865170453, + 659568973079379971, + 1233971297185431582 + ], + "disable": [ + 405917902865170453 + ], + "msg": [ + 405917902865170453 + ], + "reload": [ + 405917902865170453 + ] + } + }, + "users": { + "owner": 204232208049766400, + "co_owners": [ + 143088571656437760, + 282975975954710528 + ] + }, + "messages": { + "accepted": [ + "Your request has been **accepted**!", + "You will now be able to login with your GitHub Account and access the approved Repository on the CI.", + "", + "Remember to [visit our Documentation](https://docs.codemc.io) and [Read our FAQ](https://docs.codemc.io/faq) to learn how to setup automatic builds!" + ], + "denied": [ + "Your request has been **rejected**!", + "The reason for the denial is stated below.", + "", + "You may re-apply unless mentioned otherwise in the Reason." + ] + } +} \ No newline at end of file