diff --git a/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java b/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java index 61fa91a..e183d62 100644 --- a/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java +++ b/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java @@ -13,7 +13,9 @@ import sciwhiz12.janitor.commands.bot.ShutdownCommand; import sciwhiz12.janitor.commands.misc.HelloCommand; import sciwhiz12.janitor.commands.misc.OKCommand; import sciwhiz12.janitor.commands.misc.PingCommand; +import sciwhiz12.janitor.commands.moderation.BanCommand; import sciwhiz12.janitor.commands.moderation.KickCommand; +import sciwhiz12.janitor.commands.moderation.UnbanCommand; import sciwhiz12.janitor.utils.Util; import java.util.HashMap; @@ -38,6 +40,8 @@ public class CommandRegistry implements EventListener { addCommand(new OKCommand(this)); addCommand(new HelloCommand(this)); addCommand(new KickCommand(this)); + addCommand(new BanCommand(this)); + addCommand(new UnbanCommand(this)); if (bot.getConfig().getOwnerID().isPresent()) { addCommand(new ShutdownCommand(this, bot.getConfig().getOwnerID().get())); } diff --git a/src/main/java/sciwhiz12/janitor/commands/arguments/CustomStringArgumentType.java b/src/main/java/sciwhiz12/janitor/commands/arguments/CustomStringArgumentType.java new file mode 100644 index 0000000..8a18e7e --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/commands/arguments/CustomStringArgumentType.java @@ -0,0 +1,88 @@ +package sciwhiz12.janitor.commands.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import sciwhiz12.janitor.utils.StringReaderUtil; + +import java.util.Collection; + +public class CustomStringArgumentType implements ArgumentType { + private final StringArgumentType.StringType type; + + private CustomStringArgumentType(final StringArgumentType.StringType type) { + this.type = type; + } + + public static CustomStringArgumentType word() { + return new CustomStringArgumentType( + StringArgumentType.StringType.SINGLE_WORD); + } + + public static CustomStringArgumentType string() { + return new CustomStringArgumentType( + StringArgumentType.StringType.QUOTABLE_PHRASE); + } + + public static CustomStringArgumentType greedyString() { + return new CustomStringArgumentType( + StringArgumentType.StringType.GREEDY_PHRASE); + } + + public static String getString(final CommandContext context, final String name) { + return context.getArgument(name, String.class); + } + + public StringArgumentType.StringType getType() { + return type; + } + + @Override + public String parse(final StringReader reader) throws CommandSyntaxException { + if (type == StringArgumentType.StringType.GREEDY_PHRASE) { + final String text = reader.getRemaining(); + reader.setCursor(reader.getTotalLength()); + return text; + } else if (type == StringArgumentType.StringType.SINGLE_WORD) { + return StringReaderUtil.readUnquotedString(reader); + } else { + return StringReaderUtil.readString(reader); + } + } + + @Override + public String toString() { + return "string()"; + } + + @Override + public Collection getExamples() { + return type.getExamples(); + } + + public static String escapeIfRequired(final String input) { + for (final char c : input.toCharArray()) { + if (!StringReader.isAllowedInUnquotedString(c)) { + return escape(input); + } + } + return input; + } + + private static String escape(final String input) { + final StringBuilder result = new StringBuilder("\""); + + for (int i = 0; i < input.length(); i++) { + final char c = input.charAt(i); + if (c == '\\' || c == '"') { + result.append('\\'); + } + result.append(c); + } + + result.append("\""); + return result.toString(); + } +} diff --git a/src/main/java/sciwhiz12/janitor/commands/arguments/GuildMemberArgument.java b/src/main/java/sciwhiz12/janitor/commands/arguments/GuildMemberArgument.java index 2f3ebd9..3f91b05 100644 --- a/src/main/java/sciwhiz12/janitor/commands/arguments/GuildMemberArgument.java +++ b/src/main/java/sciwhiz12/janitor/commands/arguments/GuildMemberArgument.java @@ -105,7 +105,7 @@ public class GuildMemberArgument implements ArgumentType fromGuild(Guild guild) throws CommandSyntaxException { final String nameLowercase = name.toLowerCase(Locale.ROOT); final List members = guild.getMembers().stream() - .filter(member -> (member.getUser().getName() + '#' + member.getUser().getDiscriminator()).replaceAll("\\s", "").toLowerCase(Locale.ROOT).startsWith(nameLowercase)) + .filter(member -> member.getUser().getAsTag().replaceAll("\\s", "").toLowerCase(Locale.ROOT).startsWith(nameLowercase)) .collect(Collectors.toList()); if (!multiple && members.size() > 1) { throw MULTIPLE_MEMBERS.create(); diff --git a/src/main/java/sciwhiz12/janitor/commands/moderation/BanCommand.java b/src/main/java/sciwhiz12/janitor/commands/moderation/BanCommand.java new file mode 100644 index 0000000..a16c482 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/commands/moderation/BanCommand.java @@ -0,0 +1,102 @@ +package sciwhiz12.janitor.commands.moderation; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +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.MessageChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import org.checkerframework.checker.nullness.qual.Nullable; +import sciwhiz12.janitor.commands.BaseCommand; +import sciwhiz12.janitor.commands.CommandRegistry; +import sciwhiz12.janitor.commands.util.ModerationHelper; + +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; + +import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger; +import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static sciwhiz12.janitor.commands.arguments.GuildMemberArgument.getMembers; +import static sciwhiz12.janitor.commands.arguments.GuildMemberArgument.member; +import static sciwhiz12.janitor.commands.util.CommandHelper.argument; +import static sciwhiz12.janitor.commands.util.CommandHelper.literal; + +public class BanCommand extends BaseCommand { + public static final EnumSet BAN_PERMISSION = EnumSet.of(Permission.BAN_MEMBERS); + + /* + ban command + !ban [reason] + !ban delete [reason] + */ + + public BanCommand(CommandRegistry registry) { + super(registry); + } + + @Override + public LiteralArgumentBuilder getNode() { + return literal("ban") + .then(argument("member", member()) + .then(argument("reason", greedyString()) + .executes(ctx -> this.run(ctx, 0, getString(ctx, "reason"))) + ) + .executes(ctx -> this.run(ctx, 0, null)) + ) + .then(literal("delete") + .then(argument("days", integer(0, 7)) + .then(argument("member", member()) + .then(argument("reason", greedyString()) + .executes(ctx -> this.run(ctx, getInteger(ctx, "days"), getString(ctx, "reason"))) + ) + .executes(ctx -> this.run(ctx, getInteger(ctx, "days"), null)) + ) + ) + ); + } + + public int run(CommandContext ctx, int days, @Nullable String reason) throws CommandSyntaxException { + realRun(ctx, days, reason); + return 1; + } + + void realRun(CommandContext ctx, int days, @Nullable String reason) throws CommandSyntaxException { + MessageChannel channel = ctx.getSource().getChannel(); + if (!ctx.getSource().isFromGuild()) { + messages().GENERAL.guildOnlyCommand(channel).queue(); + return; + } + final Guild guild = ctx.getSource().getGuild(); + final Member performer = Objects.requireNonNull(ctx.getSource().getMember()); + + final List members = getMembers("member", ctx).fromGuild(performer.getGuild()); + if (members.size() < 1) return; + final Member target = members.get(0); + + if (guild.getSelfMember().equals(target)) + messages().GENERAL.cannotActionSelf(channel).queue(); + else if (performer.equals(target)) + messages().GENERAL.cannotActionPerformer(channel, performer).queue(); + else if (!guild.getSelfMember().hasPermission(BAN_PERMISSION)) + messages().GENERAL.insufficientPermissions(channel, BAN_PERMISSION).queue(); + else if (!guild.getSelfMember().canInteract(target)) + messages().GENERAL.cannotInteract(channel, target).queue(); + else if (!performer.hasPermission(BAN_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, BAN_PERMISSION).queue(); + else if (!performer.canInteract(target)) + messages().MODERATION.cannotModerate(channel, performer, target).queue(); + else + target.getUser().openPrivateChannel() + .flatMap(dm -> messages().MODERATION.bannedDM(dm, performer, target, reason)) + .mapToResult() + .flatMap(res -> ModerationHelper.banUser(target.getGuild(), performer, target, days, reason) + .flatMap( + v -> messages().MODERATION.banUser(channel, performer, target, reason, days, res.isSuccess()))) + .queue(); + } +} diff --git a/src/main/java/sciwhiz12/janitor/commands/moderation/UnbanCommand.java b/src/main/java/sciwhiz12/janitor/commands/moderation/UnbanCommand.java new file mode 100644 index 0000000..f898632 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/commands/moderation/UnbanCommand.java @@ -0,0 +1,109 @@ +package sciwhiz12.janitor.commands.moderation; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +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.MessageChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import sciwhiz12.janitor.commands.BaseCommand; +import sciwhiz12.janitor.commands.CommandRegistry; +import sciwhiz12.janitor.commands.util.ModerationHelper; + +import java.util.EnumSet; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.mojang.brigadier.arguments.LongArgumentType.getLong; +import static com.mojang.brigadier.arguments.LongArgumentType.longArg; +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static sciwhiz12.janitor.commands.util.CommandHelper.argument; +import static sciwhiz12.janitor.commands.util.CommandHelper.literal; + +public class UnbanCommand extends BaseCommand { + public static final EnumSet UNBAN_PERMISSION = EnumSet.of(Permission.BAN_MEMBERS); + + public UnbanCommand(CommandRegistry registry) { + super(registry); + } + + @Override + public LiteralArgumentBuilder getNode() { + return literal("unban") + .then(argument("user", StringArgumentType.string()) + .executes(this::namedRun) + ).then(argument("userID", longArg()) + .executes(this::idRun) + ); + } + + public int namedRun(CommandContext ctx) { + realNamedRun(ctx); + return 1; + } + + void realNamedRun(CommandContext ctx) { + MessageChannel channel = ctx.getSource().getChannel(); + if (!ctx.getSource().isFromGuild()) { + messages().GENERAL.guildOnlyCommand(channel).queue(); + return; + } + final Guild guild = ctx.getSource().getGuild(); + final Member performer = Objects.requireNonNull(ctx.getSource().getMember()); + + final String username = getString(ctx, "user").toLowerCase(Locale.ROOT); + guild.retrieveBanList() + .map(list -> list.stream().parallel() + .filter(ban -> ban.getUser().getAsTag().replaceAll("\\s", "").toLowerCase(Locale.ROOT) + .startsWith(username)) + .collect(Collectors.toList())) + .queue(bans -> { + if (bans.size() > 1) + messages().GENERAL.ambiguousMember(channel); + else if (bans.size() == 1) + tryUnban(channel, guild, performer, bans.get(0).getUser()); + }); + } + + public int idRun(CommandContext ctx) { + realIdRun(ctx); + return 1; + } + + void realIdRun(CommandContext ctx) { + MessageChannel channel = ctx.getSource().getChannel(); + if (!ctx.getSource().isFromGuild()) { + messages().GENERAL.guildOnlyCommand(channel).queue(); + return; + } + final Guild guild = ctx.getSource().getGuild(); + final Member performer = Objects.requireNonNull(ctx.getSource().getMember()); + + final long id = getLong(ctx, "userID"); + guild.retrieveBanList() + .map(list -> list.stream().parallel() + .filter(ban -> ban.getUser().getIdLong() == id) + .collect(Collectors.toList())) + .queue(bans -> { + if (bans.size() != 1) { + return; + } + tryUnban(channel, guild, performer, bans.get(0).getUser()); + }); + } + + void tryUnban(MessageChannel channel, Guild guild, Member performer, User target) { + if (!guild.getSelfMember().hasPermission(UNBAN_PERMISSION)) + messages().GENERAL.insufficientPermissions(channel, UNBAN_PERMISSION).queue(); + else if (!performer.hasPermission(UNBAN_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, UNBAN_PERMISSION).queue(); + else + ModerationHelper.unbanUser(guild, target) + .flatMap(v -> messages().MODERATION.unbanUser(channel, performer, target)) + .queue(); + } +} diff --git a/src/main/java/sciwhiz12/janitor/commands/util/ModerationHelper.java b/src/main/java/sciwhiz12/janitor/commands/util/ModerationHelper.java index d8b88c4..e2304c3 100644 --- a/src/main/java/sciwhiz12/janitor/commands/util/ModerationHelper.java +++ b/src/main/java/sciwhiz12/janitor/commands/util/ModerationHelper.java @@ -2,6 +2,7 @@ package sciwhiz12.janitor.commands.util; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; import org.checkerframework.checker.nullness.qual.Nullable; @@ -15,4 +16,17 @@ public class ModerationHelper { auditReason.append(" for reason: ").append(reason); return guild.kick(target, auditReason.toString()); } + + public static AuditableRestAction banUser(Guild guild, Member performer, Member target, int deleteDuration, + @Nullable String reason) { + StringBuilder auditReason = new StringBuilder(); + auditReason.append("Banned by ").append(nameFor(performer.getUser())); + if (reason != null) + auditReason.append(" for reason: ").append(reason); + return guild.ban(target, deleteDuration, auditReason.toString()); + } + + public static AuditableRestAction unbanUser(Guild guild, User target) { + return guild.unban(target); + } } diff --git a/src/main/java/sciwhiz12/janitor/msg/Messages.java b/src/main/java/sciwhiz12/janitor/msg/Messages.java index 5702b84..14d0725 100644 --- a/src/main/java/sciwhiz12/janitor/msg/Messages.java +++ b/src/main/java/sciwhiz12/janitor/msg/Messages.java @@ -6,6 +6,7 @@ import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.MessageChannel; import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageAction; import org.checkerframework.checker.nullness.qual.Nullable; @@ -94,7 +95,8 @@ public class Messages { new EmbedBuilder() .setTitle(translate("general.cannot_action_performer.title")) .setDescription(translate("general.cannot_action_performer.desc")) - .addField(translate("general.cannot_action_performer.field.performer"), performer.getUser().getAsMention(), true) + .addField(translate("general.cannot_action_performer.field.performer"), performer.getUser().getAsMention(), + true) .setColor(General.FAILURE_COLOR) .build() ); @@ -159,6 +161,42 @@ public class Messages { embed.addField(translate("moderation.kick.dm.field.reason"), reason, false); return channel.sendMessage(embed.setColor(MODERATION_COLOR).build()); } + + public MessageAction banUser(MessageChannel channel, Member performer, Member target, @Nullable String reason, + int deletionDays, boolean sentDM) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(translate("moderation.ban.info.author"), null, GAVEL_ICON_URL) + .addField(translate("moderation.ban.info.field.performer"), performer.getUser().getAsMention(), true) + .addField(translate("moderation.ban.info.field.target"), target.getUser().getAsMention(), true) + .addField(translate("moderation.ban.info.field.sent_private_message"), sentDM ? "✅" : "❌", true); + if (deletionDays != 0) + embed.addField(translate("moderation.ban.info.field.delete_duration"), + String.valueOf(deletionDays).concat(" day(s)"), true); + if (reason != null) + embed.addField(translate("moderation.ban.info.field.reason"), reason, false); + return channel.sendMessage(embed.setColor(MODERATION_COLOR).build()); + } + + public MessageAction bannedDM(MessageChannel channel, Member performer, Member target, @Nullable String reason) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(performer.getGuild().getName(), null, performer.getGuild().getIconUrl()) + .setTitle(translate("moderation.ban.dm.title")) + .addField(translate("moderation.ban.dm.field.performer"), performer.getUser().getAsMention(), true); + if (reason != null) + embed.addField(translate("moderation.ban.dm.field.reason"), reason, false); + return channel.sendMessage(embed.setColor(MODERATION_COLOR).build()); + } + + public MessageAction unbanUser(MessageChannel channel, Member performer, User target) { + return channel.sendMessage( + new EmbedBuilder() + .setAuthor(translate("moderation.unban.info.author"), null, GAVEL_ICON_URL) + .addField(translate("moderation.unban.info.field.performer"), performer.getUser().getAsMention(), true) + .addField(translate("moderation.unban.info.field.target"), target.getAsMention(), true) + .setColor(MODERATION_COLOR) + .build() + ); + } } } diff --git a/src/main/resources/english.json b/src/main/resources/english.json index f7537c8..f4a80cf 100644 --- a/src/main/resources/english.json +++ b/src/main/resources/english.json @@ -29,5 +29,17 @@ "moderation.kick.info.field.sent_private_message": "Sent DM", "moderation.kick.dm.title": "You were kicked from this server.", "moderation.kick.dm.field.performer": "Moderator", - "moderation.kick.dm.field.reason": "Reason" + "moderation.kick.dm.field.reason": "Reason", + "moderation.ban.info.author": "Banned user from server.", + "moderation.ban.info.field.performer": "Performer", + "moderation.ban.info.field.target": "Target", + "moderation.ban.info.field.reason": "Reason", + "moderation.ban.info.field.sent_private_message": "Sent DM", + "moderation.ban.info.field.delete_duration": "Days of Message Deletion", + "moderation.ban.dm.title": "You were banned from this server.", + "moderation.ban.dm.field.performer": "Moderator", + "moderation.ban.dm.field.reason": "Reason", + "moderation.unban.info.author": "Unbanned user from server.", + "moderation.unban.info.field.performer": "Performer", + "moderation.unban.info.field.target": "Target" } \ No newline at end of file