mirror of
https://github.com/sciwhiz12/Janitor.git
synced 2024-09-19 23:24:03 +00:00
Improve and add more config options, add reload command to console
This commit is contained in:
parent
cbf57fb585
commit
934fbeb2f4
|
@ -34,14 +34,25 @@ public class BotConsole {
|
||||||
|
|
||||||
public void parseCommand(String input) {
|
public void parseCommand(String input) {
|
||||||
String[] parts = input.split(" ");
|
String[] parts = input.split(" ");
|
||||||
|
outer:
|
||||||
switch (parts[0]) {
|
switch (parts[0]) {
|
||||||
case "shutdown": {
|
case "shutdown": {
|
||||||
running = false;
|
running = false;
|
||||||
bot.shutdown();
|
bot.shutdown();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "reload": {
|
||||||
|
if (parts.length >= 2)
|
||||||
|
switch (parts[1]) {
|
||||||
|
case "translations": {
|
||||||
|
CONSOLE.info("Reloading translations");
|
||||||
|
bot.getTranslations().loadTranslations();
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
CONSOLE.warn("Unknown command: " + input);
|
CONSOLE.warn("Unknown command: {}", input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +71,8 @@ public class BotConsole {
|
||||||
while (!scanner.hasNextLine()) {
|
while (!scanner.hasNextLine()) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(150);
|
Thread.sleep(150);
|
||||||
} catch (InterruptedException e) {
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
CONSOLE.warn("Console thread is interrupted");
|
CONSOLE.warn("Console thread is interrupted");
|
||||||
continue outer;
|
continue outer;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +84,8 @@ public class BotConsole {
|
||||||
}
|
}
|
||||||
CONSOLE.debug("Received command: {}", input);
|
CONSOLE.debug("Received command: {}", input);
|
||||||
BotConsole.this.parseCommand(input);
|
BotConsole.this.parseCommand(input);
|
||||||
} catch (Exception e) {
|
}
|
||||||
|
catch (Exception e) {
|
||||||
CONSOLE.error("Error while running console thread", e);
|
CONSOLE.error("Error while running console thread", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,12 @@ public class GuildStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() {
|
public void save() {
|
||||||
Logging.JANITOR.debug("Saving guild storage to files under {}...", mainFolder);
|
save(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(boolean isAutosave) {
|
||||||
|
if (!isAutosave)
|
||||||
|
Logging.JANITOR.debug("Saving guild storage to files under {}...", mainFolder);
|
||||||
boolean anySaved = false;
|
boolean anySaved = false;
|
||||||
for (Guild guild : guildStorage.keySet()) {
|
for (Guild guild : guildStorage.keySet()) {
|
||||||
final Map<String, IStorage> storageMap = guildStorage.get(guild);
|
final Map<String, IStorage> storageMap = guildStorage.get(guild);
|
||||||
|
@ -111,8 +116,8 @@ public class GuildStorage {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
while (running) {
|
while (running) {
|
||||||
storage.save();
|
storage.save(true);
|
||||||
try { Thread.sleep(10000); }
|
try { Thread.sleep(storage.getBot().getConfig().AUTOSAVE_INTERVAL.get() * 1000); }
|
||||||
catch (InterruptedException ignored) {}
|
catch (InterruptedException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package sciwhiz12.janitor.commands;
|
||||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
||||||
import sciwhiz12.janitor.JanitorBot;
|
import sciwhiz12.janitor.JanitorBot;
|
||||||
|
import sciwhiz12.janitor.config.BotConfig;
|
||||||
import sciwhiz12.janitor.msg.Messages;
|
import sciwhiz12.janitor.msg.Messages;
|
||||||
|
|
||||||
public abstract class BaseCommand {
|
public abstract class BaseCommand {
|
||||||
|
@ -24,5 +25,9 @@ public abstract class BaseCommand {
|
||||||
return getBot().getMessages();
|
return getBot().getMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected BotConfig config() {
|
||||||
|
return getBot().getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
public abstract LiteralArgumentBuilder<MessageReceivedEvent> getNode();
|
public abstract LiteralArgumentBuilder<MessageReceivedEvent> getNode();
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,7 @@ public class CommandRegistry implements EventListener {
|
||||||
addCommand(new WarnCommand(this));
|
addCommand(new WarnCommand(this));
|
||||||
addCommand(new WarnListCommand(this));
|
addCommand(new WarnListCommand(this));
|
||||||
addCommand(new UnwarnCommand(this));
|
addCommand(new UnwarnCommand(this));
|
||||||
if (bot.getConfig().getOwnerID().isPresent()) {
|
addCommand(new ShutdownCommand(this));
|
||||||
addCommand(new ShutdownCommand(this, bot.getConfig().getOwnerID().get()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandDispatcher<MessageReceivedEvent> getDispatcher() {
|
public CommandDispatcher<MessageReceivedEvent> getDispatcher() {
|
||||||
|
|
|
@ -11,17 +11,16 @@ import static sciwhiz12.janitor.Logging.JANITOR;
|
||||||
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
|
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
|
||||||
|
|
||||||
public class ShutdownCommand extends BaseCommand {
|
public class ShutdownCommand extends BaseCommand {
|
||||||
private final long ownerID;
|
public ShutdownCommand(CommandRegistry registry) {
|
||||||
|
|
||||||
public ShutdownCommand(CommandRegistry registry, long ownerID) {
|
|
||||||
super(registry);
|
super(registry);
|
||||||
this.ownerID = ownerID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
||||||
return literal("shutdown")
|
return literal("shutdown")
|
||||||
.requires(ctx -> ctx.getAuthor().getIdLong() == ownerID)
|
.requires(ctx -> getBot().getConfig().getOwnerID().map(
|
||||||
|
id -> id == ctx.getAuthor().getIdLong()).orElse(false)
|
||||||
|
)
|
||||||
.executes(this::run);
|
.executes(this::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +32,8 @@ public class ShutdownCommand extends BaseCommand {
|
||||||
.submit()
|
.submit()
|
||||||
.whenComplete(Util.handle(
|
.whenComplete(Util.handle(
|
||||||
success -> JANITOR.debug("Sent shutdown message to channel {}", Util.toString(ctx.getSource().getAuthor())),
|
success -> JANITOR.debug("Sent shutdown message to channel {}", Util.toString(ctx.getSource().getAuthor())),
|
||||||
err -> JANITOR.error("Error while sending ping message to bot owner {}", Util.toString(ctx.getSource().getAuthor()))
|
err -> JANITOR
|
||||||
|
.error("Error while sending ping message to bot owner {}", Util.toString(ctx.getSource().getAuthor()))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.join();
|
.join();
|
||||||
|
|
|
@ -31,6 +31,7 @@ public class UnwarnCommand extends BaseCommand {
|
||||||
@Override
|
@Override
|
||||||
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
||||||
return literal("unwarn")
|
return literal("unwarn")
|
||||||
|
.requires(ctx -> getBot().getConfig().WARNINGS_ENABLE.get())
|
||||||
.then(argument("caseId", IntegerArgumentType.integer(1))
|
.then(argument("caseId", IntegerArgumentType.integer(1))
|
||||||
.executes(this::run)
|
.executes(this::run)
|
||||||
);
|
);
|
||||||
|
@ -59,10 +60,16 @@ public class UnwarnCommand extends BaseCommand {
|
||||||
final WarningStorage storage = WarningStorage.get(getBot().getStorage(), guild);
|
final WarningStorage storage = WarningStorage.get(getBot().getStorage(), guild);
|
||||||
@Nullable
|
@Nullable
|
||||||
final WarningEntry entry = storage.getWarning(caseID);
|
final WarningEntry entry = storage.getWarning(caseID);
|
||||||
|
Member temp;
|
||||||
if (entry == null)
|
if (entry == null)
|
||||||
messages().MODERATION.noWarnWithID(channel, performer, caseID).queue();
|
messages().MODERATION.noWarnWithID(channel, performer, caseID).queue();
|
||||||
else if (entry.getWarned().getIdLong() == performer.getIdLong())
|
else if (entry.getWarned().getIdLong() == performer.getIdLong()
|
||||||
|
&& !config().WARNINGS_REMOVE_SELF_WARNINGS.get())
|
||||||
messages().MODERATION.cannotUnwarnSelf(channel, performer, caseID, entry).queue();
|
messages().MODERATION.cannotUnwarnSelf(channel, performer, caseID, entry).queue();
|
||||||
|
else if (config().WARNINGS_RESPECT_MOD_ROLES.get()
|
||||||
|
&& (temp = guild.getMember(entry.getPerformer())) != null
|
||||||
|
&& !performer.canInteract(temp))
|
||||||
|
messages().MODERATION.cannotRemoveHigherModerated(channel, performer, caseID, entry).queue();
|
||||||
else {
|
else {
|
||||||
storage.removeWarning(caseID);
|
storage.removeWarning(caseID);
|
||||||
messages().MODERATION.unwarn(channel, performer, caseID, entry).queue();
|
messages().MODERATION.unwarn(channel, performer, caseID, entry).queue();
|
||||||
|
|
|
@ -36,6 +36,7 @@ public class WarnCommand extends BaseCommand {
|
||||||
@Override
|
@Override
|
||||||
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
||||||
return literal("warn")
|
return literal("warn")
|
||||||
|
.requires(ctx -> getBot().getConfig().WARNINGS_ENABLE.get())
|
||||||
.then(argument("member", member())
|
.then(argument("member", member())
|
||||||
.then(argument("reason", greedyString())
|
.then(argument("reason", greedyString())
|
||||||
.executes(ctx -> this.run(ctx, getString(ctx, "reason")))
|
.executes(ctx -> this.run(ctx, getString(ctx, "reason")))
|
||||||
|
@ -70,6 +71,8 @@ public class WarnCommand extends BaseCommand {
|
||||||
messages().MODERATION.performerInsufficientPermissions(channel, performer, WARN_PERMISSION).queue();
|
messages().MODERATION.performerInsufficientPermissions(channel, performer, WARN_PERMISSION).queue();
|
||||||
else if (!performer.canInteract(target))
|
else if (!performer.canInteract(target))
|
||||||
messages().MODERATION.cannotModerate(channel, performer, target).queue();
|
messages().MODERATION.cannotModerate(channel, performer, target).queue();
|
||||||
|
else if (target.hasPermission(WARN_PERMISSION) && config().WARNINGS_PREVENT_WARNING_MODS.get())
|
||||||
|
messages().MODERATION.cannotWarnMods(channel, performer, target).queue();
|
||||||
else
|
else
|
||||||
target.getUser().openPrivateChannel()
|
target.getUser().openPrivateChannel()
|
||||||
.flatMap(dm -> messages().MODERATION.warnDM(dm, performer, target, reason, dateTime))
|
.flatMap(dm -> messages().MODERATION.warnDM(dm, performer, target, reason, dateTime))
|
||||||
|
|
|
@ -36,6 +36,7 @@ public class WarnListCommand extends BaseCommand {
|
||||||
@Override
|
@Override
|
||||||
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
|
||||||
return literal("warnlist")
|
return literal("warnlist")
|
||||||
|
.requires(ctx -> getBot().getConfig().WARNINGS_ENABLE.get())
|
||||||
.then(literal("target")
|
.then(literal("target")
|
||||||
.then(argument("target", member())
|
.then(argument("target", member())
|
||||||
.then(literal("mod")
|
.then(literal("mod")
|
||||||
|
|
|
@ -18,10 +18,19 @@ public class BotConfig {
|
||||||
|
|
||||||
private final CommentedConfigSpec.ConfigValue<String> CLIENT_TOKEN;
|
private final CommentedConfigSpec.ConfigValue<String> CLIENT_TOKEN;
|
||||||
private final CommentedConfigSpec.LongValue OWNER_ID;
|
private final CommentedConfigSpec.LongValue OWNER_ID;
|
||||||
|
|
||||||
public final CommentedConfigSpec.ConfigValue<String> STORAGE_PATH;
|
public final CommentedConfigSpec.ConfigValue<String> STORAGE_PATH;
|
||||||
|
public final CommentedConfigSpec.IntValue AUTOSAVE_INTERVAL;
|
||||||
|
|
||||||
public final CommentedConfigSpec.ConfigValue<String> CUSTOM_TRANSLATION_FILE;
|
public final CommentedConfigSpec.ConfigValue<String> CUSTOM_TRANSLATION_FILE;
|
||||||
|
|
||||||
public final CommentedConfigSpec.ConfigValue<String> COMMAND_PREFIX;
|
public final CommentedConfigSpec.ConfigValue<String> COMMAND_PREFIX;
|
||||||
|
|
||||||
|
public final CommentedConfigSpec.BooleanValue WARNINGS_ENABLE;
|
||||||
|
public final CommentedConfigSpec.BooleanValue WARNINGS_RESPECT_MOD_ROLES;
|
||||||
|
public final CommentedConfigSpec.BooleanValue WARNINGS_PREVENT_WARNING_MODS;
|
||||||
|
public final CommentedConfigSpec.BooleanValue WARNINGS_REMOVE_SELF_WARNINGS;
|
||||||
|
|
||||||
private final BotOptions options;
|
private final BotOptions options;
|
||||||
private final Path configPath;
|
private final Path configPath;
|
||||||
private final CommentedConfigSpec spec;
|
private final CommentedConfigSpec spec;
|
||||||
|
@ -32,24 +41,55 @@ public class BotConfig {
|
||||||
|
|
||||||
final CommentedConfigSpec.Builder builder = new CommentedConfigSpec.Builder();
|
final CommentedConfigSpec.Builder builder = new CommentedConfigSpec.Builder();
|
||||||
|
|
||||||
|
builder.push("discord");
|
||||||
CLIENT_TOKEN = builder
|
CLIENT_TOKEN = builder
|
||||||
.comment("The client secret/token for the bot user", "This must be set, or the application will not start up.")
|
.comment("The client secret/token for the bot user", "This must be set, or the application will not start up.")
|
||||||
.define("discord.client_token", "");
|
.define("client_token", "");
|
||||||
OWNER_ID = builder
|
OWNER_ID = builder
|
||||||
.comment("The id of the bot owner; used for sending status messages and for bot administration commands.",
|
.comment("The id of the bot owner; used for sending status messages and for bot administration commands.",
|
||||||
"If 0, then the bot has no owner set.")
|
"If 0, then the bot has no owner set.")
|
||||||
.defineInRange("discord.owner_id", 0L, Long.MIN_VALUE, Long.MAX_VALUE);
|
.defineInRange("owner_id", 0L, Long.MIN_VALUE, Long.MAX_VALUE);
|
||||||
|
builder.pop();
|
||||||
|
|
||||||
|
builder.push("storage");
|
||||||
STORAGE_PATH = builder
|
STORAGE_PATH = builder
|
||||||
.comment("The folder where per-guild storage is kept.")
|
.comment("The folder where per-guild storage is kept.")
|
||||||
.define("storage.main_path", "guild_storage");
|
.define("main_path", "guild_storage");
|
||||||
|
AUTOSAVE_INTERVAL = builder
|
||||||
|
.comment("The interval between storage autosave checks, in seconds.")
|
||||||
|
.defineInRange("autosave_internal", 20, 1, Integer.MAX_VALUE);
|
||||||
|
builder.pop();
|
||||||
|
|
||||||
CUSTOM_TRANSLATION_FILE = builder
|
CUSTOM_TRANSLATION_FILE = builder
|
||||||
.comment("A file which contains custom translation keys to load for messages.",
|
.comment("A file which contains custom translation keys to load for messages.",
|
||||||
"If blank, no file shall be loaded.")
|
"If blank, no file shall be loaded.")
|
||||||
.define("messages.custom_translations", "");
|
.define("messages.custom_translations", "");
|
||||||
|
|
||||||
COMMAND_PREFIX = builder
|
COMMAND_PREFIX = builder
|
||||||
.comment("The prefix for commands.")
|
.comment("The prefix for commands.")
|
||||||
.define("commands.prefix", "!");
|
.define("commands.prefix", "!");
|
||||||
|
|
||||||
|
builder.comment("Moderation settings").push("moderation");
|
||||||
|
{
|
||||||
|
builder.comment("Settings for the warnings system").push("warnings");
|
||||||
|
WARNINGS_ENABLE = builder
|
||||||
|
.comment("Whether to enable the warnings system. If disabled, the related commands are force-disabled.")
|
||||||
|
.define("enable", true);
|
||||||
|
WARNINGS_RESPECT_MOD_ROLES = builder
|
||||||
|
.comment(
|
||||||
|
"Whether to prevent lower-ranked moderators (in the role hierarchy) from removing warnings issued by " +
|
||||||
|
"higher-ranked moderators.")
|
||||||
|
.define("respect_mod_roles", false);
|
||||||
|
WARNINGS_PREVENT_WARNING_MODS = builder
|
||||||
|
.comment("Whether to prevent moderators from issuing warnings against other moderators.")
|
||||||
|
.define("warn_other_moderators", false);
|
||||||
|
WARNINGS_REMOVE_SELF_WARNINGS = builder
|
||||||
|
.comment("Whether to allow moderators to remove warnings from themselves.")
|
||||||
|
.define("remove_self_warnings", false);
|
||||||
|
builder.pop();
|
||||||
|
}
|
||||||
|
builder.pop();
|
||||||
|
|
||||||
spec = builder.build();
|
spec = builder.build();
|
||||||
|
|
||||||
this.configPath = options.getConfigPath().orElse(DEFAULT_CONFIG_PATH);
|
this.configPath = options.getConfigPath().orElse(DEFAULT_CONFIG_PATH);
|
||||||
|
|
|
@ -312,6 +312,33 @@ public class Messages {
|
||||||
embed.addField(translate("moderation.unwarn.field.reason"), entry.getReason(), false);
|
embed.addField(translate("moderation.unwarn.field.reason"), entry.getReason(), false);
|
||||||
return channel.sendMessage(embed.build());
|
return channel.sendMessage(embed.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MessageAction cannotWarnMods(MessageChannel channel, Member performer, Member target) {
|
||||||
|
final EmbedBuilder embed = new EmbedBuilder()
|
||||||
|
.setTitle(translate("moderation.warn.cannot_warn_mods.title"), null)
|
||||||
|
.setColor(General.FAILURE_COLOR)
|
||||||
|
.setTimestamp(OffsetDateTime.now(Clock.systemUTC()))
|
||||||
|
.setDescription(translate("moderation.warn.cannot_warn_mods.desc"))
|
||||||
|
.addField(translate("moderation.warn.cannot_warn_mods.field.performer"),
|
||||||
|
performer.getAsMention(), true)
|
||||||
|
.addField(translate("moderation.warn.cannot_warn_mods.field.target"),
|
||||||
|
target.getAsMention(), true);
|
||||||
|
return channel.sendMessage(embed.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.setTimestamp(OffsetDateTime.now(Clock.systemUTC()))
|
||||||
|
.setDescription(translate("moderation.unwarn.cannot_remove_higher_mod.desc"))
|
||||||
|
.addField(translate("moderation.unwarn.cannot_remove_higher_mod.field.performer"),
|
||||||
|
performer.getUser().getAsMention(), true)
|
||||||
|
.addField(translate("moderation.unwarn.cannot_remove_higher_mod.field.original_performer"),
|
||||||
|
entry.getPerformer().getAsMention(), true)
|
||||||
|
.addField(translate("moderation.unwarn.cannot_remove_higher_mod.field.case_id"), String.valueOf(caseID), true);
|
||||||
|
return channel.sendMessage(embed.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ public class Translations {
|
||||||
loadTranslations();
|
loadTranslations();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadTranslations() {
|
public void loadTranslations() {
|
||||||
if (translationsFile == null) {
|
if (translationsFile == null) {
|
||||||
JANITOR.info(TRANSLATIONS, "No translation file given, using default english translations");
|
JANITOR.info(TRANSLATIONS, "No translation file given, using default english translations");
|
||||||
loadDefaultTranslations();
|
loadDefaultTranslations();
|
||||||
|
|
|
@ -72,5 +72,14 @@
|
||||||
"moderation.unwarn.cannot_unwarn_self.desc": "Performer cannot remove a warning from themselves.",
|
"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.performer": "Performer/Original Target",
|
||||||
"moderation.unwarn.cannot_unwarn_self.field.original_performer": "Original Performer",
|
"moderation.unwarn.cannot_unwarn_self.field.original_performer": "Original Performer",
|
||||||
"moderation.unwarn.cannot_unwarn_self.field.case_id": "Case ID"
|
"moderation.unwarn.cannot_unwarn_self.field.case_id": "Case ID",
|
||||||
|
"moderation.warn.cannot_warn_mods.title": "Cannot warn moderators.",
|
||||||
|
"moderation.warn.cannot_warn_mods.desc": "Moderators cannot issue warnings to other moderators.",
|
||||||
|
"moderation.warn.cannot_warn_mods.field.performer": "Performer",
|
||||||
|
"moderation.warn.cannot_warn_mods.field.target": "Target",
|
||||||
|
"moderation.warn.cannot_remove_higher_mod.title": "Cannot remove warning issued by higher-ranked moderator.",
|
||||||
|
"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"
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user