From 412c499cfc0648c911de6520d039089015868716 Mon Sep 17 00:00:00 2001 From: Arnold Alejo Nunag Date: Thu, 1 Oct 2020 00:53:08 +0800 Subject: [PATCH] Add warnings system and commands, and per-guild storage --- .../java/sciwhiz12/janitor/GuildStorage.java | 120 ++++++++++++++++++ .../java/sciwhiz12/janitor/JanitorBot.java | 11 +- src/main/java/sciwhiz12/janitor/Logging.java | 1 + .../janitor/commands/CommandRegistry.java | 6 + .../commands/moderation/UnwarnCommand.java | 72 +++++++++++ .../commands/moderation/WarnCommand.java | 85 +++++++++++++ .../commands/moderation/WarnListCommand.java | 102 +++++++++++++++ .../sciwhiz12/janitor/config/BotConfig.java | 6 + .../moderation/warns/WarningEntry.java | 96 ++++++++++++++ .../moderation/warns/WarningStorage.java | 78 ++++++++++++ .../java/sciwhiz12/janitor/msg/Messages.java | 100 +++++++++++++++ .../janitor/storage/AbstractStorage.java | 22 ++++ .../sciwhiz12/janitor/storage/IStorage.java | 13 ++ .../janitor/storage/JsonStorage.java | 30 +++++ src/main/resources/english.json | 33 ++++- 15 files changed, 773 insertions(+), 2 deletions(-) create mode 100644 src/main/java/sciwhiz12/janitor/GuildStorage.java create mode 100644 src/main/java/sciwhiz12/janitor/commands/moderation/UnwarnCommand.java create mode 100644 src/main/java/sciwhiz12/janitor/commands/moderation/WarnCommand.java create mode 100644 src/main/java/sciwhiz12/janitor/commands/moderation/WarnListCommand.java create mode 100644 src/main/java/sciwhiz12/janitor/moderation/warns/WarningEntry.java create mode 100644 src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java create mode 100644 src/main/java/sciwhiz12/janitor/storage/AbstractStorage.java create mode 100644 src/main/java/sciwhiz12/janitor/storage/IStorage.java create mode 100644 src/main/java/sciwhiz12/janitor/storage/JsonStorage.java diff --git a/src/main/java/sciwhiz12/janitor/GuildStorage.java b/src/main/java/sciwhiz12/janitor/GuildStorage.java new file mode 100644 index 0000000..a69e3f7 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/GuildStorage.java @@ -0,0 +1,120 @@ +package sciwhiz12.janitor; + +import com.google.common.base.Preconditions; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.dv8tion.jda.api.entities.Guild; +import sciwhiz12.janitor.storage.IStorage; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.Supplier; + +import static java.nio.file.StandardOpenOption.*; + +public class GuildStorage { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create(); + + private final JanitorBot bot; + private final Path mainFolder; + private final Map> guildStorage = new IdentityHashMap<>(); + + public GuildStorage(JanitorBot bot, Path mainFolder) { + Preconditions.checkArgument(Files.isDirectory(mainFolder) || Files.notExists(mainFolder)); + this.bot = bot; + this.mainFolder = mainFolder; + } + + public JanitorBot getBot() { + return bot; + } + + public T getOrCreate(Guild guild, String key, Supplier defaultSupplier) { + final Map storageMap = guildStorage.computeIfAbsent(guild, g -> new HashMap<>()); + //noinspection unchecked + return (T) storageMap.computeIfAbsent(key, k -> load(guild, key, defaultSupplier.get())); + } + + private Path getFile(Guild guild, String key) { + final Path guildFolder = makeFolder(guild); + final Path file = Path.of(key + ".json"); + return mainFolder.resolve(guildFolder).resolve(file); + } + + public T load(Guild guild, String key, T storage) { + final Path file = getFile(guild, key); + if (Files.notExists(file)) return storage; + + Logging.JANITOR.debug("Loading storage {} for guild {}", key, guild); + try (Reader reader = Files.newBufferedReader(file)) { + storage.read(reader); + } + catch (IOException e) { + Logging.JANITOR.error("Error while loading storage {} for guild {}", key, guild, e); + } + return storage; + } + + public void save() { + Logging.JANITOR.debug("Saving guild storage to files under {}...", mainFolder); + boolean anySaved = false; + for (Guild guild : guildStorage.keySet()) { + final Map storageMap = guildStorage.get(guild); + for (String key : storageMap.keySet()) { + final IStorage storage = storageMap.get(key); + if (storage.dirty()) { + final Path file = getFile(guild, key); + try { + if (Files.notExists(file.getParent())) Files.createDirectories(file.getParent()); + if (Files.notExists(file)) Files.createFile(file); + try (Writer writer = Files + .newBufferedWriter(file, CREATE, WRITE, TRUNCATE_EXISTING)) { + storage.write(writer); + anySaved = true; + } + } + catch (IOException e) { + Logging.JANITOR.error("Error while writing storage {} for guild {}", key, guild, e); + } + } + } + } + if (anySaved) + Logging.JANITOR.info("Saved guild storage to files under {}", mainFolder); + } + + private Path makeFolder(Guild guild) { + return Path.of(Long.toHexString(guild.getIdLong())); + } + + public static class SavingThread extends Thread { + private final GuildStorage storage; + private volatile boolean running = true; + + public SavingThread(GuildStorage storage) { + this.storage = storage; + this.setName("GuildStorage-Saving-Thread"); + this.setDaemon(true); + } + + public void stopThread() { + running = false; + this.interrupt(); + } + + @Override + public void run() { + while (running) { + storage.save(); + try { Thread.sleep(10000); } + catch (InterruptedException ignored) {} + } + } + } +} diff --git a/src/main/java/sciwhiz12/janitor/JanitorBot.java b/src/main/java/sciwhiz12/janitor/JanitorBot.java index 79af96b..321d0c2 100644 --- a/src/main/java/sciwhiz12/janitor/JanitorBot.java +++ b/src/main/java/sciwhiz12/janitor/JanitorBot.java @@ -22,12 +22,15 @@ public class JanitorBot { private final BotConfig config; private final Messages messages; private BotConsole console; + private final GuildStorage storage; + private final GuildStorage.SavingThread storageSavingThread; private CommandRegistry cmdRegistry; private Translations translations; public JanitorBot(JDA discord, BotConfig config) { this.config = config; this.console = new BotConsole(this, System.in); + this.storage = new GuildStorage(this, config.getStoragePath()); this.cmdRegistry = new CommandRegistry(this, config.getCommandPrefix()); this.discord = discord; this.translations = new Translations(this, config.getTranslationsFile()); @@ -47,6 +50,8 @@ public class JanitorBot { error -> JANITOR.error(STATUS, "Error while sending ready message to owner", error) ) ); + storageSavingThread = new GuildStorage.SavingThread(storage); + storageSavingThread.start(); console.start(); } @@ -60,6 +65,8 @@ public class JanitorBot { public Messages getMessages() { return this.messages; } + public GuildStorage getStorage() { return this.storage; } + public CommandRegistry getCommandRegistry() { return this.cmdRegistry; } @@ -70,7 +77,6 @@ public class JanitorBot { public void shutdown() { JANITOR.info(STATUS, "Shutting down!"); - console.stop(); getConfig().getOwnerID() .map(discord::retrieveUserById) .map(owner -> @@ -90,5 +96,8 @@ public class JanitorBot { )) ).ifPresent(CompletableFuture::join); discord.shutdown(); + storageSavingThread.stopThread(); + storage.save(); + console.stop(); } } diff --git a/src/main/java/sciwhiz12/janitor/Logging.java b/src/main/java/sciwhiz12/janitor/Logging.java index 8cb8d42..dc38a81 100644 --- a/src/main/java/sciwhiz12/janitor/Logging.java +++ b/src/main/java/sciwhiz12/janitor/Logging.java @@ -9,6 +9,7 @@ public class Logging { public static final Marker STATUS = MarkerFactory.getMarker("STATUS"); public static final Marker COMMANDS = MarkerFactory.getMarker("COMMANDS"); public static final Marker TRANSLATIONS = MarkerFactory.getMarker("TRANSLATIONS"); + public static final Marker STORAGE = MarkerFactory.getMarker("STORAGE"); public static final Logger JANITOR = LoggerFactory.getLogger("janitor"); public static final Logger CONSOLE = LoggerFactory.getLogger("janitor.console"); diff --git a/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java b/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java index e183d62..46ef45b 100644 --- a/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java +++ b/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java @@ -16,6 +16,9 @@ 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.commands.moderation.UnwarnCommand; +import sciwhiz12.janitor.commands.moderation.WarnCommand; +import sciwhiz12.janitor.commands.moderation.WarnListCommand; import sciwhiz12.janitor.utils.Util; import java.util.HashMap; @@ -42,6 +45,9 @@ public class CommandRegistry implements EventListener { addCommand(new KickCommand(this)); addCommand(new BanCommand(this)); addCommand(new UnbanCommand(this)); + addCommand(new WarnCommand(this)); + addCommand(new WarnListCommand(this)); + addCommand(new UnwarnCommand(this)); if (bot.getConfig().getOwnerID().isPresent()) { addCommand(new ShutdownCommand(this, bot.getConfig().getOwnerID().get())); } diff --git a/src/main/java/sciwhiz12/janitor/commands/moderation/UnwarnCommand.java b/src/main/java/sciwhiz12/janitor/commands/moderation/UnwarnCommand.java new file mode 100644 index 0000000..01fed67 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/commands/moderation/UnwarnCommand.java @@ -0,0 +1,72 @@ +package sciwhiz12.janitor.commands.moderation; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +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.events.message.MessageReceivedEvent; +import org.checkerframework.checker.nullness.qual.Nullable; +import sciwhiz12.janitor.commands.BaseCommand; +import sciwhiz12.janitor.commands.CommandRegistry; +import sciwhiz12.janitor.moderation.warns.WarningEntry; +import sciwhiz12.janitor.moderation.warns.WarningStorage; + +import java.time.OffsetDateTime; +import java.util.EnumSet; +import java.util.Objects; + +import static sciwhiz12.janitor.commands.util.CommandHelper.argument; +import static sciwhiz12.janitor.commands.util.CommandHelper.literal; + +public class UnwarnCommand extends BaseCommand { + public static final EnumSet WARN_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS); + + public UnwarnCommand(CommandRegistry registry) { + super(registry); + } + + @Override + public LiteralArgumentBuilder getNode() { + return literal("unwarn") + .then(argument("caseId", IntegerArgumentType.integer(1)) + .executes(this::run) + ); + } + + public int run(CommandContext ctx) { + realRun(ctx); + return 1; + } + + void realRun(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()); + int caseID = IntegerArgumentType.getInteger(ctx, "caseId"); + + final OffsetDateTime dateTime = OffsetDateTime.now(); + + if (!performer.hasPermission(WARN_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, WARN_PERMISSION).queue(); + else { + final WarningStorage storage = WarningStorage.get(getBot().getStorage(), guild); + @Nullable + final WarningEntry entry = storage.getWarning(caseID); + if (entry == null) + messages().MODERATION.noWarnWithID(channel, performer, caseID).queue(); + else if (entry.getWarned().getIdLong() == performer.getIdLong()) + messages().MODERATION.cannotUnwarnSelf(channel, performer, caseID, entry).queue(); + else { + storage.removeWarning(caseID); + messages().MODERATION.unwarn(channel, performer, caseID, entry).queue(); + } + } + } +} diff --git a/src/main/java/sciwhiz12/janitor/commands/moderation/WarnCommand.java b/src/main/java/sciwhiz12/janitor/commands/moderation/WarnCommand.java new file mode 100644 index 0000000..1b46de6 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/commands/moderation/WarnCommand.java @@ -0,0 +1,85 @@ +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 sciwhiz12.janitor.commands.BaseCommand; +import sciwhiz12.janitor.commands.CommandRegistry; +import sciwhiz12.janitor.moderation.warns.WarningEntry; +import sciwhiz12.janitor.moderation.warns.WarningStorage; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; + +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 WarnCommand extends BaseCommand { + public static final EnumSet WARN_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS); + + public WarnCommand(CommandRegistry registry) { + super(registry); + } + + @Override + public LiteralArgumentBuilder getNode() { + return literal("warn") + .then(argument("member", member()) + .then(argument("reason", greedyString()) + .executes(ctx -> this.run(ctx, getString(ctx, "reason"))) + ) + ); + } + + public int run(CommandContext ctx, String reason) throws CommandSyntaxException { + realRun(ctx, reason); + return 1; + } + + void realRun(CommandContext ctx, 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); + + final OffsetDateTime dateTime = OffsetDateTime.now(ZoneOffset.UTC); + if (guild.getSelfMember().equals(target)) + messages().GENERAL.cannotActionSelf(channel).queue(); + else if (performer.equals(target)) + messages().GENERAL.cannotActionPerformer(channel, performer).queue(); + else if (!performer.hasPermission(WARN_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, WARN_PERMISSION).queue(); + else if (!performer.canInteract(target)) + messages().MODERATION.cannotModerate(channel, performer, target).queue(); + else + target.getUser().openPrivateChannel() + .flatMap(dm -> messages().MODERATION.warnDM(dm, performer, target, reason, dateTime)) + .mapToResult() + .flatMap(res -> { + int caseId = WarningStorage.get(getBot().getStorage(), guild) + .addWarning(new WarningEntry(target.getUser(), performer.getUser(), dateTime, reason)); + return messages().MODERATION + .warnUser(channel, performer, target, reason, dateTime, caseId, res.isSuccess()); + }) + .queue(); + } +} diff --git a/src/main/java/sciwhiz12/janitor/commands/moderation/WarnListCommand.java b/src/main/java/sciwhiz12/janitor/commands/moderation/WarnListCommand.java new file mode 100644 index 0000000..14fbbbc --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/commands/moderation/WarnListCommand.java @@ -0,0 +1,102 @@ +package sciwhiz12.janitor.commands.moderation; + +import com.google.common.collect.ImmutableMap; +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 sciwhiz12.janitor.commands.BaseCommand; +import sciwhiz12.janitor.commands.CommandRegistry; +import sciwhiz12.janitor.moderation.warns.WarningEntry; +import sciwhiz12.janitor.moderation.warns.WarningStorage; + +import java.time.OffsetDateTime; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +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 WarnListCommand extends BaseCommand { + public static final EnumSet WARN_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS); + + public WarnListCommand(CommandRegistry registry) { + super(registry); + } + + @Override + public LiteralArgumentBuilder getNode() { + return literal("warnlist") + .then(literal("target") + .then(argument("target", member()) + .then(literal("mod") + .then(argument("moderator", member()) + .executes(ctx -> this.run(ctx, true, true)) + ) + ) + .executes(ctx -> this.run(ctx, true, false)) + ) + ).then(literal("mod") + .then(argument("moderator", member()) + .executes(ctx -> this.run(ctx, false, true)) + ) + ) + .executes(ctx -> this.run(ctx, false, false)); + } + + public int run(CommandContext ctx, boolean filterTarget, boolean filterModerator) + throws CommandSyntaxException { + realRun(ctx, filterTarget, filterModerator); + return 1; + } + + void realRun(CommandContext ctx, boolean filterTarget, boolean filterModerator) + 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()); + Predicate> predicate = e -> true; + + if (filterTarget) { + final List members = getMembers("target", 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(); + return; + } + predicate = predicate.and(e -> e.getValue().getWarned().getIdLong() == target.getIdLong()); + } + if (filterModerator) { + final List members = getMembers("moderator", ctx).fromGuild(performer.getGuild()); + if (members.size() < 1) return; + final Member mod = members.get(0); + predicate = predicate.and(e -> e.getValue().getPerformer().getIdLong() == mod.getIdLong()); + } + + final OffsetDateTime dateTime = OffsetDateTime.now(); + + if (!performer.hasPermission(WARN_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, WARN_PERMISSION).queue(); + else + messages().MODERATION.warnList(channel, WarningStorage.get(getBot().getStorage(), guild) + .getWarnings() + .entrySet().stream() + .filter(predicate) + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)) + ).queue(); + } +} diff --git a/src/main/java/sciwhiz12/janitor/config/BotConfig.java b/src/main/java/sciwhiz12/janitor/config/BotConfig.java index 3868e73..f1ad93d 100644 --- a/src/main/java/sciwhiz12/janitor/config/BotConfig.java +++ b/src/main/java/sciwhiz12/janitor/config/BotConfig.java @@ -14,8 +14,10 @@ import static sciwhiz12.janitor.Logging.JANITOR; public class BotConfig { public static final Path DEFAULT_CONFIG_PATH = Path.of("config.toml"); + public static final Path DEFAULT_STORAGE_PATH = Path.of("guild_storage"); public static final String CLIENT_TOKEN = "discord.client_token"; public static final String OWNER_ID = "discord.owner_id"; + public static final String STORAGE_PATH = "storage.main_path"; public static final String TRANSLATION_FILE_PATH = "messages.translation_file"; public static final String COMMAND_PREFIX = "commands.prefix"; @@ -52,6 +54,10 @@ public class BotConfig { .orElse(null); } + public Path getStoragePath() { + return config.getOptional(STORAGE_PATH).map(Path::of).orElse(DEFAULT_STORAGE_PATH); + } + public Optional getToken() { return options.getToken().or(() -> config.getOptional(CLIENT_TOKEN)); } diff --git a/src/main/java/sciwhiz12/janitor/moderation/warns/WarningEntry.java b/src/main/java/sciwhiz12/janitor/moderation/warns/WarningEntry.java new file mode 100644 index 0000000..e136c77 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/moderation/warns/WarningEntry.java @@ -0,0 +1,96 @@ +package sciwhiz12.janitor.moderation.warns; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import net.dv8tion.jda.api.entities.User; +import sciwhiz12.janitor.JanitorBot; + +import java.lang.reflect.Type; +import java.time.OffsetDateTime; +import java.util.Objects; +import javax.annotation.Nullable; + +public class WarningEntry { + private final User performer; + private final User warned; + private final OffsetDateTime dateTime; + @Nullable + private final String reason; + + public WarningEntry(User warned, User performer, OffsetDateTime dateTime, @Nullable String reason) { + this.performer = performer; + this.warned = warned; + this.dateTime = dateTime; + this.reason = reason; + } + + public User getPerformer() { + return performer; + } + + public User getWarned() { + return warned; + } + + public OffsetDateTime getDateTime() { + return dateTime; + } + + @Nullable + public String getReason() { + return reason; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WarningEntry that = (WarningEntry) o; + return getPerformer().equals(that.getPerformer()) && + getWarned().equals(that.getWarned()) && + getDateTime().equals(that.getDateTime()) && + Objects.equals(getReason(), that.getReason()); + } + + @Override + public int hashCode() { + return Objects.hash(getPerformer(), getWarned(), getDateTime(), getReason()); + } + + public static class Serializer implements JsonDeserializer, JsonSerializer { + private final JanitorBot bot; + + public Serializer(JanitorBot bot) { + this.bot = bot; + } + + @Override + public WarningEntry deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + final JsonObject obj = json.getAsJsonObject(); + final User warned = bot.getDiscord().retrieveUserById(obj.get("warned").getAsLong()).complete(); + final User performer = bot.getDiscord().retrieveUserById(obj.get("performer").getAsLong()).complete(); + final OffsetDateTime dateTime = OffsetDateTime.parse(obj.get("dateTime").getAsString()); + @Nullable + final String reason = obj.has("reason") ? obj.get("reason").getAsString() : null; + return new WarningEntry(warned, performer, dateTime, reason); + } + + @Override + public JsonElement serialize(WarningEntry src, Type typeOfSrc, JsonSerializationContext context) { + final JsonObject obj = new JsonObject(); + obj.addProperty("warned", src.getWarned().getId()); + obj.addProperty("performer", src.getPerformer().getId()); + obj.addProperty("dateTime", src.getDateTime().toString()); + if (src.getReason() != null) { + obj.addProperty("reason", src.getReason()); + } + return obj; + } + } +} diff --git a/src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java b/src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java new file mode 100644 index 0000000..b540c34 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java @@ -0,0 +1,78 @@ +package sciwhiz12.janitor.moderation.warns; + +import com.electronwill.nightconfig.core.utils.ObservedMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import net.dv8tion.jda.api.entities.Guild; +import org.checkerframework.checker.nullness.qual.Nullable; +import sciwhiz12.janitor.GuildStorage; +import sciwhiz12.janitor.JanitorBot; +import sciwhiz12.janitor.storage.JsonStorage; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +public class WarningStorage extends JsonStorage { + private static final Type WARNING_MAP_TYPE = new TypeToken>() {}.getType(); + public static final String STORAGE_KEY = "warnings"; + + public static WarningStorage get(GuildStorage storage, Guild guild) { + return storage.getOrCreate(guild, STORAGE_KEY, () -> new WarningStorage(storage.getBot())); + } + + private final Gson gson; + private final JanitorBot bot; + private int lastID = 1; + private final Map warnings = new ObservedMap<>(new HashMap<>(), this::markDirty); + + public WarningStorage(JanitorBot bot) { + this.bot = bot; + this.gson = new GsonBuilder() + .registerTypeAdapter(WarningEntry.class, new WarningEntry.Serializer(bot)) + .create(); + } + + public JanitorBot getBot() { + return bot; + } + + public int addWarning(WarningEntry entry) { + int id = lastID++; + warnings.put(id, entry); + return id; + } + + @Nullable + public WarningEntry getWarning(int caseID) { + return warnings.get(caseID); + } + + public WarningEntry removeWarning(int caseID) { + return warnings.remove(caseID); + } + + public Map getWarnings() { + return warnings; + } + + @Override + public JsonElement save() { + JsonObject obj = new JsonObject(); + obj.addProperty("lastCaseID", lastID); + obj.add("warnings", gson.toJsonTree(warnings)); + return obj; + } + + @Override + public void load(JsonElement in) { + final JsonObject obj = in.getAsJsonObject(); + lastID = obj.get("lastCaseID").getAsInt(); + final Map loaded = gson.fromJson(obj.get("warnings"), WARNING_MAP_TYPE); + warnings.clear(); + warnings.putAll(loaded); + } +} diff --git a/src/main/java/sciwhiz12/janitor/msg/Messages.java b/src/main/java/sciwhiz12/janitor/msg/Messages.java index 6daacf2..79487e0 100644 --- a/src/main/java/sciwhiz12/janitor/msg/Messages.java +++ b/src/main/java/sciwhiz12/janitor/msg/Messages.java @@ -11,12 +11,18 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageAction; import org.checkerframework.checker.nullness.qual.Nullable; import sciwhiz12.janitor.JanitorBot; +import sciwhiz12.janitor.moderation.warns.WarningEntry; import java.time.Clock; import java.time.OffsetDateTime; +import java.util.Collections; +import java.util.Comparator; import java.util.EnumSet; +import java.util.Map; import java.util.stream.Collectors; +import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; + public class Messages { private final JanitorBot bot; public final General GENERAL; @@ -212,6 +218,100 @@ public class Messages { .build() ); } + + public MessageAction warnUser(MessageChannel channel, Member performer, Member target, String reason, + OffsetDateTime dateTime, int caseID, boolean sentDM) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(translate("moderation.warn.info.author"), null, GAVEL_ICON_URL) + .addField(translate("moderation.warn.info.field.performer"), performer.getUser().getAsMention(), true) + .addField(translate("moderation.warn.info.field.target"), target.getUser().getAsMention(), true) + .addField(translate("moderation.warn.info.field.sent_private_message"), sentDM ? "✅" : "❌", true) + .addField(translate("moderation.warn.info.field.case_id"), String.valueOf(caseID), true) + .addField(translate("moderation.warn.info.field.date_time"), + dateTime.format(RFC_1123_DATE_TIME), true) + .addField(translate("moderation.warn.info.field.reason"), reason, false); + return channel + .sendMessage(embed.setColor(MODERATION_COLOR).setTimestamp(OffsetDateTime.now(Clock.systemUTC())).build()); + } + + public MessageAction warnDM(MessageChannel channel, Member performer, Member target, String reason, + OffsetDateTime dateTime) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(performer.getGuild().getName(), null, performer.getGuild().getIconUrl()) + .setTitle(translate("moderation.warn.dm.title")) + .addField(translate("moderation.warn.dm.field.performer"), performer.getUser().getAsMention(), true) + .addField(translate("moderation.warn.dm.field.date_time"), + dateTime.format(RFC_1123_DATE_TIME), true) + .addField(translate("moderation.warn.dm.field.reason"), reason, false); + return channel.sendMessage(embed.build()); + } + + public MessageAction warnList(MessageChannel channel, Map displayWarnings) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(translate("moderation.warnlist.author"), null, GAVEL_ICON_URL) + .setColor(MODERATION_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())); + String warningsDesc = displayWarnings.size() > 0 ? displayWarnings.entrySet().stream() + .sorted(Collections.reverseOrder(Comparator.comparingInt(Map.Entry::getKey))) + .limit(10) + .map(entry -> + translate("moderation.warnlist.entry", + entry.getKey(), + entry.getValue().getWarned().getAsMention(), + entry.getValue().getPerformer().getAsMention(), + entry.getValue().getDateTime().format(RFC_1123_DATE_TIME), + entry.getValue().getReason() != null + ? entry.getValue().getReason() + : translate("moderation.warnlist.entry.no_reason")) + ) + .collect(Collectors.joining("\n")) + : translate("moderation.warnlist.empty"); + embed.setDescription(warningsDesc); + return channel.sendMessage(embed.build()); + } + + public MessageAction noWarnWithID(MessageChannel channel, Member performer, int caseID) { + final EmbedBuilder embed = new EmbedBuilder() + .setTitle(translate("moderation.unwarn.no_case_found.title"), null) + .setColor(General.FAILURE_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())) + .setDescription(translate("moderation.unwarn.no_case_found.desc")) + .addField(translate("moderation.unwarn.no_case_found.field.performer"), performer.getUser().getAsMention(), + true) + .addField(translate("moderation.unwarn.no_case_found.field.case_id"), String.valueOf(caseID), true); + return channel.sendMessage(embed.build()); + } + + public MessageAction cannotUnwarnSelf(MessageChannel channel, Member performer, int caseID, WarningEntry entry) { + final EmbedBuilder embed = new EmbedBuilder() + .setTitle(translate("moderation.unwarn.cannot_unwarn_self.title"), null) + .setColor(General.FAILURE_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())) + .setDescription(translate("moderation.unwarn.cannot_unwarn_self.desc")) + .addField(translate("moderation.unwarn.cannot_unwarn_self.field.performer"), + performer.getUser().getAsMention(), true) + .addField(translate("moderation.unwarn.cannot_unwarn_self.field.original_performer"), + entry.getPerformer().getAsMention(), true) + .addField(translate("moderation.unwarn.cannot_unwarn_self.field.case_id"), String.valueOf(caseID), true); + return channel.sendMessage(embed.build()); + } + + public MessageAction unwarn(MessageChannel channel, Member performer, int caseID, WarningEntry entry) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(translate("moderation.unwarn.author"), null, GAVEL_ICON_URL) + .setColor(MODERATION_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())) + .addField(translate("moderation.unwarn.field.performer"), performer.getUser().getAsMention(), true) + .addField(translate("moderation.unwarn.field.case_id"), String.valueOf(caseID), true) + .addField(translate("moderation.unwarn.field.original_target"), entry.getWarned().getAsMention(), true) + .addField(translate("moderation.unwarn.field.original_performer"), entry.getPerformer().getAsMention(), + true) + .addField(translate("moderation.unwarn.field.date_time"), + entry.getDateTime().format(RFC_1123_DATE_TIME), true); + if (entry.getReason() != null) + embed.addField(translate("moderation.unwarn.field.reason"), entry.getReason(), false); + return channel.sendMessage(embed.build()); + } } } diff --git a/src/main/java/sciwhiz12/janitor/storage/AbstractStorage.java b/src/main/java/sciwhiz12/janitor/storage/AbstractStorage.java new file mode 100644 index 0000000..567f3f6 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/storage/AbstractStorage.java @@ -0,0 +1,22 @@ +package sciwhiz12.janitor.storage; + +public abstract class AbstractStorage implements IStorage { + private boolean dirty; + + public boolean isDirty() { + return dirty; + } + + @Override + public boolean dirty() { + if (dirty) { + dirty = false; + return true; + } + return false; + } + + public void markDirty() { + this.dirty = true; + } +} diff --git a/src/main/java/sciwhiz12/janitor/storage/IStorage.java b/src/main/java/sciwhiz12/janitor/storage/IStorage.java new file mode 100644 index 0000000..6ee78c8 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/storage/IStorage.java @@ -0,0 +1,13 @@ +package sciwhiz12.janitor.storage; + +import java.io.Reader; +import java.io.Writer; + +public interface IStorage { + + boolean dirty(); + + void write(Writer output); + + void read(Reader input); +} diff --git a/src/main/java/sciwhiz12/janitor/storage/JsonStorage.java b/src/main/java/sciwhiz12/janitor/storage/JsonStorage.java new file mode 100644 index 0000000..f0a8013 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/storage/JsonStorage.java @@ -0,0 +1,30 @@ +package sciwhiz12.janitor.storage; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import java.io.Reader; +import java.io.Writer; + +public abstract class JsonStorage extends AbstractStorage { + public static final Gson GSON = new GsonBuilder() + .serializeNulls() + .setPrettyPrinting() + .create(); + + public abstract JsonElement save(); + + public abstract void load(JsonElement object); + + @Override + public void write(Writer input) { + GSON.toJson(save(), input); + } + + @Override + public void read(Reader input) { + load(JsonParser.parseReader(input)); + } +} diff --git a/src/main/resources/english.json b/src/main/resources/english.json index f4a80cf..cbe70ad 100644 --- a/src/main/resources/english.json +++ b/src/main/resources/english.json @@ -41,5 +41,36 @@ "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" + "moderation.unban.info.field.target": "Target", + "moderation.warn.info.author": "Warned user.", + "moderation.warn.info.field.performer": "Performer", + "moderation.warn.info.field.target": "Target", + "moderation.warn.info.field.sent_private_message": "Sent DM", + "moderation.warn.info.field.case_id": "Case ID", + "moderation.warn.info.field.date_time": "Date & Time", + "moderation.warn.info.field.reason": "Reason", + "moderation.warn.dm.title": "You were warned by a moderator.", + "moderation.warn.dm.field.performer": "Moderator", + "moderation.warn.dm.field.date_time": "Date & Time", + "moderation.warn.dm.field.reason": "Reason", + "moderation.warnlist.author": "Listing of Warnings", + "moderation.warnlist.empty": "**_No warnings logged matching your query._**", + "moderation.warnlist.entry": "**Case #%1$s**: Warned %2$s by %3$s %n - _Date & Time:_ %4$s %n - _Reason:_ %5$s", + "moderation.warnlist.entry.no_reason": "_no reason specified_", + "moderation.unwarn.author": "Removed warning from user.", + "moderation.unwarn.field.performer": "Performer", + "moderation.unwarn.field.original_target": "Original Target", + "moderation.unwarn.field.original_performer": "Original Performer", + "moderation.unwarn.field.case_id": "Case ID", + "moderation.unwarn.field.date_time": "Date & Time", + "moderation.unwarn.field.reason": "Reason", + "moderation.unwarn.no_case_found.title": "No warning found.", + "moderation.unwarn.no_case_found.desc": "No warning with that case ID was found.", + "moderation.unwarn.no_case_found.field.performer": "Performer", + "moderation.unwarn.no_case_found.field.case_id": "Case ID", + "moderation.unwarn.cannot_unwarn_self.title": "Cannot remove warning from self.", + "moderation.unwarn.cannot_unwarn_self.desc": "Performer cannot remove a warning from themselves.", + "moderation.unwarn.cannot_unwarn_self.field.performer": "Performer/Original Target", + "moderation.unwarn.cannot_unwarn_self.field.original_performer": "Original Performer", + "moderation.unwarn.cannot_unwarn_self.field.case_id": "Case ID" } \ No newline at end of file