1
0
mirror of https://github.com/sciwhiz12/Janitor.git synced 2024-09-19 21:04:02 +00:00

Add listing messages

This commit is contained in:
Arnold Alejo Nunag 2020-10-16 18:12:50 +08:00
parent f861308e4d
commit 09aa6a269c
Signed by: sciwhiz12
GPG Key ID: 622CF446534317E1
21 changed files with 893 additions and 124 deletions

View File

@ -10,6 +10,7 @@ import sciwhiz12.janitor.commands.CommandRegistry;
import sciwhiz12.janitor.config.BotConfig;
import sciwhiz12.janitor.msg.Messages;
import sciwhiz12.janitor.msg.TranslationMap;
import sciwhiz12.janitor.msg.emote.ReactionManager;
import sciwhiz12.janitor.msg.substitution.SubstitutionMap;
import sciwhiz12.janitor.utils.Util;
@ -29,6 +30,7 @@ public class JanitorBot {
private final TranslationMap translations;
private final SubstitutionMap substitutions;
private final Messages messages;
private final ReactionManager reactions;
public JanitorBot(JDA discord, BotConfig config) {
this.config = config;
@ -39,8 +41,9 @@ public class JanitorBot {
this.translations = new TranslationMap(this, config.getTranslationsFile());
this.substitutions = new SubstitutionMap(this);
this.messages = new Messages(this, config.getTranslationsFile());
this.reactions = new ReactionManager(this);
// TODO: find which of these can be loaded in parallel before the bot JDA is ready
discord.addEventListener(cmdRegistry);
discord.addEventListener(cmdRegistry, reactions);
discord.getPresence().setPresence(OnlineStatus.ONLINE, Activity.playing(" n' sweeping n' testing!"));
discord.getGuilds().forEach(Guild::loadMembers);
JANITOR.info("Ready!");
@ -82,6 +85,10 @@ public class JanitorBot {
return this.translations;
}
public ReactionManager getReactionManager() {
return this.reactions;
}
public void shutdown() {
JANITOR.info(STATUS, "Shutting down!");
getConfig().getOwnerID()

View File

@ -24,10 +24,9 @@ public class HelloCommand extends BaseCommand {
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
return literal("greet")
.then(
argument("member", GuildMemberArgument.member())
.executes(this::run)
);
.then(argument("member", GuildMemberArgument.member())
.executes(this::run)
);
}
int run(final CommandContext<MessageReceivedEvent> ctx) throws CommandSyntaxException {
@ -35,11 +34,22 @@ public class HelloCommand extends BaseCommand {
final List<Member> memberList = getMembers("member", ctx).fromGuild(ctx.getSource().getGuild());
if (memberList.size() == 1) {
final Member member = memberList.get(0);
ctx.getSource().getChannel().sendMessage("Hello " + member.getAsMention() + "!")
.queue(
success -> JANITOR.debug("Sent greeting message to {}, on cmd of {}", Util.toString(member.getUser()), Util.toString(ctx.getSource().getAuthor())),
err -> JANITOR.error("Error while sending greeting message to {}, on cmd of {}", Util.toString(member.getUser()), Util.toString(ctx.getSource().getAuthor()))
);
ctx.getSource().getChannel().sendMessage("Hello " + member.getAsMention() + "!").queue(
success -> {
JANITOR.debug("Sent greeting message to {}, on cmd of {}", Util.toString(member.getUser()),
Util.toString(ctx.getSource().getAuthor()));
getBot().getReactionManager().newMessage(success)
.add("\u274C", (msg, event) -> success.delete()
.flatMap(v -> event.getChannel()
.deleteMessageById(ctx.getSource().getMessageIdLong()))
.queue()
)
.owner(ctx.getSource().getAuthor().getIdLong())
.create();
},
err -> JANITOR.error("Error while sending greeting message to {}, on cmd of {}",
Util.toString(member.getUser()), Util.toString(ctx.getSource().getAuthor()))
);
}
}
return 1;

View File

@ -1,5 +1,6 @@
package sciwhiz12.janitor.commands.moderation;
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
@ -16,6 +17,7 @@ import sciwhiz12.janitor.msg.MessageHelper;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@ -32,6 +34,7 @@ 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;
import static sciwhiz12.janitor.msg.MessageHelper.*;
public class NoteCommand extends BaseCommand {
public static EnumSet<Permission> NOTE_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS);
@ -93,7 +96,7 @@ public class NoteCommand extends BaseCommand {
final MessageChannel channel = ctx.getSource().getChannel();
if (!ctx.getSource().isFromGuild()) {
messages().getRegularMessage("general/error/guild_only_command")
.apply(MessageHelper.user("performer", ctx.getSource().getAuthor()))
.apply(user("performer", ctx.getSource().getAuthor()))
.send(getBot(), channel).queue();
return 1;
@ -137,7 +140,7 @@ public class NoteCommand extends BaseCommand {
messages().getRegularMessage("moderation/note/add")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.noteEntry("note_entry", noteID, entry))
.apply(noteEntry("note_entry", noteID, entry))
.send(getBot(), channel).queue();
}
@ -154,7 +157,7 @@ public class NoteCommand extends BaseCommand {
final MessageChannel channel = ctx.getSource().getChannel();
if (!ctx.getSource().isFromGuild()) {
messages().getRegularMessage("general/error/guild_only_command")
.apply(MessageHelper.user("performer", ctx.getSource().getAuthor()))
.apply(user("performer", ctx.getSource().getAuthor()))
.send(getBot(), channel).queue();
return 1;
@ -196,16 +199,24 @@ public class NoteCommand extends BaseCommand {
.send(getBot(), channel).queue();
} else {
// channel.sendMessage(messages().MODERATION.noteList(
// NoteStorage.get(getBot().getStorage(), guild)
// .getNotes()
// .entrySet().stream()
// .filter(predicate)
// .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))
// ).build(getBot())).queue();
messages().getRegularMessage("moderation/note/list")
.send(getBot(), channel).queue();
// TODO: fix this
messages().<Map.Entry<Integer, NoteEntry>>getListingMessage("moderation/note/list")
.apply(MessageHelper.member("performer", performer))
.amountPerPage(8)
.setEntryApplier((entry, subs) -> subs
.with("note_entry.note_id", () -> String.valueOf(entry.getKey()))
.apply(user("note_entry.performer", entry.getValue().getPerformer()))
.apply(user("note_entry.target", entry.getValue().getTarget()))
.with("note_entry.date_time", () -> entry.getValue().getDateTime().format(DATE_TIME_FORMAT))
.with("note_entry.contents", entry.getValue()::getContents)
)
.build(channel, getBot(), ctx.getSource().getMessage(),
NoteStorage.get(getBot().getStorage(), guild)
.getNotes()
.entrySet().stream()
.filter(predicate)
.sorted(Comparator.<Map.Entry<Integer, NoteEntry>>comparingInt(Map.Entry::getKey).reversed())
.collect(ImmutableList.toImmutableList())
);
}
return 1;
}
@ -214,7 +225,7 @@ public class NoteCommand extends BaseCommand {
MessageChannel channel = ctx.getSource().getChannel();
if (!ctx.getSource().isFromGuild()) {
messages().getRegularMessage("general/error/guild_only_command")
.apply(MessageHelper.user("performer", ctx.getSource().getAuthor()))
.apply(user("performer", ctx.getSource().getAuthor()))
.send(getBot(), channel).queue();
return 1;
@ -243,7 +254,7 @@ public class NoteCommand extends BaseCommand {
messages().getRegularMessage("moderation/note/remove")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.noteEntry("note_entry", noteID, entry))
.apply(noteEntry("note_entry", noteID, entry))
.send(getBot(), channel).queue();
}
}

View File

@ -1,5 +1,6 @@
package sciwhiz12.janitor.commands.moderation;
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
@ -11,8 +12,10 @@ 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 sciwhiz12.janitor.msg.MessageHelper;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@ -23,6 +26,8 @@ import static sciwhiz12.janitor.commands.arguments.GuildMemberArgument.getMember
import static sciwhiz12.janitor.commands.arguments.GuildMemberArgument.member;
import static sciwhiz12.janitor.commands.util.CommandHelper.argument;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.msg.MessageHelper.DATE_TIME_FORMAT;
import static sciwhiz12.janitor.msg.MessageHelper.user;
public class WarnListCommand extends BaseCommand {
public static final EnumSet<Permission> WARN_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS);
@ -93,16 +98,24 @@ public class WarnListCommand extends BaseCommand {
.send(getBot(), channel).queue();
} else {
// channel.sendMessage(messages().MODERATION.warnList(
// WarningStorage.get(getBot().getStorage(), guild)
// .getWarnings()
// .entrySet().stream()
// .filter(predicate)
// .collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))
// ).build(getBot())).queue();
messages().getRegularMessage("moderation/warn/list")
.send(getBot(), channel).queue();
// TODO: fix this
messages().<Map.Entry<Integer, WarningEntry>>getListingMessage("moderation/warn/list")
.apply(MessageHelper.member("performer", performer))
.amountPerPage(8)
.setEntryApplier((entry, subs) -> subs
.with("warning_entry.case_id", () -> String.valueOf(entry.getKey()))
.apply(user("warning_entry.performer", entry.getValue().getPerformer()))
.apply(user("warning_entry.warned", entry.getValue().getWarned()))
.with("warning_entry.date_time", () -> entry.getValue().getDateTime().format(DATE_TIME_FORMAT))
.with("warning_entry.reason", entry.getValue()::getReason)
)
.build(channel, getBot(), ctx.getSource().getMessage(),
WarningStorage.get(getBot().getStorage(), guild)
.getWarnings()
.entrySet().stream()
.filter(predicate)
.sorted(Comparator.<Map.Entry<Integer, WarningEntry>>comparingInt(Map.Entry::getKey).reversed())
.collect(ImmutableList.toImmutableList())
);
}
return 1;
}

View File

@ -0,0 +1,161 @@
package sciwhiz12.janitor.msg;
import com.google.common.collect.ImmutableList;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.MessageEmbed;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.msg.json.ListingMessage;
import sciwhiz12.janitor.msg.substitution.CustomSubstitutions;
import sciwhiz12.janitor.msg.substitution.IHasCustomSubstitutions;
import sciwhiz12.janitor.msg.substitution.SubstitutionMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ListingMessageBuilder<T> implements IHasCustomSubstitutions<ListingMessageBuilder<T>> {
private final ListingMessage message;
private final Map<String, Supplier<String>> customSubstitutions;
private int amountPerPage = 10;
private BiConsumer<T, CustomSubstitutions> entryApplier = (entry, sub) -> {};
public ListingMessageBuilder(ListingMessage message, Map<String, Supplier<String>> customSubstitutions) {
this.message = message;
this.customSubstitutions = customSubstitutions;
}
public ListingMessageBuilder(ListingMessage message) {
this(message, new HashMap<>());
}
public ListingMessageBuilder<T> amountPerPage(int amountPerPage) {
this.amountPerPage = amountPerPage;
return this;
}
public ListingMessageBuilder<T> setEntryApplier(BiConsumer<T, CustomSubstitutions> entryApplier) {
this.entryApplier = entryApplier;
return this;
}
public ListingMessageBuilder<T> apply(Consumer<ListingMessageBuilder<T>> consumer) {
consumer.accept(this);
return this;
}
public ListingMessageBuilder<T> with(final String argument, final Supplier<String> value) {
this.customSubstitutions.put(argument, value);
return this;
}
public void build(MessageChannel channel, TranslationMap translations, SubstitutionMap globalSubstitutions,
Message triggerMessage, List<T> entries) {
final CustomSubstitutions customSubs = globalSubstitutions.with(customSubstitutions);
final ImmutableList<T> list = ImmutableList.copyOf(entries);
final PagedMessage pagedMessage = new PagedMessage(message, list, amountPerPage);
channel.sendMessage(pagedMessage.createMessage(translations, customSubs, entryApplier))
.queue(listMsg -> translations.getBot().getReactionManager().newMessage(listMsg)
.owner(triggerMessage.getAuthor().getIdLong())
.removeEmotes(true)
.add("\u2b05", (msg, event) -> { // PREVIOUS
if (pagedMessage.advancePage(PageDirection.PREVIOUS)) {
event.retrieveMessage()
.flatMap(eventMsg -> eventMsg.editMessage(
pagedMessage.createMessage(translations, customSubs, entryApplier))
)
.queue();
}
})
.add("\u274c", (msg, event) -> { // CLOSE
event.getChannel().deleteMessageById(event.getMessageIdLong())
.flatMap(v -> !triggerMessage.isFromGuild() ||
event.getGuild().getSelfMember()
.hasPermission(triggerMessage.getTextChannel(),
Permission.MESSAGE_MANAGE),
v -> triggerMessage.delete())
.queue();
})
.add("\u27a1", (msg, event) -> { // NEXT
if (pagedMessage.advancePage(PageDirection.NEXT)) {
event.retrieveMessage()
.flatMap(eventMsg -> eventMsg.editMessage(
pagedMessage.createMessage(translations, customSubs, entryApplier))
)
.queue();
}
})
.create()
);
}
public void build(MessageChannel channel, JanitorBot bot, Message triggerMessage, List<T> entries) {
build(channel, bot.getTranslations(), bot.getSubstitutions(), triggerMessage, entries);
}
class PagedMessage {
private final ListingMessage message;
private final ImmutableList<T> list;
private final int maxPages;
private final int amountPerPage;
private int currentPage = 0;
private int lastPage = -1;
private EmbedBuilder cachedMessage;
PagedMessage(ListingMessage message, ImmutableList<T> list, int amountPerPage) {
this.message = message;
this.list = list;
this.amountPerPage = amountPerPage;
this.maxPages = Math.floorDiv(list.size(), ListingMessageBuilder.this.amountPerPage);
}
public int getMaxPages() {
return maxPages;
}
public int getCurrentPage() {
return currentPage;
}
public boolean advancePage(PageDirection direction) {
if (direction == PageDirection.PREVIOUS && currentPage > 0) {
currentPage -= 1;
return true;
} else if (direction == PageDirection.NEXT && currentPage < maxPages) {
currentPage += 1;
return true;
}
return false;
}
public MessageEmbed createMessage(TranslationMap translations, CustomSubstitutions substitutions,
BiConsumer<T, CustomSubstitutions> applier) {
if (currentPage != lastPage) {
cachedMessage = message.create(
translations,
substitutions.with(new HashMap<>())
.with("page.max", () -> String.valueOf(maxPages + 1))
.with("page.current", () -> String.valueOf(currentPage + 1)),
list.stream()
.skip(currentPage * amountPerPage)
.limit(amountPerPage)
.collect(Collectors.toList()),
applier);
lastPage = currentPage;
}
return cachedMessage.build();
}
}
enum PageDirection {
PREVIOUS, NEXT
}
}

View File

@ -8,6 +8,7 @@ import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import sciwhiz12.janitor.moderation.notes.NoteEntry;
import sciwhiz12.janitor.moderation.warns.WarningEntry;
import sciwhiz12.janitor.msg.substitution.IHasCustomSubstitutions;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
@ -18,19 +19,19 @@ import static java.time.temporal.ChronoField.*;
public class MessageHelper {
private MessageHelper() {}
public static Consumer<RegularMessageBuilder> snowflake(String head, ISnowflake snowflake) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> snowflake(String head, ISnowflake snowflake) {
return builder -> builder
.with(head + ".id", snowflake::getId)
.with(head + ".creation_datetime", () -> snowflake.getTimeCreated().format(DATE_TIME_FORMAT));
}
public static Consumer<RegularMessageBuilder> mentionable(String head, IMentionable mentionable) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> mentionable(String head, IMentionable mentionable) {
return builder -> builder
.apply(snowflake(head, mentionable))
.with(head + ".mention", mentionable::getAsMention);
}
public static Consumer<RegularMessageBuilder> role(String head, Role role) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> role(String head, Role role) {
return builder -> builder
.apply(mentionable(head, role))
.with(head + ".color_hex", () -> Integer.toHexString(role.getColorRaw()))
@ -38,7 +39,7 @@ public class MessageHelper {
.with(head + ".permissions", role.getPermissions()::toString);
}
public static Consumer<RegularMessageBuilder> user(String head, User user) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> user(String head, User user) {
return builder -> builder
.apply(mentionable(head, user))
.with(head + ".name", user::getName)
@ -47,7 +48,7 @@ public class MessageHelper {
.with(head + ".flags", user.getFlags()::toString);
}
public static Consumer<RegularMessageBuilder> guild(String head, Guild guild) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> guild(String head, Guild guild) {
return builder -> builder
.apply(snowflake(head, guild))
.with(head + ".name", guild::getName)
@ -60,7 +61,7 @@ public class MessageHelper {
.with(head + ".icon_url", guild::getIconUrl);
}
public static Consumer<RegularMessageBuilder> member(String head, Member member) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> member(String head, Member member) {
return builder -> builder
.apply(user(head, member.getUser()))
.apply(guild(head + ".guild", member.getGuild()))
@ -70,7 +71,7 @@ public class MessageHelper {
.with(head + ".color", () -> String.valueOf(member.getColorRaw()));
}
public static Consumer<RegularMessageBuilder> warningEntry(String head, int caseID, WarningEntry entry) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> warningEntry(String head, int caseID, WarningEntry entry) {
return builder -> builder
.with(head + ".case_id", () -> String.valueOf(caseID))
.apply(user(head + ".performer", entry.getPerformer()))
@ -79,7 +80,7 @@ public class MessageHelper {
.with(head + ".reason", entry::getReason);
}
public static Consumer<RegularMessageBuilder> noteEntry(String head, int noteID, NoteEntry entry) {
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> noteEntry(String head, int noteID, NoteEntry entry) {
return builder -> builder
.with(head + ".note_id", () -> String.valueOf(noteID))
.apply(user(head + ".performer", entry.getPerformer()))

View File

@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.dv8tion.jda.api.entities.MessageEmbed;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.msg.json.ListingMessage;
import sciwhiz12.janitor.msg.json.RegularMessage;
import java.io.IOException;
@ -31,6 +32,7 @@ public class Messages {
private final JanitorBot bot;
private final Path messagesFolder;
private final Map<String, RegularMessage> regularMessages = new HashMap<>();
private final Map<String, ListingMessage> listingMessages = new HashMap<>();
private final ObjectMapper jsonMapper = new ObjectMapper();
public Messages(JanitorBot bot, Path messagesFolder) {
@ -39,13 +41,17 @@ public class Messages {
loadMessages();
}
public JanitorBot getBot() {
return bot;
}
public void loadMessages() {
boolean success = false;
if (messagesFolder != null) {
JANITOR.debug(MESSAGES, "Loading messages from folder {}", messagesFolder);
success = loadMessages(
path -> Files.newBufferedReader(messagesFolder.resolve(path + JSON_FILE_SUFFIX))
path -> Files.newBufferedReader(messagesFolder.resolve(path + JSON_FILE_SUFFIX))
);
} else {
JANITOR.info(MESSAGES, "No custom messages folder specified");
@ -55,7 +61,7 @@ public class Messages {
JANITOR.info(MESSAGES, "Loading default messages");
//noinspection UnstableApiUsage
loadMessages(
file -> new InputStreamReader(getResource(DEFAULT_MESSAGES_FOLDER + file + JSON_FILE_SUFFIX).openStream())
file -> new InputStreamReader(getResource(DEFAULT_MESSAGES_FOLDER + file + JSON_FILE_SUFFIX).openStream())
);
}
}
@ -68,8 +74,11 @@ public class Messages {
final String path = messageKey.replace("/", FileSystems.getDefault().getSeparator());
try (Reader reader = files.open(path)) {
final JsonNode tree = jsonMapper.readTree(reader);
if ("regular".equals(tree.path("type").asText("regular"))) {
final String type = tree.path("type").asText("regular");
if ("regular".equals(type)) {
regularMessages.put(messageKey, jsonMapper.convertValue(tree, RegularMessage.class));
} else if ("listing".equals(type)) {
listingMessages.put(messageKey, jsonMapper.convertValue(tree, ListingMessage.class));
} else {
JANITOR.warn(MESSAGES, "Unknown message type {} for {}", tree.path("type").asText(), messageKey);
}
@ -92,28 +101,61 @@ public class Messages {
public RegularMessageBuilder getRegularMessage(String messageKey) {
final RegularMessage msg = regularMessages.get(messageKey);
if (msg == null) {
JANITOR.warn(MESSAGES, "Attempted to get unknown message with key {}", messageKey);
return new RegularMessageBuilder(UNKNOWN_MESSAGE).with("key", () -> messageKey);
JANITOR.warn(MESSAGES, "Attempted to get unknown regular message with key {}", messageKey);
return new RegularMessageBuilder(UNKNOWN_REGULAR_MESSAGE).with("key", () -> messageKey);
}
return new RegularMessageBuilder(msg);
}
public Map<String, ListingMessage> getListingMessages() {
return listingMessages;
}
public <T> ListingMessageBuilder<T> getListingMessage(String messageKey) {
final ListingMessage msg = listingMessages.get(messageKey);
if (msg == null) {
JANITOR.warn(MESSAGES, "Attempted to get unknown listing message with key {}", messageKey);
return new ListingMessageBuilder<T>(UNKNOWN_LISTING_MESSAGE).with("key", () -> messageKey);
}
return new ListingMessageBuilder<>(msg);
}
interface FileOpener {
Reader open(String filePath) throws IOException;
}
public static final RegularMessage UNKNOWN_MESSAGE = new RegularMessage(
"UNKNOWN MESSAGE!",
null,
"A message was tried to be looked up, but was not found. Please report this to your bot maintainer/administrator.",
String.valueOf(0xFF0000),
null,
null,
null,
null,
null,
null,
null,
Collections.singletonList(new MessageEmbed.Field("Message Key", "${key}", false))
public static final RegularMessage UNKNOWN_REGULAR_MESSAGE = new RegularMessage(
"UNKNOWN MESSAGE!",
null,
"A regular message was tried to be looked up, but was not found. Please report this to your bot " +
"maintainer/administrator.",
String.valueOf(0xFF0000),
null,
null,
null,
null,
null,
null,
null,
Collections.singletonList(new MessageEmbed.Field("Message Key", "${key}", false))
);
public static final ListingMessage UNKNOWN_LISTING_MESSAGE = new ListingMessage(
"UNKNOWN MESSAGE!",
null,
"A listing message was tried to be looked up, but was not found. " +
"Please report this to your bot maintainer/administrator.",
String.valueOf(0xFF0000),
null,
null,
null,
null,
null,
null,
null,
null,
new ListingMessage.DescriptionEntry(null, ""),
Collections.singletonList(new MessageEmbed.Field("Message Key", "${key}", false)),
Collections.emptyList()
);
}

View File

@ -5,6 +5,7 @@ import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.msg.json.RegularMessage;
import sciwhiz12.janitor.msg.substitution.IHasCustomSubstitutions;
import sciwhiz12.janitor.msg.substitution.SubstitutionMap;
import java.util.HashMap;
@ -12,7 +13,7 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class RegularMessageBuilder {
public class RegularMessageBuilder implements IHasCustomSubstitutions<RegularMessageBuilder> {
private final RegularMessage message;
private final Map<String, Supplier<String>> customSubstitutions;

View File

@ -77,4 +77,8 @@ public class TranslationMap {
return matcher.replaceAll(
matchResult -> quoteReplacement(translations.getOrDefault(matchResult.group(1), matchResult.group(0))));
}
public JanitorBot getBot() {
return bot;
}
}

View File

@ -0,0 +1,43 @@
package sciwhiz12.janitor.msg.emote;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import sciwhiz12.janitor.JanitorBot;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
public class ReactionManager extends ListenerAdapter {
private final JanitorBot bot;
private final Map<Long, ReactionMessage> messageMap = new HashMap<>();
public ReactionManager(JanitorBot bot) {
this.bot = bot;
}
public ReactionMessage newMessage(Message message) {
if (messageMap.containsKey(message.getIdLong())) {
throw new IllegalArgumentException("Reaction message already exists for message with id " + message.getIdLong());
}
final ReactionMessage msg = new ReactionMessage(bot, message);
messageMap.put(message.getIdLong(), msg);
return msg;
}
public void removeMessage(long messageID) {
bot.getDiscord().removeEventListener(messageMap.remove(messageID));
}
public Map<Long, ReactionMessage> getRegisteredMessages() {
return messageMap;
}
@Override
public void onMessageDelete(@Nonnull MessageDeleteEvent event) {
if (messageMap.containsKey(event.getMessageIdLong())) {
bot.getDiscord().removeEventListener(messageMap.get(event.getMessageIdLong()));
}
}
}

View File

@ -0,0 +1,112 @@
package sciwhiz12.janitor.msg.emote;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageReaction.ReactionEmote;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
import net.dv8tion.jda.api.exceptions.ErrorHandler;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.requests.ErrorResponse;
import sciwhiz12.janitor.JanitorBot;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.annotation.Nonnull;
import static net.dv8tion.jda.api.Permission.MESSAGE_MANAGE;
public class ReactionMessage extends ListenerAdapter {
private final JanitorBot bot;
private final Message message;
private final Map<ReactionEmote, IReactionListener> emotes = new LinkedHashMap<>();
private boolean removeEmotes = true;
private long ownerID;
private boolean onlyOwner;
public ReactionMessage(JanitorBot bot, Message message, boolean onlyOwner, long ownerID) {
this.bot = bot;
this.message = message;
this.ownerID = ownerID;
this.onlyOwner = onlyOwner;
}
public ReactionMessage(JanitorBot bot, Message message) {
this(bot, message, false, 0);
}
public ReactionMessage add(ReactionEmote emote, IReactionListener listener) {
emotes.put(emote, listener);
return this;
}
public ReactionMessage add(String emote, IReactionListener listener) {
return add(ReactionEmote.fromUnicode(emote, bot.getDiscord()), listener);
}
public ReactionMessage add(Emote emote, IReactionListener listener) {
return add(ReactionEmote.fromCustom(emote), listener);
}
public ReactionMessage removeEmotes(boolean remove) {
this.removeEmotes = remove;
return this;
}
public ReactionMessage owner(long ownerID) {
this.ownerID = ownerID;
this.onlyOwner = true;
return this;
}
public void create() {
for (ReactionEmote reaction : emotes.keySet()) {
if (reaction.isEmote()) {
message.addReaction(reaction.getEmote()).queue();
} else {
message.addReaction(reaction.getEmoji()).queue();
}
}
bot.getDiscord().addEventListener(this);
}
@Override
public void onMessageReactionAdd(@Nonnull MessageReactionAddEvent event) {
if (event.getMessageIdLong() != message.getIdLong()) return;
if (event.getUserIdLong() == bot.getDiscord().getSelfUser().getIdLong()) return;
if (onlyOwner && event.getUserIdLong() != ownerID) return;
emotes.keySet().stream()
.filter(emote -> event.getReactionEmote().equals(emote))
.forEach(emote -> emotes.get(emote).accept(this, event));
if (removeEmotes && (!event.isFromGuild()
|| event.getGuild().getSelfMember().hasPermission(event.getTextChannel(), MESSAGE_MANAGE))) {
event.retrieveUser()
.flatMap(user -> event.getReaction().removeReaction(user))
.queue(null, new ErrorHandler().ignore(ErrorResponse.UNKNOWN_MESSAGE));
}
}
public JanitorBot getBot() {
return bot;
}
public long getOwnerID() {
return ownerID;
}
public boolean isOwnerOnly() {
return onlyOwner;
}
public Map<ReactionEmote, IReactionListener> getListeners() {
return Collections.unmodifiableMap(emotes);
}
@FunctionalInterface
public interface IReactionListener extends BiConsumer<ReactionMessage, MessageReactionAddEvent> {
void accept(ReactionMessage message, MessageReactionAddEvent event);
}
}

View File

@ -1,9 +0,0 @@
package sciwhiz12.janitor.msg.json;
import net.dv8tion.jda.api.EmbedBuilder;
import sciwhiz12.janitor.msg.substitution.ISubstitutor;
import sciwhiz12.janitor.msg.TranslationMap;
public interface IMessage {
EmbedBuilder create(TranslationMap translations, ISubstitutor substitutions);
}

View File

@ -1,61 +1,254 @@
package sciwhiz12.janitor.msg.json;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.primitives.Ints;
import joptsimple.internal.Strings;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.Role;
import sciwhiz12.janitor.msg.TranslationMap;
import sciwhiz12.janitor.msg.substitution.CustomSubstitutions;
import sciwhiz12.janitor.msg.substitution.ISubstitutor;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import javax.annotation.Nullable;
@JsonDeserialize(using = ListingMessageDeserializer.class)
public class ListingMessage {
protected final String url;
protected final String title;
protected final String description;
protected final OffsetDateTime timestamp;
protected final int color;
protected final MessageEmbed.Thumbnail thumbnail;
protected final MessageEmbed.AuthorInfo author;
protected final MessageEmbed.Footer footer;
protected final MessageEmbed.ImageInfo image;
protected final Multimap<FieldPlacement, MessageEmbed.Field> fields;
@Nullable protected final String title;
@Nullable protected final String url;
@Nullable protected final String description;
@Nullable protected final String color;
@Nullable protected final String authorName;
@Nullable protected final String authorUrl;
@Nullable protected final String authorIconUrl;
@Nullable protected final String footerText;
@Nullable protected final String footerIconUrl;
@Nullable protected final String imageUrl;
@Nullable protected final String thumbnailUrl;
@Nullable protected final String emptyText;
protected final Entry entry;
protected final List<MessageEmbed.Field> beforeFields;
protected final List<MessageEmbed.Field> afterFields;
@Deprecated
public ListingMessage() {
this(null, null, null, null, 0, null, null, null, null, null);
}
public ListingMessage(MessageEmbed embed) {
this(embed.getUrl(),
embed.getTitle(),
embed.getDescription(),
embed.getTimestamp(),
embed.getColorRaw(),
embed.getThumbnail(),
embed.getAuthor(),
embed.getFooter(),
embed.getImage(),
Multimaps.index(embed.getFields(), k -> FieldPlacement.BEFORE));
}
public ListingMessage(String url, String title, String description, OffsetDateTime timestamp, int color,
MessageEmbed.Thumbnail thumbnail, MessageEmbed.AuthorInfo author, MessageEmbed.Footer footer,
MessageEmbed.ImageInfo image, Multimap<FieldPlacement, MessageEmbed.Field> fields) {
this.url = url;
public ListingMessage(
@Nullable String title,
@Nullable String url,
@Nullable String description,
@Nullable String color,
@Nullable String authorName,
@Nullable String authorUrl,
@Nullable String authorIconUrl,
@Nullable String footerText,
@Nullable String footerIconUrl,
@Nullable String imageUrl,
@Nullable String thumbnailUrl,
@Nullable String emptyText,
Entry entry,
List<MessageEmbed.Field> beforeFields,
List<MessageEmbed.Field> afterFields
) {
this.title = title;
this.url = url;
this.description = description;
this.timestamp = timestamp;
this.color = color;
this.thumbnail = thumbnail;
this.author = author;
this.footer = footer;
this.image = image;
this.fields = fields;
this.authorName = authorName;
this.authorUrl = authorUrl;
this.authorIconUrl = authorIconUrl;
this.footerText = footerText;
this.footerIconUrl = footerIconUrl;
this.imageUrl = imageUrl;
this.thumbnailUrl = thumbnailUrl;
this.emptyText = emptyText;
this.entry = entry;
this.beforeFields = beforeFields;
this.afterFields = afterFields;
}
public enum ListingType {
DESCRIPTION, FIELDS
@Nullable
public String getTitle() {
return title;
}
public enum FieldPlacement {
BEFORE, AFTER;
@Nullable
public String getUrl() {
return url;
}
@Nullable
public String getDescription() {
return description;
}
@Nullable
public String getColor() {
return color;
}
@Nullable
public String getAuthorName() {
return authorName;
}
@Nullable
public String getAuthorUrl() {
return authorUrl;
}
@Nullable
public String getAuthorIconUrl() {
return authorIconUrl;
}
@Nullable
public String getFooterText() {
return footerText;
}
@Nullable
public String getFooterIconUrl() {
return footerIconUrl;
}
@Nullable
public String getImageUrl() {
return imageUrl;
}
@Nullable
public String getThumbnailUrl() {
return thumbnailUrl;
}
@Nullable
public String getEmptyText() {
return emptyText;
}
public Entry getEntry() {
return entry;
}
public List<MessageEmbed.Field> getBeforeFields() {
return beforeFields;
}
public List<MessageEmbed.Field> getAfterFields() {
return afterFields;
}
public <T> EmbedBuilder create(
TranslationMap translations,
ISubstitutor global,
Iterable<T> iterable,
BiConsumer<T, CustomSubstitutions> entryApplier
) {
final Function<String, String> func = str -> str != null ? global.substitute(translations.translate(str)) : null;
final EmbedBuilder builder = new EmbedBuilder();
builder.setTitle(func.apply(title), func.apply(url));
builder.setColor(parseColor(global.substitute(color)));
builder.setAuthor(func.apply(authorName), func.apply(authorUrl), func.apply(authorIconUrl));
builder.setDescription(func.apply(description));
builder.setImage(func.apply(imageUrl));
builder.setThumbnail(func.apply(thumbnailUrl));
builder.setTimestamp(OffsetDateTime.now(ZoneOffset.UTC));
builder.setFooter(func.apply(footerText), func.apply(footerIconUrl));
for (MessageEmbed.Field field : beforeFields) {
builder.addField(func.apply(field.getName()), func.apply(field.getValue()), field.isInline());
}
final CustomSubstitutions entrySubs = new CustomSubstitutions();
final Function<String, String> entryFunc = str -> str != null ? entrySubs.substitute(func.apply(str)) : null;
int count = 0;
for (T listEntry : iterable) {
entryApplier.accept(listEntry, entrySubs);
if (entry instanceof FieldEntry) {
FieldEntry fieldEntry = (FieldEntry) entry;
builder.addField(
entryFunc.apply(fieldEntry.getFieldName()),
entryFunc.apply(fieldEntry.getFieldValue()),
fieldEntry.isInline()
);
} else if (entry instanceof DescriptionEntry) {
DescriptionEntry descEntry = (DescriptionEntry) entry;
builder.getDescriptionBuilder().append(entryFunc.apply(descEntry.getDescription()));
builder.getDescriptionBuilder().append(descEntry.getJoiner());
}
count++;
}
if (count < 1) {
builder.getDescriptionBuilder().append(func.apply(emptyText));
}
for (MessageEmbed.Field field : afterFields) {
builder.addField(func.apply(field.getName()), func.apply(field.getValue()), field.isInline());
}
return builder;
}
private static int parseColor(String str) {
if (Strings.isNullOrEmpty(str)) return Role.DEFAULT_COLOR_RAW;
if (str.startsWith("0x")) {
// noinspection UnstableApiUsage
final Integer res = Ints.tryParse(str.substring(2), 16);
if (res != null) {
return res;
}
}
// noinspection UnstableApiUsage
final Integer res = Ints.tryParse(str, 10);
if (res != null) {
return res;
}
return Role.DEFAULT_COLOR_RAW;
}
public interface Entry {}
public static class DescriptionEntry implements Entry {
public static final String DEFAULT_JOINER = "\n";
private final String joiner;
private final String descriptionEntry;
public DescriptionEntry(@Nullable String joiner, String descriptionEntry) {
this.joiner = joiner != null ? joiner : DEFAULT_JOINER;
this.descriptionEntry = descriptionEntry;
}
public String getJoiner() {
return joiner;
}
public String getDescription() {
return descriptionEntry;
}
}
public static class FieldEntry implements Entry {
private final String fieldName;
private final String fieldValue;
private final boolean inline;
public FieldEntry(String fieldName, String fieldValue, boolean inline) {
this.fieldName = fieldName;
this.fieldValue = fieldValue;
this.inline = inline;
}
public String getFieldName() {
return fieldName;
}
public String getFieldValue() {
return fieldValue;
}
public boolean isInline() {
return inline;
}
}
}

View File

@ -0,0 +1,117 @@
package sciwhiz12.janitor.msg.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
public class ListingMessageDeserializer extends StdDeserializer<ListingMessage> {
public ListingMessageDeserializer() {
super(ListingMessage.class);
}
@Override
public ListingMessage deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
final JsonNode root = ctx.readTree(p);
String title = null;
String url = null;
String description = root.path("description").asText(null);
String color = root.path("color").asText(null);
String authorName = null;
String authorUrl = null;
String authorIconUrl = null;
String footerText = null;
String footerIconUrl = null;
String imageUrl = root.path("image").asText(null);
String thumbnailUrl = root.path("thumbnail").asText(null);
String emptyText = root.path("empty").asText(null);
List<MessageEmbed.Field> beforeFields = readFields(root.path("fields").path("before"));
List<MessageEmbed.Field> afterFields = readFields(root.path("fields").path("after"));
// Title
if (root.path("title").isTextual()) {
title = root.path("title").asText();
} else if (root.path("title").path("text").isTextual()) {
title = root.path("title").path("text").asText();
url = root.path("title").path("url").asText(null);
}
// Author
if (root.path("author").isTextual()) {
authorName = root.path("author").asText();
} else if (root.path("author").path("name").isTextual()) {
authorName = root.path("author").path("name").asText();
authorUrl = root.path("author").path("url").asText(null);
authorIconUrl = root.path("author").path("icon_url").asText(null);
}
// Footer
if (root.path("footer").isTextual()) {
footerText = root.path("footer").asText();
} else if (root.path("footer").path("text").isTextual()) {
footerText = root.path("footer").path("text").asText();
footerIconUrl = root.path("footer").path("icon_url").asText(null);
}
// ENTRY
final ListingMessage.Entry entry = readEntry(root.path("entry"));
return new ListingMessage(title, url, description, color, authorName, authorUrl, authorIconUrl, footerText,
footerIconUrl, imageUrl, thumbnailUrl, emptyText, entry, beforeFields, afterFields);
}
public static ListingMessage.Entry readEntry(JsonNode root) {
switch (root.path("type").asText()) {
case "field": {
return new ListingMessage.FieldEntry(
root.path("name").asText(EmbedBuilder.ZERO_WIDTH_SPACE),
root.path("value").asText(EmbedBuilder.ZERO_WIDTH_SPACE),
root.path("inline").asBoolean(false)
);
}
default:
case "description": {
return new ListingMessage.DescriptionEntry(
root.path("joiner").asText(null),
root.path("text").asText());
}
}
}
public static List<MessageEmbed.Field> readFields(JsonNode node) {
if (node.isArray()) {
final ArrayList<MessageEmbed.Field> fields = new ArrayList<>();
for (int i = 0; i < node.size(); i++) {
final MessageEmbed.Field field = readField(node.path(i));
if (field != null) {
fields.add(field);
}
}
return fields;
}
return Collections.emptyList();
}
@Nullable
public static MessageEmbed.Field readField(JsonNode fieldNode) {
if (fieldNode.path("name").isTextual() && fieldNode.path("value").isTextual()) {
return new MessageEmbed.Field(
fieldNode.path("name").asText(),
fieldNode.path("value").asText(),
fieldNode.path("inline").asBoolean(false)
);
}
return null;
}
}

View File

@ -6,8 +6,8 @@ import joptsimple.internal.Strings;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.Role;
import sciwhiz12.janitor.msg.substitution.ISubstitutor;
import sciwhiz12.janitor.msg.TranslationMap;
import sciwhiz12.janitor.msg.substitution.ISubstitutor;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@ -20,7 +20,7 @@ import java.util.function.Function;
import javax.annotation.Nullable;
@JsonDeserialize(using = RegularMessageDeserializer.class)
public class RegularMessage implements IMessage {
public class RegularMessage {
@Nullable
protected final String title;
@Nullable
@ -176,7 +176,6 @@ public class RegularMessage implements IMessage {
thumbnailUrl, fields);
}
@Override
public EmbedBuilder create(TranslationMap translations, ISubstitutor substitutions) {
final Function<String, String> func = str -> str != null ? substitutions.substitute(translations.translate(str)) : null;
final EmbedBuilder builder = new EmbedBuilder();

View File

@ -1,20 +1,47 @@
package sciwhiz12.janitor.msg.substitution;
import org.apache.commons.collections4.TransformerUtils;
import org.apache.commons.collections4.map.DefaultedMap;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class CustomSubstitutions implements ISubstitutor {
public class CustomSubstitutions implements ISubstitutor, IHasCustomSubstitutions<CustomSubstitutions> {
private final Map<String, Supplier<String>> map;
public CustomSubstitutions(Map<String, Supplier<String>> map) {
this.map = map;
}
public CustomSubstitutions() {
this(new HashMap<>());
}
@Override
public String substitute(String text) {
return SubstitutionMap.substitute(text, map);
}
public CustomSubstitutions apply(Consumer<CustomSubstitutions> consumer) {
consumer.accept(this);
return this;
}
public CustomSubstitutions with(final String argument, final Supplier<String> value) {
map.put(argument, value);
return this;
}
public CustomSubstitutions with(Map<String, Supplier<String>> customSubstitutions) {
return new CustomSubstitutions(createDefaultedMap(customSubstitutions));
}
public Map<String, Supplier<String>> createDefaultedMap(Map<String, Supplier<String>> custom) {
return DefaultedMap.defaultedMap(custom, TransformerUtils.mapTransformer(map));
}
public Map<String, Supplier<String>> getMap() {
return map;
}

View File

@ -0,0 +1,10 @@
package sciwhiz12.janitor.msg.substitution;
import java.util.function.Consumer;
import java.util.function.Supplier;
public interface IHasCustomSubstitutions<T extends IHasCustomSubstitutions<?>> {
T with(String argument, Supplier<String> value);
T apply(Consumer<T> consumer);
}

View File

@ -110,9 +110,8 @@
"moderation.note.remove.field.contents": "Text",
"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.note.list.author": "Listing of Notes",
"moderation.warnlist.entry": "**Case #${warning_entry.case_id}**: Warned ${warning_entry.warned.mention} by ${warning_entry.performer.mention} \n - _Date & Time:_ ${warning_entry.date_time} \n - _Reason:_ ${nullcheck;warning_entry.reason;_No reason specified._}",
"moderation.note.list.author": "Listing of Notes (Page ${page.current}/${page.max})",
"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.list.entry": "**#${note_entry.note_id}**: for ${note_entry.target.mention} by ${note_entry.performer.mention} \n - _Date & Time:_ ${note_entry.date_time} \n - _Text:_ ${note_entry.contents}"
}

View File

@ -14,6 +14,7 @@
"moderation/unban/info",
"moderation/warn/info",
"moderation/warn/dm",
"moderation/warn/list",
"moderation/unwarn/info",
"moderation/error/unwarn/no_case_found",
"moderation/error/unwarn/cannot_unwarn_self",
@ -22,5 +23,6 @@
"moderation/error/note/max_amount_of_notes",
"moderation/error/note/no_note_found",
"moderation/note/add",
"moderation/note/remove"
"moderation/note/remove",
"moderation/note/list"
]

View File

@ -0,0 +1,13 @@
{
"type": "listing",
"color": "${moderation.color}",
"author": {
"name": "<moderation.note.list.author>",
"icon_url": "${moderation.icon_url}"
},
"entry": {
"type": "description",
"text": "<moderation.note.list.entry>"
},
"empty": "<moderation.note.list.empty>"
}

View File

@ -0,0 +1,13 @@
{
"type": "listing",
"color": "${moderation.color}",
"author": {
"name": "<moderation.warnlist.author>",
"icon_url": "${moderation.icon_url}"
},
"entry": {
"type": "description",
"text": "<moderation.warnlist.entry>"
},
"empty": "<moderation.warnlist.empty>"
}