diff --git a/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java b/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java index 71f5bde..b3f0a48 100644 --- a/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java +++ b/src/main/java/sciwhiz12/janitor/commands/CommandRegistry.java @@ -15,6 +15,7 @@ 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.NoteCommand; import sciwhiz12.janitor.commands.moderation.UnbanCommand; import sciwhiz12.janitor.commands.moderation.UnwarnCommand; import sciwhiz12.janitor.commands.moderation.WarnCommand; @@ -49,6 +50,7 @@ public class CommandRegistry implements EventListener { addCommand(new WarnListCommand(this)); addCommand(new UnwarnCommand(this)); addCommand(new ShutdownCommand(this)); + addCommand(new NoteCommand(this)); } public CommandDispatcher getDispatcher() { diff --git a/src/main/java/sciwhiz12/janitor/commands/moderation/NoteCommand.java b/src/main/java/sciwhiz12/janitor/commands/moderation/NoteCommand.java new file mode 100644 index 0000000..a3b773b --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/commands/moderation/NoteCommand.java @@ -0,0 +1,201 @@ +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 org.checkerframework.checker.nullness.qual.Nullable; +import sciwhiz12.janitor.commands.BaseCommand; +import sciwhiz12.janitor.commands.CommandRegistry; +import sciwhiz12.janitor.moderation.notes.NoteEntry; +import sciwhiz12.janitor.moderation.notes.NoteStorage; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +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.moderation.NoteCommand.ModeratorFilter.*; +import static sciwhiz12.janitor.commands.util.CommandHelper.argument; +import static sciwhiz12.janitor.commands.util.CommandHelper.literal; + +public class NoteCommand extends BaseCommand { + public static EnumSet NOTE_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS); + + public NoteCommand(CommandRegistry registry) { + super(registry); + } + + @Override + public LiteralArgumentBuilder getNode() { + return literal("note") + .requires(ctx -> config().NOTES_ENABLE.get()) + .then(literal("add") + .then(argument("target", member()) + .then(argument("contents", greedyString()) + .executes(ctx -> this.addNote(ctx, getString(ctx, "contents"))) + ) + ) + ) + .then(literal("list") + .then(literal("mod") + .then(argument("moderator", member()) + .then(argument("target", member()) + .executes(ctx -> this.listNotes(ctx, true, ARGUMENT)) + ) + .executes(ctx -> this.listNotes(ctx, false, ARGUMENT)) + ) + ) + .then(literal("me") + .then(argument("target", member()) + .executes(ctx -> this.listNotes(ctx, true, PERFORMER)) + ) + ) + .then(argument("target", member()) + .executes(ctx -> this.listNotes(ctx, true, NONE)) + ) + .executes(ctx -> this.listNotes(ctx, false, NONE)) + ) + .then(literal("remove") + .then(argument("noteId", integer(1)) + .executes(ctx -> this.removeNote(ctx, getInteger(ctx, "noteId"))) + ) + ) + .then(literal("me") + .then(argument("target", member()) + .executes(ctx -> this.listNotes(ctx, true, PERFORMER)) + ) + .executes(ctx -> this.listNotes(ctx, false, PERFORMER)) + ) + .then(argument("target", member()) + .executes(ctx -> this.listNotes(ctx, true, NONE)) + .then(argument("contents", greedyString()) + .executes(ctx -> this.addNote(ctx, getString(ctx, "contents"))) + ) + ); + } + + private int addNote(CommandContext ctx, String noteContents) throws CommandSyntaxException { + if (!ctx.getSource().isFromGuild()) { + messages().GENERAL.guildOnlyCommand(ctx.getSource().getChannel()); + return 1; + } + final Member performer = Objects.requireNonNull(ctx.getSource().getMember()); + final Guild guild = performer.getGuild(); + final MessageChannel channel = ctx.getSource().getChannel(); + final List members = getMembers("target", ctx).fromGuild(guild); + if (members.size() < 1) return 1; + 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(NOTE_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, NOTE_PERMISSION).queue(); + else { + final NoteStorage storage = NoteStorage.get(getBot().getStorage(), guild); + final int maxAmount = config().NOTES_MAX_AMOUNT_PER_MOD.get(); + if (storage.getAmountOfNotes(target.getUser()) >= maxAmount) { + messages().MODERATION.maxAmountOfNotes(channel, performer, target, maxAmount).queue(); + } else { + int noteID = storage.addNote(new NoteEntry(performer.getUser(), target.getUser(), dateTime, noteContents)); + messages().MODERATION.addNote(channel, performer, target, noteContents, dateTime, noteID).queue(); + } + } + return 1; + } + + enum ModeratorFilter { + NONE, PERFORMER, ARGUMENT + } + + private int listNotes(CommandContext ctx, boolean filterTarget, ModeratorFilter modFilter) + throws CommandSyntaxException { + MessageChannel channel = ctx.getSource().getChannel(); + if (!ctx.getSource().isFromGuild()) { + messages().GENERAL.guildOnlyCommand(channel).queue(); + return 1; + } + 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 1; + final Member target = members.get(0); + if (guild.getSelfMember().equals(target)) { + messages().GENERAL.cannotActionSelf(channel).queue(); + return 1; + } + predicate = predicate.and(e -> e.getValue().getTarget().getIdLong() == target.getIdLong()); + } + switch (modFilter) { + case ARGUMENT: { + final List members = getMembers("moderator", ctx).fromGuild(performer.getGuild()); + if (members.size() < 1) return 1; + final Member mod = members.get(0); + predicate = predicate.and(e -> e.getValue().getPerformer().getIdLong() == mod.getIdLong()); + } + case PERFORMER: { + predicate = predicate.and(e -> e.getValue().getPerformer().getIdLong() == performer.getIdLong()); + } + } + + final OffsetDateTime dateTime = OffsetDateTime.now(); + + if (!performer.hasPermission(NOTE_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, NOTE_PERMISSION).queue(); + else + messages().MODERATION.noteList(channel, NoteStorage.get(getBot().getStorage(), guild) + .getNotes() + .entrySet().stream() + .filter(predicate) + .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)) + ).queue(); + return 1; + } + + private int removeNote(CommandContext ctx, int noteID) { + MessageChannel channel = ctx.getSource().getChannel(); + if (!ctx.getSource().isFromGuild()) { + messages().GENERAL.guildOnlyCommand(channel).queue(); + return 1; + } + final Guild guild = ctx.getSource().getGuild(); + final Member performer = Objects.requireNonNull(ctx.getSource().getMember()); + + final OffsetDateTime dateTime = OffsetDateTime.now(); + + if (!performer.hasPermission(NOTE_PERMISSION)) + messages().MODERATION.performerInsufficientPermissions(channel, performer, NOTE_PERMISSION).queue(); + else { + final NoteStorage storage = NoteStorage.get(getBot().getStorage(), guild); + @Nullable + final NoteEntry entry = storage.getNote(noteID); + if (entry == null) + messages().MODERATION.noNoteFound(channel, performer, noteID).queue(); + else { + storage.removeNote(noteID); + messages().MODERATION.removeNote(channel, performer, noteID, entry).queue(); + } + } + return 1; + } +} diff --git a/src/main/java/sciwhiz12/janitor/config/BotConfig.java b/src/main/java/sciwhiz12/janitor/config/BotConfig.java index 50be96e..8c73d3b 100644 --- a/src/main/java/sciwhiz12/janitor/config/BotConfig.java +++ b/src/main/java/sciwhiz12/janitor/config/BotConfig.java @@ -31,6 +31,9 @@ public class BotConfig { public final CommentedConfigSpec.BooleanValue WARNINGS_PREVENT_WARNING_MODS; public final CommentedConfigSpec.BooleanValue WARNINGS_REMOVE_SELF_WARNINGS; + public final CommentedConfigSpec.IntValue NOTES_MAX_AMOUNT_PER_MOD; + public final CommentedConfigSpec.BooleanValue NOTES_ENABLE; + private final BotOptions options; private final Path configPath; private final CommentedConfigSpec spec; @@ -87,6 +90,15 @@ public class BotConfig { .comment("Whether to allow moderators to remove warnings from themselves.") .define("remove_self_warnings", false); builder.pop(); + + builder.comment("Settings for the notes system").push("notes"); + NOTES_ENABLE = builder + .comment("Whether to enable the notes system. If disabled, the related commands are force-disabled.") + .define("enable", true); + NOTES_MAX_AMOUNT_PER_MOD = builder + .comment("The max amount of notes for a user per moderator.") + .defineInRange("max_amount", Integer.MAX_VALUE, 0, Integer.MAX_VALUE); + builder.pop(); } builder.pop(); diff --git a/src/main/java/sciwhiz12/janitor/moderation/notes/NoteEntry.java b/src/main/java/sciwhiz12/janitor/moderation/notes/NoteEntry.java new file mode 100644 index 0000000..5d79756 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/moderation/notes/NoteEntry.java @@ -0,0 +1,90 @@ +package sciwhiz12.janitor.moderation.notes; + +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; + +public class NoteEntry { + private final User performer; + private final User target; + private final OffsetDateTime dateTime; + private final String contents; + + public NoteEntry(User performer, User target, OffsetDateTime dateTime, String contents) { + this.performer = performer; + this.target = target; + this.dateTime = dateTime; + this.contents = contents; + } + + public User getPerformer() { + return performer; + } + + public User getTarget() { + return target; + } + + public OffsetDateTime getDateTime() { + return dateTime; + } + + public String getContents() { + return contents; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NoteEntry noteEntry = (NoteEntry) o; + return getPerformer().equals(noteEntry.getPerformer()) && + getTarget().equals(noteEntry.getTarget()) && + getDateTime().equals(noteEntry.getDateTime()) && + getContents().equals(noteEntry.getContents()); + } + + @Override + public int hashCode() { + return Objects.hash(getPerformer(), getTarget(), getDateTime(), getContents()); + } + + public static class Serializer implements JsonDeserializer, JsonSerializer { + private final JanitorBot bot; + + public Serializer(JanitorBot bot) { + this.bot = bot; + } + + @Override + public NoteEntry deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + final JsonObject obj = json.getAsJsonObject(); + final User performer = bot.getDiscord().retrieveUserById(obj.get("performer").getAsLong()).complete(); + final User target = bot.getDiscord().retrieveUserById(obj.get("target").getAsLong()).complete(); + final OffsetDateTime dateTime = OffsetDateTime.parse(obj.get("dateTime").getAsString()); + final String reason = obj.get("contents").getAsString(); + return new NoteEntry(performer, target, dateTime, reason); + } + + @Override + public JsonElement serialize(NoteEntry src, Type typeOfSrc, JsonSerializationContext context) { + final JsonObject obj = new JsonObject(); + obj.addProperty("performer", src.getPerformer().getId()); + obj.addProperty("target", src.getTarget().getId()); + obj.addProperty("dateTime", src.getDateTime().toString()); + obj.addProperty("contents", src.getContents()); + return obj; + } + } +} diff --git a/src/main/java/sciwhiz12/janitor/moderation/notes/NoteStorage.java b/src/main/java/sciwhiz12/janitor/moderation/notes/NoteStorage.java new file mode 100644 index 0000000..c82d205 --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/moderation/notes/NoteStorage.java @@ -0,0 +1,85 @@ +package sciwhiz12.janitor.moderation.notes; + +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 net.dv8tion.jda.api.entities.User; +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 NoteStorage extends JsonStorage { + private static final Type NOTE_MAP_TYPE = new TypeToken>() {}.getType(); + public static final String STORAGE_KEY = "notes"; + + public static NoteStorage get(GuildStorage storage, Guild guild) { + return storage.getOrCreate(guild, STORAGE_KEY, () -> new NoteStorage(storage.getBot())); + } + + private final Gson gson; + private final JanitorBot bot; + private int lastID = 1; + private final Map notes = new ObservedMap<>(new HashMap<>(), this::markDirty); + + public NoteStorage(JanitorBot bot) { + this.bot = bot; + this.gson = new GsonBuilder() + .registerTypeAdapter(NoteEntry.class, new NoteEntry.Serializer(bot)) + .create(); + } + + public JanitorBot getBot() { + return bot; + } + + public int addNote(NoteEntry entry) { + int id = lastID++; + notes.put(id, entry); + return id; + } + + @Nullable + public NoteEntry getNote(int noteID) { + return notes.get(noteID); + } + + public NoteEntry removeNote(int noteID) { + return notes.remove(noteID); + } + + public int getAmountOfNotes(User target) { + return (int) notes.values().stream() + .filter(entry -> entry.getTarget() == target) + .count(); + } + + public Map getNotes() { + return notes; + } + + @Override + public JsonElement save() { + JsonObject obj = new JsonObject(); + obj.addProperty("lastNoteID", lastID); + obj.add("notes", gson.toJsonTree(notes)); + return obj; + } + + @Override + public void load(JsonElement in) { + final JsonObject obj = in.getAsJsonObject(); + lastID = obj.get("lastNoteID").getAsInt(); + final Map loaded = gson.fromJson(obj.get("notes"), NOTE_MAP_TYPE); + notes.clear(); + notes.putAll(loaded); + } +} diff --git a/src/main/java/sciwhiz12/janitor/msg/Messages.java b/src/main/java/sciwhiz12/janitor/msg/Messages.java index 86e7dd0..e3eff37 100644 --- a/src/main/java/sciwhiz12/janitor/msg/Messages.java +++ b/src/main/java/sciwhiz12/janitor/msg/Messages.java @@ -11,6 +11,7 @@ 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.notes.NoteEntry; import sciwhiz12.janitor.moderation.warns.WarningEntry; import java.time.Clock; @@ -326,7 +327,8 @@ public class Messages { return channel.sendMessage(embed.build()); } - public MessageAction cannotRemoveHigherModerated(MessageChannel channel, Member performer, int caseID, WarningEntry entry) { + public MessageAction cannotRemoveHigherModerated(MessageChannel channel, Member performer, int caseID, + WarningEntry entry) { final EmbedBuilder embed = new EmbedBuilder() .setTitle(translate("moderation.unwarn.cannot_remove_higher_mod.title"), null) .setColor(General.FAILURE_COLOR) @@ -339,6 +341,81 @@ public class Messages { .addField(translate("moderation.unwarn.cannot_remove_higher_mod.field.case_id"), String.valueOf(caseID), true); return channel.sendMessage(embed.build()); } + + public MessageAction maxAmountOfNotes(MessageChannel channel, Member performer, Member target, int amount) { + final EmbedBuilder embed = new EmbedBuilder() + .setTitle(translate("moderation.note.max_amount_of_notes.title"), null) + .setColor(General.FAILURE_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())) + .setDescription(translate("moderation.note.max_amount_of_notes.desc")) + .addField(translate("moderation.note.max_amount_of_notes.field.performer"), performer.getAsMention(), true) + .addField(translate("moderation.note.max_amount_of_notes.field.target"), target.getAsMention(), true) + .addField(translate("moderation.note.max_amount_of_notes.field.amount"), String.valueOf(amount), true); + return channel.sendMessage(embed.build()); + } + + public MessageAction noNoteFound(MessageChannel channel, Member performer, int noteID) { + final EmbedBuilder embed = new EmbedBuilder() + .setTitle(translate("moderation.note.no_note_found.title"), null) + .setColor(General.FAILURE_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())) + .setDescription(translate("moderation.note.no_note_found.desc")) + .addField(translate("moderation.note.no_note_found.field.performer"), performer.getAsMention(), true) + .addField(translate("moderation.note.no_note_found.field.note_id"), String.valueOf(noteID), true); + return channel.sendMessage(embed.build()); + } + + public MessageAction addNote(MessageChannel channel, Member performer, Member target, String contents, + OffsetDateTime dateTime, int noteID) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(translate("moderation.note.add.author"), null, GAVEL_ICON_URL) + .setColor(MODERATION_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())) + .addField(translate("moderation.note.add.field.performer"), performer.getUser().getAsMention(), true) + .addField(translate("moderation.note.add.field.target"), target.getUser().getAsMention(), true) + .addField(translate("moderation.note.add.field.note_id"), String.valueOf(noteID), true) + .addField(translate("moderation.note.add.field.date_time"), dateTime.format(RFC_1123_DATE_TIME), true) + .addField(translate("moderation.note.add.field.contents"), contents, false); + return channel.sendMessage(embed.build()); + } + + public MessageAction noteList(MessageChannel channel, Map displayNotes) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(translate("moderation.note.list.author"), null, GAVEL_ICON_URL) + .setColor(MODERATION_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())); + String warningsDesc = displayNotes.size() > 0 ? displayNotes.entrySet().stream() + .sorted(Collections.reverseOrder(Comparator.comparingInt(Map.Entry::getKey))) + .limit(10) + .map(entry -> + translate("moderation.note.list.entry", + entry.getKey(), + entry.getValue().getTarget().getAsMention(), + entry.getValue().getPerformer().getAsMention(), + entry.getValue().getDateTime().format(RFC_1123_DATE_TIME), + entry.getValue().getContents()) + ) + .collect(Collectors.joining("\n")) + : translate("moderation.note.list.empty"); + embed.setDescription(warningsDesc); + return channel.sendMessage(embed.build()); + } + + public MessageAction removeNote(MessageChannel channel, Member performer, int noteID, NoteEntry entry) { + final EmbedBuilder embed = new EmbedBuilder() + .setAuthor(translate("moderation.note.remove.author"), null, GAVEL_ICON_URL) + .setColor(MODERATION_COLOR) + .setTimestamp(OffsetDateTime.now(Clock.systemUTC())) + .addField(translate("moderation.note.remove.field.performer"), performer.getAsMention(), true) + .addField(translate("moderation.note.remove.field.note_id"), String.valueOf(noteID), true) + .addField(translate("moderation.note.remove.field.original_target"), entry.getTarget().getAsMention(), true) + .addField(translate("moderation.note.remove.field.original_performer"), entry.getPerformer().getAsMention(), + true) + .addField(translate("moderation.note.remove.field.date_time"), entry.getDateTime().format(RFC_1123_DATE_TIME), + true) + .addField(translate("moderation.note.remove.field.contents"), entry.getContents(), false); + return channel.sendMessage(embed.build()); + } } } diff --git a/src/main/resources/english.json b/src/main/resources/english.json index 43ec741..a4a2dd1 100644 --- a/src/main/resources/english.json +++ b/src/main/resources/english.json @@ -81,5 +81,30 @@ "moderation.warn.cannot_remove_higher_mod.desc": "The performer cannot remove this warning, as this was issued by a higher-ranking moderator.", "moderation.warn.cannot_remove_higher_mod.field.performer": "Performer", "moderation.warn.cannot_remove_higher_mod.field.original_performer": "Original Performer", - "moderation.warn.cannot_remove_higher_mod.field.case_id": "Case ID" + "moderation.warn.cannot_remove_higher_mod.field.case_id": "Case ID", + "moderation.note.max_amount_of_notes.title": "Max notes reached.", + "moderation.note.max_amount_of_notes.desc": "The performer has reached the maximum amount of notes for the target user.", + "moderation.note.max_amount_of_notes.field.performer": "Performer", + "moderation.note.max_amount_of_notes.field.target": "Target", + "moderation.note.max_amount_of_notes.field.amount": "(Max.) Amount", + "moderation.note.no_note_found.title": "No note found.", + "moderation.note.no_note_found.desc": "No note with that note ID was found.", + "moderation.note.no_note_found.field.performer": "Performer", + "moderation.note.no_note_found.field.note_id": "Note ID", + "moderation.note.list.author": "Listing of Notes", + "moderation.note.list.empty": "**_No recorded notes matching your query._**", + "moderation.note.list.entry": "**#%1$s**: for %2$s by %3$s %n - _Date & Time:_ %4$s %n - _Text:_ %5$s", + "moderation.note.add.author": "Recorded note for user.", + "moderation.note.add.field.performer": "Performer", + "moderation.note.add.field.target": "Target", + "moderation.note.add.field.note_id": "Note ID", + "moderation.note.add.field.date_time": "Date & Time", + "moderation.note.add.field.contents": "Text", + "moderation.note.remove.author": "Removed note.", + "moderation.note.remove.field.performer": "Performer", + "moderation.note.remove.field.original_target": "Original Target", + "moderation.note.remove.field.original_performer": "Original Performer", + "moderation.note.remove.field.note_id": "Note ID", + "moderation.note.remove.field.date_time": "Date & Time", + "moderation.note.remove.field.contents": "Text" } \ No newline at end of file