diff --git a/src/main/java/pw/chew/chewbotcca/commands/DiffCommand.java b/src/main/java/pw/chew/chewbotcca/commands/DiffCommand.java
new file mode 100644
index 00000000..7e0d409e
--- /dev/null
+++ b/src/main/java/pw/chew/chewbotcca/commands/DiffCommand.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2021 Chewbotcca
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package pw.chew.chewbotcca.commands;
+
+import com.jagrosh.jdautilities.command.Command;
+import com.jagrosh.jdautilities.command.CommandEvent;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.Permission;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Role;
+import pw.chew.chewbotcca.util.ArgsParser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DiffCommand extends Command {
+ public DiffCommand() {
+ this.name = "diff";
+ this.aliases = new String[]{"compare"};
+ this.guildOnly = true;
+ this.cooldown = 5;
+ this.cooldownScope = CooldownScope.USER;
+ this.children = new Command[]{new CompareRolesSubCommand(), new CompareMembersSubCommand()};
+ }
+
+ @Override
+ protected void execute(CommandEvent event) {
+ String[] args = event.getArgs().split(" ");
+ if (args.length != 3) {
+ event.reply("Invalid amount of arguments specified. Example: " + event.getPrefix() + "diff ROLE 708085624514543728 134445052805120001");
+ return;
+ }
+
+ event.reply("Invalid comparison type specified. Valid: `ROLE`, `CHANNEL`, `MEMBER`");
+ }
+
+ /**
+ * Compares roles.
+ * Takes as input 2 role IDs. Example: diff role 708085624514543728 134445052805120001
+ */
+ private static class CompareRolesSubCommand extends Command {
+
+ public CompareRolesSubCommand() {
+ this.name = "role";
+ this.guildOnly = true;
+ this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
+ }
+
+ @Override
+ protected void execute(CommandEvent event) {
+ String[] args = event.getArgs().split(" ");
+
+ if (args.length != 2) {
+ event.reply("Invalid number of arguments provided. Please provide valid role IDs for this server.");
+ return;
+ }
+
+ Role base = event.getGuild().getRoleById(args[0]);
+ if (base == null) {
+ event.reply("Unable to find base (first) role. Please ensure you are providing a valid ID to compare.");
+ return;
+ }
+ Role compare = event.getGuild().getRoleById(args[1]);
+ if (compare == null) {
+ event.reply("Unable to find compare (second) role. Please ensure you are providing a valid ID to compare.");
+ return;
+ }
+
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setTitle("Comparing roles")
+ .setDescription("Comparing name, color, permissions, and other information.\n" +
+ "Base role: " + base.getName() + " ( " + base.getId() + " ) " + "\n" +
+ "Compare role: " + compare.getName() + " ( " + compare.getId() + " ) ");
+
+ if (base.getName().equals(compare.getName())) {
+ embed.addField("Name", "*Nothing to compare.\n Names are identical.*", true);
+ } else {
+ String name = base.getName() + "\n" +
+ compare.getName();
+ embed.addField("Name", name, true);
+ }
+
+
+ if (base.getColor() == compare.getColor()) {
+ embed.addField("Color", "*Nothing to compare.\n Colors are identical.*", true);
+ } else {
+ String baseHex = colorToHex(base.getColor().getRed(), base.getColor().getGreen(), base.getColor().getBlue());
+ String compareHex = colorToHex(compare.getColor().getRed(), compare.getColor().getGreen(), compare.getColor().getBlue());
+ String color = (base.getColorRaw() == Role.DEFAULT_COLOR_RAW ? "Default color" : baseHex) + "\n" +
+ (compare.getColorRaw() == Role.DEFAULT_COLOR_RAW ? "Default color" : compareHex);
+ embed.addField("Color", color, true);
+ }
+ embed.addBlankField(false);
+
+ // compare info section
+
+ String information = """
+ Hoisted
+ Mentionable
+ Bot role
+ Boost role
+ Integration role""";
+ embed.addField("Information", information, true);
+
+ List baseInfo = new ArrayList<>();
+ baseInfo.add(getInfoState(base.isHoisted()));
+ baseInfo.add(getInfoState(base.isMentionable()));
+ baseInfo.add(getInfoState(base.getTags().isBot()));
+ baseInfo.add(getInfoState(base.getTags().isBoost()));
+ baseInfo.add(getInfoState(base.getTags().isIntegration()));
+
+ embed.addField(base.getName(), String.join("\n", baseInfo), true);
+
+ List compareInfo = new ArrayList<>();
+ compareInfo.add(getInfoState(compare.isHoisted()));
+ compareInfo.add(getInfoState(compare.isMentionable()));
+ compareInfo.add(getInfoState(compare.getTags().isBot()));
+ compareInfo.add(getInfoState(compare.getTags().isBoost()));
+ compareInfo.add(getInfoState(compare.getTags().isIntegration()));
+
+ embed.addField(compare.getName(), String.join("\n", compareInfo), true);
+
+
+ if (base.getPermissionsRaw() == compare.getPermissionsRaw()) {
+ embed.addField("✅ Permissions 🚫", "*Nothing to compare.\n Permissions are identical*", false);
+ } else {
+ List baseOnly = new ArrayList<>();
+ List compareOnly = new ArrayList<>();
+
+
+ for (Permission perm : base.getPermissions()) {
+ if (!compare.getPermissions().contains(perm)) {
+ baseOnly.add(perm);
+ }
+ }
+
+ for (Permission perm : compare.getPermissions()) {
+ if (!base.getPermissions().contains(perm)) {
+ compareOnly.add(perm);
+ }
+ }
+
+ List perms = new ArrayList<>();
+
+ perms.add("""
+ + means compare role has the permission, and base doesn't.
+ - means compare role doesn't have the perm, but base does.
+ ```diff""");
+
+ for (Permission perm : compareOnly) {
+ perms.add("+ " + perm.getName());
+ }
+
+ for (Permission perm : baseOnly) {
+ perms.add("- " + perm.getName());
+ }
+
+ perms.add("```");
+
+ embed.addField("✅ Permissions 🚫", String.join("\n", perms), false);
+ }
+
+ event.reply(embed.build());
+ }
+
+ /**
+ * Helper method for %^diff role command about a role's info state.
+ *
+ * @param yes if it's green or not
+ * @return a green sign if true, red if false
+ */
+ private String getInfoState(boolean yes) {
+ if (yes) {
+ return "\uD83D\uDFE2";
+ } else {
+ return "\uD83D\uDD34";
+ }
+ }
+
+ /**
+ * Source: https://stackoverflow.com/questions/3607858/convert-a-rgb-color-value-to-a-hexadecimal-string
+ * Function that converts RGB values to hexadecimal code.
+ *
+ * @param red red value of color
+ * @param green green value of color
+ * @param blue blue value of color
+ * @return the hexadecimal code of the color
+ */
+ private String colorToHex(int red, int green, int blue) {
+ return String.format("#%02x%02x%02x", red, green, blue);
+ }
+ }
+
+ /**
+ * Compares members
+ * Takes as input 2 mentioned members. Example diff member @random @random2
+ */
+ private static class CompareMembersSubCommand extends Command {
+
+ public CompareMembersSubCommand() {
+ this.name = "member";
+ this.guildOnly = true;
+ this.botPermissions = new Permission[]{Permission.MESSAGE_EMBED_LINKS};
+ }
+
+ @Override
+ protected void execute(CommandEvent event) {
+ List members;
+
+ try {
+ members = ArgsParser.parseMembers(event.getArgs(), 2, event.getMessage());
+ } catch (IllegalArgumentException e) {
+ event.reply("""
+ Invalid number of arguments provided.
+ Please provide valid members.
+ Example : diff member @random @random2.""");
+ return;
+ }
+
+ Member base = members.get(0);
+ Member compare = members.get(1);
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setTitle("Comparing members")
+ .setDescription("Comparing time joined, time boosted, roles and other information.\n" +
+ "Base member: " + base.getUser().getAsTag() + "\n" +
+ "Compare member: " + compare.getUser().getAsTag());
+
+ // Dates of arrival in server
+ String date = base.getTimeJoined().toString().substring(0, 10) + "\n" + compare.getTimeJoined().toString().substring(0, 10);
+ embed.addField("Date Joined", date, true);
+ embed.addBlankField(false);
+
+ // Status comparison
+ String onlineStatus = base.getUser().getAsTag() + ": " + base.getOnlineStatus().name() + "\n" +
+ compare.getUser().getAsTag() + ": " + compare.getOnlineStatus().name();
+ if (base.getOnlineStatus().equals(compare.getOnlineStatus())) {
+ embed.addField("Status", "Nothing to compare.", true);
+ } else {
+ embed.addField("Status", onlineStatus, true);
+ }
+
+ // Starting date of boosting time for a member.
+ String baseBoostingTime;
+ String compareBoostingTime;
+ if (base.getTimeBoosted() != null) {
+ // keeps yyyy-mm-dd
+ baseBoostingTime = "Boosting since: " + base.getTimeBoosted().toString().substring(0, 10);
+ } else {
+ baseBoostingTime = "Not boosting";
+ }
+
+ if (compare.getTimeBoosted() != null) {
+ // keeps yyyy-mm-dd
+ compareBoostingTime = "Boosting since: " + compare.getTimeBoosted().toString().substring(0, 10);
+ } else {
+ compareBoostingTime = "Not boosting";
+ }
+
+ embed.addField("Server Boosting", baseBoostingTime + "\n" + compareBoostingTime, true);
+ embed.addBlankField(false);
+
+ // Roles comparison of members
+ List baseRoles = new ArrayList<>();
+ List compareRoles = new ArrayList<>();
+
+ baseRoles.add("""
+ ```diff""");
+ compareRoles.add("""
+ ```diff""");
+ for (Role role : base.getRoles()) {
+ baseRoles.add(role.getName());
+ }
+
+ for (Role role : compare.getRoles()) {
+ compareRoles.add(role.getName());
+ }
+
+ baseRoles.add("```");
+ compareRoles.add("```");
+
+ embed.addField(base.getUser().getName() + "' Roles ", String.join("\n", baseRoles), true);
+ embed.addField(compare.getUser().getName() + "' Roles ", String.join("\n", compareRoles), true);
+
+ event.reply(embed.build());
+ }
+ }
+}
diff --git a/src/main/java/pw/chew/chewbotcca/util/ArgsParser.java b/src/main/java/pw/chew/chewbotcca/util/ArgsParser.java
new file mode 100644
index 00000000..c23667e6
--- /dev/null
+++ b/src/main/java/pw/chew/chewbotcca/util/ArgsParser.java
@@ -0,0 +1,133 @@
+package pw.chew.chewbotcca.util;
+
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.utils.MiscUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ArgsParser {
+ /**
+ * Parse arguments and return a list of members.
+ * Supports:
+ * 1) User name#0000
+ * 2) <@!mention>
+ * 3) IDs
+ *
+ * @param arg the arguments
+ * @param amount the amount of members needed to parse
+ * @param msg the message (to get members from)
+ * @return a list of (possibly null) members with length amount
+ * @throws IllegalArgumentException if only one member can be parsed
+ */
+ public static List parseMembers(String arg, int amount, Message msg) {
+ // Try convenience method first
+ List mentionedMember = msg.getMentionedMembers();
+ if (mentionedMember.size() == amount) {
+ return mentionedMember;
+ }
+
+ List members = new ArrayList<>();
+
+ String[] tags = arg.split("#");
+ // joe bob#0000 bob joe#0000 becomes "joe bob" "0000 bob joe" "0000", so need amount + 1
+ if (tags.length > amount + 1) {
+ throw new IllegalArgumentException("Too many tags provided!");
+ }
+
+ // First check for args
+ String[] args = new String[amount];
+ int i = 0;
+
+ String temp = "";
+ for (String anArg : arg.split(" ")) {
+ if ((i + 1) > args.length) {
+ throw new IllegalArgumentException("Too many arguments provided!");
+ }
+ // Check for mention
+ if (Mention.isValidMention(Message.MentionType.USER, anArg)) {
+ args[i] = anArg;
+ i++;
+ continue;
+ }
+
+ // Check for User ID
+ if (anArg.length() >= 17) {
+ try {
+ MiscUtil.parseSnowflake(anArg);
+ args[i] = anArg;
+ i++;
+ continue;
+ } catch (NumberFormatException ignored) {}
+ }
+
+ // Check if it's a valid tag on its own
+ if (isValidTag(anArg)) {
+ args[i] = anArg;
+ i++;
+ temp = ""; // Reset temp just in case
+ continue;
+ }
+
+ // Check if temp is valid
+ if (isValidTag(temp)) {
+ args[i] = anArg;
+ i++;
+ temp = "";
+ } else {
+ // Add to temp and pray for the best
+ temp += anArg;
+ }
+ }
+
+ // Cycle through each split arg
+ for (String anArg : args) {
+ if (anArg == null) {
+ members.add(null);
+ continue;
+ }
+ try {
+ // Try parsing ID
+ long id = Long.parseLong(anArg);
+ members.add(msg.getGuild().retrieveMemberById(id).complete());
+ continue;
+ } catch (NumberFormatException e) {
+ // ID failed, let's try parsing mention
+ Object parsed = Mention.parseMention(anArg, msg.getGuild(), msg.getJDA());
+ if (parsed instanceof Member) {
+ members.add((Member) parsed);
+ continue;
+ }
+ // Okay, that failed. Let's check if it's a tag
+ if (parsed == null && isValidTag(anArg)) {
+ members.add(msg.getGuild().getMemberByTag(anArg));
+ continue;
+ }
+ }
+ // If all else fails...
+ members.add(null);
+ }
+ return members;
+ }
+
+ /**
+ * Checks for "Valid Tag#0000"
+ *
+ * @param input the input to test
+ * @return true if valid, false if not
+ */
+ private static boolean isValidTag(String input) {
+ if (!input.contains("#")) {
+ return false;
+ }
+
+ String[] test = input.split("#");
+ if (test.length > 2) {
+ return false;
+ }
+
+ // Not valid discriminator, final check so IntelliJ decided to merge these. thanks.
+ return test[1].length() == 4;
+ }
+}
diff --git a/src/main/java/pw/chew/chewbotcca/util/Mention.java b/src/main/java/pw/chew/chewbotcca/util/Mention.java
index 043ec731..9f2dc44a 100644
--- a/src/main/java/pw/chew/chewbotcca/util/Mention.java
+++ b/src/main/java/pw/chew/chewbotcca/util/Mention.java
@@ -19,7 +19,9 @@
import net.dv8tion.jda.api.JDA;
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.User;
+import net.dv8tion.jda.api.utils.MiscUtil;
public class Mention {
public static User parseUserMention(String mention, JDA jda) {
@@ -55,4 +57,43 @@ public static Object parseMention(String mention, Guild server, JDA jda) {
}
return null;
}
+
+ public static boolean isValidMention(Message.MentionType type, String mention) {
+ String internal = "not numbers";
+ if (!(mention.startsWith("<") && mention.endsWith(">"))) {
+ return false;
+ }
+ switch (type) {
+ case USER -> {
+ if (mention.startsWith("<@!")) {
+ internal = mention.replaceAll("[<>!@]", "");
+ } else {
+ return false;
+ }
+ }
+ case CHANNEL -> {
+ if (mention.startsWith("<#")) {
+ internal = mention.replaceAll("[<>#]", "");
+ } else {
+ return false;
+ }
+ }
+ case ROLE -> {
+ if (mention.startsWith("@&")) {
+ internal = mention.replaceAll("[<>@&]", "");
+ } else {
+ return false;
+ }
+ }
+ }
+ if (internal.length() < 17) {
+ return false;
+ }
+ try {
+ MiscUtil.parseSnowflake(internal);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
}