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

Compare commits

...

2 Commits

Author SHA1 Message Date
199b40b160
Majorly refactor project structure
* Split project into two subprojects, for core and moderation
 * Move to modular system using Java's ServiceLoader
 * Improve messages system to allow message definitions in modules
 * Adjust some messages for brevity
2020-11-23 11:13:09 +08:00
42adb8238c
Refactor, remove translations, add guild config system 2020-11-23 11:12:26 +08:00
139 changed files with 2674 additions and 1824 deletions

View File

@ -9,15 +9,6 @@ buildscript {
}
}
repositories {
mavenCentral()
jcenter()
maven {
name = "Minecraft Libraries"
url = "https://libraries.minecraft.net"
}
}
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'org.ajoberstar.grgit'
apply plugin: 'idea'
@ -25,8 +16,6 @@ apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'maven-publish'
group = 'sciwhiz12.janitor'
archivesBaseName = 'janitor_bot'
version = getVersion()
println("Version: ${version}")
@ -40,34 +29,117 @@ tasks.withType(JavaCompile) {
}
dependencies {
implementation group: 'net.dv8tion', name: 'JDA', version: jda_version
implementation group: 'com.electronwill.night-config', name: 'toml', version: nightconfig_version
implementation group: 'net.sf.jopt-simple', name: 'jopt-simple', version: jopt_version
implementation group: 'com.google.guava', name: 'guava', version: guava_version
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jackson_version
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jackson_version
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jackson_version
implementation group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: log4j_bridge_version
implementation group: 'ch.qos.logback', name: 'logback-classic', version: logback_version
implementation group: 'com.mojang', name: 'brigadier', version: brigadier_version
testImplementation group: 'junit', name: 'junit', version: junit_version
implementation project(':core')
implementation project(path: ':core', configuration: 'api')
runtimeOnly project(':moderation')
runtimeOnly project(path: ':moderation', configuration: 'api')
}
application {
mainClassName = 'sciwhiz12.janitor.BotStartup'
}
task sourcesJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
classifier "sources"
jar.finalizedBy 'shadowJar'
shadowJar {
exclude 'META-INF/NOTICE**'
exclude 'META-INF/LICENSE**'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/proguard/**'
exclude 'META-INF/maven/**'
classifier ''
includeEmptyDirs false
}
allprojects {
apply plugin: 'java'
group = 'sciwhiz12.janitor'
version = rootProject.version
repositories {
mavenCentral()
jcenter()
maven {
name = "Minecraft Libraries"
url = "https://libraries.minecraft.net"
}
}
}
subprojects {
sourceSets { api }
configurations {
apiImplementation.extendsFrom apiDependency
api {
canBeConsumed = true
canBeResolved = true
extendsFrom apiDependency
}
implementation.extendsFrom apiImplementation, api
'default' {
extendsFrom api, apiDependency
}
}
dependencies {
api sourceSets.api.output
testImplementation group: 'junit', name: 'junit', version: junit_version
}
jar {
from sourceSets.api.output
finalizedBy 'apiJar', 'sourcesJar'
}
task apiJar(type: Jar, dependsOn: apiClasses) {
from sourceSets.api.allJava
classifier "api"
}
task sourcesJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
from sourceSets.api.allSource
classifier "sources"
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
project(':core') {
apply plugin: 'com.github.johnrengelman.shadow'
archivesBaseName = 'janitor-core'
dependencies {
apiDependency group: 'net.dv8tion', name: 'JDA', version: jda_version
apiDependency group: 'com.electronwill.night-config', name: 'toml', version: nightconfig_version
apiDependency group: 'net.sf.jopt-simple', name: 'jopt-simple', version: jopt_version
apiDependency group: 'com.google.guava', name: 'guava', version: guava_version
apiDependency group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jackson_version
apiDependency group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jackson_version
apiDependency group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jackson_version
apiDependency group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: log4j_bridge_version
apiDependency group: 'ch.qos.logback', name: 'logback-classic', version: logback_version
apiDependency group: 'com.mojang', name: 'brigadier', version: brigadier_version
}
shadowJar.configurations = [project.configurations.api]
jar.finalizedBy 'shadowJar'
}
project(':moderation') {
archivesBaseName = 'janitor-moderation'
dependencies {
apiDependency project(path: ':core', configuration: 'api')
}
}
jar.finalizedBy('sourcesJar')
jar.finalizedBy('shadowJar')
def getVersion() {
try {
def raw_version = grgit.describe(longDescr: true, tags: true)
String raw_version = grgit.describe(longDescr: true, tags: true)
def versionSep = raw_version.split "-"
def startVer = versionSep[0].substring(1)
return startVer + "." + versionSep[1]

View File

@ -0,0 +1,33 @@
package sciwhiz12.janitor.api;
import net.dv8tion.jda.api.JDA;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.config.BotConfig;
import sciwhiz12.janitor.api.config.ConfigManager;
import sciwhiz12.janitor.api.messages.Messages;
import sciwhiz12.janitor.api.messages.emote.ReactionManager;
import sciwhiz12.janitor.api.messages.substitution.SubstitutionsMap;
import sciwhiz12.janitor.api.module.ModuleManager;
import sciwhiz12.janitor.api.storage.GuildStorageManager;
public interface JanitorBot {
BotConfig getBotConfig();
CommandRegistry getCommands();
GuildStorageManager getGuildStorage();
ConfigManager getConfigs();
SubstitutionsMap getSubstitutions();
ReactionManager getReactions();
Messages getMessages();
ModuleManager getModuleManager();
void shutdown();
JDA getDiscord();
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor;
package sciwhiz12.janitor.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -8,11 +8,10 @@ import org.slf4j.MarkerFactory;
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 MESSAGES = MarkerFactory.getMarker("MESSAGES");
public static final Marker STORAGE = MarkerFactory.getMarker("STORAGE");
public static final Marker MODULE = MarkerFactory.getMarker("MODULE");
public static final Logger JANITOR = LoggerFactory.getLogger("janitor");
public static final Logger CONSOLE = LoggerFactory.getLogger("janitor.console");
public static final Logger CONFIG = LoggerFactory.getLogger("janitor.config");
}

View File

@ -1,12 +1,10 @@
package sciwhiz12.janitor.commands;
package sciwhiz12.janitor.api.command;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.config.BotConfig;
import sciwhiz12.janitor.msg.Messages;
import sciwhiz12.janitor.api.JanitorBot;
public abstract class BaseCommand {
public abstract class BaseCommand implements Command {
private final CommandRegistry registry;
public BaseCommand(CommandRegistry registry) {
@ -21,13 +19,5 @@ public abstract class BaseCommand {
return registry.getBot();
}
protected Messages messages() {
return getBot().getMessages();
}
protected BotConfig config() {
return getBot().getConfig();
}
public abstract LiteralArgumentBuilder<MessageReceivedEvent> getNode();
}

View File

@ -0,0 +1,30 @@
package sciwhiz12.janitor.api.command;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.config.GuildConfig;
import sciwhiz12.janitor.api.messages.Messages;
public interface Command {
LiteralArgumentBuilder<MessageReceivedEvent> getNode();
JanitorBot getBot();
default Messages messages() {
return getBot().getMessages();
}
default GuildConfig config(MessageReceivedEvent event) {
return config(event.getGuild().getIdLong());
}
default GuildConfig config(Guild guild) {
return config(guild.getIdLong());
}
default GuildConfig config(long guildID) {
return getBot().getConfigs().get(guildID);
}
}

View File

@ -0,0 +1,15 @@
package sciwhiz12.janitor.api.command;
import com.mojang.brigadier.CommandDispatcher;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.function.Function;
public interface CommandRegistry {
CommandDispatcher<MessageReceivedEvent> getDispatcher();
void addCommand(Function<CommandRegistry, Command> command);
JanitorBot getBot();
}

View File

@ -1,11 +1,11 @@
package sciwhiz12.janitor.commands.arguments;
package sciwhiz12.janitor.api.command.arguments;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import sciwhiz12.janitor.utils.StringReaderUtil;
import sciwhiz12.janitor.api.utils.StringReaderUtil;
import java.util.Collection;
@ -17,18 +17,15 @@ public class CustomStringArgumentType implements ArgumentType<String> {
}
public static CustomStringArgumentType word() {
return new CustomStringArgumentType(
StringArgumentType.StringType.SINGLE_WORD);
return new CustomStringArgumentType(StringArgumentType.StringType.SINGLE_WORD);
}
public static CustomStringArgumentType string() {
return new CustomStringArgumentType(
StringArgumentType.StringType.QUOTABLE_PHRASE);
return new CustomStringArgumentType(StringArgumentType.StringType.QUOTABLE_PHRASE);
}
public static CustomStringArgumentType greedyString() {
return new CustomStringArgumentType(
StringArgumentType.StringType.GREEDY_PHRASE);
return new CustomStringArgumentType(StringArgumentType.StringType.GREEDY_PHRASE);
}
public static String getString(final CommandContext<?> context, final String name) {

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.arguments;
package sciwhiz12.janitor.api.command.arguments;
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.LiteralMessage;
@ -9,7 +9,7 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import sciwhiz12.janitor.utils.StringReaderUtil;
import sciwhiz12.janitor.api.utils.StringReaderUtil;
import java.util.Collection;
import java.util.Collections;
@ -71,6 +71,11 @@ public class GuildMemberArgument implements ArgumentType<GuildMemberArgument.IMe
throw UNKNOWN_MEMBER_IDENTIFIER.create();
}
@Override
public String toString() {
return "member()";
}
@Override
public Collection<String> getExamples() {
return ImmutableList.of("<@!607058472709652501>", "<@750291676764962816>");

View File

@ -0,0 +1,18 @@
package sciwhiz12.janitor.api.config;
import java.nio.file.Path;
import java.util.Optional;
import javax.annotation.Nullable;
public interface BotConfig {
@Nullable
Path getMessagesFolder();
Path getConfigsFolder();
String getToken();
String getCommandPrefix();
Optional<Long> getOwnerID();
}

View File

@ -0,0 +1,21 @@
package sciwhiz12.janitor.api.config;
import sciwhiz12.janitor.api.JanitorBot;
public interface ConfigManager {
GuildConfig get(long guildID);
void save();
void close();
void registerNode(ConfigNode<?> node);
default void registerNodes(ConfigNode<?>... nodes) {
for (ConfigNode<?> node : nodes) {
registerNode(node);
}
}
JanitorBot getBot();
}

View File

@ -0,0 +1,50 @@
package sciwhiz12.janitor.api.config;
import com.google.common.base.Joiner;
import java.util.Objects;
import java.util.function.Supplier;
public class ConfigNode<T> {
private static final Joiner NEWLINE = Joiner.on('\n');
private final String path;
private final String comment;
private final Supplier<T> defaultValue;
public ConfigNode(String path, Supplier<T> defaultValue, String... comment) {
Objects.requireNonNull(path, "Config node path must not be null");
Objects.requireNonNull(defaultValue, "Default value supplier must not be null");
Objects.requireNonNull(comment, "Config node comments must not be null");
this.path = path;
this.defaultValue = defaultValue;
this.comment = NEWLINE.join(comment);
}
public String path() {
return path;
}
public String comment() {
return comment;
}
public Supplier<T> defaultValue() {
return defaultValue;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConfigNode<?> that = (ConfigNode<?>) o;
return path.equals(that.path) &&
comment.equals(that.comment) &&
defaultValue.equals(that.defaultValue);
}
@Override
public int hashCode() {
return Objects.hash(path, comment, defaultValue);
}
}

View File

@ -0,0 +1,14 @@
package sciwhiz12.janitor.api.config;
public final class CoreConfigs {
public static final ConfigNode<Boolean> ENABLE = new ConfigNode<>(
"enable", () -> true,
"Whether the bot is enabled for this guild.",
"Can be used to temporarily disable the bot in emergency situations.");
public static final ConfigNode<String> COMMAND_PREFIX = new ConfigNode<>(
"commands.prefix", () -> "!",
"The prefix for all commands.");
private CoreConfigs() {}
}

View File

@ -0,0 +1,26 @@
package sciwhiz12.janitor.api.config;
import com.electronwill.nightconfig.core.CommentedConfig;
import net.dv8tion.jda.api.entities.GuildChannel;
public interface GuildConfig {
CommentedConfig getChannelOverrides();
CommentedConfig getChannelConfig(GuildChannel channel);
<T> T forGuild(ConfigNode<T> node);
<T> void forGuild(ConfigNode<T> node, T newValue);
<T> T forChannel(GuildChannel channel, ConfigNode<T> node);
<T> void forChannel(GuildChannel channel, ConfigNode<T> node, T newValue);
long getGuildID();
CommentedConfig getRawConfig();
void save();
void close();
}

View File

@ -1,23 +1,19 @@
package sciwhiz12.janitor.msg.json;
package sciwhiz12.janitor.api.messages;
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.Message;
import net.dv8tion.jda.api.entities.MessageChannel;
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 sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.messages.substitution.ModifiableSubstitutions;
import sciwhiz12.janitor.api.messages.substitution.ModifiableSubstitutor;
import sciwhiz12.janitor.api.messages.substitution.SubstitutionsMap;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.Nullable;
@JsonDeserialize(using = ListingMessageDeserializer.class)
public class ListingMessage {
@Nullable protected final String title;
@Nullable protected final String url;
@ -141,72 +137,6 @@ public class ListingMessage {
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 {
@ -251,4 +181,23 @@ public class ListingMessage {
return inline;
}
}
public interface Builder<T> extends ModifiableSubstitutions<Builder<T>> {
Builder<T> amountPerPage(int amountPerPage);
Builder<T> setEntryApplier(BiConsumer<T, ModifiableSubstitutor<?>> entryApplier);
Builder<T> apply(Consumer<Builder<T>> consumer);
Builder<T> with(final String argument, final Supplier<String> value);
void build(MessageChannel channel,
SubstitutionsMap globalSubstitutions,
Message triggerMessage,
List<T> entries);
default void build(MessageChannel channel, JanitorBot bot, Message triggerMessage, List<T> entries) {
build(channel, bot.getSubstitutions(), triggerMessage, entries);
}
}
}

View File

@ -0,0 +1,51 @@
package sciwhiz12.janitor.api.messages;
import net.dv8tion.jda.api.entities.MessageEmbed;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.Collections;
public interface Messages {
JanitorBot getBot();
void loadMessages();
RegularMessage.Builder<?> getRegularMessage(String messageKey);
<T> ListingMessage.Builder<T> getListingMessage(String messageKey);
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))
);
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

@ -1,48 +1,31 @@
package sciwhiz12.janitor.msg.json;
package sciwhiz12.janitor.api.messages;
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.MessageChannel;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.Role;
import sciwhiz12.janitor.msg.TranslationMap;
import sciwhiz12.janitor.msg.substitution.ISubstitutor;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.messages.substitution.ModifiableSubstitutions;
import sciwhiz12.janitor.api.messages.substitution.SubstitutionsMap;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import javax.annotation.Nullable;
@JsonDeserialize(using = RegularMessageDeserializer.class)
public class RegularMessage {
@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 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;
protected final List<MessageEmbed.Field> fields;
public RegularMessage(
@ -176,37 +159,14 @@ public class RegularMessage {
thumbnailUrl, fields);
}
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();
builder.setTitle(func.apply(title), func.apply(url));
builder.setColor(parseColor(substitutions.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 : fields) {
builder.addField(func.apply(field.getName()), func.apply(field.getValue()), field.isInline());
}
return builder;
}
public interface Builder<T extends Builder<?>> extends ModifiableSubstitutions<T> {
MessageEmbed build(SubstitutionsMap substitutions);
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;
}
MessageEmbed build(JanitorBot bot);
default MessageAction send(JanitorBot bot, MessageChannel channel) {
return channel.sendMessage(build(bot));
}
// noinspection UnstableApiUsage
final Integer res = Ints.tryParse(str, 10);
if (res != null) {
return res;
}
return Role.DEFAULT_COLOR_RAW;
}
}

View File

@ -0,0 +1,16 @@
package sciwhiz12.janitor.api.messages.emote;
import net.dv8tion.jda.api.entities.Message;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.Map;
public interface ReactionManager {
ReactionMessage newMessage(Message message);
void removeMessage(long messageID);
Map<Long, ReactionMessage> getRegisteredMessages();
JanitorBot getBot();
}

View File

@ -0,0 +1,36 @@
package sciwhiz12.janitor.api.messages.emote;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.MessageReaction.ReactionEmote;
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.Map;
import java.util.function.BiConsumer;
public interface ReactionMessage {
ReactionMessage add(ReactionEmote emote, ReactionListener listener);
ReactionMessage add(String emote, ReactionListener listener);
ReactionMessage add(Emote emote, ReactionListener listener);
ReactionMessage removeEmotes(boolean remove);
ReactionMessage owner(long ownerID);
void create();
long getOwnerID();
boolean isOwnerOnly();
Map<ReactionEmote, ReactionListener> getListeners();
JanitorBot getBot();
@FunctionalInterface
interface ReactionListener extends BiConsumer<ReactionMessage, MessageReactionAddEvent> {
void accept(ReactionMessage message, MessageReactionAddEvent event);
}
}

View File

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

View File

@ -0,0 +1,4 @@
package sciwhiz12.janitor.api.messages.substitution;
public interface ModifiableSubstitutor<T extends ModifiableSubstitutor<?>> extends ModifiableSubstitutions<T>, Substitutor {
}

View File

@ -0,0 +1,45 @@
package sciwhiz12.janitor.api.messages.substitution;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.Map;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
public interface SubstitutionsMap extends Substitutor {
Pattern ARGUMENT_REGEX = Pattern.compile("\\$\\{(.+?)}", CASE_INSENSITIVE);
Pattern NULL_ARGUMENT_REGEX = Pattern.compile("nullcheck;(.+?);(.+)", CASE_INSENSITIVE);
static String quote(@Nullable String input) {
return input != null ? Matcher.quoteReplacement(input) : "";
}
@Nullable
static String substitute(@Nullable String text, Map<String, Supplier<String>> arguments) {
if (text == null || text.isBlank()) return null;
final Matcher matcher = ARGUMENT_REGEX.matcher(text);
return matcher.replaceAll(matchResult -> {
final Matcher nullMatcher = NULL_ARGUMENT_REGEX.matcher(matchResult.group(1));
if (nullMatcher.matches()) {
final String grp1 = nullMatcher.group(1);
return quote(arguments.getOrDefault(
grp1,
() -> arguments.getOrDefault(nullMatcher.group(2), () -> nullMatcher.group(2)).get()
).get());
}
return quote(arguments.getOrDefault(matchResult.group(1), () -> matchResult.group(0)).get());
});
}
String with(String text, Map<String, Supplier<String>> substitutions);
ModifiableSubstitutor<?> with(Map<String, Supplier<String>> customSubstitutions);
Map<String, Supplier<String>> createDefaultedMap(Map<String, Supplier<String>> custom);
JanitorBot getBot();
}

View File

@ -0,0 +1,15 @@
package sciwhiz12.janitor.api.messages.substitution;
import java.util.function.UnaryOperator;
import javax.annotation.Nullable;
public interface Substitutor extends UnaryOperator<String> {
@Override
@Nullable
default String apply(@Nullable String input) {
return substitute(input);
}
@Nullable
String substitute(@Nullable String text);
}

View File

@ -0,0 +1,11 @@
package sciwhiz12.janitor.api.module;
import sciwhiz12.janitor.api.JanitorBot;
public interface Module {
void activate();
void shutdown();
JanitorBot getBot();
}

View File

@ -0,0 +1,40 @@
package sciwhiz12.janitor.api.module;
import com.google.common.base.Preconditions;
import java.util.Objects;
public class ModuleKey<M extends Module> {
private final String moduleID;
private final Class<M> type;
public ModuleKey(String storageID, Class<M> type) {
Preconditions.checkNotNull(storageID, "Module ID must not be null");
Preconditions.checkArgument(!storageID.isBlank(), "Module ID must not be empty or blank");
Preconditions.checkNotNull(type, "Class type must not be null");
this.moduleID = storageID;
this.type = type;
}
public String getModuleID() {
return moduleID;
}
public Class<M> getType() {
return type;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ModuleKey<?> that = (ModuleKey<?>) o;
return moduleID.equals(that.moduleID) &&
type.equals(that.type);
}
@Override
public int hashCode() {
return Objects.hash(moduleID, type);
}
}

View File

@ -0,0 +1,23 @@
package sciwhiz12.janitor.api.module;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.Set;
import javax.annotation.Nullable;
public interface ModuleManager {
void disableModule(String id);
void enableModule(String id);
Set<ModuleKey<?>> getAvailableModules();
Set<ModuleKey<?>> getActiveModules();
@Nullable
<M extends Module> M getModule(ModuleKey<M> moduleKey);
boolean isActivated();
JanitorBot getBot();
}

View File

@ -0,0 +1,13 @@
package sciwhiz12.janitor.api.module;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.Set;
import javax.annotation.Nullable;
public interface ModuleProvider {
Set<ModuleKey<?>> getAvailableModules();
@Nullable
<M extends Module> M createModule(ModuleKey<M> moduleID, JanitorBot bot);
}

View File

@ -1,6 +1,6 @@
package sciwhiz12.janitor.storage;
package sciwhiz12.janitor.api.storage;
public abstract class AbstractStorage implements IStorage {
public abstract class AbstractStorage implements Storage {
private boolean dirty;
public boolean isDirty() {

View File

@ -0,0 +1,18 @@
package sciwhiz12.janitor.api.storage;
import net.dv8tion.jda.api.entities.Guild;
import sciwhiz12.janitor.api.JanitorBot;
import java.util.function.Supplier;
public interface GuildStorageManager {
default <S extends Storage> S getOrCreate(Guild guild, StorageKey<S> key, Supplier<S> defaultSupplier) {
return getOrCreate(guild.getIdLong(), key, defaultSupplier);
}
<S extends Storage> S getOrCreate(long guildID, StorageKey<S> key, Supplier<S> defaultSupplier);
void save();
JanitorBot getBot();
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.storage;
package sciwhiz12.janitor.api.storage;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@ -1,11 +1,10 @@
package sciwhiz12.janitor.storage;
package sciwhiz12.janitor.api.storage;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
public interface IStorage {
public interface Storage {
boolean dirty();
void write(Writer output) throws IOException;

View File

@ -1,15 +1,15 @@
package sciwhiz12.janitor.storage;
package sciwhiz12.janitor.api.storage;
import com.google.common.base.Preconditions;
import java.util.Objects;
/**
* A storage key, used to retrieve an instance of an {@link IStorage} from a {@link GuildStorage}.
* A storage key, used to retrieve an instance of an {@link Storage} from a {@link GuildStorageManager}.
*
* @param <S> the type of the {@link IStorage}
* @param <S> the type of the {@link Storage}
*/
public class StorageKey<S extends IStorage> {
public class StorageKey<S extends Storage> {
private final String storageID;
private final Class<S> type;
@ -31,9 +31,9 @@ public class StorageKey<S extends IStorage> {
}
/**
* Returns the storage ID, used by {@link GuildStorage} to uniquely identify this storage's data.
* Returns the storage ID, used by {@link GuildStorageManager} to uniquely identify this storage's data.
*
* <p>This is currently used by {@code GuildStorage} as the folder name of the storage.
* <p>This is currently used by {@code GuildStorageManager} as the folder name of the storage.
*
* @return the storage ID
*/
@ -42,7 +42,7 @@ public class StorageKey<S extends IStorage> {
}
/**
* Returns the class of the {@link IStorage} subtype that this storage key represents, which
* Returns the class of the {@link Storage} subtype that this storage key represents, which
* is also used in the key's generics.
*
* @return the class of the generic type

View File

@ -1,11 +1,13 @@
package sciwhiz12.janitor.commands.util;
package sciwhiz12.janitor.api.utils;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
public class CommandHelper {
public final class CommandHelper {
private CommandHelper() {}
public static LiteralArgumentBuilder<MessageReceivedEvent> literal(String command) {
return LiteralArgumentBuilder.literal(command);
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.msg;
package sciwhiz12.janitor.api.utils;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.IMentionable;
@ -6,32 +6,31 @@ import net.dv8tion.jda.api.entities.ISnowflake;
import net.dv8tion.jda.api.entities.Member;
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 sciwhiz12.janitor.api.messages.substitution.ModifiableSubstitutions;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Objects;
import java.util.function.Consumer;
import static java.time.temporal.ChronoField.*;
public class MessageHelper {
public final class MessageHelper {
private MessageHelper() {}
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> snowflake(String head, ISnowflake snowflake) {
public static <T extends ModifiableSubstitutions<?>> 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 <T extends IHasCustomSubstitutions<?>> Consumer<T> mentionable(String head, IMentionable mentionable) {
public static <T extends ModifiableSubstitutions<?>> Consumer<T> mentionable(String head, IMentionable mentionable) {
return builder -> builder
.apply(snowflake(head, mentionable))
.with(head + ".mention", mentionable::getAsMention);
}
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> role(String head, Role role) {
public static <T extends ModifiableSubstitutions<?>> Consumer<T> role(String head, Role role) {
return builder -> builder
.apply(mentionable(head, role))
.with(head + ".color_hex", () -> Integer.toHexString(role.getColorRaw()))
@ -39,7 +38,7 @@ public class MessageHelper {
.with(head + ".permissions", role.getPermissions()::toString);
}
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> user(String head, User user) {
public static <T extends ModifiableSubstitutions<?>> Consumer<T> user(String head, User user) {
return builder -> builder
.apply(mentionable(head, user))
.with(head + ".name", user::getName)
@ -48,7 +47,7 @@ public class MessageHelper {
.with(head + ".flags", user.getFlags()::toString);
}
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> guild(String head, Guild guild) {
public static <T extends ModifiableSubstitutions<?>> Consumer<T> guild(String head, Guild guild) {
return builder -> builder
.apply(snowflake(head, guild))
.with(head + ".name", guild::getName)
@ -58,10 +57,10 @@ public class MessageHelper {
.with(head + ".boost.count", () -> String.valueOf(guild.getBoostCount()))
.with(head + ".locale", guild.getLocale()::toString)
.with(head + ".verification_level", guild.getVerificationLevel()::toString)
.with(head + ".icon_url", guild::getIconUrl);
.with(head + ".icon_url", () -> Objects.toString(guild.getIconUrl(), ""));
}
public static <T extends IHasCustomSubstitutions<?>> Consumer<T> member(String head, Member member) {
public static <T extends ModifiableSubstitutions<?>> Consumer<T> member(String head, Member member) {
return builder -> builder
.apply(user(head, member.getUser()))
.apply(guild(head + ".guild", member.getGuild()))
@ -71,24 +70,6 @@ public class MessageHelper {
.with(head + ".color", () -> String.valueOf(member.getColorRaw()));
}
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()))
.apply(user(head + ".target", entry.getWarned()))
.with(head + ".date_time", () -> entry.getDateTime().format(DATE_TIME_FORMAT))
.with(head + ".reason", entry::getReason);
}
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()))
.apply(user(head + ".target", entry.getTarget()))
.with(head + ".date_time", () -> entry.getDateTime().format(DATE_TIME_FORMAT))
.with(head + ".contents", entry::getContents);
}
public static final DateTimeFormatter DATE_TIME_FORMAT = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.parseLenient()

View File

@ -1,11 +1,13 @@
package sciwhiz12.janitor.utils;
package sciwhiz12.janitor.api.utils;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import static com.mojang.brigadier.StringReader.isQuotedStringStart;
public class StringReaderUtil {
public final class StringReaderUtil {
private StringReaderUtil() {}
public static boolean isAllowedInUnquotedString(final char c) {
return c >= '0' && c <= '9'
|| c >= 'A' && c <= 'Z'
@ -24,7 +26,7 @@ public class StringReaderUtil {
return reader.getString().substring(start, reader.getCursor());
}
public String readQuotedString(StringReader reader) throws CommandSyntaxException {
public static String readQuotedString(StringReader reader) throws CommandSyntaxException {
if (!reader.canRead()) {
return "";
}

View File

@ -1,18 +1,20 @@
package sciwhiz12.janitor;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.util.Scanner;
import static sciwhiz12.janitor.Logging.CONSOLE;
public class BotConsole {
private final JanitorBot bot;
public static final Logger CONSOLE = LoggerFactory.getLogger("janitor.console");
private final JanitorBotImpl bot;
private final Thread thread;
private volatile boolean running = true;
public BotConsole(JanitorBot bot, InputStream input) {
public BotConsole(JanitorBotImpl bot, InputStream input) {
this.bot = bot;
this.thread = new Thread(this.new ConsoleThread(input));
this.thread.setName("janitor_console");
@ -44,11 +46,6 @@ public class BotConsole {
case "reload": {
if (parts.length >= 2)
switch (parts[1]) {
case "translations": {
CONSOLE.info("Reloading translations");
bot.getTranslations().loadTranslations();
break outer;
}
case "messages": {
CONSOLE.info("Reloading messages");
bot.getMessages().loadMessages();

View File

@ -6,46 +6,52 @@ import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.PrivateChannel;
import net.dv8tion.jda.api.entities.User;
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.storage.GuildStorage;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.commands.CommandRegistryImpl;
import sciwhiz12.janitor.config.BotConfigImpl;
import sciwhiz12.janitor.config.ConfigManagerImpl;
import sciwhiz12.janitor.messages.MessagesImpl;
import sciwhiz12.janitor.messages.emote.ReactionManagerImpl;
import sciwhiz12.janitor.messages.substitution.SubstitutionsMapImpl;
import sciwhiz12.janitor.module.ModuleManagerImpl;
import sciwhiz12.janitor.storage.GuildStorageManagerImpl;
import sciwhiz12.janitor.utils.Util;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.Logging.STATUS;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.api.Logging.STATUS;
public class JanitorBot {
public class JanitorBotImpl implements JanitorBot {
private final JDA discord;
private final BotConfig config;
private final BotConfigImpl config;
private final BotConsole console;
private final GuildStorage storage;
private final GuildStorage.SavingThread storageSavingThread;
private final CommandRegistry cmdRegistry;
private final TranslationMap translations;
private final SubstitutionMap substitutions;
private final Messages messages;
private final ReactionManager reactions;
private final GuildStorageManagerImpl storage;
private final GuildStorageManagerImpl.SavingThread storageSavingThread;
private final ConfigManagerImpl configManager;
private final CommandRegistryImpl cmdRegistry;
private final SubstitutionsMapImpl substitutions;
private final MessagesImpl messages;
private final ReactionManagerImpl reactions;
private final ModuleManagerImpl modules;
public JanitorBot(JDA discord, BotConfig config) {
public JanitorBotImpl(JDA discord, BotConfigImpl config) {
this.config = config;
this.discord = discord;
this.console = new BotConsole(this, System.in);
this.storage = new GuildStorage(this, Path.of(config.STORAGE_PATH.get()));
this.cmdRegistry = new CommandRegistry(this, config.getCommandPrefix());
this.translations = new TranslationMap(this, config.getTranslationsFile());
this.substitutions = new SubstitutionMap(this);
this.messages = new Messages(this, config.getTranslationsFile());
this.reactions = new ReactionManager(this);
this.storage = new GuildStorageManagerImpl(this, Path.of(config.STORAGE_PATH.get()));
this.configManager = new ConfigManagerImpl(this, config.getConfigsFolder());
this.cmdRegistry = new CommandRegistryImpl(this);
this.substitutions = new SubstitutionsMapImpl(this);
this.messages = new MessagesImpl(this);
this.reactions = new ReactionManagerImpl(this);
this.modules = new ModuleManagerImpl(this);
modules.activateModules();
// TODO: find which of these can be loaded in parallel before the bot JDA is ready
discord.addEventListener(cmdRegistry, reactions);
discord.getPresence().setPresence(OnlineStatus.ONLINE, Activity.playing(" n' sweeping n' testing!"));
//noinspection ResultOfMethodCallIgnored
discord.getGuilds().forEach(Guild::loadMembers);
JANITOR.info("Ready!");
config.getOwnerID()
@ -59,40 +65,60 @@ public class JanitorBot {
error -> JANITOR.error(STATUS, "Error while sending ready message to owner", error)
)
);
storageSavingThread = new GuildStorage.SavingThread(storage);
storageSavingThread = new GuildStorageManagerImpl.SavingThread(storage);
storageSavingThread.start();
console.start();
}
@Override
public JDA getDiscord() {
return this.discord;
}
public BotConfig getConfig() {
@Override
public BotConfigImpl getBotConfig() {
return this.config;
}
public Messages getMessages() {
@Override
public MessagesImpl getMessages() {
return messages;
}
public GuildStorage getStorage() { return this.storage; }
@Override
public SubstitutionsMapImpl getSubstitutions() {
return substitutions;
}
public CommandRegistry getCommandRegistry() {
@Override
public GuildStorageManagerImpl getGuildStorage() {
return this.storage;
}
@Override
public ConfigManagerImpl getConfigs() {
return configManager;
}
@Override
public CommandRegistryImpl getCommands() {
return this.cmdRegistry;
}
public TranslationMap getTranslations() {
return this.translations;
}
public ReactionManager getReactionManager() {
@Override
public ReactionManagerImpl getReactions() {
return this.reactions;
}
@Override
public ModuleManagerImpl getModuleManager() {
return modules;
}
@Override
public void shutdown() {
JANITOR.info(STATUS, "Shutting down!");
getConfig().getOwnerID()
getBotConfig().getOwnerID()
.map(discord::retrieveUserById)
.map(owner ->
owner
@ -110,13 +136,13 @@ public class JanitorBot {
.error(STATUS, "Error while sending shutdown message to owner", err)
))
).ifPresent(CompletableFuture::join);
modules.shutdown();
discord.shutdown();
storageSavingThread.stopThread();
storage.save();
configManager.save();
configManager.close();
console.stop();
}
public SubstitutionMap getSubstitutions() {
return substitutions;
config.close();
}
}

View File

@ -8,78 +8,80 @@ import net.dv8tion.jda.api.events.GenericEvent;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.EventListener;
import org.jetbrains.annotations.NotNull;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.command.Command;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.config.CoreConfigs;
import sciwhiz12.janitor.commands.bot.ShutdownCommand;
import sciwhiz12.janitor.commands.misc.HelloCommand;
import sciwhiz12.janitor.commands.misc.OKCommand;
import sciwhiz12.janitor.commands.misc.PingCommand;
import sciwhiz12.janitor.commands.moderation.BanCommand;
import sciwhiz12.janitor.commands.moderation.KickCommand;
import sciwhiz12.janitor.commands.moderation.NoteCommand;
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;
import java.util.Map;
import java.util.function.Function;
import static sciwhiz12.janitor.Logging.COMMANDS;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.api.Logging.COMMANDS;
import static sciwhiz12.janitor.api.Logging.JANITOR;
public class CommandRegistry implements EventListener {
private final JanitorBot bot;
private final String prefix;
private final Map<String, BaseCommand> registry = new HashMap<>();
public class CommandRegistryImpl implements CommandRegistry, EventListener {
private final JanitorBotImpl bot;
private final Map<String, Command> registry = new HashMap<>();
private final CommandDispatcher<MessageReceivedEvent> dispatcher;
public CommandRegistry(JanitorBot bot, String prefix) {
public CommandRegistryImpl(JanitorBotImpl bot) {
this.bot = bot;
this.prefix = prefix;
this.dispatcher = new CommandDispatcher<>();
addCommand(new PingCommand(this, "ping", "Pong!"));
addCommand(new PingCommand(this, "pong", "Ping!"));
addCommand(new OKCommand(this));
addCommand(new HelloCommand(this));
addCommand(new KickCommand(this));
addCommand(new BanCommand(this));
addCommand(new UnbanCommand(this));
addCommand(new WarnCommand(this));
addCommand(new WarnListCommand(this));
addCommand(new UnwarnCommand(this));
addCommand(new ShutdownCommand(this));
addCommand(new NoteCommand(this));
addCommand(reg -> new PingCommand(reg, "ping", "Pong!"));
addCommand(reg -> new PingCommand(reg, "pong", "Ping!"));
addCommand(OKCommand::new);
addCommand(HelloCommand::new);
addCommand(ShutdownCommand::new);
}
@Override
public CommandDispatcher<MessageReceivedEvent> getDispatcher() {
return this.dispatcher;
}
public JanitorBot getBot() {
@Override
public JanitorBotImpl getBot() {
return this.bot;
}
public void addCommand(BaseCommand instance) {
dispatcher.register(instance.getNode());
@Override
public void addCommand(Function<CommandRegistry, Command> command) {
dispatcher.register(command.apply(this).getNode());
}
@Override
public void onEvent(@NotNull GenericEvent genericEvent) {
if (!(genericEvent instanceof MessageReceivedEvent)) return;
MessageReceivedEvent event = (MessageReceivedEvent) genericEvent;
if (event.getAuthor().isBot()) return;
final String prefix;
if (event.isFromGuild()) {
prefix = getBot().getConfigs().get(event.getGuild().getIdLong())
.forGuild(CoreConfigs.COMMAND_PREFIX);
} else {
prefix = getBot().getBotConfig().getCommandPrefix();
}
String msg = event.getMessage().getContentRaw();
if (!msg.startsWith(this.prefix)) return;
if (!msg.startsWith(prefix)) return;
JANITOR.debug(COMMANDS, "Received message starting with valid command prefix. Author: {}, full message: {}",
Util.toString(event.getAuthor()), msg);
try {
StringReader command = new StringReader(msg.substring(this.prefix.length()));
StringReader command = new StringReader(msg.substring(prefix.length()));
ParseResults<MessageReceivedEvent> parseResults = this.dispatcher.parse(command, event);
if (parseResults.getReader().canRead()) {
// Parsing did not succeed, i.e. command not found
// TODO: add separate code path when insufficient permissions / requires fails
JANITOR.error(COMMANDS, "Error while parsing command: {}", parseResults.getExceptions().values());
if (parseResults.getExceptions().isEmpty()) {
JANITOR.info(COMMANDS, "Command not found.");
} else {
JANITOR.error(COMMANDS, "Error while parsing command: {}", parseResults.getExceptions().values());
}
return;
}
JANITOR.debug(COMMANDS, "Executing command.");

View File

@ -3,12 +3,12 @@ package sciwhiz12.janitor.commands.bot;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.commands.BaseCommand;
import sciwhiz12.janitor.commands.CommandRegistry;
import sciwhiz12.janitor.api.command.BaseCommand;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.utils.Util;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
public class ShutdownCommand extends BaseCommand {
public ShutdownCommand(CommandRegistry registry) {
@ -18,7 +18,7 @@ public class ShutdownCommand extends BaseCommand {
@Override
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
return literal("shutdown")
.requires(ctx -> getBot().getConfig().getOwnerID().map(
.requires(ctx -> getBot().getBotConfig().getOwnerID().map(
id -> id == ctx.getAuthor().getIdLong()).orElse(false)
)
.executes(this::run);

View File

@ -5,17 +5,17 @@ import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.commands.BaseCommand;
import sciwhiz12.janitor.commands.CommandRegistry;
import sciwhiz12.janitor.commands.arguments.GuildMemberArgument;
import sciwhiz12.janitor.api.command.BaseCommand;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.command.arguments.GuildMemberArgument;
import sciwhiz12.janitor.utils.Util;
import java.util.List;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.commands.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.commands.util.CommandHelper.argument;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.api.utils.CommandHelper.argument;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
public class HelloCommand extends BaseCommand {
public HelloCommand(CommandRegistry registry) {
@ -38,7 +38,7 @@ public class HelloCommand extends BaseCommand {
success -> {
JANITOR.debug("Sent greeting message to {}, on cmd of {}", Util.toString(member.getUser()),
Util.toString(ctx.getSource().getAuthor()));
getBot().getReactionManager().newMessage(success)
getBot().getReactions().newMessage(success)
.add("\u274C", (msg, event) -> success.delete()
.flatMap(v -> event.getChannel()
.deleteMessageById(ctx.getSource().getMessageIdLong()))

View File

@ -3,12 +3,12 @@ package sciwhiz12.janitor.commands.misc;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.commands.BaseCommand;
import sciwhiz12.janitor.commands.CommandRegistry;
import sciwhiz12.janitor.api.command.BaseCommand;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.utils.Util;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
public class OKCommand extends BaseCommand {
public OKCommand(CommandRegistry registry) {

View File

@ -3,12 +3,12 @@ package sciwhiz12.janitor.commands.misc;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.commands.BaseCommand;
import sciwhiz12.janitor.commands.CommandRegistry;
import sciwhiz12.janitor.api.command.BaseCommand;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.utils.Util;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
public class PingCommand extends BaseCommand {
private final String command;

View File

@ -4,43 +4,38 @@ import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
import com.electronwill.nightconfig.core.file.FileWatcher;
import com.electronwill.nightconfig.toml.TomlFormat;
import com.google.common.base.Preconditions;
import sciwhiz12.janitor.api.config.BotConfig;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import javax.annotation.Nullable;
import static sciwhiz12.janitor.Logging.CONFIG;
import static sciwhiz12.janitor.Logging.JANITOR;
import static sciwhiz12.janitor.api.Logging.CONFIG;
import static sciwhiz12.janitor.api.Logging.JANITOR;
public class BotConfig {
public class BotConfigImpl implements BotConfig {
public static final Path DEFAULT_CONFIG_PATH = Path.of("config.toml");
private final CommentedConfigSpec.ConfigValue<String> CLIENT_TOKEN;
private final CommentedConfigSpec.LongValue OWNER_ID;
public final CommentedConfigSpec.ConfigValue<String> CONFIGS_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_MESSAGES_DIRECTORY;
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;
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;
private final CommentedFileConfig config;
public BotConfig(BotOptions options) {
public BotConfigImpl(BotOptions options) {
this.options = options;
final CommentedConfigSpec.Builder builder = new CommentedConfigSpec.Builder();
@ -55,6 +50,10 @@ public class BotConfig {
.defineInRange("owner_id", 0L, Long.MIN_VALUE, Long.MAX_VALUE);
builder.pop();
CONFIGS_PATH = builder
.comment("The folder where guild configs are kept.")
.define("configs_path", "configs");
builder.push("storage");
STORAGE_PATH = builder
.comment("The folder where per-guild storage is kept.")
@ -64,10 +63,6 @@ public class BotConfig {
.defineInRange("autosave_internal", 20, 1, Integer.MAX_VALUE);
builder.pop();
CUSTOM_TRANSLATION_FILE = builder
.comment("A file which contains custom translation keys to load for messages.",
"If blank, no file shall be loaded.")
.define("messages.custom_translations", "");
CUSTOM_MESSAGES_DIRECTORY = builder
.comment("A folder containing custom messages, with a 'messages.json' key file.",
"If blank, no folder shall be loaded and defaults will be used.")
@ -77,36 +72,6 @@ public class BotConfig {
.comment("The prefix for commands.")
.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.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();
spec = builder.build();
this.configPath = options.getConfigPath().orElse(DEFAULT_CONFIG_PATH);
@ -123,21 +88,16 @@ public class BotConfig {
} catch (IOException ex) {
JANITOR.error("Error while building config from file {}", configPath, ex);
}
Preconditions.checkArgument(!getToken().isEmpty(), "Supply a client token through config or command line");
Preconditions.checkArgument(options.getConfigsFolder().
or(() -> Optional.ofNullable(Path.of(CONFIGS_PATH.get()))).isPresent(), "No guilds config folder defined");
}
public CommentedFileConfig getRawConfig() {
return config;
}
@Nullable
public Path getTranslationsFile() {
return options.getTranslationsFile().
or(() -> CUSTOM_TRANSLATION_FILE.get().isBlank() ?
Optional.empty() :
Optional.of(Path.of(CUSTOM_TRANSLATION_FILE.get())))
.orElse(null);
}
@Override
@Nullable
public Path getMessagesFolder() {
return options.getMessagesFolder().
@ -147,16 +107,25 @@ public class BotConfig {
.orElse(null);
}
@Override
public Path getConfigsFolder() {
return options.getConfigsFolder().
orElseGet(() -> Path.of(CONFIGS_PATH.get()));
}
@Override
public String getToken() {
return options.getToken().orElse(CLIENT_TOKEN.get());
}
@Override
public String getCommandPrefix() {
return options.getCommandPrefix().orElseGet(COMMAND_PREFIX::get);
}
@Override
public Optional<Long> getOwnerID() {
final Long ret = options.getOwnerID().orElse(OWNER_ID.get());
final long ret = options.getOwnerID().orElse(OWNER_ID.get());
if (ret == 0) return Optional.empty();
return Optional.of(ret);
}

View File

@ -13,8 +13,8 @@ import static joptsimple.util.PathProperties.*;
public class BotOptions {
private final OptionSet options;
private final ArgumentAcceptingOptionSpec<Path> configPath;
private final ArgumentAcceptingOptionSpec<Path> translationsPath;
private final ArgumentAcceptingOptionSpec<Path> messagesFolder;
private final ArgumentAcceptingOptionSpec<Path> configsFolder;
private final ArgumentAcceptingOptionSpec<String> token;
private final ArgumentAcceptingOptionSpec<String> prefix;
private final ArgumentAcceptingOptionSpec<Long> owner;
@ -25,12 +25,12 @@ public class BotOptions {
.accepts("config", "The path to the config file; defaults to 'config.toml'")
.withRequiredArg()
.withValuesConvertedBy(new PathConverter(FILE_EXISTING, READABLE, WRITABLE));
this.translationsPath = parser
.accepts("translations", "The path to the translations file")
.withRequiredArg()
.withValuesConvertedBy(new PathConverter(FILE_EXISTING, READABLE));
this.messagesFolder = parser
.accepts("translations", "The path to the custom messages folder")
.accepts("messages", "The path to the custom messages folder")
.withRequiredArg()
.withValuesConvertedBy(new PathConverter(DIRECTORY_EXISTING, READABLE));
this.configsFolder = parser
.accepts("guildConfigs", "The path to the guild configs folder")
.withRequiredArg()
.withValuesConvertedBy(new PathConverter(DIRECTORY_EXISTING, READABLE));
this.token = parser
@ -50,14 +50,14 @@ public class BotOptions {
return configPath.valueOptional(options);
}
public Optional<Path> getTranslationsFile() {
return translationsPath.valueOptional(options);
}
public Optional<Path> getMessagesFolder() {
return messagesFolder.valueOptional(options);
}
public Optional<Path> getConfigsFolder() {
return configsFolder.valueOptional(options);
}
public Optional<String> getToken() {
return token.valueOptional(options);
}

View File

@ -32,7 +32,7 @@ import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.ObjectArrays;
import sciwhiz12.janitor.Logging;
import sciwhiz12.janitor.api.Logging;
import sciwhiz12.janitor.utils.Pair;
import java.util.ArrayList;
@ -60,7 +60,7 @@ import static com.google.common.base.Preconditions.checkState;
* Like {@link com.electronwill.nightconfig.core.ConfigSpec} except in builder format, and extended to accept comments.
*
* @author @MinecraftForge
* @author SciWhiz12 (modified to remove unneede parts)
* @author SciWhiz12 (modified to remove unneeded parts)
*/
//TODO: Remove extends and pipe everything through getSpec/getValues?
public class CommentedConfigSpec extends UnmodifiableConfigWrapper<UnmodifiableConfig> {

View File

@ -0,0 +1,72 @@
package sciwhiz12.janitor.config;
import com.electronwill.nightconfig.core.CommentedConfig;
import sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.config.ConfigManager;
import sciwhiz12.janitor.api.config.ConfigNode;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static sciwhiz12.janitor.api.config.CoreConfigs.COMMAND_PREFIX;
import static sciwhiz12.janitor.api.config.CoreConfigs.ENABLE;
public class ConfigManagerImpl implements ConfigManager {
private final JanitorBotImpl bot;
private final Path configPath;
private final Map<Long, GuildConfigImpl> configMap = new HashMap<>();
private final List<ConfigNode<?>> configNodes = new ArrayList<>();
public ConfigManagerImpl(JanitorBotImpl bot, Path configPath) {
this.bot = bot;
this.configPath = configPath;
registerNodes(ENABLE, COMMAND_PREFIX);
}
@Override
public GuildConfigImpl get(long guildID) {
final GuildConfigImpl config = configMap.computeIfAbsent(guildID, (id) -> new GuildConfigImpl(id, getFile(guildID)));
configNodes.forEach(config::forGuild); // Ensures the config is correct
return config;
}
@Override
public JanitorBotImpl getBot() {
return bot;
}
@Override
public void save() {
configMap.values().forEach(GuildConfigImpl::save);
}
@Override
public void registerNode(ConfigNode<?> node) {
configNodes.add(node);
}
@Override
public void close() {
configMap.values().forEach(GuildConfigImpl::close);
for (Iterator<GuildConfigImpl> iterator = configMap.values().iterator(); iterator.hasNext(); ) {
iterator.next().close();
iterator.remove();
}
}
private Path getFile(long guildID) {
final Path file = Path.of(Long.toHexString(guildID) + ".toml");
return configPath.resolve(file);
}
static void ensureComment(CommentedConfig config, String path, String expectedComment) {
if (!Objects.equals(config.getComment(path), expectedComment)) {
config.setComment(path, expectedComment);
}
}
}

View File

@ -0,0 +1,140 @@
package sciwhiz12.janitor.config;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
import com.electronwill.nightconfig.core.file.FileWatcher;
import com.electronwill.nightconfig.toml.TomlFormat;
import com.google.common.base.Joiner;
import net.dv8tion.jda.api.entities.GuildChannel;
import sciwhiz12.janitor.api.config.ConfigNode;
import sciwhiz12.janitor.api.config.GuildConfig;
import java.io.IOException;
import java.nio.file.Path;
import static sciwhiz12.janitor.api.Logging.CONFIG;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.config.ConfigManagerImpl.ensureComment;
public class GuildConfigImpl implements GuildConfig {
private static final Joiner NEWLINE = Joiner.on("\n");
private final long guild;
private final CommentedFileConfig config;
private boolean closed = false;
GuildConfigImpl(long guild, Path configPath) {
this.guild = guild;
this.config = CommentedFileConfig.builder(configPath, TomlFormat.instance())
.onFileNotFound(FileNotFoundAction.CREATE_EMPTY)
.preserveInsertionOrder()
.autosave()
.build();
try {
CONFIG.info("Building guild config for {} from {}", Long.toHexString(this.guild), configPath);
config.load();
FileWatcher.defaultInstance().addWatch(configPath, this::onFileChange);
// ConfigNode.nodes.forEach(this::forGuild);
save();
} catch (IOException ex) {
JANITOR.error("Error while building config from file {}", configPath, ex);
}
}
@Override
public CommentedConfig getChannelOverrides() {
final String channelOverridesID = "channel_overrides";
CommentedConfig channelConfigs = config.get(channelOverridesID);
if (channelConfigs == null) {
channelConfigs = config.createSubConfig();
config.set(channelOverridesID, channelConfigs);
config.setComment(channelOverridesID, "Channel overrides for certain configuration options");
}
return channelConfigs;
}
@Override
public CommentedConfig getChannelConfig(GuildChannel channel) {
final String id = channel.getId();
CommentedConfig overrides = getChannelOverrides();
CommentedConfig channelOverride = overrides.get(id);
if (channelOverride == null) {
channelOverride = overrides.createSubConfig();
overrides.set(id, channelOverride);
overrides.setComment(id, "Channel overrides for channel with name " + channel.getName());
}
return channelOverride;
}
@Override
public <T> T forGuild(ConfigNode<T> node) {
ensureComment(config, node.path(), node.comment());
T value = config.get(node.path());
if (value == null) {
value = node.defaultValue().get();
config.set(node.path(), value);
}
return value;
}
@Override
public <T> void forGuild(ConfigNode<T> node, T newValue) {
ensureComment(config, node.path(), node.comment());
config.set(node.path(), newValue);
}
@Override
public <T> T forChannel(GuildChannel channel, ConfigNode<T> node) {
CommentedConfig channelConfig = getChannelConfig(channel);
ensureComment(channelConfig, node.path(), node.comment());
T value = channelConfig.getRaw(node.path());
if (value == null) {
value = node.defaultValue().get();
channelConfig.set(node.path(), node.defaultValue().get());
}
return value;
}
@Override
public <T> void forChannel(GuildChannel channel, ConfigNode<T> node, T newValue) {
CommentedConfig channelConfig = getChannelConfig(channel);
ensureComment(channelConfig, node.path(), node.comment());
channelConfig.set(node.path(), newValue);
}
@Override
public long getGuildID() {
return guild;
}
@Override
public CommentedFileConfig getRawConfig() {
return config;
}
@Override
public void save() {
if (!closed) {
config.save();
}
}
@Override
public void close() {
if (!closed) {
closed = true;
config.close();
}
}
void onFileChange() {
if (closed) return;
try {
CONFIG.info("Reloading config due to file change {}", config.getNioPath());
config.load();
} catch (Exception ex) {
CONFIG.error("Error while reloading config from {}", config.getNioPath(), ex);
}
}
}

View File

@ -0,0 +1,238 @@
package sciwhiz12.janitor.messages;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import joptsimple.internal.Strings;
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 net.dv8tion.jda.api.entities.Role;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.messages.ListingMessage;
import sciwhiz12.janitor.api.messages.substitution.ModifiableSubstitutor;
import sciwhiz12.janitor.api.messages.substitution.SubstitutionsMap;
import sciwhiz12.janitor.api.messages.substitution.Substitutor;
import sciwhiz12.janitor.messages.substitution.CustomSubstitutions;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
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.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ListingMessageBuilder<T> implements ListingMessage.Builder<T> {
private final ListingMessage message;
private final Map<String, Supplier<String>> customSubstitutions;
private int amountPerPage = 6;
private BiConsumer<T, ModifiableSubstitutor<?>> 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, ModifiableSubstitutor<?>> entryApplier) {
this.entryApplier = entryApplier;
return this;
}
public ListingMessageBuilder<T> apply(Consumer<ListingMessage.Builder<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,
SubstitutionsMap globalSubstitutions,
Message triggerMessage,
List<T> entries) {
final ModifiableSubstitutor<?> customSubs = globalSubstitutions.with(customSubstitutions);
final ImmutableList<T> list = ImmutableList.copyOf(entries);
final PagedMessage pagedMessage = new PagedMessage(message, list, amountPerPage);
channel.sendMessage(pagedMessage.createMessage(customSubs, entryApplier))
.queue(listMsg -> globalSubstitutions.getBot().getReactions().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(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(customSubs, entryApplier))
)
.queue();
}
})
.create()
);
}
public void build(MessageChannel channel, JanitorBot bot, Message triggerMessage, List<T> entries) {
build(channel, 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(ModifiableSubstitutor<?> substitutions,
BiConsumer<T, ModifiableSubstitutor<?>> applier) {
if (currentPage != lastPage) {
cachedMessage = create(
message,
substitutions
.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
}
public static <T> EmbedBuilder create(
ListingMessage message,
Substitutor global,
Iterable<T> iterable,
BiConsumer<T, ModifiableSubstitutor<?>> entryApplier
) {
final EmbedBuilder builder = new EmbedBuilder();
builder.setTitle(global.substitute(message.getTitle()), global.substitute(message.getUrl()));
builder.setColor(parseColor(global.substitute(message.getColor())));
builder.setAuthor(global.substitute(message.getAuthorName()), global.substitute(message.getAuthorUrl()),
global.substitute(message.getAuthorIconUrl()));
builder.setDescription(global.substitute(message.getDescription()));
builder.setImage(global.substitute(message.getImageUrl()));
builder.setThumbnail(global.substitute(message.getThumbnailUrl()));
builder.setTimestamp(OffsetDateTime.now(ZoneOffset.UTC));
builder.setFooter(global.substitute(message.getFooterText()), global.substitute(message.getFooterIconUrl()));
for (MessageEmbed.Field field : message.getBeforeFields()) {
builder.addField(global.substitute(field.getName()), global.substitute(field.getValue()), field.isInline());
}
final ListingMessage.Entry entry = message.getEntry();
final CustomSubstitutions entrySubs = new CustomSubstitutions();
final Function<String, String> entryFunc = str -> str != null ? entrySubs.substitute(global.substitute(str)) : null;
int count = 0;
for (T listEntry : iterable) {
entryApplier.accept(listEntry, entrySubs);
if (entry instanceof ListingMessage.FieldEntry) {
ListingMessage.FieldEntry fieldEntry = (ListingMessage.FieldEntry) entry;
builder.addField(
entryFunc.apply(fieldEntry.getFieldName()),
entryFunc.apply(fieldEntry.getFieldValue()),
fieldEntry.isInline()
);
} else if (entry instanceof ListingMessage.DescriptionEntry) {
ListingMessage.DescriptionEntry descEntry = (ListingMessage.DescriptionEntry) entry;
builder.getDescriptionBuilder().append(entryFunc.apply(descEntry.getDescription()));
builder.getDescriptionBuilder().append(descEntry.getJoiner());
}
count++;
}
if (count < 1) {
builder.getDescriptionBuilder().append(global.substitute(message.getEmptyText()));
}
for (MessageEmbed.Field field : message.getAfterFields()) {
builder.addField(global.substitute(field.getName()), global.substitute(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;
}
}

View File

@ -0,0 +1,159 @@
package sciwhiz12.janitor.messages;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.messages.ListingMessage;
import sciwhiz12.janitor.api.messages.Messages;
import sciwhiz12.janitor.api.messages.RegularMessage;
import sciwhiz12.janitor.messages.json.ListingMessageDeserializer;
import sciwhiz12.janitor.messages.json.RegularMessageDeserializer;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.api.Logging.MESSAGES;
public class MessagesImpl implements Messages {
public static final String JSON_FILE_SUFFIX = ".json";
public static final String MESSAGE_LIST_FILENAME = "messages.json";
public static final String MESSAGES_FOLDER = "messages/";
public static final TypeReference<List<String>> LIST_TYPE = new TypeReference<>() {};
private final JanitorBotImpl bot;
private final Map<String, RegularMessage> defaultRegularMessages = new HashMap<>();
private final Map<String, ListingMessage> defaultListingMessages = new HashMap<>();
private final Map<String, RegularMessage> customRegularMessages = new HashMap<>();
private final Map<String, ListingMessage> customListingMessages = new HashMap<>();
private final ObjectMapper jsonMapper = new ObjectMapper();
public MessagesImpl(JanitorBotImpl bot) {
this.bot = bot;
SimpleModule messageModule = new SimpleModule();
messageModule.addDeserializer(ListingMessage.class, new ListingMessageDeserializer());
messageModule.addDeserializer(RegularMessage.class, new RegularMessageDeserializer());
jsonMapper.registerModule(messageModule);
loadMessages();
}
public JanitorBotImpl getBot() {
return bot;
}
public void loadMessages() {
ClassLoader ctxLoader = Thread.currentThread().getContextClassLoader();
JANITOR.info(MESSAGES, "Loading default messages");
defaultListingMessages.clear();
ctxLoader.resources(MESSAGES_FOLDER + MESSAGE_LIST_FILENAME)
.forEach(url -> {
JANITOR.info("Loading messages from {}", url.getPath());
try (Reader keyReader = new InputStreamReader(url.openStream())) {
int loadedCount = 0;
for (String messageKey : jsonMapper.readValue(keyReader, LIST_TYPE)) {
InputStream resourceStream = ctxLoader
.getResourceAsStream(MESSAGES_FOLDER + messageKey + JSON_FILE_SUFFIX);
if (resourceStream == null) {
JANITOR.warn("Defined message {} cannot be found", messageKey);
continue;
}
loadedCount += readMessage(messageKey,
() -> new InputStreamReader(resourceStream),
defaultRegularMessages,
defaultListingMessages);
}
JANITOR.debug(MESSAGES, "Loaded {} messages", loadedCount);
} catch (Exception e) {
JANITOR.error(MESSAGES, "Error while loading default messages from {}", url.getPath(), e);
}
});
Path messagesFolder = bot.getBotConfig().getMessagesFolder();
if (messagesFolder != null) {
JANITOR.info(MESSAGES, "Loading custom messages from folder {}", messagesFolder);
try (Reader keyReader = Files.newBufferedReader(messagesFolder.resolve(MESSAGE_LIST_FILENAME))) {
int loadedCount = 0;
for (String messageKey : jsonMapper.readValue(keyReader, LIST_TYPE)) {
final Path messagePath = messagesFolder.resolve(messageKey + JSON_FILE_SUFFIX);
if (Files.notExists(messagePath)) {
JANITOR.warn("Defined message {} cannot be found", messageKey);
continue;
}
readMessage(messageKey, () -> Files.newBufferedReader(messagePath), customRegularMessages,
customListingMessages);
loadedCount++;
}
JANITOR.debug(MESSAGES, "Loaded {} messages", loadedCount);
} catch (Exception e) {
JANITOR.error(MESSAGES, "Error while loading custom messages", e);
}
} else {
JANITOR.info(MESSAGES, "No custom messages folder specified");
}
}
@FunctionalInterface
interface ThrowableSupplier<T, E extends Throwable> {
T get() throws E;
}
private int readMessage(String messageKey, ThrowableSupplier<Reader, Exception> input,
Map<String, RegularMessage> regularMessages, Map<String, ListingMessage> listingMessages) {
try {
final JsonNode tree = jsonMapper.readTree(input.get());
final String type = tree.path("type").asText("regular");
switch (type) {
case "regular": {
regularMessages.put(messageKey, jsonMapper.convertValue(tree, RegularMessage.class));
break;
}
case "listing": {
listingMessages.put(messageKey, jsonMapper.convertValue(tree, ListingMessage.class));
break;
}
default: {
JANITOR.warn(MESSAGES, "Unknown message type {} for {}", tree.path("type").asText(), messageKey);
return 0;
}
}
return 1;
} catch (Exception e) {
JANITOR.error(MESSAGES, "Error while loading message {}", messageKey, e);
return 0;
}
}
public RegularMessageBuilder getRegularMessage(String messageKey) {
RegularMessage msg = customRegularMessages.get(messageKey);
if (msg == null) {
msg = defaultRegularMessages.get(messageKey);
}
if (msg == null) {
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 <T> ListingMessageBuilder<T> getListingMessage(String messageKey) {
ListingMessage msg = customListingMessages.get(messageKey);
if (msg == null) {
msg = defaultListingMessages.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);
}
}

View File

@ -0,0 +1,93 @@
package sciwhiz12.janitor.messages;
import com.google.common.primitives.Ints;
import joptsimple.internal.Strings;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.requests.restaction.MessageAction;
import sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.messages.RegularMessage;
import sciwhiz12.janitor.api.messages.substitution.SubstitutionsMap;
import sciwhiz12.janitor.api.messages.substitution.Substitutor;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class RegularMessageBuilder implements RegularMessage.Builder<RegularMessageBuilder> {
private final RegularMessage message;
private final Map<String, Supplier<String>> customSubstitutions;
public RegularMessageBuilder(RegularMessage message, Map<String, Supplier<String>> customSubstitutions) {
this.message = message;
this.customSubstitutions = customSubstitutions;
}
public RegularMessageBuilder(RegularMessage message) {
this(message, new HashMap<>());
}
public RegularMessageBuilder apply(Consumer<RegularMessageBuilder> consumer) {
consumer.accept(this);
return this;
}
public RegularMessageBuilder with(final String argument, final Supplier<String> value) {
customSubstitutions.put(argument, value);
return this;
}
@Override
public MessageEmbed build(SubstitutionsMap substitutions) {
return create(message, substitutions.with(customSubstitutions)).build();
}
@Override
public MessageEmbed build(JanitorBot bot) {
return build(bot.getSubstitutions());
}
public MessageAction send(JanitorBotImpl bot, MessageChannel channel) {
return channel.sendMessage(build(bot));
}
public static EmbedBuilder create(RegularMessage message, Substitutor subs) {
final EmbedBuilder builder = new EmbedBuilder();
builder.setTitle(subs.substitute(message.getTitle()), subs.substitute(message.getUrl()));
builder.setColor(parseColor(subs.substitute(message.getColor())));
builder.setAuthor(subs.substitute(message.getAuthorName()), subs.substitute(message.getAuthorUrl()), subs.substitute(
message.getAuthorIconUrl()));
builder.setDescription(subs.substitute(message.getDescription()));
builder.setImage(subs.substitute(message.getImageUrl()));
builder.setThumbnail(subs.substitute(message.getThumbnailUrl()));
builder.setTimestamp(OffsetDateTime.now(ZoneOffset.UTC));
builder.setFooter(subs.substitute(message.getFooterText()), subs.substitute(message.getFooterIconUrl()));
for (MessageEmbed.Field field : message.getFields()) {
builder.addField(subs.substitute(field.getName()), subs.substitute(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;
}
}

View File

@ -1,27 +1,29 @@
package sciwhiz12.janitor.msg.emote;
package sciwhiz12.janitor.messages.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 sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.messages.emote.ReactionManager;
import sciwhiz12.janitor.api.messages.emote.ReactionMessage;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nonnull;
public class ReactionManager extends ListenerAdapter {
private final JanitorBot bot;
public class ReactionManagerImpl extends ListenerAdapter implements ReactionManager {
private final JanitorBotImpl bot;
private final Map<Long, ReactionMessage> messageMap = new HashMap<>();
public ReactionManager(JanitorBot bot) {
public ReactionManagerImpl(JanitorBotImpl bot) {
this.bot = bot;
}
public ReactionMessage newMessage(Message message) {
public ReactionMessageImpl 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);
final ReactionMessageImpl msg = new ReactionMessageImpl(bot, message);
messageMap.put(message.getIdLong(), msg);
return msg;
}
@ -34,6 +36,11 @@ public class ReactionManager extends ListenerAdapter {
return messageMap;
}
@Override
public JanitorBotImpl getBot() {
return bot;
}
@Override
public void onMessageDelete(@Nonnull MessageDeleteEvent event) {
if (messageMap.containsKey(event.getMessageIdLong())) {

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.msg.emote;
package sciwhiz12.janitor.messages.emote;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.Message;
@ -7,54 +7,54 @@ 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 sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.messages.emote.ReactionMessage;
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;
public class ReactionMessageImpl extends ListenerAdapter implements ReactionMessage {
private final JanitorBotImpl bot;
private final Message message;
private final Map<ReactionEmote, IReactionListener> emotes = new LinkedHashMap<>();
private final Map<ReactionEmote, ReactionListener> emotes = new LinkedHashMap<>();
private boolean removeEmotes = true;
private long ownerID;
private boolean onlyOwner;
public ReactionMessage(JanitorBot bot, Message message, boolean onlyOwner, long ownerID) {
public ReactionMessageImpl(JanitorBotImpl 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) {
public ReactionMessageImpl(JanitorBotImpl bot, Message message) {
this(bot, message, false, 0);
}
public ReactionMessage add(ReactionEmote emote, IReactionListener listener) {
public ReactionMessageImpl add(ReactionEmote emote, ReactionListener listener) {
emotes.put(emote, listener);
return this;
}
public ReactionMessage add(String emote, IReactionListener listener) {
public ReactionMessageImpl add(String emote, ReactionListener listener) {
return add(ReactionEmote.fromUnicode(emote, bot.getDiscord()), listener);
}
public ReactionMessage add(Emote emote, IReactionListener listener) {
public ReactionMessageImpl add(Emote emote, ReactionListener listener) {
return add(ReactionEmote.fromCustom(emote), listener);
}
public ReactionMessage removeEmotes(boolean remove) {
public ReactionMessageImpl removeEmotes(boolean remove) {
this.removeEmotes = remove;
return this;
}
public ReactionMessage owner(long ownerID) {
public ReactionMessageImpl owner(long ownerID) {
this.ownerID = ownerID;
this.onlyOwner = true;
return this;
@ -89,7 +89,7 @@ public class ReactionMessage extends ListenerAdapter {
}
}
public JanitorBot getBot() {
public JanitorBotImpl getBot() {
return bot;
}
@ -101,12 +101,7 @@ public class ReactionMessage extends ListenerAdapter {
return onlyOwner;
}
public Map<ReactionEmote, IReactionListener> getListeners() {
public Map<ReactionEmote, ReactionListener> getListeners() {
return Collections.unmodifiableMap(emotes);
}
@FunctionalInterface
public interface IReactionListener extends BiConsumer<ReactionMessage, MessageReactionAddEvent> {
void accept(ReactionMessage message, MessageReactionAddEvent event);
}
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.msg.json;
package sciwhiz12.janitor.messages.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
@ -6,6 +6,7 @@ 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 sciwhiz12.janitor.api.messages.ListingMessage;
import java.io.IOException;
import java.util.ArrayList;

View File

@ -1,10 +1,11 @@
package sciwhiz12.janitor.msg.json;
package sciwhiz12.janitor.messages.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.entities.MessageEmbed;
import sciwhiz12.janitor.api.messages.RegularMessage;
import java.io.IOException;
import java.util.ArrayList;

View File

@ -1,14 +1,17 @@
package sciwhiz12.janitor.msg.substitution;
package sciwhiz12.janitor.messages.substitution;
import org.apache.commons.collections4.TransformerUtils;
import org.apache.commons.collections4.map.DefaultedMap;
import sciwhiz12.janitor.api.messages.substitution.ModifiableSubstitutor;
import sciwhiz12.janitor.api.messages.substitution.SubstitutionsMap;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.Nullable;
public class CustomSubstitutions implements ISubstitutor, IHasCustomSubstitutions<CustomSubstitutions> {
public class CustomSubstitutions implements ModifiableSubstitutor<CustomSubstitutions> {
private final Map<String, Supplier<String>> map;
public CustomSubstitutions(Map<String, Supplier<String>> map) {
@ -20,8 +23,9 @@ public class CustomSubstitutions implements ISubstitutor, IHasCustomSubstitution
}
@Override
public String substitute(String text) {
return SubstitutionMap.substitute(text, map);
@Nullable
public String substitute(@Nullable String text) {
return SubstitutionsMap.substitute(text, map);
}
public CustomSubstitutions apply(Consumer<CustomSubstitutions> consumer) {

View File

@ -1,8 +1,9 @@
package sciwhiz12.janitor.msg.substitution;
package sciwhiz12.janitor.messages.substitution;
import org.apache.commons.collections4.TransformerUtils;
import org.apache.commons.collections4.map.DefaultedMap;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.messages.substitution.SubstitutionsMap;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@ -11,34 +12,40 @@ import java.util.Map;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import static java.util.regex.Matcher.quoteReplacement;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static sciwhiz12.janitor.msg.MessageHelper.DATE_TIME_FORMAT;
import static sciwhiz12.janitor.api.utils.MessageHelper.DATE_TIME_FORMAT;
public class SubstitutionMap implements ISubstitutor {
public class SubstitutionsMapImpl implements SubstitutionsMap {
public static final Pattern ARGUMENT_REGEX = Pattern.compile("\\$\\{(.+?)}", CASE_INSENSITIVE);
public static final Pattern NULL_ARGUMENT_REGEX = Pattern.compile("nullcheck;(.+?);(.+)", CASE_INSENSITIVE);
public static String substitute(String text, Map<String, Supplier<String>> arguments) {
private static String quote(@Nullable String input) {
return input != null ? Matcher.quoteReplacement(input) : "";
}
@Nullable
public static String substitute(@Nullable String text, Map<String, Supplier<String>> arguments) {
if (text == null || text.isBlank()) return null;
final Matcher matcher = ARGUMENT_REGEX.matcher(text);
return matcher.replaceAll(matchResult -> {
final Matcher nullMatcher = NULL_ARGUMENT_REGEX.matcher(matchResult.group(1));
if (nullMatcher.matches()) {
final String grp1 = nullMatcher.group(1);
final String str = arguments.containsKey(grp1) ? arguments.get(grp1).get() : null;
return str != null ?
quoteReplacement(str) :
quoteReplacement(arguments.getOrDefault(nullMatcher.group(2), () -> nullMatcher.group(2)).get());
return quote(arguments.getOrDefault(
grp1,
() -> arguments.getOrDefault(nullMatcher.group(2), () -> nullMatcher.group(2)).get()
).get());
}
return quoteReplacement(arguments.getOrDefault(matchResult.group(1), () -> matchResult.group(0)).get());
return quote(arguments.getOrDefault(matchResult.group(1), () -> matchResult.group(0)).get());
});
}
private final JanitorBot bot;
private final JanitorBotImpl bot;
private final Map<String, Supplier<String>> defaultSubstitutions = new HashMap<>();
public SubstitutionMap(JanitorBot bot) {
public SubstitutionsMapImpl(JanitorBotImpl bot) {
this.bot = bot;
defaultSubstitutions.put("time.now", () -> OffsetDateTime.now(ZoneOffset.UTC).format(DATE_TIME_FORMAT));
defaultSubstitutions.put("moderation.color", () -> "0xF1BD25");
@ -47,16 +54,17 @@ public class SubstitutionMap implements ISubstitutor {
defaultSubstitutions.put("general.error.color", () -> "0xF73132");
}
public JanitorBot getBot() {
public JanitorBotImpl getBot() {
return bot;
}
public String substitute(String text) {
return SubstitutionMap.substitute(text, defaultSubstitutions);
@Nullable
public String substitute(@Nullable String text) {
return SubstitutionsMap.substitute(text, defaultSubstitutions);
}
public String with(String text, Map<String, Supplier<String>> substitutions) {
return SubstitutionMap.substitute(text, createDefaultedMap(substitutions));
return SubstitutionsMap.substitute(text, createDefaultedMap(substitutions));
}
public CustomSubstitutions with(Map<String, Supplier<String>> customSubstitutions) {
@ -66,5 +74,4 @@ public class SubstitutionMap implements ISubstitutor {
public Map<String, Supplier<String>> createDefaultedMap(Map<String, Supplier<String>> custom) {
return DefaultedMap.defaultedMap(custom, TransformerUtils.mapTransformer(defaultSubstitutions));
}
}

View File

@ -0,0 +1,158 @@
package sciwhiz12.janitor.module;
import com.google.common.collect.ImmutableSet;
import sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.module.Module;
import sciwhiz12.janitor.api.module.ModuleKey;
import sciwhiz12.janitor.api.module.ModuleManager;
import sciwhiz12.janitor.api.module.ModuleProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import javax.annotation.Nullable;
import static sciwhiz12.janitor.api.Logging.JANITOR;
import static sciwhiz12.janitor.api.Logging.MODULE;
public class ModuleManagerImpl implements ModuleManager {
private final JanitorBotImpl bot;
private final ServiceLoader<ModuleProvider> moduleProviders;
private boolean active = false;
private final Set<ModuleKey<?>> availableModules = new HashSet<>();
private final Set<String> disabledModules = new HashSet<>();
private final Map<String, InnerStorage<?>> activeModules = new HashMap<>();
public ModuleManagerImpl(JanitorBotImpl bot) {
this.bot = bot;
this.moduleProviders = ServiceLoader.load(ModuleProvider.class);
loadProviders();
}
public void loadProviders() {
final Set<String> knownModules = new HashSet<>();
moduleProviders.reload();
availableModules.clear();
for (ModuleProvider provider : moduleProviders) {
for (ModuleKey<?> moduleID : provider.getAvailableModules()) {
if (knownModules.contains(moduleID.getModuleID()))
throw new RuntimeException("Duplicate modules with module id " + moduleID);
availableModules.add(moduleID);
knownModules.add(moduleID.getModuleID());
}
}
}
public void activateModules() {
if (active)
throw new IllegalStateException("Modules are already activated");
active = true;
JANITOR.debug(MODULE, "Activating modules...");
for (ModuleProvider provider : moduleProviders) {
for (ModuleKey<?> moduleID : provider.getAvailableModules()) {
String providerName = provider.getClass().getName();
if (disabledModules.contains(moduleID.getModuleID())) {
JANITOR.debug(MODULE, "Module with ID {} from provider {} is disabled, skipping", moduleID, providerName);
} else if (!availableModules.contains(moduleID)) {
JANITOR
.debug(MODULE, "Module with ID {} from provider {} was not previously available at loading, skipping",
moduleID, providerName);
} else {
if (activateModule(provider, moduleID)) {
JANITOR.debug(MODULE, "Module with ID {} from provider {} is now activated", moduleID, providerName);
} else {
JANITOR.warn(MODULE,
"Module with ID {} was declared to be available by provider {}, but did not create a module; " +
"skipping",
moduleID, providerName);
}
}
}
}
JANITOR.info(MODULE, "Modules are now activated");
}
private <M extends Module> boolean activateModule(ModuleProvider provider, ModuleKey<M> moduleID) {
final M module = provider.createModule(moduleID, bot);
if (module == null) {
return false;
}
module.activate();
activeModules.put(moduleID.getModuleID(), new InnerStorage<>(moduleID, module));
return true;
}
public void shutdown() {
if (!active)
throw new IllegalStateException("Modules are not activated");
}
@Override
public void disableModule(String id) {
if (active)
throw new IllegalStateException("Cannot disable modules, as modules are already activated");
disabledModules.add(id);
}
@Override
public void enableModule(String id) {
if (active)
throw new IllegalStateException("Cannot reenable modules, as modules are already activated");
disabledModules.remove(id);
}
@Override
public Set<ModuleKey<?>> getAvailableModules() {
return Collections.unmodifiableSet(availableModules);
}
@Override
public Set<ModuleKey<?>> getActiveModules() {
return activeModules.values().stream()
.map(InnerStorage::getKey)
.collect(ImmutableSet.toImmutableSet());
}
@Nullable
@Override
public <M extends Module> M getModule(ModuleKey<M> moduleKey) {
if (activeModules.containsKey(moduleKey.getModuleID())) {
return moduleKey.getType().cast(activeModules.get(moduleKey.getModuleID()).module);
}
return null;
}
public boolean isActivated() {
return active;
}
@Override
public JanitorBotImpl getBot() {
return bot;
}
/**
* <strong>For internal use only.</strong>
*/
static class InnerStorage<M extends Module> {
private final ModuleKey<M> key;
private final M module;
InnerStorage(ModuleKey<M> key, M storage) {
this.key = key;
this.module = storage;
}
public ModuleKey<M> getKey() {
return key;
}
public M getModule() {
return module;
}
}
}

View File

@ -2,8 +2,11 @@ package sciwhiz12.janitor.storage;
import com.google.common.base.Preconditions;
import net.dv8tion.jda.api.entities.Guild;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.Logging;
import sciwhiz12.janitor.JanitorBotImpl;
import sciwhiz12.janitor.api.Logging;
import sciwhiz12.janitor.api.storage.GuildStorageManager;
import sciwhiz12.janitor.api.storage.Storage;
import sciwhiz12.janitor.api.storage.StorageKey;
import java.io.IOException;
import java.io.Reader;
@ -19,26 +22,24 @@ import static java.nio.file.StandardOpenOption.*;
/**
* A storage system for guild-specific data.
*/
public class GuildStorage {
private final JanitorBot bot;
public class GuildStorageManagerImpl implements GuildStorageManager {
private final JanitorBotImpl bot;
private final Path mainFolder;
private final Map<Long, Map<String, InnerStorage<?>>> guildStorages = new HashMap<>();
public GuildStorage(JanitorBot bot, Path mainFolder) {
public GuildStorageManagerImpl(JanitorBotImpl bot, Path mainFolder) {
Preconditions.checkArgument(Files.isDirectory(mainFolder) || Files.notExists(mainFolder));
this.bot = bot;
this.mainFolder = mainFolder;
}
public JanitorBot getBot() {
@Override
public JanitorBotImpl getBot() {
return bot;
}
public <S extends IStorage> S getOrCreate(Guild guild, StorageKey<S> key, Supplier<S> defaultSupplier) {
return getOrCreate(guild.getIdLong(), key, defaultSupplier);
}
public <S extends IStorage> S getOrCreate(long guildID, StorageKey<S> key, Supplier<S> defaultSupplier) {
@Override
public <S extends Storage> S getOrCreate(long guildID, StorageKey<S> key, Supplier<S> defaultSupplier) {
final Map<String, InnerStorage<?>> storageMappy = guildStorages.computeIfAbsent(guildID, id -> new HashMap<>());
return key.getType().cast(storageMappy.computeIfAbsent(key.getStorageID(),
k -> new InnerStorage<>(key, load(guildID, key.getStorageID(), defaultSupplier.get()))).getStorage());
@ -50,7 +51,7 @@ public class GuildStorage {
return mainFolder.resolve(guildFolder).resolve(file);
}
public <T extends IStorage> T load(long guildID, String key, T storage) {
public <T extends Storage> T load(long guildID, String key, T storage) {
final Path file = getFile(guildID, key);
if (Files.notExists(file)) return storage;
@ -63,6 +64,7 @@ public class GuildStorage {
return storage;
}
@Override
public void save() {
save(false);
}
@ -96,13 +98,13 @@ public class GuildStorage {
}
/**
* A thread that calls {@link GuildStorage#save(boolean)} between specified delays.
* A thread that calls {@link GuildStorageManagerImpl#save(boolean)} between specified delays.
*/
public static class SavingThread extends Thread {
private final GuildStorage storage;
private final GuildStorageManagerImpl storage;
private volatile boolean running = true;
public SavingThread(GuildStorage storage) {
public SavingThread(GuildStorageManagerImpl storage) {
this.storage = storage;
this.setName("GuildStorage-Saving-Thread");
this.setDaemon(true);
@ -118,7 +120,7 @@ public class GuildStorage {
while (running) {
storage.save(true);
try {
Thread.sleep(storage.getBot().getConfig().AUTOSAVE_INTERVAL.get() * 1000);
Thread.sleep(storage.getBot().getBotConfig().AUTOSAVE_INTERVAL.get() * 1000);
} catch (InterruptedException ignored) {}
}
}
@ -127,7 +129,7 @@ public class GuildStorage {
/**
* <strong>For internal use only.</strong>
*/
static class InnerStorage<S extends IStorage> {
static class InnerStorage<S extends Storage> {
private final StorageKey<S> key;
private final S storage;

View File

@ -21,7 +21,7 @@ public class Util {
public static String toString(@Nullable final User user) {
return user != null ?
String.format("{User,%s#%s}:%s", user.getName(), user.getDiscriminator(), getID(user)) :
String.format("{User,%s#%s}:%s", user.getName(), user.getDiscriminator(), user.getAsMention()) :
"unknown";
}
@ -51,10 +51,6 @@ public class Util {
return String.format("<%s%s>", prefix, entity.getId());
}
public static String nameFor(User user) {
return user.getName().concat("#").concat(user.getDiscriminator());
}
public static <Success, Err> BiConsumer<Success, Err> handle(final Consumer<Success> success,
final Consumer<Err> exceptionally) {
return (suc, ex) -> {

View File

@ -0,0 +1,5 @@
{
"color": "${general.error.color}",
"title": "Ambiguous member argument!",
"description": "The name you have specified is too ambiguous (leads to more than 1 member)!\nPlease narrow down the specified name until it can uniquely identify a member of this guild."
}

View File

@ -0,0 +1,4 @@
{
"color": "${general.error.color}",
"title": "Performer cannot act against self."
}

View File

@ -0,0 +1,4 @@
{
"color": "${general.error.color}",
"title": "Cannot perform this action against myself."
}

View File

@ -0,0 +1,11 @@
{
"color": "${general.error.color}",
"description": "Cannot perform action on the given member, likely due to me being lower in the role hierarchy.",
"fields": [
{
"name": "Target",
"value": "${target.mention}",
"inline": true
}
]
}

View File

@ -0,0 +1,4 @@
{
"color": "${general.error.color}",
"title": "Guild only command!"
}

View File

@ -0,0 +1,11 @@
{
"color": "${general.error.color}",
"description": "I do not have sufficient permissions to carry out this action.\nPlease contact your server administrators if you believe this is in error.",
"fields": [
{
"name": "Required permissions",
"value": "${required_permissions}",
"inline": true
}
]
}

View File

@ -0,0 +1,8 @@
[
"general/error/ambiguous_member",
"general/error/guild_only_command",
"general/error/insufficient_permissions",
"general/error/cannot_interact",
"general/error/cannot_action_self",
"general/error/cannot_action_performer"
]

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1,27 @@
package sciwhiz12.janitor.moderation;
import sciwhiz12.janitor.api.config.ConfigNode;
public class ModerationConfigs {
public static final ConfigNode<Boolean> ENABLE_WARNS = new ConfigNode<>(
"sciwhiz12.janitor.moderation.warns.enable", () -> true,
"Whether to enable the warnings system. If disabled, the related commands are force-disabled.");
public static final ConfigNode<Boolean> WARNS_RESPECT_MOD_ROLES = new ConfigNode<>(
"sciwhiz12.janitor.moderation.warns.respect_mod_roles", () -> false,
"Whether to prevent lower-ranked moderators (in the role hierarchy) from removing warnings " +
"issued by higher-ranked moderators.");
public static final ConfigNode<Boolean> ALLOW_WARN_OTHER_MODERATORS = new ConfigNode<>(
"sciwhiz12.janitor.moderation.warns.warn_other_moderators", () -> true,
"Whether to allow moderators to issue warnings against other moderators.");
public static final ConfigNode<Boolean> ALLOW_REMOVE_SELF_WARNINGS = new ConfigNode<>(
"sciwhiz12.janitor.moderation.warns.remove_self_warnings", () -> false,
"Whether to allow moderators to remove warnings from themselves.");
public static final ConfigNode<Boolean> ENABLE_NOTES = new ConfigNode<>(
"sciwhiz12.janitor.moderation.notes.enable", () -> true,
"Whether to enable the notes system. If disabled, the related commands are force-disabled.");
public static final ConfigNode<Integer> MAX_NOTES_PER_MOD = new ConfigNode<>(
"sciwhiz12.janitor.moderation.notes.max_amount", () -> Integer.MAX_VALUE,
"The max amount of notes for a user per moderator.");
}

View File

@ -0,0 +1,64 @@
package sciwhiz12.janitor.moderation;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction;
import sciwhiz12.janitor.api.messages.substitution.ModifiableSubstitutions;
import sciwhiz12.janitor.moderation.notes.NoteEntry;
import sciwhiz12.janitor.moderation.warns.WarningEntry;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import static sciwhiz12.janitor.api.utils.MessageHelper.DATE_TIME_FORMAT;
import static sciwhiz12.janitor.api.utils.MessageHelper.user;
public class ModerationHelper {
public static AuditableRestAction<Void> kickUser(Guild guild, Member performer, Member target, @Nullable String reason) {
StringBuilder auditReason = new StringBuilder();
auditReason.append("Kicked by ")
.append(performer.getUser().getAsTag())
.append(" on ")
.append(Instant.now().atOffset(ZoneOffset.UTC).format(DATE_TIME_FORMAT));
if (reason != null)
auditReason.append(" for reason: ").append(reason);
return guild.kick(target, auditReason.toString());
}
public static AuditableRestAction<Void> banUser(Guild guild, Member performer, Member target, int deleteDuration,
@Nullable String reason) {
StringBuilder auditReason = new StringBuilder();
auditReason.append("Banned by ")
.append(performer.getUser().getAsTag())
.append(" on ")
.append(Instant.now().atOffset(ZoneOffset.UTC).format(DATE_TIME_FORMAT));
if (reason != null)
auditReason.append(" for reason: ").append(reason);
return guild.ban(target, deleteDuration, auditReason.toString());
}
public static AuditableRestAction<Void> unbanUser(Guild guild, User target) {
return guild.unban(target);
}
public static <T extends ModifiableSubstitutions<?>> 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()))
.apply(user(head + ".target", entry.getWarned()))
.with(head + ".date_time", () -> entry.getDateTime().format(DATE_TIME_FORMAT))
.with(head + ".reason", entry::getReason);
}
public static <T extends ModifiableSubstitutions<?>> 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()))
.apply(user(head + ".target", entry.getTarget()))
.with(head + ".date_time", () -> entry.getDateTime().format(DATE_TIME_FORMAT))
.with(head + ".contents", entry::getContents);
}
}

View File

@ -0,0 +1,18 @@
package sciwhiz12.janitor.moderation;
import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sciwhiz12.janitor.api.module.Module;
import sciwhiz12.janitor.api.module.ModuleKey;
import sciwhiz12.janitor.moderation.notes.NoteStorage;
import sciwhiz12.janitor.moderation.warns.WarningStorage;
public interface ModerationModule extends Module {
Logger LOGGER = LoggerFactory.getLogger("janitor.moderation");
ModuleKey<ModerationModule> ID = new ModuleKey<>("moderation", ModerationModule.class);
NoteStorage getNotes(Guild guild);
WarningStorage getWarns(Guild guild);
}

View File

@ -0,0 +1,53 @@
package sciwhiz12.janitor.moderation.notes;
import net.dv8tion.jda.api.entities.User;
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());
}
}

View File

@ -0,0 +1,25 @@
package sciwhiz12.janitor.moderation.notes;
import net.dv8tion.jda.api.entities.User;
import org.checkerframework.checker.nullness.qual.Nullable;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.storage.Storage;
import sciwhiz12.janitor.api.storage.StorageKey;
import java.util.Map;
public interface NoteStorage extends Storage {
StorageKey<NoteStorage> KEY = new StorageKey<>("notes", NoteStorage.class);
int addNote(NoteEntry entry);
@Nullable NoteEntry getNote(int noteID);
void removeNote(int noteID);
int getAmountOfNotes(User target);
Map<Integer, NoteEntry> getNotes();
JanitorBot getBot();
}

View File

@ -0,0 +1,56 @@
package sciwhiz12.janitor.moderation.warns;
import net.dv8tion.jda.api.entities.User;
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 performer, User warned, 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());
}
}

View File

@ -0,0 +1,22 @@
package sciwhiz12.janitor.moderation.warns;
import org.checkerframework.checker.nullness.qual.Nullable;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.storage.Storage;
import sciwhiz12.janitor.api.storage.StorageKey;
import java.util.Map;
public interface WarningStorage extends Storage {
StorageKey<WarningStorage> KEY = new StorageKey<>("warnings", WarningStorage.class);
JanitorBot getBot();
int addWarning(WarningEntry entry);
@Nullable WarningEntry getWarning(int caseID);
void removeWarning(int caseID);
Map<Integer, WarningEntry> getWarnings();
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.moderation;
package sciwhiz12.janitor.moderation;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
@ -8,10 +8,8 @@ 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.commands.util.ModerationHelper;
import sciwhiz12.janitor.msg.MessageHelper;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.utils.MessageHelper;
import java.util.EnumSet;
import java.util.List;
@ -22,22 +20,16 @@ import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
import static com.mojang.brigadier.arguments.StringArgumentType.getString;
import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
import static sciwhiz12.janitor.commands.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.commands.arguments.GuildMemberArgument.member;
import static sciwhiz12.janitor.commands.util.CommandHelper.argument;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.member;
import static sciwhiz12.janitor.api.utils.CommandHelper.argument;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
public class BanCommand extends BaseCommand {
public class BanCommand extends ModBaseCommand {
public static final EnumSet<Permission> BAN_PERMISSION = EnumSet.of(Permission.BAN_MEMBERS);
/*
ban command
!ban <user> [reason]
!ban delete <number of days> <user> [reason]
*/
public BanCommand(CommandRegistry registry) {
super(registry);
public BanCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(module, registry);
}
@Override
@ -124,7 +116,7 @@ public class BanCommand extends BaseCommand {
.flatMap(v -> messages().getRegularMessage("moderation/ban/info")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.member("target", target))
.with("private_message", () -> res.isSuccess() ? "" : "")
.with("private_message", () -> res.isSuccess() ? "\u2705" : "\u274C")
.with("delete_duration", () -> String.valueOf(days))
.with("reason", () -> reason)
.send(getBot(), channel)

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.moderation;
package sciwhiz12.janitor.moderation;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
@ -8,11 +8,9 @@ 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.commands.util.CommandHelper;
import sciwhiz12.janitor.commands.util.ModerationHelper;
import sciwhiz12.janitor.msg.MessageHelper;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.utils.CommandHelper;
import sciwhiz12.janitor.api.utils.MessageHelper;
import java.util.EnumSet;
import java.util.List;
@ -21,14 +19,14 @@ import javax.annotation.Nullable;
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.api.command.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.member;
public class KickCommand extends BaseCommand {
public class KickCommand extends ModBaseCommand {
public static final EnumSet<Permission> KICK_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS);
public KickCommand(CommandRegistry registry) {
super(registry);
public KickCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(module, registry);
}
@Override
@ -110,7 +108,7 @@ public class KickCommand extends BaseCommand {
.flatMap(v -> messages().getRegularMessage("moderation/kick/info")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.member("target", target))
.with("private_message", () -> res.isSuccess() ? "" : "")
.with("private_message", () -> res.isSuccess() ? "\u2705" : "\u274C")
.with("reason", () -> reason)
.send(getBot(), channel)
)

View File

@ -0,0 +1,24 @@
package sciwhiz12.janitor.moderation;
import net.dv8tion.jda.api.entities.Guild;
import sciwhiz12.janitor.api.command.BaseCommand;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.moderation.notes.NoteStorage;
import sciwhiz12.janitor.moderation.warns.WarningStorage;
public abstract class ModBaseCommand extends BaseCommand {
protected ModerationModuleImpl module;
public ModBaseCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(registry);
this.module = module;
}
protected NoteStorage getNotes(Guild guild) {
return module.getNotes(guild);
}
protected WarningStorage getWarns(Guild guild) {
return module.getWarns(guild);
}
}

View File

@ -0,0 +1,84 @@
package sciwhiz12.janitor.moderation;
import com.google.common.collect.ImmutableSet;
import net.dv8tion.jda.api.entities.Guild;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.module.Module;
import sciwhiz12.janitor.api.module.ModuleKey;
import sciwhiz12.janitor.api.module.ModuleProvider;
import sciwhiz12.janitor.moderation.notes.NoteCommand;
import sciwhiz12.janitor.moderation.notes.NoteStorage;
import sciwhiz12.janitor.moderation.notes.NoteStorageImpl;
import sciwhiz12.janitor.moderation.warns.UnwarnCommand;
import sciwhiz12.janitor.moderation.warns.WarnCommand;
import sciwhiz12.janitor.moderation.warns.WarnListCommand;
import sciwhiz12.janitor.moderation.warns.WarningStorage;
import sciwhiz12.janitor.moderation.warns.WarningStorageImpl;
import java.util.Set;
import javax.annotation.Nullable;
import static sciwhiz12.janitor.moderation.ModerationConfigs.*;
public class ModerationModuleImpl implements ModerationModule {
private final JanitorBot bot;
ModerationModuleImpl(JanitorBot bot) {
this.bot = bot;
}
@Override
public void activate() {
bot.getCommands().addCommand(reg -> new KickCommand(this, reg));
bot.getCommands().addCommand(reg -> new BanCommand(this, reg));
bot.getCommands().addCommand(reg -> new UnbanCommand(this, reg));
bot.getCommands().addCommand(reg -> new WarnCommand(this, reg));
bot.getCommands().addCommand(reg -> new WarnListCommand(this, reg));
bot.getCommands().addCommand(reg -> new UnwarnCommand(this, reg));
bot.getCommands().addCommand(reg -> new NoteCommand(this, reg));
bot.getConfigs().registerNodes(
ENABLE_WARNS,
WARNS_RESPECT_MOD_ROLES,
ALLOW_WARN_OTHER_MODERATORS,
ALLOW_REMOVE_SELF_WARNINGS,
ENABLE_NOTES,
MAX_NOTES_PER_MOD
);
LOGGER.info("Moderation module is activated");
}
@Override
public void shutdown() {}
@Override
public JanitorBot getBot() {
return bot;
}
@Override
public NoteStorage getNotes(Guild guild) {
return bot.getGuildStorage().getOrCreate(guild, NoteStorage.KEY, () -> new NoteStorageImpl(bot));
}
@Override
public WarningStorage getWarns(Guild guild) {
return bot.getGuildStorage().getOrCreate(guild, WarningStorage.KEY, () -> new WarningStorageImpl(bot));
}
public static class Provider implements ModuleProvider {
@Override
public Set<ModuleKey<?>> getAvailableModules() {
return ImmutableSet.of(ID);
}
@Nullable
@Override
public <M extends Module> M createModule(ModuleKey<M> moduleID, JanitorBot bot) {
if (ID.equals(moduleID)) {
//noinspection unchecked
return (M) new ModerationModuleImpl(bot);
}
return null;
}
}
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.moderation;
package sciwhiz12.janitor.moderation;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@ -9,10 +9,8 @@ import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.MessageChannel;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import sciwhiz12.janitor.commands.BaseCommand;
import sciwhiz12.janitor.commands.CommandRegistry;
import sciwhiz12.janitor.commands.util.ModerationHelper;
import sciwhiz12.janitor.msg.MessageHelper;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.utils.MessageHelper;
import java.util.EnumSet;
import java.util.Locale;
@ -22,14 +20,14 @@ import java.util.stream.Collectors;
import static com.mojang.brigadier.arguments.LongArgumentType.getLong;
import static com.mojang.brigadier.arguments.LongArgumentType.longArg;
import static com.mojang.brigadier.arguments.StringArgumentType.getString;
import static sciwhiz12.janitor.commands.util.CommandHelper.argument;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.api.utils.CommandHelper.argument;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
public class UnbanCommand extends BaseCommand {
public class UnbanCommand extends ModBaseCommand {
public static final EnumSet<Permission> UNBAN_PERMISSION = EnumSet.of(Permission.BAN_MEMBERS);
public UnbanCommand(CommandRegistry registry) {
super(registry);
public UnbanCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(module, registry);
}
@Override

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.moderation;
package sciwhiz12.janitor.moderation.notes;
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@ -9,11 +9,11 @@ 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.notes.NoteEntry;
import sciwhiz12.janitor.moderation.notes.NoteStorage;
import sciwhiz12.janitor.msg.MessageHelper;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.utils.MessageHelper;
import sciwhiz12.janitor.moderation.ModBaseCommand;
import sciwhiz12.janitor.moderation.ModerationHelper;
import sciwhiz12.janitor.moderation.ModerationModuleImpl;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@ -29,24 +29,26 @@ 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;
import static sciwhiz12.janitor.msg.MessageHelper.*;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.member;
import static sciwhiz12.janitor.api.utils.CommandHelper.argument;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
import static sciwhiz12.janitor.api.utils.MessageHelper.user;
import static sciwhiz12.janitor.moderation.ModerationConfigs.ENABLE_NOTES;
import static sciwhiz12.janitor.moderation.ModerationConfigs.MAX_NOTES_PER_MOD;
import static sciwhiz12.janitor.moderation.notes.NoteCommand.ModeratorFilter.*;
public class NoteCommand extends BaseCommand {
public class NoteCommand extends ModBaseCommand {
public static EnumSet<Permission> NOTE_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS);
public NoteCommand(CommandRegistry registry) {
super(registry);
public NoteCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(module, registry);
}
@Override
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
return literal("note")
.requires(ctx -> config().NOTES_ENABLE.get())
.requires(event -> config(event).forGuild(ENABLE_NOTES))
.then(literal("add")
.then(argument("target", member())
.then(argument("contents", greedyString())
@ -125,8 +127,9 @@ public class NoteCommand extends BaseCommand {
.send(getBot(), channel).queue();
} else {
final NoteStorage storage = NoteStorage.get(getBot().getStorage(), guild);
final int maxAmount = config().NOTES_MAX_AMOUNT_PER_MOD.get();
final NoteStorage storage = getNotes(guild);
final int maxAmount = config(ctx.getSource()).forGuild(MAX_NOTES_PER_MOD);
if (storage.getAmountOfNotes(target.getUser()) >= maxAmount) {
messages().getRegularMessage("moderation/error/insufficient_permissions")
.apply(MessageHelper.member("performer", performer))
@ -140,7 +143,7 @@ public class NoteCommand extends BaseCommand {
messages().getRegularMessage("moderation/note/add")
.apply(MessageHelper.member("performer", performer))
.apply(noteEntry("note_entry", noteID, entry))
.apply(ModerationHelper.noteEntry("note_entry", noteID, entry))
.send(getBot(), channel).queue();
}
@ -201,16 +204,11 @@ public class NoteCommand extends BaseCommand {
} else {
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)
.apply(ModerationHelper.noteEntry("note_entry", entry.getKey(), entry.getValue()))
)
.build(channel, getBot(), ctx.getSource().getMessage(),
NoteStorage.get(getBot().getStorage(), guild)
getNotes(guild)
.getNotes()
.entrySet().stream()
.filter(predicate)
@ -240,7 +238,7 @@ public class NoteCommand extends BaseCommand {
.send(getBot(), channel).queue();
} else {
final NoteStorage storage = NoteStorage.get(getBot().getStorage(), guild);
final NoteStorage storage = getNotes(guild);
@Nullable
final NoteEntry entry = storage.getNote(noteID);
if (entry == null) {
@ -254,7 +252,7 @@ public class NoteCommand extends BaseCommand {
messages().getRegularMessage("moderation/note/remove")
.apply(MessageHelper.member("performer", performer))
.apply(noteEntry("note_entry", noteID, entry))
.apply(ModerationHelper.noteEntry("note_entry", noteID, entry))
.send(getBot(), channel).queue();
}
}

View File

@ -0,0 +1,33 @@
package sciwhiz12.janitor.moderation.notes;
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.entities.User;
import sciwhiz12.janitor.api.JanitorBot;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.function.Supplier;
public class NoteEntryDeserializer extends StdDeserializer<NoteEntry> {
private static final long serialVersionUID = 1L;
private final Supplier<JanitorBot> bot;
public NoteEntryDeserializer(Supplier<JanitorBot> bot) {
super(NoteEntry.class);
this.bot = bot;
}
@Override
public NoteEntry deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
final JsonNode obj = ctx.readTree(p);
User performer = bot.get().getDiscord().retrieveUserById(obj.get("performer").asLong()).complete();
User target = bot.get().getDiscord().retrieveUserById(obj.get("target").asLong()).complete();
OffsetDateTime dateTime = OffsetDateTime.parse(obj.get("dateTime").asText());
String contents = obj.get("contents").asText();
return new NoteEntry(performer, target, dateTime, contents);
}
}

View File

@ -0,0 +1,25 @@
package sciwhiz12.janitor.moderation.notes;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class NoteEntrySerializer extends StdSerializer<NoteEntry> {
private static final long serialVersionUID = 1L;
public NoteEntrySerializer() {
super(NoteEntry.class);
}
@Override
public void serialize(NoteEntry value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeNumberField("performer", value.getPerformer().getIdLong());
gen.writeNumberField("target", value.getTarget().getIdLong());
gen.writeStringField("dateTime", value.getDateTime().toString());
gen.writeStringField("contents", value.getContents());
gen.writeEndObject();
}
}

View File

@ -6,31 +6,23 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import org.checkerframework.checker.nullness.qual.Nullable;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.storage.GuildStorage;
import sciwhiz12.janitor.storage.JsonStorage;
import sciwhiz12.janitor.storage.StorageKey;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.storage.JsonStorage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class NoteStorage extends JsonStorage {
public class NoteStorageImpl extends JsonStorage implements NoteStorage {
private static final TypeReference<Map<Integer, NoteEntry>> NOTE_MAP_TYPE = new TypeReference<>() {};
public static final StorageKey<NoteStorage> KEY = new StorageKey<>("notes", NoteStorage.class);
public static NoteStorage get(GuildStorage storage, Guild guild) {
return storage.getOrCreate(guild, KEY, () -> new NoteStorage(storage.getBot()));
}
private final JanitorBot bot;
private int lastID = 1;
private final Map<Integer, NoteEntry> notes = new ObservedMap<>(new HashMap<>(), this::markDirty);
public NoteStorage(JanitorBot bot) {
public NoteStorageImpl(JanitorBot bot) {
this.bot = bot;
}
@ -68,8 +60,8 @@ public class NoteStorage extends JsonStorage {
super.initialize(mapper);
mapper.registerModule(
new SimpleModule()
.addSerializer(NoteEntry.class, new NoteEntry.Serializer())
.addDeserializer(NoteEntry.class, new NoteEntry.Deserializer(this::getBot))
.addSerializer(NoteEntry.class, new NoteEntrySerializer())
.addDeserializer(NoteEntry.class, new NoteEntryDeserializer(this::getBot))
);
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.moderation;
package sciwhiz12.janitor.moderation.warns;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@ -8,30 +8,31 @@ 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 sciwhiz12.janitor.msg.MessageHelper;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.utils.MessageHelper;
import sciwhiz12.janitor.moderation.ModBaseCommand;
import sciwhiz12.janitor.moderation.ModerationHelper;
import sciwhiz12.janitor.moderation.ModerationModuleImpl;
import java.util.EnumSet;
import java.util.Objects;
import javax.annotation.Nullable;
import static sciwhiz12.janitor.commands.util.CommandHelper.argument;
import static sciwhiz12.janitor.commands.util.CommandHelper.literal;
import static sciwhiz12.janitor.api.utils.CommandHelper.argument;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
import static sciwhiz12.janitor.moderation.ModerationConfigs.*;
public class UnwarnCommand extends BaseCommand {
public class UnwarnCommand extends ModBaseCommand {
public static final EnumSet<Permission> WARN_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS);
public UnwarnCommand(CommandRegistry registry) {
super(registry);
public UnwarnCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(module, registry);
}
@Override
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
return literal("unwarn")
.requires(ctx -> getBot().getConfig().WARNINGS_ENABLE.get())
.requires(ctx -> config(ctx).forGuild(ENABLE_WARNS))
.then(argument("caseId", IntegerArgumentType.integer(1))
.executes(this::run)
);
@ -62,7 +63,7 @@ public class UnwarnCommand extends BaseCommand {
.send(getBot(), channel).queue();
} else {
final WarningStorage storage = WarningStorage.get(getBot().getStorage(), guild);
final WarningStorage storage = getWarns(guild);
@Nullable
final WarningEntry entry = storage.getWarning(caseID);
Member temp;
@ -73,24 +74,24 @@ public class UnwarnCommand extends BaseCommand {
.send(getBot(), channel).queue();
} else if (entry.getWarned().getIdLong() == performer.getIdLong()
&& !config().WARNINGS_REMOVE_SELF_WARNINGS.get()) {
&& !config(guild).forGuild(ALLOW_REMOVE_SELF_WARNINGS)) {
messages().getRegularMessage("moderation/error/unwarn/cannot_unwarn_self")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.warningEntry("warning_entry", caseID, entry))
.apply(ModerationHelper.warningEntry("warning_entry", caseID, entry))
.send(getBot(), channel).queue();
} else if (config().WARNINGS_RESPECT_MOD_ROLES.get()
} else if (config(guild).forGuild(WARNS_RESPECT_MOD_ROLES)
&& (temp = guild.getMember(entry.getPerformer())) != null && !performer.canInteract(temp)) {
messages().getRegularMessage("moderation/error/unwarn/cannot_remove_higher_mod")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.warningEntry("warning_entry", caseID, entry))
.apply(ModerationHelper.warningEntry("warning_entry", caseID, entry))
.send(getBot(), channel).queue();
} else {
storage.removeWarning(caseID);
messages().getRegularMessage("moderation/unwarn/info")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.warningEntry("warning_entry", caseID, entry))
.apply(ModerationHelper.warningEntry("warning_entry", caseID, entry))
.send(getBot(), channel).queue();
}

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.moderation;
package sciwhiz12.janitor.moderation.warns;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
@ -8,11 +8,11 @@ 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 sciwhiz12.janitor.msg.MessageHelper;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.api.utils.MessageHelper;
import sciwhiz12.janitor.moderation.ModBaseCommand;
import sciwhiz12.janitor.moderation.ModerationHelper;
import sciwhiz12.janitor.moderation.ModerationModuleImpl;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@ -22,22 +22,24 @@ 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;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.member;
import static sciwhiz12.janitor.api.utils.CommandHelper.argument;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
import static sciwhiz12.janitor.moderation.ModerationConfigs.ALLOW_WARN_OTHER_MODERATORS;
import static sciwhiz12.janitor.moderation.ModerationConfigs.ENABLE_WARNS;
public class WarnCommand extends BaseCommand {
public class WarnCommand extends ModBaseCommand {
public static final EnumSet<Permission> WARN_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS);
public WarnCommand(CommandRegistry registry) {
super(registry);
public WarnCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(module, registry);
}
@Override
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
return literal("warn")
.requires(ctx -> getBot().getConfig().WARNINGS_ENABLE.get())
.requires(ctx -> config(ctx).forGuild(ENABLE_WARNS))
.then(argument("member", member())
.then(argument("reason", greedyString())
.executes(ctx -> this.run(ctx, getString(ctx, "reason")))
@ -84,27 +86,27 @@ public class WarnCommand extends BaseCommand {
.apply(MessageHelper.member("target", target))
.send(getBot(), channel).queue();
} else if (target.hasPermission(WARN_PERMISSION) && config().WARNINGS_PREVENT_WARNING_MODS.get()) {
} else if (target.hasPermission(WARN_PERMISSION) && config(guild).forGuild(ALLOW_WARN_OTHER_MODERATORS)) {
messages().getRegularMessage("moderation/error/warn/cannot_warn_mods")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.member("target", target))
.send(getBot(), channel).queue();
} else {
WarningEntry entry = new WarningEntry(target.getUser(), performer.getUser(), dateTime, reason);
int caseId = WarningStorage.get(getBot().getStorage(), guild).addWarning(entry);
WarningEntry entry = new WarningEntry(performer.getUser(), target.getUser(), dateTime, reason);
int caseId = getWarns(guild).addWarning(entry);
target.getUser().openPrivateChannel()
.flatMap(dm -> messages().getRegularMessage("moderation/warn/dm")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.warningEntry("warning_entry", caseId, entry))
.apply(ModerationHelper.warningEntry("warning_entry", caseId, entry))
.send(getBot(), dm)
)
.mapToResult()
.flatMap(res -> messages().getRegularMessage("moderation/warn/info")
.apply(MessageHelper.member("performer", performer))
.apply(MessageHelper.warningEntry("warning_entry", caseId, entry))
.with("private_message", () -> res.isSuccess() ? "" : "")
.apply(ModerationHelper.warningEntry("warning_entry", caseId, entry))
.with("private_message", () -> res.isSuccess() ? "\u2705" : "\u274C")
.send(getBot(), channel)
)
.queue();

View File

@ -1,4 +1,4 @@
package sciwhiz12.janitor.commands.moderation;
package sciwhiz12.janitor.moderation.warns;
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@ -9,11 +9,9 @@ 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 sciwhiz12.janitor.msg.MessageHelper;
import sciwhiz12.janitor.api.command.CommandRegistry;
import sciwhiz12.janitor.moderation.ModBaseCommand;
import sciwhiz12.janitor.moderation.ModerationModuleImpl;
import java.util.Comparator;
import java.util.EnumSet;
@ -22,24 +20,26 @@ 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;
import static sciwhiz12.janitor.msg.MessageHelper.DATE_TIME_FORMAT;
import static sciwhiz12.janitor.msg.MessageHelper.user;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.getMembers;
import static sciwhiz12.janitor.api.command.arguments.GuildMemberArgument.member;
import static sciwhiz12.janitor.api.utils.CommandHelper.argument;
import static sciwhiz12.janitor.api.utils.CommandHelper.literal;
import static sciwhiz12.janitor.api.utils.MessageHelper.member;
import static sciwhiz12.janitor.api.utils.MessageHelper.user;
import static sciwhiz12.janitor.moderation.ModerationConfigs.ENABLE_WARNS;
import static sciwhiz12.janitor.moderation.ModerationHelper.warningEntry;
public class WarnListCommand extends BaseCommand {
public class WarnListCommand extends ModBaseCommand {
public static final EnumSet<Permission> WARN_PERMISSION = EnumSet.of(Permission.KICK_MEMBERS);
public WarnListCommand(CommandRegistry registry) {
super(registry);
public WarnListCommand(ModerationModuleImpl module, CommandRegistry registry) {
super(module, registry);
}
@Override
public LiteralArgumentBuilder<MessageReceivedEvent> getNode() {
return literal("warnlist")
.requires(ctx -> getBot().getConfig().WARNINGS_ENABLE.get())
.requires(ctx -> config(ctx).forGuild(ENABLE_WARNS))
.then(literal("target")
.then(argument("target", member())
.then(literal("mod")
@ -62,7 +62,7 @@ public class WarnListCommand 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;
@ -77,7 +77,7 @@ public class WarnListCommand extends BaseCommand {
final Member target = members.get(0);
if (guild.getSelfMember().equals(target)) {
messages().getRegularMessage("general/error/cannot_interact")
.apply(MessageHelper.member("target", target))
.apply(member("target", target))
.send(getBot(), channel).queue();
return 1;
@ -93,23 +93,18 @@ public class WarnListCommand extends BaseCommand {
if (!performer.hasPermission(WARN_PERMISSION)) {
messages().getRegularMessage("moderation/error/insufficient_permissions")
.apply(MessageHelper.member("performer", performer))
.apply(member("performer", performer))
.with("required_permissions", WARN_PERMISSION::toString)
.send(getBot(), channel).queue();
} else {
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)
.apply(member("performer", performer))
.setEntryApplier((entry, subs) ->
subs.apply(warningEntry("warning_entry", entry.getKey(), entry.getValue()))
)
.build(channel, getBot(), ctx.getSource().getMessage(),
WarningStorage.get(getBot().getStorage(), guild)
getWarns(guild)
.getWarnings()
.entrySet().stream()
.filter(predicate)

View File

@ -0,0 +1,33 @@
package sciwhiz12.janitor.moderation.warns;
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.entities.User;
import sciwhiz12.janitor.api.JanitorBot;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.function.Supplier;
public class WarningEntryDeserializer extends StdDeserializer<WarningEntry> {
private static final long serialVersionUID = 1L;
private final Supplier<JanitorBot> bot;
public WarningEntryDeserializer(Supplier<JanitorBot> bot) {
super(WarningEntry.class);
this.bot = bot;
}
@Override
public WarningEntry deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
final JsonNode obj = ctx.readTree(p);
User performer = bot.get().getDiscord().retrieveUserById(obj.get("performer").asLong()).complete();
User warned = bot.get().getDiscord().retrieveUserById(obj.get("warned").asLong()).complete();
OffsetDateTime dateTime = OffsetDateTime.parse(obj.get("dateTime").asText());
String contents = obj.get("reason").asText();
return new WarningEntry(performer, warned, dateTime, contents);
}
}

View File

@ -0,0 +1,25 @@
package sciwhiz12.janitor.moderation.warns;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
public class WarningEntrySerializer extends StdSerializer<WarningEntry> {
private static final long serialVersionUID = 1L;
public WarningEntrySerializer() {
super(WarningEntry.class);
}
@Override
public void serialize(WarningEntry value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeNumberField("performer", value.getPerformer().getIdLong());
gen.writeNumberField("warned", value.getWarned().getIdLong());
gen.writeStringField("dateTime", value.getDateTime().toString());
gen.writeStringField("reason", value.getReason());
gen.writeEndObject();
}
}

View File

@ -6,29 +6,21 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import net.dv8tion.jda.api.entities.Guild;
import org.checkerframework.checker.nullness.qual.Nullable;
import sciwhiz12.janitor.JanitorBot;
import sciwhiz12.janitor.storage.GuildStorage;
import sciwhiz12.janitor.storage.JsonStorage;
import sciwhiz12.janitor.storage.StorageKey;
import sciwhiz12.janitor.api.JanitorBot;
import sciwhiz12.janitor.api.storage.JsonStorage;
import java.util.HashMap;
import java.util.Map;
public class WarningStorage extends JsonStorage {
public class WarningStorageImpl extends JsonStorage implements WarningStorage {
private static final TypeReference<Map<Integer, WarningEntry>> WARNING_MAP_TYPE = new TypeReference<>() {};
public static final StorageKey<WarningStorage> KEY = new StorageKey<>("warnings", WarningStorage.class);
public static WarningStorage get(GuildStorage storage, Guild guild) {
return storage.getOrCreate(guild, KEY, () -> new WarningStorage(storage.getBot()));
}
private final JanitorBot bot;
private int lastID = 1;
private final Map<Integer, WarningEntry> warnings = new ObservedMap<>(new HashMap<>(), this::markDirty);
public WarningStorage(JanitorBot bot) {
public WarningStorageImpl(JanitorBot bot) {
this.bot = bot;
}
@ -60,8 +52,8 @@ public class WarningStorage extends JsonStorage {
super.initialize(mapper);
mapper.registerModule(
new SimpleModule()
.addSerializer(WarningEntry.class, new WarningEntry.Serializer())
.addDeserializer(WarningEntry.class, new WarningEntry.Deserializer(this::getBot))
.addSerializer(WarningEntry.class, new WarningEntrySerializer())
.addDeserializer(WarningEntry.class, new WarningEntryDeserializer(this::getBot))
);
}

View File

@ -0,0 +1 @@
sciwhiz12.janitor.moderation.ModerationModuleImpl$Provider

View File

@ -1,10 +1,4 @@
[
"general/error/ambiguous_member",
"general/error/guild_only_command",
"general/error/insufficient_permissions",
"general/error/cannot_interact",
"general/error/cannot_action_self",
"general/error/cannot_action_performer",
"moderation/error/cannot_interact",
"moderation/error/insufficient_permissions",
"moderation/kick/info",

View File

@ -4,16 +4,16 @@
"name": "${performer.guild.name}",
"icon_url": "${performer.guild.icon_url}"
},
"title": "<moderation.ban.dm.title>",
"title": "You were banned from this server.",
"fields": [
{
"name": "<moderation.ban.dm.field.performer>",
"name": "Moderator",
"value": "${performer.mention}",
"inline": true
},
{
"name": "<moderation.ban.dm.field.reason.name>",
"value": "<moderation.ban.dm.field.reason.value>",
"name": "Reason",
"value": "${nullcheck;reason;_No reason specified._}",
"inline": true
}
]

View File

@ -1,28 +1,33 @@
{
"color": "${moderation.color}",
"author": {
"name": "<moderation.kick.info.author>",
"name": "Banned user from server.",
"icon_url": "${moderation.icon_url}"
},
"fields": [
{
"name": "<moderation.kick.info.field.performer>",
"name": "Performer",
"value": "${performer.mention}",
"inline": true
},
{
"name": "<moderation.kick.info.field.target>",
"name": "Target",
"value": "${target.mention}",
"inline": true
},
{
"name": "<moderation.kick.info.field.private_message>",
"name": "Sent DM",
"value": "${private_message}",
"inline": true
},
{
"name": "<moderation.kick.info.field.reason.name>",
"value": "<moderation.kick.info.field.reason.value>",
"name": "Message Deletion",
"value": "${delete_duration} day(s)",
"inline": true
},
{
"name": "Reason",
"value": "${nullcheck;reason;_No reason specified._}",
"inline": false
}
]

View File

@ -0,0 +1,11 @@
{
"color": "${general.error.color}",
"description": "The performer of this command cannot moderate the target user, likely due to being lower in the role hierarchy.",
"fields": [
{
"name": "Target",
"value": "${target.mention}",
"inline": true
}
]
}

View File

@ -0,0 +1,11 @@
{
"color": "${general.error.color}",
"description": "The performer of this command has insufficient permissions to use this command.",
"fields": [
{
"name": "Required permissions",
"value": "${required_permissions}",
"inline": true
}
]
}

View File

@ -0,0 +1,16 @@
{
"color": "${general.error.color}",
"description": "The performer has reached the maximum amount of notes for the target user.",
"fields": [
{
"name": "Target",
"value": "${target.mention}",
"inline": true
},
{
"name": "(Max.) Amount",
"value": "${notes_amount}",
"inline": true
}
]
}

View File

@ -0,0 +1,4 @@
{
"color": "${general.error.color}",
"description": "No note with that note ID was found."
}

View File

@ -0,0 +1,16 @@
{
"color": "${general.error.color}",
"title": "Cannot remove warning issued by higher-ranked moderator.",
"fields": [
{
"name": "Performer",
"value": "${performer.mention}",
"inline": true
},
{
"name": "Issuer",
"value": "${warning_entry.performer.mention}",
"inline": true
}
]
}

View File

@ -0,0 +1,16 @@
{
"color": "${general.error.color}",
"title": "Cannot remove warning from self.",
"fields": [
{
"name": "Performer/Target",
"value": "${performer.mention}",
"inline": true
},
{
"name": "Issuer",
"value": "${warning_entry.performer.mention}",
"inline": true
}
]
}

View File

@ -0,0 +1,4 @@
{
"color": "${general.error.color}",
"description": "No warning with that case ID was found."
}

Some files were not shown because too many files have changed in this diff Show More