Compare commits
90 Commits
@ -1,14 +1,93 @@
|
||||
group 'net.pingex'
|
||||
version '0.1'
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'maven'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: "com.zoltu.git-versioning"
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url "https://jitpack.io"
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath "gradle.plugin.com.zoltu.gradle.plugin:git-versioning:2.0.12"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Logging
|
||||
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.6.1'
|
||||
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.6.1'
|
||||
compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.21' // D4J Logger routing
|
||||
compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.6.1' // D4J Logger routing
|
||||
|
||||
// Configuration framework
|
||||
compile group: 'org.apache.commons', name: 'commons-configuration2', version: '2.0'
|
||||
compile group: 'commons-beanutils', name: 'commons-beanutils', version: '1.9.2'
|
||||
|
||||
// Discord Gateway API
|
||||
compile group: 'com.github.austinv11', name: 'Discord4j', version: '2.7.0'
|
||||
|
||||
// Util
|
||||
compile group: 'net.jodah', name: 'failsafe', version: '0.8.3'
|
||||
|
||||
// Permissions
|
||||
compile group: 'org.mapdb', name: 'mapdb', version: '3.0.1'
|
||||
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Implementation-Title': 'DiscordCommandableFramework',
|
||||
'Main-Class': 'net.pingex.dcf.DiscordCommandableFramework'
|
||||
}
|
||||
}
|
||||
|
||||
task runtimeJar(type: Jar) {
|
||||
classifier = 'runtime'
|
||||
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||
with jar
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar, dependsOn: 'classes') {
|
||||
classifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
|
||||
task javadocJar(type: Jar, dependsOn: 'javadoc') {
|
||||
classifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives jar
|
||||
archives runtimeJar
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication){
|
||||
from components.java
|
||||
|
||||
artifact jar
|
||||
artifact runtimeJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
# Bot display name
|
||||
general.bot_name = DCF Bot
|
||||
|
||||
# Bot owner ID
|
||||
general.bot_owner = 123456789012345678
|
||||
|
||||
# Plugins directory location
|
||||
general.plugins_dir = plugins
|
||||
|
||||
# Data storage location
|
||||
general.storage_dir = data
|
||||
|
||||
# Initial connection token
|
||||
discord.token = tokenGoesHere
|
||||
|
||||
# Command prefix
|
||||
commands.prefix = !
|
@ -1,2 +1,2 @@
|
||||
rootProject.name = 'dcm'
|
||||
rootProject.name = 'dcf'
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
package net.pingex.dcf;
|
||||
|
||||
import net.pingex.dcf.commands.CommandRegistry;
|
||||
import net.pingex.dcf.commands.InternalCommands;
|
||||
import net.pingex.dcf.commands.permissions.PermissionsCommands;
|
||||
import net.pingex.dcf.core.Configuration;
|
||||
import net.pingex.dcf.core.GatewayConnectionManager;
|
||||
import net.pingex.dcf.modularity.PluginLoader;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import sx.blah.discord.api.ClientBuilder;
|
||||
|
||||
/**
|
||||
* Main class for DCF
|
||||
*/
|
||||
public class DiscordCommandableFramework
|
||||
{
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static final Logger LOGGER = LogManager.getLogger(DiscordCommandableFramework.class);
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// Load configuration
|
||||
Configuration.load();
|
||||
Configuration.init();
|
||||
|
||||
LOGGER.info("Hello World");
|
||||
LOGGER.info(Configuration.BOT_NAME);
|
||||
LOGGER.info("Framework version {}", DiscordCommandableFramework.class.getPackage().getImplementationVersion() != null ? DiscordCommandableFramework.class.getPackage().getImplementationVersion() : "UNKNOWN");
|
||||
|
||||
// Load plugins
|
||||
PluginLoader.getInstance().discoverPluginsDirectory();
|
||||
PluginLoader.getInstance().bulkLoadPlugins();
|
||||
|
||||
// Set up initial connection
|
||||
ClientBuilder builder = new ClientBuilder().withToken(Configuration.CONNECTION_TOKEN);
|
||||
GatewayConnectionManager.registerConnection(builder);
|
||||
|
||||
// Register internal commands
|
||||
new InternalCommands().getCommands().forEach(CommandRegistry::registerCommand);
|
||||
new PermissionsCommands().getCommands().forEach(CommandRegistry::registerCommand);
|
||||
|
||||
// Run plugins
|
||||
PluginLoader.getInstance().bulkRunPlugins();
|
||||
}
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
package net.pingex.dcf.commands;
|
||||
|
||||
import net.pingex.dcf.commands.options.ICommandOption;
|
||||
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents a single command.
|
||||
*/
|
||||
public abstract class Command implements ICommandExecutor
|
||||
{
|
||||
/**
|
||||
* Main name of the command
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Command can also be called using the following list of aliases
|
||||
*/
|
||||
private List<String> aliases;
|
||||
|
||||
/**
|
||||
* Description of the command.
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* Is the command enabled ? Can it be invoked ?
|
||||
*/
|
||||
private boolean isEnabled;
|
||||
|
||||
/**
|
||||
* Command usage help
|
||||
*/
|
||||
private String usage;
|
||||
|
||||
/**
|
||||
* Contains all options for this command.
|
||||
*/
|
||||
private Set<ICommandOption> commandOptions;
|
||||
|
||||
/**
|
||||
* Basic constructor.
|
||||
* @param name Name of the command
|
||||
* @param aliases Aliases, if any
|
||||
* @param description Description of the command
|
||||
* @param isEnabled Is the command enabled ?
|
||||
* @param usage Command usage help
|
||||
* @param options Command options.
|
||||
*/
|
||||
public Command(String name, List<String> aliases, String description, boolean isEnabled, String usage, Set<ICommandOption> options)
|
||||
{
|
||||
this.name = name;
|
||||
this.aliases = aliases;
|
||||
this.description = description;
|
||||
this.isEnabled = isEnabled;
|
||||
this.usage = usage;
|
||||
this.commandOptions = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains Command default values
|
||||
*/
|
||||
public static class Defaults
|
||||
{
|
||||
public static final List<String> ALIASES = Collections.emptyList();
|
||||
public static final String DESCRIPTION = "No command description provided.";
|
||||
public static final boolean IS_ENABLED = true;
|
||||
public static final String USAGE = "No command usage provided.";
|
||||
public static final Set<ICommandOption> OPTIONS = Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for a command
|
||||
*/
|
||||
public static class Builder
|
||||
{
|
||||
/**
|
||||
* Main name of the command
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Command can also be called using the following list of aliases
|
||||
*/
|
||||
private List<String> aliases = Defaults.ALIASES;
|
||||
|
||||
/**
|
||||
* Description of the command.
|
||||
*/
|
||||
private String description = Defaults.DESCRIPTION;
|
||||
|
||||
/**
|
||||
* Is the command enabled ? Can it be invoked ?
|
||||
*/
|
||||
private boolean isEnabled = Defaults.IS_ENABLED;
|
||||
|
||||
/**
|
||||
* Command usage help
|
||||
*/
|
||||
private String usage = Defaults.USAGE;
|
||||
|
||||
/**
|
||||
* Command options
|
||||
*/
|
||||
private Set<ICommandOption> commandOptions = Defaults.OPTIONS;
|
||||
|
||||
public Builder(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Builder aliases(List<String> aliases)
|
||||
{
|
||||
this.aliases = aliases;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set aliases using varargs
|
||||
* @param aliases Aliases varargs
|
||||
*/
|
||||
public Builder aliases(String... aliases)
|
||||
{
|
||||
this.aliases = Arrays.asList(aliases);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder description(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder enabled(boolean enabled)
|
||||
{
|
||||
isEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder usage(String usage)
|
||||
{
|
||||
this.usage = usage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder options(Set<ICommandOption> options)
|
||||
{
|
||||
commandOptions = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new Command using a supplied executor.
|
||||
* @param toExecute The body of the command.
|
||||
* @return Built command.
|
||||
*/
|
||||
public Command build(ICommandExecutor toExecute)
|
||||
{
|
||||
return new Command(name, aliases, description, isEnabled, usage, commandOptions)
|
||||
{
|
||||
@Override
|
||||
public void execute(Context context) throws Throwable
|
||||
{
|
||||
toExecute.execute(context);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a builder for a new command.
|
||||
* @param name The name of the future command
|
||||
* @return A new manipulable builder to build the command.
|
||||
*/
|
||||
public static Builder builder(String name)
|
||||
{
|
||||
return new Builder(name);
|
||||
}
|
||||
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<String> getAliases()
|
||||
{
|
||||
return Collections.unmodifiableList(aliases);
|
||||
}
|
||||
|
||||
public String getDescription()
|
||||
{
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean isEnabled()
|
||||
{
|
||||
return isEnabled;
|
||||
}
|
||||
|
||||
public String getUsage()
|
||||
{
|
||||
return usage;
|
||||
}
|
||||
|
||||
public Set<ICommandOption> getOptions()
|
||||
{
|
||||
return Collections.unmodifiableSet(commandOptions);
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled)
|
||||
{
|
||||
isEnabled = enabled;
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
package net.pingex.dcf.commands;
|
||||
|
||||
import net.pingex.dcf.commands.audit.AuditManager;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.parser.BasicParser;
|
||||
import net.pingex.dcf.commands.parser.ICommandParser;
|
||||
import net.pingex.dcf.commands.parser.ParserException;
|
||||
import net.pingex.dcf.core.Configuration;
|
||||
import net.pingex.dcf.modularity.PluginWrapper;
|
||||
import net.pingex.dcf.util.DiscordInteractionsUtil;
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Landing class for the whole command package
|
||||
*/
|
||||
public class CommandHandler
|
||||
{
|
||||
/**
|
||||
* The one and only valid parser for now.
|
||||
*/
|
||||
private static final ICommandParser PARSER = new BasicParser();
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static final Logger LOGGER = LogManager.getLogger(CommandHandler.class);
|
||||
|
||||
/**
|
||||
* Executor Service for dispatching commands to threads
|
||||
* TODO: ExecutorService.shutdown() when using a stop command or whatever
|
||||
*/
|
||||
private static final ExecutorService threadPool = Executors.newCachedThreadPool(new BasicThreadFactory.Builder().namingPattern("CommandHandler-%d").build());
|
||||
|
||||
/**
|
||||
* Landing method for a MRE
|
||||
*/
|
||||
public static void handle(MessageReceivedEvent event)
|
||||
{
|
||||
String command;
|
||||
List<String> arguments;
|
||||
|
||||
// Parse
|
||||
try
|
||||
{
|
||||
command = PARSER.parseCommand(event.getMessage().getContent());
|
||||
arguments = PARSER.parseArguments(event.getMessage().getContent());
|
||||
}
|
||||
catch(ParserException e)
|
||||
{
|
||||
return; // Not a command
|
||||
}
|
||||
LOGGER.debug("Attempting to run command {} for user #{}.", command, event.getMessage().getAuthor().getID());
|
||||
|
||||
// Query command from the bank
|
||||
Optional<Command> targetCommand = CommandRegistry.getCommandOrAliasByName(command);
|
||||
if(!targetCommand.isPresent())
|
||||
{
|
||||
LOGGER.debug("No command with that name was found.");
|
||||
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Command not found. Use `"+ Configuration.COMMAND_PREFIX + "internal:list` to get a list of all available commands.");
|
||||
return;
|
||||
}
|
||||
|
||||
final Context invocationContext = new Context(targetCommand.get(), arguments, event.getMessage(), event.getClient());
|
||||
|
||||
// Audit
|
||||
AuditResult result = AuditManager.doAudit(invocationContext);
|
||||
if(result.getOpcode().equals(AuditResult.ResultCode.FAIL))
|
||||
{
|
||||
LOGGER.info("Denied command {} for user #{}. OPCode: {}, Reason: {}", targetCommand.get().getName(), event.getMessage().getAuthor().getID(), result.getOpcode(), result.getMessage());
|
||||
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), result.getMessage());
|
||||
return;
|
||||
}
|
||||
if(result.getOpcode().equals(AuditResult.ResultCode.WARN))
|
||||
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Warning: " + result.getMessage());
|
||||
|
||||
LOGGER.info("Accepted command {} for user #{}. OPCode: {}, Reason: {}", targetCommand.get().getName(), event.getMessage().getAuthor().getID(), result.getOpcode(), result.getMessage() != null ? result.getMessage() : "N/A");
|
||||
|
||||
// Run it
|
||||
threadPool.submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
targetCommand.get().execute(invocationContext);
|
||||
}
|
||||
catch(Throwable throwable)
|
||||
{
|
||||
LOGGER.error("Error while executing command " + command + ".", throwable);
|
||||
DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Error while executing the command.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a plugin's commands, if any
|
||||
* @param pluginWrapper Target plugin
|
||||
*/
|
||||
public static void registerPlugin(PluginWrapper pluginWrapper)
|
||||
{
|
||||
if(IWithCommands.class.isAssignableFrom(pluginWrapper.getInstance().getClass()))
|
||||
{
|
||||
LOGGER.debug("Registering commands for plugin {}.", pluginWrapper.getId());
|
||||
((IWithCommands) pluginWrapper.getInstance()).getCommands().forEach(CommandRegistry::registerCommand);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a plugin's commands, if any
|
||||
* @param pluginWrapper Target plugin
|
||||
*/
|
||||
public static void unregisterPlugin(PluginWrapper pluginWrapper)
|
||||
{
|
||||
if(IWithCommands.class.isAssignableFrom(pluginWrapper.getInstance().getClass()))
|
||||
{
|
||||
LOGGER.debug("Removing commands for plugin {}.", pluginWrapper.getId());
|
||||
((IWithCommands) pluginWrapper.getInstance()).getCommands().forEach(CommandRegistry::unregisterCommand);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package net.pingex.dcf.commands;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Command bank
|
||||
*/
|
||||
public class CommandRegistry
|
||||
{
|
||||
/**
|
||||
* Command main store
|
||||
*/
|
||||
private static Set<Command> commandSet = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static final Logger LOGGER = LogManager.getLogger(CommandRegistry.class);
|
||||
|
||||
/**
|
||||
* Register a command and its aliases to the bank
|
||||
* @param toRegister Command to register
|
||||
*/
|
||||
public static void registerCommand(Command toRegister)
|
||||
{
|
||||
LOGGER.debug("Attempting to register command {}.", toRegister.getName());
|
||||
|
||||
if(commandExists(toRegister.getName()))
|
||||
{
|
||||
LOGGER.warn("Fail to add command: {} is already registered as a command.", toRegister.getName());
|
||||
return;
|
||||
}
|
||||
if(aliasExists(toRegister.getName())) LOGGER.info("Overriding registered alias {} for a new command with the same name.", toRegister.getName());
|
||||
|
||||
commandSet.add(toRegister);
|
||||
LOGGER.info("Registered command {}.", toRegister.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a command from the bank.
|
||||
* A command cannot be removed using one of its alias.
|
||||
* @param commandName Target command
|
||||
*/
|
||||
public static void unregisterCommandByName(String commandName)
|
||||
{
|
||||
LOGGER.debug("Resolving command {}.", commandName);
|
||||
Optional<Command> target = getCommandByName(commandName);
|
||||
|
||||
if(target.isPresent()) unregisterCommand(target.get());
|
||||
else LOGGER.warn("Attempting to remove a command which is unknown to DCF.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a command from the bank.
|
||||
* @param toUnregister Target command
|
||||
*/
|
||||
public static void unregisterCommand(Command toUnregister)
|
||||
{
|
||||
LOGGER.debug("Attempting to remove command {} from the bank.", toUnregister.getName());
|
||||
if(!commandSet.contains(toUnregister))
|
||||
{
|
||||
LOGGER.warn("Attempting to remove a command which is unknown to DCF.");
|
||||
return;
|
||||
}
|
||||
|
||||
commandSet.remove(toUnregister);
|
||||
LOGGER.info("Removed command {}.", toUnregister.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state of the registry.
|
||||
* @return RO set of the bank.
|
||||
*/
|
||||
public static Set<Command> getRegistry()
|
||||
{
|
||||
return Collections.unmodifiableSet(commandSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the command is registered (excluding aliases).
|
||||
* @param commandName Target command
|
||||
* @return `true` if the command is registered, `false` otherwise
|
||||
*/
|
||||
public static boolean commandExists(String commandName)
|
||||
{
|
||||
return commandSet.stream().anyMatch(e -> e.getName().equalsIgnoreCase(commandName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the alias is registered.
|
||||
* @param aliasName Target alias
|
||||
* @return `true` if the alias is registered, `false` otherwise
|
||||
*/
|
||||
public static boolean aliasExists(String aliasName)
|
||||
{
|
||||
return commandSet.stream().anyMatch(e -> e.getAliases().stream().anyMatch(f -> f.equalsIgnoreCase(aliasName)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the command exists (including aliases).
|
||||
* Command have precedence over aliases.
|
||||
* @param commandName Target command
|
||||
* @return `true` if the command exists, `false` otherwise
|
||||
*/
|
||||
public static boolean exists(String commandName)
|
||||
{
|
||||
return commandExists(commandName) || aliasExists(commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a command or an alias designed by its name. Commands have precedence over aliases.
|
||||
* @param commandName Target command
|
||||
* @return {@code Optional.empty()} if no command was found, {@code Optional.of(Command)} otherwise
|
||||
*/
|
||||
public static Optional<Command> getCommandOrAliasByName(String commandName)
|
||||
{
|
||||
return commandExists(commandName) ? getCommandByName(commandName) : getAliasByName(commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a command by one of its aliases.
|
||||
* @param aliasName Target command with this alias.
|
||||
* @return {@code Optional.empty()} if no command was found, {@code Optional.of(Command)} otherwise
|
||||
*/
|
||||
public static Optional<Command> getAliasByName(String aliasName)
|
||||
{
|
||||
return commandSet.stream().filter(e -> e.getAliases().stream().anyMatch(f -> f.equalsIgnoreCase(aliasName))).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a command designed by its name, excluding aliases.
|
||||
* @param commandName Target command
|
||||
* @return {@code Optional.empty()} if no command was found, {@code Optional.of(Command)} otherwise
|
||||
*/
|
||||
public static Optional<Command> getCommandByName(String commandName)
|
||||
{
|
||||
return commandSet.stream().filter(e -> e.getName().equalsIgnoreCase(commandName)).findFirst();
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package net.pingex.dcf.commands;
|
||||
|
||||
import sx.blah.discord.api.IDiscordClient;
|
||||
import sx.blah.discord.handle.obj.IChannel;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IMessage;
|
||||
import sx.blah.discord.handle.obj.IUser;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A Context contains all informations related to a command invocation, like the Command being invoked, its provided arguments and the originating message.
|
||||
*/
|
||||
public class Context
|
||||
{
|
||||
/**
|
||||
* The command being executed
|
||||
*/
|
||||
private Command command;
|
||||
|
||||
/**
|
||||
* Arguments provided
|
||||
*/
|
||||
private List<String> arguments;
|
||||
|
||||
/**
|
||||
* User to audit against
|
||||
*/
|
||||
private IUser user;
|
||||
|
||||
/**
|
||||
* Channel to audit against
|
||||
*/
|
||||
private IChannel channel;
|
||||
|
||||
/**
|
||||
* Guild to audit against, null if not relevant.
|
||||
*/
|
||||
private IGuild guild;
|
||||
|
||||
/**
|
||||
* The originating reference message
|
||||
*/
|
||||
private IMessage originatingMessage;
|
||||
|
||||
/**
|
||||
* Discord client for context
|
||||
*/
|
||||
private IDiscordClient client;
|
||||
|
||||
/**
|
||||
* Construct a context using the provided message. user and channel will be derived from the message.
|
||||
* @param command Command being executed.
|
||||
* @param arguments Its arguments.
|
||||
* @param originatingMessage The messaged used to invoke the command.
|
||||
* @param client Discord client.
|
||||
*/
|
||||
public Context(Command command, List<String> arguments, IMessage originatingMessage, IDiscordClient client)
|
||||
{
|
||||
this.command = command;
|
||||
this.arguments = arguments;
|
||||
this.user = originatingMessage.getAuthor();
|
||||
this.channel = originatingMessage.getChannel();
|
||||
this.guild = originatingMessage.getChannel().isPrivate() ? null : originatingMessage.getChannel().getGuild();
|
||||
this.originatingMessage = originatingMessage;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a context without an explicit IMessage. Used to provide a mock context for auditing purposes.
|
||||
* @param command Command being executed.
|
||||
* @param arguments Its arguments.
|
||||
* @param user User invoking the command.
|
||||
* @param channel Channel used to invoke the command.
|
||||
* @param guild Originating guild.
|
||||
* @param client Discord client.
|
||||
*/
|
||||
public Context(Command command, List<String> arguments, IUser user, IChannel channel, IGuild guild, IDiscordClient client)
|
||||
{
|
||||
this.command = command;
|
||||
this.arguments = arguments;
|
||||
this.user = user;
|
||||
this.channel = channel;
|
||||
this.guild = guild;
|
||||
this.originatingMessage = null;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Command getCommand()
|
||||
{
|
||||
return command;
|
||||
}
|
||||
|
||||
public List<String> getArguments()
|
||||
{
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public IUser getUser()
|
||||
{
|
||||
return user;
|
||||
}
|
||||
|
||||
public IChannel getChannel()
|
||||
{
|
||||
return channel;
|
||||
}
|
||||
|
||||
public IGuild getGuild()
|
||||
{
|
||||
return guild;
|
||||
}
|
||||
|
||||
public IMessage getOriginatingMessage()
|
||||
{
|
||||
return originatingMessage;
|
||||
}
|
||||
|
||||
public IDiscordClient getClient()
|
||||
{
|
||||
return client;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package net.pingex.dcf.commands;
|
||||
|
||||
/**
|
||||
* The body of a command.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ICommandExecutor
|
||||
{
|
||||
void execute(Context context) throws Throwable;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.pingex.dcf.commands;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Indicates a plugin which can run commands (using commands object)
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface IWithCommands
|
||||
{
|
||||
/**
|
||||
* Give all commands
|
||||
* @return ALL THE COMMANDS \o/
|
||||
*/
|
||||
Set<Command> getCommands();
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
package net.pingex.dcf.commands;
|
||||
|
||||
import net.pingex.dcf.commands.options.ICommandOption;
|
||||
import net.pingex.dcf.commands.options.ScopeOption;
|
||||
import net.pingex.dcf.commands.permissions.DefaultPermissionOption;
|
||||
import net.pingex.dcf.core.Configuration;
|
||||
import net.pingex.dcf.util.ArgumentParser;
|
||||
import net.pingex.dcf.util.DiscordInteractionsUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import sx.blah.discord.handle.obj.IRole;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Internal commands of DCF.
|
||||
*/
|
||||
public class InternalCommands implements IWithCommands
|
||||
{
|
||||
@Override
|
||||
public Set<Command> getCommands()
|
||||
{
|
||||
return new HashSet<>(Arrays.asList(ListCommand.INSTANCE, UsageCommand.INSTANCE, dumpRoles));
|
||||
}
|
||||
|
||||
/**
|
||||
* This command list all available commands on DCF.
|
||||
*/
|
||||
private static class ListCommand extends Command
|
||||
{
|
||||
private static final String NAME = "internal:list";
|
||||
private static final List<String> ALIASES = Arrays.asList("list", "help");
|
||||
private static final String DESCRIPTION = "List all available commands.";
|
||||
private static final boolean IS_ENABLED = true;
|
||||
private static final String USAGE = "Page";
|
||||
private static final Set<ICommandOption> OPTIONS = Collections.singleton(new ScopeOption(ScopeOption.CommandScope.ANYWHERE));
|
||||
|
||||
/**
|
||||
* How many commands should be displayed on each page
|
||||
*/
|
||||
public static final int COMMANDS_PER_PAGE = 5;
|
||||
|
||||
static final ListCommand INSTANCE = new ListCommand();
|
||||
|
||||
private ListCommand()
|
||||
{
|
||||
super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE, OPTIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Context context)
|
||||
{
|
||||
// Parameters
|
||||
Set<Command> bank = CommandRegistry.getRegistry();
|
||||
|
||||
int amountPages = (int) Math.ceil(bank.size()/(double)COMMANDS_PER_PAGE);
|
||||
int requestedPage;
|
||||
int longestCommand = bank.stream().max((o1, o2) -> Integer.compare(o1.getName().length(), o2.getName().length())).get().getName().length();
|
||||
int longestDesc = bank.stream().max((o1, o2) -> Integer.compare(o1.getDescription().length(), o2.getDescription().length())).get().getDescription().length();
|
||||
|
||||
// Parsing
|
||||
try
|
||||
{
|
||||
List<Object> output = ArgumentParser.parseAll(Collections.singletonList(Integer.class), context.getArguments());
|
||||
requestedPage = output.get(0) != null ? (int) output.get(0) : 1;
|
||||
}
|
||||
catch(ArgumentParser.ParserException e)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// Checks
|
||||
if(requestedPage <= 0 || requestedPage > amountPages)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Requested page is invalid. Number of available pages: " + amountPages);
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder output = new StringBuilder
|
||||
("**List of commands available for " + Configuration.BOT_NAME + "** (page " + requestedPage + "/" + amountPages + ")\n");
|
||||
output.append("Use `").append(UsageCommand.NAME).append("`")
|
||||
.append(" to get details and usage about any command.\n");
|
||||
output.append("```");
|
||||
|
||||
int pos = 1;
|
||||
for(Command i : bank)
|
||||
{
|
||||
if(pos > (requestedPage-1)*COMMANDS_PER_PAGE && pos <= requestedPage*COMMANDS_PER_PAGE)
|
||||
{
|
||||
output.append("+ ").append(StringUtils.rightPad(i.getName(), longestCommand)); // Name
|
||||
output.append(" ").append(StringUtils.rightPad(i.getDescription(), longestDesc)); // Desc
|
||||
if(i.getAliases().size() > 0)
|
||||
output.append(" ").append("(aliases: ").append(StringUtils.join(i.getAliases(), ", ")).append(")"); // Aliases
|
||||
|
||||
output.append("\n");
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
output.append("```");
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), output.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the usage of a command.
|
||||
*/
|
||||
private static class UsageCommand extends Command
|
||||
{
|
||||
private static final String NAME = "internal:usage";
|
||||
private static final List<String> ALIASES = Collections.singletonList("usage");
|
||||
private static final String DESCRIPTION = "Gives the usage of a command.";
|
||||
private static final boolean IS_ENABLED = true;
|
||||
private static final String USAGE = "Command";
|
||||
private static final Set<ICommandOption> OPTIONS = Collections.singleton(new ScopeOption(ScopeOption.CommandScope.ANYWHERE));
|
||||
|
||||
static final UsageCommand INSTANCE = new UsageCommand();
|
||||
|
||||
private UsageCommand()
|
||||
{
|
||||
super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE, OPTIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Context context) throws Throwable
|
||||
{
|
||||
// Checks
|
||||
if(context.getArguments().size() != 1) // Arg check
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Invalid argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Command> uncheckedTarget = CommandRegistry.getCommandOrAliasByName(context.getArguments().get(0));
|
||||
if(!uncheckedTarget.isPresent()) // Command existence
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target command not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Print everything
|
||||
Command target = uncheckedTarget.get();
|
||||
StringBuilder output = new StringBuilder();
|
||||
output.append("**Details and usage for command `")
|
||||
.append(Configuration.COMMAND_PREFIX).append(target.getName())
|
||||
.append("`**\n")
|
||||
.append("```");
|
||||
|
||||
output.append("Description: ").append(target.getDescription()).append("\n");
|
||||
if(target.getAliases().size() > 0)
|
||||
output.append("Aliases: ").append(StringUtils.join(target.getAliases(), ", ")).append("\n");
|
||||
output.append("Usage: ").append(Configuration.COMMAND_PREFIX).append(target.getName()).append(" ").append(target.getUsage()).append("\n");
|
||||
output.append("Enabled: ").append(target.isEnabled() ? "Yes" : "No").append("\n");
|
||||
|
||||
output.append("```");
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), output.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static final Command dumpRoles = Command.builder("internal:dumpRoles")
|
||||
.aliases("dumpRoles")
|
||||
.description("Dump all Roles ID for the current Guild.")
|
||||
.options(new HashSet<>(Arrays.asList(
|
||||
new DefaultPermissionOption(DefaultPermissionOption.Value.ANY_OWNER),
|
||||
new ScopeOption(ScopeOption.CommandScope.GUILD_CHAT))))
|
||||
.build(InternalCommands::dumpRolesImpl);
|
||||
|
||||
private static void dumpRolesImpl(Context context)
|
||||
{
|
||||
if(context.getGuild().getRoles().size() == 0)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "This guild has no role defined.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("**Roles ID** for ").append(context.getGuild().getName()).append("\n")
|
||||
.append("```\n");
|
||||
|
||||
int longestName = context.getGuild().getRoles().stream().max(Comparator.comparingInt(i -> i.getName().length())).get().getName().length();
|
||||
for(IRole i : context.getGuild().getRoles())
|
||||
sb.append("* ").append(StringUtils.rightPad(i.getName(), longestName)).append(" - ").append(i.getID()).append("\n");
|
||||
|
||||
sb.append("```");
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), sb.toString());
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package net.pingex.dcf.commands.audit;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.basic.EnabledCheck;
|
||||
import net.pingex.dcf.commands.audit.basic.ScopeCheck;
|
||||
import net.pingex.dcf.commands.permissions.PermissionCheck;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The Audit Manager stores all components and executes an audit when told to, usually from CommandHandler.
|
||||
*/
|
||||
public class AuditManager
|
||||
{
|
||||
/**
|
||||
* This pool contains all main components to be tested when executing an audit.
|
||||
* This pool does not have to contain every sub-component.
|
||||
* This pool is automatically sorted, thus
|
||||
*/
|
||||
private static Set<IAuditComponentProvider> mainComponentsPool = new TreeSet<>(Comparator.comparingInt(IAuditComponentProvider::priority));
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AuditManager.class);
|
||||
|
||||
static
|
||||
{
|
||||
addAll(Arrays.asList(new EnabledCheck(), new ScopeCheck(), new PermissionCheck()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an audit against known components.
|
||||
* @param context The invocation with its context.
|
||||
* @return The result of this audit.
|
||||
*/
|
||||
public static AuditResult doAudit(Context context)
|
||||
{
|
||||
LOGGER.trace("Auditing {} invocation for user #{}.", context.getCommand().getName(), context.getOriginatingMessage().getAuthor().getID());
|
||||
if(mainComponentsPool.isEmpty()) return new AuditResult(AuditResult.ResultCode.NOOP, null);
|
||||
|
||||
AuditResult.Builder resultBuilder = AuditResult.builder();
|
||||
for(IAuditComponentProvider component : mainComponentsPool)
|
||||
{
|
||||
AuditResult result = component.doAudit(context);
|
||||
resultBuilder.appendAuditResult(component, result);
|
||||
LOGGER.trace("Auditing component {} [{}].", component.name(), result.getOpcode());
|
||||
|
||||
if(result.hasSubAudits())
|
||||
for(Map.Entry<IAuditComponentProvider, AuditResult> i : result.getSubAuditsResults())
|
||||
LOGGER.trace("=> Subaudit {} [{}]", i.getKey().name(), i.getValue().getOpcode());
|
||||
}
|
||||
resultBuilder.setOpcode(resultBuilder.getSubAuditsResults().stream().min(Comparator.comparing(entry -> entry.getValue().getOpcode())).get().getValue().getOpcode());
|
||||
resultBuilder.setMessage(resultBuilder.getSubAuditsResults().stream().min(Comparator.comparing(entry -> entry.getValue().getOpcode())).get().getValue().getMessage());
|
||||
|
||||
LOGGER.debug("Command {} audit for user #{} returned {}.", context.getCommand().getName(), context.getOriginatingMessage().getAuthor().getID(), resultBuilder.getOpcode());
|
||||
return resultBuilder.build();
|
||||
}
|
||||
|
||||
// DELEGATED METHODS
|
||||
// =================
|
||||
|
||||
public static int size()
|
||||
{
|
||||
return mainComponentsPool.size();
|
||||
}
|
||||
|
||||
public static boolean contains(IAuditComponentProvider o)
|
||||
{
|
||||
return mainComponentsPool.contains(o);
|
||||
}
|
||||
|
||||
public static boolean add(IAuditComponentProvider iAuditComponentProvider)
|
||||
{
|
||||
LOGGER.debug("Adding audit component {}", iAuditComponentProvider.name());
|
||||
return mainComponentsPool.add(iAuditComponentProvider);
|
||||
}
|
||||
|
||||
public static boolean remove(IAuditComponentProvider o)
|
||||
{
|
||||
LOGGER.debug("Removing audit component {}", o.name());
|
||||
return mainComponentsPool.remove(o);
|
||||
}
|
||||
|
||||
public static boolean containsAll(Collection<? extends IAuditComponentProvider> c)
|
||||
{
|
||||
return mainComponentsPool.containsAll(c);
|
||||
}
|
||||
|
||||
public static void addAll(Collection<? extends IAuditComponentProvider> c)
|
||||
{
|
||||
c.forEach(AuditManager::add);
|
||||
}
|
||||
|
||||
public static void removeAll(Collection<? extends IAuditComponentProvider> c)
|
||||
{
|
||||
c.forEach(AuditManager::remove);
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
package net.pingex.dcf.commands.audit;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* This class contains results for an audit.
|
||||
*/
|
||||
public class AuditResult
|
||||
{
|
||||
/**
|
||||
* Result code for this audit
|
||||
*/
|
||||
private ResultCode opcode;
|
||||
|
||||
/**
|
||||
* Details message about what happened. Mostly used with FAIL and WARN opcodes.
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* Sub audits.
|
||||
*/
|
||||
private List<Entry<IAuditComponentProvider, AuditResult>> subAuditsResults;
|
||||
|
||||
/**
|
||||
* Full constructor, with sub audits.
|
||||
* @param opcode The result code of the operation.
|
||||
* @param message Optional message, pass `null` for no message.
|
||||
* @param subAuditsResults A result for each sub audit.
|
||||
*/
|
||||
public AuditResult(ResultCode opcode, String message, List<Entry<IAuditComponentProvider, AuditResult>> subAuditsResults)
|
||||
{
|
||||
this.opcode = opcode;
|
||||
this.message = message;
|
||||
this.subAuditsResults = subAuditsResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor without sub audits.
|
||||
* @param opcode The result code of the operation.
|
||||
* @param message Optional message, pass `null` for no message.
|
||||
*/
|
||||
public AuditResult(ResultCode opcode, String message)
|
||||
{
|
||||
this.opcode = opcode;
|
||||
this.message = message;
|
||||
this.subAuditsResults = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor without sub audits or a message.
|
||||
* @param opcode The result code of the operation.
|
||||
*/
|
||||
public AuditResult(ResultCode opcode)
|
||||
{
|
||||
this.opcode = opcode;
|
||||
this.message = null;
|
||||
this.subAuditsResults = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result code for this audit
|
||||
*/
|
||||
public ResultCode getOpcode()
|
||||
{
|
||||
return opcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Details message about what happened. Mostly used with FAIL and WARN opcodes.
|
||||
*/
|
||||
public String getMessage()
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub audits, if any.
|
||||
* @throws UnsupportedOperationException in case there is not sub audits.
|
||||
*/
|
||||
public List<Entry<IAuditComponentProvider, AuditResult>> getSubAuditsResults() throws UnsupportedOperationException
|
||||
{
|
||||
if(subAuditsResults != null)
|
||||
return subAuditsResults;
|
||||
else throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether this object contains subaudits.
|
||||
* @return `true` when this object contains subaudits, `false` otherwise
|
||||
*/
|
||||
public boolean hasSubAudits()
|
||||
{
|
||||
return subAuditsResults != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* All the possible result codes for an andit.
|
||||
*/
|
||||
public enum ResultCode implements Comparable<ResultCode>
|
||||
{
|
||||
/**
|
||||
* The audit failed.
|
||||
*/
|
||||
FAIL,
|
||||
|
||||
/**
|
||||
* The test passed with a warning (contained in the message field)
|
||||
*/
|
||||
WARN,
|
||||
|
||||
/**
|
||||
* The test passed without any issue.
|
||||
*/
|
||||
PASS,
|
||||
|
||||
/**
|
||||
* Test was ignored.
|
||||
*/
|
||||
NOOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to construct a proper AuditResult.
|
||||
*/
|
||||
public static class Builder
|
||||
{
|
||||
private ResultCode opcode;
|
||||
private String message;
|
||||
private List<Entry<IAuditComponentProvider, AuditResult>> subAuditsResults = new ArrayList<>();
|
||||
|
||||
public Builder setOpcode(ResultCode opcode)
|
||||
{
|
||||
this.opcode = opcode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder appendAuditResult(IAuditComponentProvider provider, AuditResult result)
|
||||
{
|
||||
appendAuditResult(new AbstractMap.SimpleEntry<IAuditComponentProvider, AuditResult>(provider, result));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder appendAuditResult(Entry<IAuditComponentProvider, AuditResult> entry)
|
||||
{
|
||||
subAuditsResults.add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<Entry<IAuditComponentProvider, AuditResult>> getSubAuditsResults()
|
||||
{
|
||||
return subAuditsResults;
|
||||
}
|
||||
|
||||
public ResultCode getOpcode()
|
||||
{
|
||||
return opcode;
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
public AuditResult build()
|
||||
{
|
||||
return new AuditResult(opcode, message, subAuditsResults.isEmpty() ? null : subAuditsResults);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fork a new builder.
|
||||
*/
|
||||
public static Builder builder()
|
||||
{
|
||||
return new Builder();
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package net.pingex.dcf.commands.audit;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
|
||||
/**
|
||||
* Each module which wants to audit an incoming command have to implement this interface.
|
||||
*/
|
||||
public interface IAuditComponentProvider
|
||||
{
|
||||
/**
|
||||
* This is the proper test method.
|
||||
* @param context Command context
|
||||
* @return The result of this component's audit.
|
||||
*/
|
||||
AuditResult doAudit(Context context);
|
||||
|
||||
/**
|
||||
* Gives the name of the component.
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Gives a short descrpition about what this component does.
|
||||
*/
|
||||
String description();
|
||||
|
||||
/**
|
||||
* Gives the priority of this audit. Lower priority takes precedence.
|
||||
*/
|
||||
int priority();
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package net.pingex.dcf.commands.audit.basic;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
|
||||
/**
|
||||
* This component checks whether the command is enabled, or not.
|
||||
*/
|
||||
public class EnabledCheck implements IAuditComponentProvider
|
||||
{
|
||||
@Override
|
||||
public AuditResult doAudit(Context context)
|
||||
{
|
||||
if(context.getCommand().isEnabled())
|
||||
return new AuditResult(AuditResult.ResultCode.PASS, null);
|
||||
else return new AuditResult(AuditResult.ResultCode.FAIL, "Command is not enabled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "Enabled command check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Checks whether the command is enabled, or not";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int priority()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package net.pingex.dcf.commands.audit.basic;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
import net.pingex.dcf.commands.options.ICommandOption;
|
||||
import net.pingex.dcf.commands.options.ScopeOption;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This component checks whether the command is invoked in the right scope.
|
||||
*/
|
||||
public class ScopeCheck implements IAuditComponentProvider
|
||||
{
|
||||
@Override
|
||||
public AuditResult doAudit(Context context)
|
||||
{
|
||||
Optional<ICommandOption> option = context.getCommand().getOptions().stream().filter(i -> i instanceof ScopeOption).findFirst();
|
||||
if(!option.isPresent()) return new AuditResult(AuditResult.ResultCode.NOOP, "ScopeOption not present.");
|
||||
|
||||
if(((ScopeOption)option.get()).getScope().test(context.getChannel()))
|
||||
return new AuditResult(AuditResult.ResultCode.PASS, null);
|
||||
else return new AuditResult(AuditResult.ResultCode.FAIL, "Cannot run this command outside of its intended scope. Valid scope is: " + ((ScopeOption)option.get()).getScope() + ".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "Invocation scope check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Checks whether the command is invoked in the right scope, ie. guild chat or PM.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int priority()
|
||||
{
|
||||
return -2;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Commands Audit allows command invokations to be checked for runnability.
|
||||
* New audit components can be added later with minimal impact on existent code.
|
||||
* Audit examples: permissions, arguments, scope, etc
|
||||
*/
|
||||
package net.pingex.dcf.commands.audit;
|
@ -0,0 +1,18 @@
|
||||
package net.pingex.dcf.commands.options;
|
||||
|
||||
/**
|
||||
* Interface to add specific behaviors to a command.
|
||||
* Behaviors are stored in a Set object in the main Command object.
|
||||
*/
|
||||
public interface ICommandOption
|
||||
{
|
||||
/**
|
||||
* Give the name of the option.
|
||||
*/
|
||||
String getOptionName();
|
||||
|
||||
/**
|
||||
* Gives the description of the option.
|
||||
*/
|
||||
String getOptionDescription();
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package net.pingex.dcf.commands.options;
|
||||
|
||||
import sx.blah.discord.handle.obj.IChannel;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* This option allows to specify where the command should run.
|
||||
*/
|
||||
public class ScopeOption implements ICommandOption
|
||||
{
|
||||
private CommandScope commandScope;
|
||||
|
||||
public ScopeOption(CommandScope commandScope)
|
||||
{
|
||||
this.commandScope = commandScope;
|
||||
}
|
||||
|
||||
public CommandScope getScope()
|
||||
{
|
||||
return commandScope;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOptionName()
|
||||
{
|
||||
return "Command scope";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOptionDescription()
|
||||
{
|
||||
return "This option allows to specify where the command can be run.";
|
||||
}
|
||||
|
||||
/**
|
||||
* CommandScope allows devs to tell where Commands should run.
|
||||
* ie. PM, guild chat, etc
|
||||
*/
|
||||
public enum CommandScope
|
||||
{
|
||||
/**
|
||||
* Allows only in a guild chat
|
||||
*/
|
||||
GUILD_CHAT(iChannel -> !iChannel.isPrivate()),
|
||||
|
||||
/**
|
||||
* Only via PM with the bot
|
||||
*/
|
||||
PRIVATE_MESSAGE(IChannel::isPrivate),
|
||||
|
||||
/**
|
||||
* Allows unconditionally
|
||||
*/
|
||||
ANYWHERE(iChannel -> true),
|
||||
|
||||
/**
|
||||
* Denies unconditionally
|
||||
* Default value
|
||||
*/
|
||||
NOWHERE(iChannel -> false);
|
||||
|
||||
private Predicate<IChannel> channel;
|
||||
|
||||
CommandScope(Predicate<IChannel> channel)
|
||||
{
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public boolean test(IChannel iChannel)
|
||||
{
|
||||
return channel.test(iChannel);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* This package contains everything related to command options.
|
||||
*/
|
||||
package net.pingex.dcf.commands.options;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains everything related to commands parsing and executing.
|
||||
*/
|
||||
package net.pingex.dcf.commands;
|
@ -0,0 +1,47 @@
|
||||
package net.pingex.dcf.commands.parser;
|
||||
|
||||
import net.pingex.dcf.core.Configuration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Basic command parser.
|
||||
*/
|
||||
public class BasicParser implements ICommandParser
|
||||
{
|
||||
private Pattern commandPattern = Pattern.compile("^" + Configuration.COMMAND_PREFIX + "([:\\w]+)(?: (.*))?$");
|
||||
private Pattern argsPattern = Pattern.compile("([^\"]\\S*|\".+?\")\\s*");
|
||||
|
||||
@Override
|
||||
public boolean checkSyntax(String input)
|
||||
{
|
||||
return commandPattern.matcher(input).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parseCommand(String input) throws ParserException
|
||||
{
|
||||
Matcher m = commandPattern.matcher(input);
|
||||
if(!m.matches()) throw new ParserException("String cannot be parsed for a command.");
|
||||
return m.group(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> parseArguments(String input) throws ParserException
|
||||
{
|
||||
Matcher mc = commandPattern.matcher(input);
|
||||
if(!mc.matches()) throw new ParserException("String cannot be parsed for a command.");
|
||||
|
||||
// CASE: Command has no arg
|
||||
if(mc.group(2) == null) return Collections.emptyList();
|
||||
|
||||
// CASE: Command has args
|
||||
Matcher ma = argsPattern.matcher(mc.group(2));
|
||||
List<String> argsOut = new ArrayList<>();
|
||||
while(ma.find()) argsOut.add(ma.group(1).replace("\"", ""));
|
||||
return argsOut;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package net.pingex.dcf.commands.parser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* General interface for a command parser.
|
||||
*/
|
||||
public interface ICommandParser
|
||||
{
|
||||
/**
|
||||
* Checks the syntax of a command.
|
||||
* @param input Input String, ie. the message content.
|
||||
* @return `true` if the string is a command, `false` otherwise.
|
||||
*/
|
||||
boolean checkSyntax(String input);
|
||||
|
||||
/**
|
||||
* Gives the command part of a command string.
|
||||
* @param input Input String, ie. the message content.
|
||||
* @return The command part of the inputted string.
|
||||
* @throws ParserException whether the inputted string cannot be parsed for a command.
|
||||
*/
|
||||
String parseCommand(String input) throws ParserException;
|
||||
|
||||
/**
|
||||
* Gives the arguments list of a command string.
|
||||
* @param input Input String, ie. the message content.
|
||||
* @return The arguments list of the inputted string.
|
||||
* @throws ParserException whether the inputted string cannot be parsed for a command.
|
||||
*/
|
||||
List<String> parseArguments(String input) throws ParserException;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.pingex.dcf.commands.parser;
|
||||
|
||||
/**
|
||||
* Thrown when the parser cannot parse.
|
||||
*/
|
||||
public class ParserException extends Exception
|
||||
{
|
||||
public ParserException()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
public ParserException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains command parsers.
|
||||
*/
|
||||
package net.pingex.dcf.commands.parser;
|
@ -0,0 +1,80 @@
|
||||
package net.pingex.dcf.commands.permissions;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.options.ICommandOption;
|
||||
import net.pingex.dcf.core.Configuration;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* This class defines the default behavior of a command when checking for permissions.
|
||||
*/
|
||||
public class DefaultPermissionOption implements ICommandOption
|
||||
{
|
||||
private Value defaultValue;
|
||||
|
||||
public DefaultPermissionOption(Value defaultValue)
|
||||
{
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public Value getDefaultValue()
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOptionName()
|
||||
{
|
||||
return "Default permission";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOptionDescription()
|
||||
{
|
||||
return "This option defines the default behavior of a command when checking for permissions.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Default behavior when a permissions provider doesn't return any value
|
||||
*/
|
||||
public enum Value implements Predicate<Context>
|
||||
{
|
||||
/**
|
||||
* Everyone is allowed to run the command.
|
||||
*/
|
||||
EVERYONE(context -> true),
|
||||
|
||||
/**
|
||||
* Only the guild owner is allowed to run the command.
|
||||
*/
|
||||
GUILD_OWNER(context -> context.getGuild() != null && context.getUser().getID().equals(context.getGuild().getOwnerID())),
|
||||
|
||||
/**
|
||||
* Only the bot owner is allowed to run the command.
|
||||
*/
|
||||
BOT_OWNER(context -> context.getUser().getID().equals(Configuration.BOT_OWNER)),
|
||||
|
||||
/**
|
||||
* Guild Owner x Bot Owner
|
||||
*/
|
||||
ANY_OWNER(context -> GUILD_OWNER.test(context) || BOT_OWNER.test(context)),
|
||||
|
||||
/**
|
||||
* Nobody is allowed to run the command
|
||||
*/
|
||||
NONE(context -> false);
|
||||
|
||||
Value(Predicate<Context> predicate)
|
||||
{
|
||||
this.predicate = predicate;
|
||||
}
|
||||
|
||||
private Predicate<Context> predicate;
|
||||
|
||||
@Override
|
||||
public boolean test(Context o)
|
||||
{
|
||||
return predicate.test(o);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package net.pingex.dcf.commands.permissions;
|
||||
|
||||
import net.pingex.dcf.commands.Command;
|
||||
import net.pingex.dcf.core.Configuration;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.mapdb.DB;
|
||||
import org.mapdb.DBException;
|
||||
import org.mapdb.DBMaker;
|
||||
import org.mapdb.Serializer;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IRole;
|
||||
import sx.blah.discord.handle.obj.IUser;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The one and only permissions provider for now.
|
||||
*/
|
||||
public class DefaultPermissionsProvider implements ICommandPermissionsProvider
|
||||
{
|
||||
/**
|
||||
* Datastore instance
|
||||
*/
|
||||
private DB datastore;
|
||||
|
||||
/**
|
||||
* User permissions store
|
||||
*/
|
||||
private Map<String, Boolean> userStore;
|
||||
|
||||
/**
|
||||
* Group permissions store
|
||||
*/
|
||||
private Map<String, Boolean> groupStore;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(DefaultPermissionsProvider.class);
|
||||
|
||||
public DefaultPermissionsProvider()
|
||||
{
|
||||
LOGGER.info("Building datastore.");
|
||||
|
||||
try
|
||||
{
|
||||
datastore = DBMaker.fileDB(new File(Configuration.DATA_DIR + "/permissions.db"))
|
||||
.closeOnJvmShutdown()
|
||||
.make();
|
||||
|
||||
userStore = datastore.hashMap("user", Serializer.STRING, Serializer.BOOLEAN).createOrOpen();
|
||||
groupStore = datastore.hashMap("group", Serializer.STRING, Serializer.BOOLEAN).createOrOpen();
|
||||
}
|
||||
catch(DBException e)
|
||||
{
|
||||
LOGGER.error("Error setting up the datastore.", e);
|
||||
LOGGER.error("Switching to local memory storage. PERMISSIONS ARE NOT LOADED AND CHANGES WON'T BE SAVED.");
|
||||
userStore = new HashMap<>();
|
||||
groupStore = new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean validateUser(IGuild guild, IUser user, Command command)
|
||||
{
|
||||
return userStore.get(generateUserKey(guild, user, command));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean validateGroup(IRole role, Command command)
|
||||
{
|
||||
return groupStore.get(generateGroupKey(role, command));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserPermission(IGuild guild, IUser user, Command command, Boolean value)
|
||||
{
|
||||
if(value == null) // Unset
|
||||
userStore.remove(generateUserKey(guild, user, command));
|
||||
else
|
||||
userStore.put(generateUserKey(guild, user, command), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGroupPermissions(IRole role, Command command, Boolean value)
|
||||
{
|
||||
if(value == null) // Unset
|
||||
groupStore.remove(generateGroupKey(role, command));
|
||||
else
|
||||
groupStore.put(generateGroupKey(role, command), value);
|
||||
}
|
||||
|
||||
private String generateUserKey(IGuild guild, IUser user, Command command)
|
||||
{
|
||||
return (guild != null ? guild.getID() : "*") + "/" + user.getID() + "/" + command.getName();
|
||||
}
|
||||
|
||||
private String generateGroupKey(IRole role, Command command)
|
||||
{
|
||||
return role.getID() + "/" + command.getName();
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.pingex.dcf.commands.permissions;
|
||||
|
||||
import net.pingex.dcf.commands.Command;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IRole;
|
||||
import sx.blah.discord.handle.obj.IUser;
|
||||
|
||||
/**
|
||||
* Provider of permissions, ie. a database which stores permissions data.
|
||||
*/
|
||||
public interface ICommandPermissionsProvider
|
||||
{
|
||||
/**
|
||||
* Validate individual user permission.
|
||||
* @param guild Guild where command is executed. `null` = globally
|
||||
* @param user Target user
|
||||
* @param command Command to test
|
||||
* @return `true` if the user is able to run the command, `false` if he isn't able to, `null` if there is no answer.
|
||||
*/
|
||||
Boolean validateUser(IGuild guild, IUser user, Command command);
|
||||
|
||||
/**
|
||||
* Validate a role's ability to run a command.
|
||||
* @param role Target role
|
||||
* @param command Command to test
|
||||
* @return `true` if the role is able to run the command, `false` if he isn't able to, `null` if there is no answer.
|
||||
*/
|
||||
Boolean validateGroup(IRole role, Command command);
|
||||
|
||||
/**
|
||||
* Set an user's permission to access a command.
|
||||
* @param guild On which guild does the permission apply ? Set to `null` for global range.
|
||||
* @param user Target user.
|
||||
* @param command Target command.
|
||||
* @param value `null` to remove the key, `false` to deny, `true` to allow.
|
||||
*/
|
||||
void setUserPermission(IGuild guild, IUser user, Command command, Boolean value);
|
||||
|
||||
/**
|
||||
* Set an group's permission to access a command.
|
||||
* @param role Target role.
|
||||
* @param command Target command.
|
||||
* @param value `null` to remove the key, `false` to deny, `true` to allow.
|
||||
*/
|
||||
void setGroupPermissions(IRole role, Command command, Boolean value);
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package net.pingex.dcf.commands.permissions;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
import net.pingex.dcf.commands.permissions.audit.DefaultPermissionCheck;
|
||||
import net.pingex.dcf.commands.permissions.audit.UserGlobalCheck;
|
||||
import net.pingex.dcf.commands.permissions.audit.UserGuildCheck;
|
||||
import net.pingex.dcf.commands.permissions.audit.UserGuildRoleCheck;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* This class is used to check whether an user can run a command.
|
||||
*/
|
||||
public class PermissionCheck implements IAuditComponentProvider
|
||||
{
|
||||
private static final ICommandPermissionsProvider CURRENT_PROVIDER = new DefaultPermissionsProvider();
|
||||
|
||||
private static final Set<IAuditComponentProvider> SUB_CHECKS = new TreeSet<>(Comparator.comparingInt(IAuditComponentProvider::priority));
|
||||
|
||||
static
|
||||
{
|
||||
SUB_CHECKS.addAll(Arrays.asList(
|
||||
new DefaultPermissionCheck(),
|
||||
new UserGlobalCheck(getProvider()),
|
||||
new UserGuildCheck(getProvider()),
|
||||
new UserGuildRoleCheck(getProvider())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider used to check for permissions.
|
||||
*/
|
||||
public static ICommandPermissionsProvider getProvider()
|
||||
{
|
||||
return CURRENT_PROVIDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditResult doAudit(Context context)
|
||||
{
|
||||
AuditResult.Builder globalResult = new AuditResult.Builder();
|
||||
|
||||
for(IAuditComponentProvider i : SUB_CHECKS)
|
||||
{
|
||||
AuditResult interResult = i.doAudit(context);
|
||||
globalResult.appendAuditResult(i, interResult);
|
||||
|
||||
// Check opcode has not been written yet and interResult has opcode either FAIL or PASS.
|
||||
if(globalResult.getOpcode() == null && (interResult.getOpcode() == AuditResult.ResultCode.FAIL || interResult.getOpcode() == AuditResult.ResultCode.PASS))
|
||||
{
|
||||
globalResult.setOpcode(interResult.getOpcode());
|
||||
globalResult.setMessage(interResult.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if(globalResult.getOpcode() == null)
|
||||
{
|
||||
globalResult.setOpcode(globalResult.getSubAuditsResults().get(globalResult.getSubAuditsResults().size()-1).getValue().getOpcode());
|
||||
globalResult.setMessage(globalResult.getSubAuditsResults().get(globalResult.getSubAuditsResults().size()-1).getValue().getMessage());
|
||||
}
|
||||
|
||||
return globalResult.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "Permission check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Checks whether an user can run a command.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int priority()
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
}
|
@ -0,0 +1,267 @@
|
||||
package net.pingex.dcf.commands.permissions;
|
||||
|
||||
import net.pingex.dcf.commands.Command;
|
||||
import net.pingex.dcf.commands.CommandRegistry;
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.IWithCommands;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
import net.pingex.dcf.commands.options.ScopeOption;
|
||||
import net.pingex.dcf.util.ArgumentParser;
|
||||
import net.pingex.dcf.util.DiscordInteractionsUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IRole;
|
||||
import sx.blah.discord.handle.obj.IUser;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Contains every command about the permissions system.
|
||||
*/
|
||||
public class PermissionsCommands implements IWithCommands
|
||||
{
|
||||
@Override
|
||||
public Set<Command> getCommands()
|
||||
{
|
||||
return new HashSet<>(Arrays.asList(
|
||||
permissionAudit, setUser, setRole
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick helper function to convert a bool to a String.
|
||||
* @param input Input boolean, can be `null`
|
||||
* @return "PASS" => `true`, "FAIL" => `false`, "NOOP" => `null`
|
||||
*/
|
||||
private static String booleanToString(Boolean input)
|
||||
{
|
||||
return input != null ? (input ? "PASS" : "FAIL") : "NOOP";
|
||||
}
|
||||
|
||||
private static final Command permissionAudit = Command.builder("permissions:audit")
|
||||
.aliases("perm:audit", "auditperm")
|
||||
.description("Audits permissions status for an user to run a command.")
|
||||
.usage("<command> <user#disc|uid|me> [server|sid|*]")
|
||||
.options(Collections.singleton(new DefaultPermissionOption(DefaultPermissionOption.Value.EVERYONE)))
|
||||
.build(PermissionsCommands::permissionAuditImpl);
|
||||
|
||||
private static void permissionAuditImpl(Context context)
|
||||
{
|
||||
// argchk#1 arg counts
|
||||
if(context.getArguments().size() != 2 && context.getArguments().size() != 3)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Invalid arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
// argchk#2 command
|
||||
Optional<Command> targetCommand = CommandRegistry.getCommandOrAliasByName(context.getArguments().get(0));
|
||||
if(!targetCommand.isPresent())
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target command not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// argchk#3 user
|
||||
Optional<IUser> targetUser;
|
||||
if(context.getArguments().get(1).equalsIgnoreCase("me")) targetUser = Optional.of(context.getUser());
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
targetUser = ArgumentParser.checkParseUsernameOrID(context.getArguments().get(1), context.getClient());
|
||||
}
|
||||
catch(ArgumentParser.ParserException e)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Invalid arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!targetUser.isPresent())
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target user not found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// argchk#4 serverid
|
||||
Optional<IGuild> targetGuild = context.getGuild() != null ? Optional.of(context.getGuild()) : Optional.empty();
|
||||
if(context.getArguments().size() == 3)
|
||||
{
|
||||
if(context.getArguments().get(2).equals("*")) targetGuild = Optional.empty();
|
||||
else
|
||||
{
|
||||
targetGuild = ArgumentParser.checkParseGuildOrID(context.getArguments().get(2), context.getClient());
|
||||
if(!targetGuild.isPresent())
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target guild not found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Context parsedContext = new Context(targetCommand.get(), null, targetUser.get(), null, targetGuild.orElseGet(() -> null), context.getClient());
|
||||
AuditResult result = new PermissionCheck().doAudit(parsedContext);
|
||||
|
||||
StringBuilder sb = new StringBuilder()
|
||||
.append("**Permissions audit**\n")
|
||||
.append("```");
|
||||
|
||||
// Target infos
|
||||
sb.append("Target infos\n")
|
||||
.append("=> User: ").append(parsedContext.getUser().getName()).append("#").append(parsedContext.getUser().getDiscriminator()).append(" - ").append(parsedContext.getUser().getID()).append("\n")
|
||||
.append("=> Command: ").append(parsedContext.getCommand().getName()).append(" (").append(parsedContext.getCommand().getDescription()).append(")").append("\n")
|
||||
.append("=> Guild: ");
|
||||
if(parsedContext.getGuild() == null) sb.append("N/A");
|
||||
else sb.append(parsedContext.getGuild().getName()).append(" - ").append(parsedContext.getGuild().getID());
|
||||
sb.append("\n\n");
|
||||
|
||||
// Result
|
||||
sb.append("Audit results\n")
|
||||
.append("=> OPCode: ").append(result.getOpcode()).append("\n")
|
||||
.append("=> Message: ").append(result.getMessage() != null ? result.getMessage() : "N/A").append("\n\n");
|
||||
|
||||
// Walkthrough
|
||||
sb.append("Walkthrough\n");
|
||||
int maxPadding = result.getSubAuditsResults().stream().max(Comparator.comparingInt(i -> i.getKey().name().length())).get().getKey().name().length()+2;
|
||||
for(Map.Entry<IAuditComponentProvider, AuditResult> i : result.getSubAuditsResults())
|
||||
{
|
||||
sb.append("=> ").append(StringUtils.rightPad(i.getKey().name() + ":", maxPadding))
|
||||
.append(i.getValue().getOpcode()).append(" - ")
|
||||
.append(i.getValue().getMessage() != null ? i.getValue().getMessage() : "N/A")
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
sb.append("```");
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), sb.toString());
|
||||
}
|
||||
|
||||
private static final Command setUser = Command.builder("permissions:setUser")
|
||||
.aliases("perm:setuser", "setuserperm")
|
||||
.description("Sets an user's permission to run a command.")
|
||||
.usage("<command> <user#disc|uid> <true|false|null>")
|
||||
.options(Collections.singleton(new DefaultPermissionOption(DefaultPermissionOption.Value.ANY_OWNER)))
|
||||
.build(PermissionsCommands::setUserImpl);
|
||||
|
||||
private static void setUserImpl(Context context)
|
||||
{
|
||||
// argchk#1 count
|
||||
if(context.getArguments().size() != 3)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Invalid arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
// argchk#2 command
|
||||
Optional<Command> optionalCommand = CommandRegistry.getCommandOrAliasByName(context.getArguments().get(0));
|
||||
if(!optionalCommand.isPresent())
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target command not found.");
|
||||
return;
|
||||
}
|
||||
Command targetCommand = optionalCommand.get();
|
||||
|
||||
// argchk#3 user
|
||||
Optional<IUser> optionalUser;
|
||||
try
|
||||
{
|
||||
optionalUser = ArgumentParser.checkParseUsernameOrID(context.getArguments().get(1), context.getClient());
|
||||
}
|
||||
catch(ArgumentParser.ParserException e)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Invalid arguments.");
|
||||
return;
|
||||
}
|
||||
if(!optionalUser.isPresent())
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target user not found.");
|
||||
return;
|
||||
}
|
||||
IUser targetUser = optionalUser.get();
|
||||
|
||||
// argchk#4 target
|
||||
Boolean targetAction = (Boolean) ArgumentParser.parse(Boolean.class, context.getArguments().get(2));
|
||||
|
||||
// Arg: Guild
|
||||
IGuild targetGuild = context.getGuild();
|
||||
|
||||
// Operation
|
||||
Boolean oldPermission = PermissionCheck.getProvider().validateUser(targetGuild, targetUser, targetCommand);
|
||||
PermissionCheck.getProvider().setUserPermission(targetGuild, targetUser, targetCommand, targetAction);
|
||||
Boolean newPermission = PermissionCheck.getProvider().validateUser(targetGuild, targetUser, targetCommand);
|
||||
|
||||
// Output
|
||||
StringBuilder sb = new StringBuilder("**Permissions changes**\n").append("```\n");
|
||||
sb.append("Target infos\n")
|
||||
.append("=> User: ").append(targetUser.getName()).append("#").append(targetUser.getDiscriminator()).append(" - ").append(targetUser.getID()).append("\n")
|
||||
.append("=> Command: ").append(targetCommand.getName()).append(" (").append(targetCommand.getDescription()).append(")").append("\n")
|
||||
.append("=> Guild: ");
|
||||
if(targetGuild == null) sb.append("[Global]");
|
||||
else sb.append(targetGuild.getName()).append(" - ").append(targetGuild.getID());
|
||||
sb.append("\n\n");
|
||||
sb.append("Permissions\n")
|
||||
.append("=> Old permission: ").append(booleanToString(oldPermission)).append("\n")
|
||||
.append("=> New permission: ").append(booleanToString(newPermission)).append("\n");
|
||||
sb.append("```");
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), sb.toString());
|
||||
}
|
||||
|
||||
private static final Command setRole = Command.builder("permissions:setRole")
|
||||
.aliases("perm:setrole", "setroleperm")
|
||||
.description("Sets a role's permission to run a command.")
|
||||
.usage("<command> <rolename|rid> <true|false|null>")
|
||||
.options(new HashSet<>(Arrays.asList(
|
||||
new DefaultPermissionOption(DefaultPermissionOption.Value.ANY_OWNER),
|
||||
new ScopeOption(ScopeOption.CommandScope.GUILD_CHAT))))
|
||||
.build(PermissionsCommands::setRoleImpl);
|
||||
|
||||
private static void setRoleImpl(Context context)
|
||||
{
|
||||
// argchk#1 count
|
||||
if(context.getArguments().size() != 3)
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Invalid arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
// argchk#2 command
|
||||
Optional<Command> optionalCommand = CommandRegistry.getCommandOrAliasByName(context.getArguments().get(0));
|
||||
if(!optionalCommand.isPresent())
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target command not found.");
|
||||
return;
|
||||
}
|
||||
Command targetCommand = optionalCommand.get();
|
||||
|
||||
// argchk#3 role
|
||||
Optional<IRole> optionalRole;
|
||||
optionalRole = ArgumentParser.checkParseRoleOrID(context.getArguments().get(1), context.getGuild());
|
||||
if(!optionalRole.isPresent())
|
||||
{
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), "Target role not found.");
|
||||
return;
|
||||
}
|
||||
IRole targetRole = optionalRole.get();
|
||||
|
||||
// argchk#4 target
|
||||
Boolean targetAction = (Boolean) ArgumentParser.parse(Boolean.class, context.getArguments().get(2));
|
||||
|
||||
// Operation
|
||||
Boolean oldPermission = PermissionCheck.getProvider().validateGroup(targetRole, targetCommand);
|
||||
PermissionCheck.getProvider().setGroupPermissions(targetRole, targetCommand, targetAction);
|
||||
Boolean newPermission = PermissionCheck.getProvider().validateGroup(targetRole, targetCommand);
|
||||
|
||||
// Output
|
||||
StringBuilder sb = new StringBuilder("**Permissions changes**\n").append("```\n");
|
||||
sb.append("Target infos\n")
|
||||
.append("=> Role: ").append(targetRole.getName()).append(" - ").append(targetRole.getID()).append("\n")
|
||||
.append("=> Command: ").append(targetCommand.getName()).append(" (").append(targetCommand.getDescription()).append(")").append("\n")
|
||||
.append("=> Guild: ").append(targetRole.getGuild().getName()).append(" - ").append(targetRole.getGuild().getID());;
|
||||
sb.append("\n\n");
|
||||
sb.append("Permissions\n")
|
||||
.append("=> Old permission: ").append(booleanToString(oldPermission)).append("\n")
|
||||
.append("=> New permission: ").append(booleanToString(newPermission)).append("\n");
|
||||
sb.append("```");
|
||||
DiscordInteractionsUtil.sendMessage(context.getChannel(), sb.toString());
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.pingex.dcf.commands.permissions.audit;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
import net.pingex.dcf.commands.options.ICommandOption;
|
||||
import net.pingex.dcf.commands.permissions.DefaultPermissionOption;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* This class checks permission against a default behavior defined in a command.
|
||||
*/
|
||||
public class DefaultPermissionCheck implements IAuditComponentProvider
|
||||
{
|
||||
@Override
|
||||
public AuditResult doAudit(Context context)
|
||||
{
|
||||
Optional<ICommandOption> option = context.getCommand().getOptions().stream().filter(i -> i instanceof DefaultPermissionOption).findFirst();
|
||||
|
||||
if(!option.isPresent())
|
||||
return new AuditResult(AuditResult.ResultCode.NOOP, "No default permission defined.");
|
||||
|
||||
if(((DefaultPermissionOption) option.get()).getDefaultValue().test(context))
|
||||
return new AuditResult(AuditResult.ResultCode.PASS);
|
||||
else
|
||||
return new AuditResult(AuditResult.ResultCode.FAIL, "Default policy denied access to command.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "Default permission check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Checks against a default permission option defined in a command.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int priority()
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.pingex.dcf.commands.permissions.audit;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
import net.pingex.dcf.commands.permissions.ICommandPermissionsProvider;
|
||||
|
||||
/**
|
||||
* This class checks permission against a rule defined for a specific user globally.
|
||||
*/
|
||||
public class UserGlobalCheck implements IAuditComponentProvider
|
||||
{
|
||||
/**
|
||||
* Permissions provider to use
|
||||
*/
|
||||
private ICommandPermissionsProvider provider;
|
||||
|
||||
/**
|
||||
* Provider for this object to use
|
||||
* @param provider Permissions provider this object will use
|
||||
*/
|
||||
public UserGlobalCheck(ICommandPermissionsProvider provider)
|
||||
{
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditResult doAudit(Context context)
|
||||
{
|
||||
Boolean returnedValue = provider.validateUser(null, context.getUser(), context.getCommand());
|
||||
|
||||
if(returnedValue == null) // No rule for user
|
||||
return new AuditResult(AuditResult.ResultCode.NOOP, "No global rule for this user.");
|
||||
else if(returnedValue) // Rule says yes
|
||||
return new AuditResult(AuditResult.ResultCode.PASS);
|
||||
else // Rule says no
|
||||
return new AuditResult(AuditResult.ResultCode.FAIL, "Global rule denied access for this user.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "User global check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Checks against a global rule defined for a specific user.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int priority()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package net.pingex.dcf.commands.permissions.audit;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
import net.pingex.dcf.commands.permissions.ICommandPermissionsProvider;
|
||||
|
||||
/**
|
||||
* This class checks permission against a rule defined for a specific user in a specific guild.
|
||||
*/
|
||||
public class UserGuildCheck implements IAuditComponentProvider
|
||||
{
|
||||
/**
|
||||
* Permissions provider to use
|
||||
*/
|
||||
private ICommandPermissionsProvider provider;
|
||||
|
||||
/**
|
||||
* Provider for this object to use
|
||||
* @param provider Permissions provider this object will use
|
||||
*/
|
||||
public UserGuildCheck(ICommandPermissionsProvider provider)
|
||||
{
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditResult doAudit(Context context)
|
||||
{
|
||||
// Check for guild
|
||||
if(context.getGuild() == null)
|
||||
return new AuditResult(AuditResult.ResultCode.NOOP, "This channel is not part of a guild.");
|
||||
|
||||
Boolean returnedValue = provider.validateUser(context.getGuild(), context.getUser(), context.getCommand());
|
||||
|
||||
if(returnedValue == null) // No rule for user in this guild
|
||||
return new AuditResult(AuditResult.ResultCode.NOOP, "No guild rule for this user.");
|
||||
else if(returnedValue) // Rule says yes
|
||||
return new AuditResult(AuditResult.ResultCode.PASS);
|
||||
else // Rule says no
|
||||
return new AuditResult(AuditResult.ResultCode.FAIL, "Guild rule denied access for this user.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "User vs guild check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Checks against a guild rule defined for an user.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int priority()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.pingex.dcf.commands.permissions.audit;
|
||||
|
||||
import net.pingex.dcf.commands.Context;
|
||||
import net.pingex.dcf.commands.audit.AuditResult;
|
||||
import net.pingex.dcf.commands.audit.IAuditComponentProvider;
|
||||
import net.pingex.dcf.commands.permissions.ICommandPermissionsProvider;
|
||||
import sx.blah.discord.handle.obj.IRole;
|
||||
|
||||
/**
|
||||
* This class checks permission against a rule defined for a specific role in a guild.
|
||||
*/
|
||||
public class UserGuildRoleCheck implements IAuditComponentProvider
|
||||
{
|
||||
/**
|
||||
* Permissions provider to use
|
||||
*/
|
||||
private ICommandPermissionsProvider provider;
|
||||
|
||||
/**
|
||||
* Provider for this object to use
|
||||
* @param provider Permissions provider this object will use
|
||||
*/
|
||||
public UserGuildRoleCheck(ICommandPermissionsProvider provider)
|
||||
{
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditResult doAudit(Context context)
|
||||
{
|
||||
// Check for guild
|
||||
if(context.getGuild() == null)
|
||||
return new AuditResult(AuditResult.ResultCode.NOOP, "This channel is not part of a guild.");
|
||||
|
||||
if(context.getGuild().getRolesForUser(context.getUser()) != null) // User has roles
|
||||
for(IRole i : context.getGuild().getRolesForUser(context.getUser()))
|
||||
{
|
||||
Boolean returnedQuery = provider.validateGroup(i, context.getCommand());
|
||||
if(returnedQuery != null)
|
||||
{
|
||||
if(returnedQuery) return new AuditResult(AuditResult.ResultCode.PASS);
|
||||
else return new AuditResult(AuditResult.ResultCode.FAIL, "Role rule denied access for this user.");
|
||||
}
|
||||
}
|
||||
else // User has no role
|
||||
return new AuditResult(AuditResult.ResultCode.NOOP, "No role defined for this user.");
|
||||
|
||||
// No rule for any role
|
||||
return new AuditResult(AuditResult.ResultCode.NOOP, "No rule defined for any role this user has.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "Guild role check";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Checks permission against a rule defined for a specific role in a guild.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int priority()
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* This package contains all sub-checks for PermissionCheck.
|
||||
*/
|
||||
package net.pingex.dcf.commands.permissions.audit;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* This package contains everything related to permissions and the ability for an user to run something.
|
||||
*/
|
||||
package net.pingex.dcf.commands.permissions;
|
@ -0,0 +1,141 @@
|
||||
package net.pingex.dcf.core;
|
||||
|
||||
import org.apache.commons.configuration2.ConfigurationUtils;
|
||||
import org.apache.commons.configuration2.FileBasedConfiguration;
|
||||
import org.apache.commons.configuration2.ImmutableConfiguration;
|
||||
import org.apache.commons.configuration2.PropertiesConfiguration;
|
||||
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
|
||||
import org.apache.commons.configuration2.builder.fluent.Parameters;
|
||||
import org.apache.commons.configuration2.ex.ConfigurationException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Configuration manager
|
||||
*/
|
||||
public class Configuration
|
||||
{
|
||||
/**
|
||||
* File name to look for
|
||||
*/
|
||||
private static final String FILENAME = "dcf.properties";
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static final Logger LOGGER = LogManager.getLogger(Configuration.class);
|
||||
|
||||
/**
|
||||
* Main configuration store
|
||||
*/
|
||||
private static org.apache.commons.configuration2.Configuration store;
|
||||
|
||||
/**
|
||||
* Load configuration file
|
||||
*/
|
||||
public static void load()
|
||||
{
|
||||
try
|
||||
{
|
||||
LOGGER.info("Loading configuration file");
|
||||
store = new FileBasedConfigurationBuilder<FileBasedConfiguration>(PropertiesConfiguration.class)
|
||||
.configure(new Parameters().fileBased().setFileName(FILENAME))
|
||||
.getConfiguration();
|
||||
} catch (ConfigurationException e)
|
||||
{
|
||||
LOGGER.fatal("Failed to load configuration", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store in read-only mode
|
||||
* @return The read-only store
|
||||
*/
|
||||
public static ImmutableConfiguration getStore()
|
||||
{
|
||||
return ConfigurationUtils.unmodifiableConfiguration(store);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Bot display name
|
||||
*/
|
||||
public static String BOT_NAME = "DCF-enabled Discord Bot";
|
||||
|
||||
/**
|
||||
* Initial connection token
|
||||
*/
|
||||
public static String CONNECTION_TOKEN = null;
|
||||
|
||||
/**
|
||||
* Initial connection username
|
||||
*/
|
||||
public static String CONNECTION_USERNAME = null;
|
||||
|
||||
/**
|
||||
* Initial connection password
|
||||
*/
|
||||
public static String CONNECTION_PASSWORD = null;
|
||||
|
||||
/**
|
||||
* Plugins directory
|
||||
*/
|
||||
public static String PLUGINS_DIR = "plugins";
|
||||
|
||||
/**
|
||||
* Data storage directory
|
||||
*/
|
||||
public static String DATA_DIR = "data";
|
||||
|
||||
/**
|
||||
* Command prefix string
|
||||
*/
|
||||
public static String COMMAND_PREFIX = "!";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public static String BOT_OWNER = "";
|
||||
|
||||
/**
|
||||
* Tells if the bot is configured to connect using a token or an username/password tuple.
|
||||
* @return Whether the main connection is a bot, or not
|
||||
*/
|
||||
public static boolean isConnectionToken()
|
||||
{
|
||||
return store.containsKey("discord.token");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load config keys
|
||||
*/
|
||||
public static void init()
|
||||
{
|
||||
BOT_NAME = store.getString("general.bot_name", BOT_NAME);
|
||||
PLUGINS_DIR = store.getString("general.plugins_dir", PLUGINS_DIR);
|
||||
DATA_DIR = store.getString("general.storage_dir", DATA_DIR);
|
||||
COMMAND_PREFIX = store.getString("commands.prefix", COMMAND_PREFIX);
|
||||
BOT_OWNER = store.getString("general.bot_owner", BOT_OWNER);
|
||||
|
||||
// Validate main connection username/password or token
|
||||
if(isConnectionToken())
|
||||
CONNECTION_TOKEN = store.getString("discord.token"); // We imply that the token exists, as its existence is already checked in isConnectionToken()
|
||||
else
|
||||
if(!store.containsKey("discord.username") || !store.containsKey("discord.password"))
|
||||
{
|
||||
LOGGER.error("Connection username/password not specified !");
|
||||
}
|
||||
else
|
||||
{
|
||||
CONNECTION_USERNAME = store.getString("discord.username");
|
||||
CONNECTION_PASSWORD = store.getString("discord.password");
|
||||
}
|
||||
|
||||
// Validate plugins directory
|
||||
File testingPath = new File(PLUGINS_DIR);
|
||||
if(!testingPath.exists()) LOGGER.error("Specified plugins path doesn't exist.");
|
||||
else if(!testingPath.isDirectory()) LOGGER.error("Specified plugins directory is not a directory.");
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
package net.pingex.dcf.core;
|
||||
|
||||
import net.pingex.dcf.commands.CommandHandler;
|
||||
import net.pingex.dcf.modularity.PluginWrapper;
|
||||
import net.pingex.dcf.modularity.events.EventManager;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import sx.blah.discord.api.events.EventSubscriber;
|
||||
import sx.blah.discord.handle.impl.events.*;
|
||||
|
||||
/**
|
||||
* Very core events handler, should be running independently from the whole events manager thingy.
|
||||
* It handles things like auto reconnect after gateway disconnection, etc.
|
||||
*/
|
||||
public class CoreEventsHandler
|
||||
{
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static Logger LOGGER = LogManager.getLogger(CoreEventsHandler.class);
|
||||
|
||||
@EventSubscriber
|
||||
public static void onReady(ReadyEvent event)
|
||||
{
|
||||
LOGGER.info("Connection with user #{} ready.", event.getClient().getOurUser().getID());
|
||||
EventManager.getInstance().updateConnectionHandlers();
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public static void onDiscordDisconnected(DiscordDisconnectedEvent event)
|
||||
{
|
||||
LOGGER.warn("Discord connection with user #{} lost ({}).", event.getClient().getOurUser().getID(), event.getReason());
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public static void onDiscordReconnected(DiscordReconnectedEvent event)
|
||||
{
|
||||
LOGGER.info("Gateway connection reconnected (user #{}).", event.getClient().getOurUser().getID());
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public static void onMessageSend(MessageSendEvent event)
|
||||
{
|
||||
LOGGER.trace("Sent message to channel #{}.", event.getMessage().getChannel().getID());
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public static void onMention(MentionEvent event)
|
||||
{
|
||||
LOGGER.trace("Received mention from channel #{}.", event.getMessage().getChannel().getID());
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public static void onMessageReceived(MessageReceivedEvent event)
|
||||
{
|
||||
LOGGER.trace("Received message from channel #{}.", event.getMessage().getChannel().getID());
|
||||
CommandHandler.handle(event);
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public static void onGuildUnavailable(GuildUnavailableEvent event)
|
||||
{
|
||||
LOGGER.warn("Guild #{} is unavailable.", event.getGuildID());
|
||||
}
|
||||
|
||||
@EventSubscriber
|
||||
public static void onGuildCreate(GuildCreateEvent event)
|
||||
{
|
||||
LOGGER.info("Joined guild #{}.", event.getGuild().getID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a new plugin has been preloaded.
|
||||
* @param plugin Target plugin
|
||||
*/
|
||||
public static void pluginPreloaded(PluginWrapper plugin)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a new plugin has been laoded.
|
||||
* @param plugin Target plugin
|
||||
*/
|
||||
public static void pluginLoaded(PluginWrapper plugin)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a plugin is running.
|
||||
* @param plugin Target plugin
|
||||
*/
|
||||
public static void pluginRunning(PluginWrapper plugin)
|
||||
{
|
||||
EventManager.getInstance().checkAndRegister(plugin); // Hook: event handlers
|
||||
CommandHandler.registerPlugin(plugin); // Hook: commands handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a plugin has stopped.
|
||||
* @param plugin Target plugin
|
||||
*/
|
||||
public static void pluginStopped(PluginWrapper plugin)
|
||||
{
|
||||
EventManager.getInstance().unregister(plugin); // Hook: event handlers
|
||||
CommandHandler.unregisterPlugin(plugin); // Hook: commands handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever a plugin has unloaded.
|
||||
* @param plugin Target plugin
|
||||
*/
|
||||
public static void pluginUnloaded(PluginWrapper plugin)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package net.pingex.dcf.core;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import sx.blah.discord.api.ClientBuilder;
|
||||
import sx.blah.discord.api.IDiscordClient;
|
||||
import sx.blah.discord.util.DiscordException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Storage of all active connection to Discord gateway
|
||||
*/
|
||||
public class GatewayConnectionManager
|
||||
{
|
||||
/**
|
||||
* Main connection
|
||||
*/
|
||||
private static IDiscordClient connection;
|
||||
|
||||
/**
|
||||
* List of registered listeners as we cannot access registered listeners in IDC dispatcher.
|
||||
*/
|
||||
private static Set<Object> registeredListeners = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static final Logger LOGGER = LogManager.getLogger(GatewayConnectionManager.class);
|
||||
|
||||
/**
|
||||
* Registers a connection and login automatically, called once in main
|
||||
* @param builder Builder filled and ready to login
|
||||
*/
|
||||
public static void registerConnection(ClientBuilder builder)
|
||||
{
|
||||
LOGGER.info("Registering new connection");
|
||||
try
|
||||
{
|
||||
connection = builder.login();
|
||||
connection.getDispatcher().registerListener(CoreEventsHandler.class); // Register the DCF core events handler needed for core operations and logging.
|
||||
}
|
||||
catch (DiscordException e)
|
||||
{
|
||||
LOGGER.warn("Failed to login to Discord.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update listeners for target connection
|
||||
* @param refListeners Reference listeners list
|
||||
*/
|
||||
public static void updateListeners(Set<Class<?>> refListeners)
|
||||
{
|
||||
LOGGER.debug("Updating connection listeners.");
|
||||
|
||||
// Case 1: item is not registered in IDC
|
||||
long toRegister = refListeners.stream().filter(item -> !registeredListeners.contains(item)).count();
|
||||
refListeners.stream().filter(item -> !registeredListeners.contains(item)).forEach(item ->
|
||||
{
|
||||
connection.getDispatcher().registerListener(item);
|
||||
registeredListeners.add(item);
|
||||
});
|
||||
|
||||
// Case 2: item is registered, but shouldn't be
|
||||
long toUnregister = registeredListeners.stream().filter(item -> !refListeners.contains(item)).count();
|
||||
registeredListeners.stream().filter(item -> !refListeners.contains(item)).forEach(item ->
|
||||
{
|
||||
connection.getDispatcher().unregisterListener(item);
|
||||
registeredListeners.remove(item);
|
||||
});
|
||||
|
||||
LOGGER.debug("Update done. diff: +{} -{}", toRegister, toUnregister);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Very core components: configuration, handlers, and so on
|
||||
*/
|
||||
package net.pingex.dcf.core;
|
@ -0,0 +1,30 @@
|
||||
package net.pingex.dcf.modularity;
|
||||
|
||||
import org.apache.commons.configuration2.ImmutableConfiguration;
|
||||
|
||||
/**
|
||||
* Basic declaration of a plugin
|
||||
*/
|
||||
public interface IPlugin
|
||||
{
|
||||
/**
|
||||
* Load basic parameters, instantiate some objects, load configuration
|
||||
* @param configuration Configuration from dcf.properties
|
||||
*/
|
||||
void load(ImmutableConfiguration configuration) throws Throwable;
|
||||
|
||||
/**
|
||||
* Power up objects, schedule tasks, and so on
|
||||
*/
|
||||
void run() throws Throwable;
|
||||
|
||||
/**
|
||||
* Stop everything, doesn't mean we'll unload the plugin
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Unload the plugin, clean up, etc
|
||||
*/
|
||||
void unload();
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.pingex.dcf.modularity;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation containing some details about annotated {@code IPlugin}.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Plugin
|
||||
{
|
||||
/**
|
||||
* Unique plugin id.
|
||||
*/
|
||||
String id();
|
||||
|
||||
/**
|
||||
* Version of the plugin.
|
||||
*/
|
||||
String version();
|
||||
|
||||
/**
|
||||
* Plugin short description
|
||||
*/
|
||||
String description() default "No description provided.";
|
||||
}
|
@ -0,0 +1,310 @@
|
||||
package net.pingex.dcf.modularity;
|
||||
|
||||
import net.pingex.dcf.core.Configuration;
|
||||
import net.pingex.dcf.core.CoreEventsHandler;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Class that load the plugins, a very powerful thingy.
|
||||
*/
|
||||
public class PluginLoader
|
||||
{
|
||||
/**
|
||||
* Singleton instance
|
||||
*/
|
||||
private static PluginLoader INSTANCE = new PluginLoader();
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static Logger LOGGER = LogManager.getLogger(PluginLoader.class);
|
||||
|
||||
/**
|
||||
* All plugins
|
||||
*/
|
||||
private Map<String, PluginWrapper> plugins;
|
||||
|
||||
/**
|
||||
* Gives the singleton instance
|
||||
* @return The singleton instance
|
||||
*/
|
||||
public static PluginLoader getInstance()
|
||||
{
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce singleton with a private constructor.
|
||||
*/
|
||||
private PluginLoader()
|
||||
{
|
||||
plugins = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover all plugins contained in .jar files in the plugins directory.
|
||||
*/
|
||||
public void discoverPluginsDirectory()
|
||||
{
|
||||
LOGGER.debug("Discovering all IPlugin classes in plugins directory.");
|
||||
|
||||
int counter = 0;
|
||||
try
|
||||
{
|
||||
Set<Path> files = Files.walk(Paths.get(Configuration.PLUGINS_DIR))
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> FilenameUtils.getExtension(path.toString()).equals("jar"))
|
||||
.collect(Collectors.toSet());
|
||||
for(Path i : files) counter += loadJarFile(i);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
LOGGER.error("IO error while discovering plugins.", e);
|
||||
}
|
||||
|
||||
LOGGER.debug("Loaded {} plugins from {}", counter, Paths.get(Configuration.PLUGINS_DIR).toAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Look in a .jar file for IPlugins and preload them.
|
||||
*/
|
||||
public int loadJarFile(Path target) throws IOException
|
||||
{
|
||||
LOGGER.debug("Looking up {} for plugins.", target.getFileName());
|
||||
|
||||
// Get DCF-Plugins manifest attribute
|
||||
Attributes jarAttributes = new JarFile(target.toFile()).getManifest().getMainAttributes();
|
||||
LOGGER.trace("DCF-Plugins attribute: {}", jarAttributes.getValue("DCF-Plugins"));
|
||||
if(jarAttributes.getValue("DCF-Plugins") == null)
|
||||
{
|
||||
LOGGER.debug("Jar file has no `DCF-Plugins` attribute in its manifest. Ignoring archive.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
String[] pluginsToLoad = jarAttributes.getValue("DCF-Plugins").split(" ");
|
||||
LOGGER.debug("Manifest returned {} plugins to load.", pluginsToLoad.length);
|
||||
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{target.toUri().toURL()});
|
||||
|
||||
int counter = 0;
|
||||
for(String item : pluginsToLoad)
|
||||
{
|
||||
LOGGER.debug("Attempting to preload {}.", item);
|
||||
|
||||
Class<?> clazz;
|
||||
try
|
||||
{
|
||||
clazz = Class.forName(item, true, classLoader);
|
||||
}
|
||||
catch(ClassNotFoundException e) // Class with that name doesn't exist.
|
||||
{
|
||||
LOGGER.debug("No class with name {} was found.", item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Class doesn't implement IPlugin, or doesn't have @Plugin annotation.
|
||||
if(!clazz.isAnnotationPresent(Plugin.class) || !IPlugin.class.isAssignableFrom(clazz))
|
||||
{
|
||||
LOGGER.debug("Class does not implement IPlugin or doesn't have @Plugin annotation.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if(plugins.containsKey(clazz.getAnnotation(Plugin.class).id()))
|
||||
LOGGER.warn("Found duplicate plugin with id {}. Newly found plugin won't be preloaded.", clazz.getAnnotation(Plugin.class).id());
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
PluginWrapper plugin = new PluginWrapper(clazz.asSubclass(IPlugin.class), classLoader);
|
||||
plugins.put(clazz.getAnnotation(Plugin.class).id(), plugin);
|
||||
LOGGER.debug("Plugin {} version {} preloaded.", clazz.getAnnotation(Plugin.class).id(), clazz.getAnnotation(Plugin.class).version());
|
||||
counter++;
|
||||
CoreEventsHandler.pluginPreloaded(plugin);
|
||||
}
|
||||
catch(IllegalAccessException | InstantiationException e)
|
||||
{
|
||||
LOGGER.warn("Plugin " + clazz.getAnnotation(Plugin.class).id() + " failed to preload.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("Preloaded {} plugins from {}.", counter, target.getFileName());
|
||||
return counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all plugins that aren't.
|
||||
*/
|
||||
public void bulkLoadPlugins()
|
||||
{
|
||||
LOGGER.info("Bulk loading plugins.");
|
||||
plugins.entrySet().stream()
|
||||
.filter(i -> i.getValue().getState().equals(PluginState.UNLOADED))
|
||||
.forEach(i -> loadPlugin(i.getKey()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a plugin.
|
||||
* @param id The unique ID of the plugin.
|
||||
*/
|
||||
public void loadPlugin(String id)
|
||||
{
|
||||
// Check for plugin existence.
|
||||
if(!plugins.containsKey(id))
|
||||
{
|
||||
LOGGER.debug("Plugin with ID {} doesn't exist.", id);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Loading plugin {}.", id);
|
||||
PluginWrapper plugin = plugins.get(id);
|
||||
|
||||
// Check if plugin state is UNLOADED
|
||||
if(!plugin.getState().equals(PluginState.UNLOADED))
|
||||
{
|
||||
LOGGER.debug("Current plugin state does not allow it to be loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
plugin.getInstance().load(Configuration.getStore());
|
||||
plugin.setState(PluginState.LOADED);
|
||||
LOGGER.info("Loaded plugin {}.", id);
|
||||
CoreEventsHandler.pluginLoaded(plugin);
|
||||
}
|
||||
catch(Throwable throwable)
|
||||
{
|
||||
LOGGER.warn("Plugin " + id + " failed to load. Entering FAILED state.", throwable);
|
||||
plugin.setState(PluginState.FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all plugins that aren't
|
||||
*/
|
||||
public void bulkRunPlugins()
|
||||
{
|
||||
LOGGER.info("Bulk running plugins.");
|
||||
plugins.entrySet().stream()
|
||||
.filter(i -> i.getValue().getState().equals(PluginState.LOADED) || i.getValue().getState().equals(PluginState.STOPPED))
|
||||
.forEach(i -> runPlugin(i.getKey()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a plugin.
|
||||
* @param id The unique ID of the plugin.
|
||||
*/
|
||||
public void runPlugin(String id)
|
||||
{
|
||||
// Check for plugin existence.
|
||||
if(!plugins.containsKey(id))
|
||||
{
|
||||
LOGGER.debug("Plugin with ID {} doesn't exist.", id);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Attempting to run plugin {}.", id);
|
||||
PluginWrapper plugin = plugins.get(id);
|
||||
|
||||
// Check if plugin state is LOADED or STOPPED
|
||||
if(!plugin.getState().equals(PluginState.LOADED) && !plugin.getState().equals(PluginState.STOPPED))
|
||||
{
|
||||
LOGGER.debug("Current plugin state does not allow it to run.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
plugin.getInstance().run();
|
||||
LOGGER.info("Plugin {} is now running. (last state: {})", id, plugin.getState());
|
||||
plugin.setState(PluginState.RUNNING);
|
||||
CoreEventsHandler.pluginRunning(plugin);
|
||||
}
|
||||
catch(Throwable throwable)
|
||||
{
|
||||
LOGGER.warn("Plugin " + id + " failed to run. Entering FAILED state. (last state: " + plugin.getState() + ")", throwable);
|
||||
plugin.setState(PluginState.FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a plugin from running.
|
||||
* @param id The unique ID of the plugin.
|
||||
*/
|
||||
public void stopPlugin(String id)
|
||||
{
|
||||
// Check for plugin existence.
|
||||
if(!plugins.containsKey(id))
|
||||
{
|
||||
LOGGER.debug("Plugin with ID {} doesn't exist.", id);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Stopping plugin {}.", id);
|
||||
PluginWrapper plugin = plugins.get(id);
|
||||
|
||||
// Check if plugin state is RUNNING
|
||||
if(!plugin.getState().equals(PluginState.RUNNING))
|
||||
{
|
||||
LOGGER.debug("Current plugin state does not allow it to stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getInstance().stop();
|
||||
LOGGER.info("Stopped plugin {}.", id);
|
||||
plugin.setState(PluginState.STOPPED);
|
||||
CoreEventsHandler.pluginStopped(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload a plugin.
|
||||
* @param id The unique ID of the plugin.
|
||||
*/
|
||||
public void unloadPlugin(String id)
|
||||
{
|
||||
// Check for plugin existence.
|
||||
if(!plugins.containsKey(id))
|
||||
{
|
||||
LOGGER.debug("Plugin with ID {} doesn't exist.", id);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Unloading plugin {}.", id);
|
||||
PluginWrapper plugin = plugins.get(id);
|
||||
|
||||
// Check if plugin state is STOPPED
|
||||
if(!plugin.getState().equals(PluginState.STOPPED))
|
||||
{
|
||||
LOGGER.debug("Current plugin state does not allow it to be unloaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getInstance().unload();
|
||||
LOGGER.info("Unloaded plugin {}.", id);
|
||||
plugin.setState(PluginState.UNLOADED);
|
||||
CoreEventsHandler.pluginUnloaded(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all plugins and their state.
|
||||
*/
|
||||
public Map<String, PluginWrapper> getPlugins()
|
||||
{
|
||||
return Collections.unmodifiableMap(plugins);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package net.pingex.dcf.modularity;
|
||||
|
||||
/**
|
||||
* Every possible plugin states
|
||||
*/
|
||||
public enum PluginState
|
||||
{
|
||||
/**
|
||||
* The plugin is unloaded, ie. the loader did not run {@code load()}, or executed {@code unload()} on the associated instance.
|
||||
*/
|
||||
UNLOADED,
|
||||
|
||||
/**
|
||||
* The loader ran {@code load()}.
|
||||
*/
|
||||
LOADED,
|
||||
|
||||
/**
|
||||
* The loader ran {@code run()}.
|
||||
*/
|
||||
RUNNING,
|
||||
|
||||
/**
|
||||
* The plugin has been running once, but has now stopped.
|
||||
*/
|
||||
STOPPED,
|
||||
|
||||
/**
|
||||
* The plugin fucked up somewhere.
|
||||
*/
|
||||
FAILED
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package net.pingex.dcf.modularity;
|
||||
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* A class wrapping a {@code IPlugin} class.
|
||||
*/
|
||||
public class PluginWrapper
|
||||
{
|
||||
/**
|
||||
* Unique ID of the plugin.
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* Version of this plugin.
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* Plugin short description
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* Matching plugin class.
|
||||
*/
|
||||
private Class<? extends IPlugin> pluginClass;
|
||||
|
||||
/**
|
||||
* The associated class loader.
|
||||
*/
|
||||
private URLClassLoader classLoader;
|
||||
|
||||
/**
|
||||
* The plugin main instance.
|
||||
*/
|
||||
private IPlugin instance;
|
||||
|
||||
/**
|
||||
* The current state of this plugin.
|
||||
*/
|
||||
private PluginState state;
|
||||
|
||||
public PluginWrapper(Class<? extends IPlugin> pluginClass, URLClassLoader classLoader) throws IllegalAccessException, InstantiationException
|
||||
{
|
||||
this.id = pluginClass.getAnnotation(Plugin.class).id();
|
||||
this.version = pluginClass.getAnnotation(Plugin.class).version();
|
||||
this.description = pluginClass.getAnnotation(Plugin.class).description();
|
||||
this.pluginClass = pluginClass;
|
||||
this.classLoader = classLoader;
|
||||
this.instance = pluginClass.newInstance();
|
||||
this.state = PluginState.UNLOADED;
|
||||
}
|
||||
|
||||
// GETTERS
|
||||
|
||||
public String getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getVersion()
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getDescription()
|
||||
{
|
||||
return description;
|
||||
}
|
||||
|
||||
public Class<? extends IPlugin> getPluginClass()
|
||||
{
|
||||
return pluginClass;
|
||||
}
|
||||
|
||||
public URLClassLoader getClassLoader()
|
||||
{
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
public IPlugin getInstance()
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
public PluginState getState()
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
// SETTERS
|
||||
|
||||
public void setInstance(IPlugin instance)
|
||||
{
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
public void setState(PluginState state)
|
||||
{
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
return obj instanceof PluginWrapper && id.equals(((PluginWrapper) obj).getId());
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package net.pingex.dcf.modularity.events;
|
||||
|
||||
import net.pingex.dcf.core.GatewayConnectionManager;
|
||||
import net.pingex.dcf.modularity.PluginWrapper;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Contains all foreign event handlers for D4J.
|
||||
*/
|
||||
public class EventManager
|
||||
{
|
||||
/**
|
||||
* Singleton instance.
|
||||
*/
|
||||
private static EventManager ourInstance = new EventManager();
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static Logger LOGGER = LogManager.getLogger(EventManager.class);
|
||||
|
||||
/**
|
||||
* Get current instance
|
||||
* @return The unique instance.
|
||||
*/
|
||||
public static EventManager getInstance()
|
||||
{
|
||||
return ourInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains all plugins which implements IWithEvents
|
||||
*/
|
||||
private Set<PluginWrapper> handlers;
|
||||
|
||||
/**
|
||||
* Enforce singleton.
|
||||
*/
|
||||
private EventManager()
|
||||
{
|
||||
handlers = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the plugin has event handlers and register it to active handlers pool.
|
||||
* @param plugin Target plugin.
|
||||
*/
|
||||
public void checkAndRegister(PluginWrapper plugin)
|
||||
{
|
||||
LOGGER.trace("Checking {} for event handlers.", plugin.getId());
|
||||
|
||||
if(IWithEventHandlers.class.isAssignableFrom(plugin.getPluginClass()))
|
||||
{
|
||||
LOGGER.debug("Registering event handlers for plugin {}.", plugin.getId());
|
||||
handlers.add(plugin);
|
||||
updateConnectionHandlers(); // Trigger update
|
||||
}
|
||||
else LOGGER.trace("Plugin {} has no event handlers.", plugin.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister plugin from active handlers pool.
|
||||
* @param plugin Target plugin.
|
||||
*/
|
||||
public void unregister(PluginWrapper plugin)
|
||||
{
|
||||
if(handlers.contains(plugin))
|
||||
{
|
||||
LOGGER.debug("Unregistering event handlers for plugin {}.", plugin.getId());
|
||||
handlers.remove(plugin);
|
||||
updateConnectionHandlers(); // Trigger update
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all event handlers into a single set
|
||||
* @return Final set
|
||||
*/
|
||||
public Set<Class<?>> collectAllHandlers()
|
||||
{
|
||||
Set<Class<?>> toReturn = new HashSet<>();
|
||||
handlers.forEach(pluginWrapper -> toReturn.addAll(((IWithEventHandlers) pluginWrapper.getInstance()).getEventHandlers()));
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegated method
|
||||
*/
|
||||
public void updateConnectionHandlers()
|
||||
{
|
||||
GatewayConnectionManager.updateListeners(collectAllHandlers());
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.pingex.dcf.modularity.events;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Interface which indicates that the plugin has event handlers.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface IWithEventHandlers
|
||||
{
|
||||
/**
|
||||
* Gives all event handlers classes for the plugin.
|
||||
* @return All events handlers to submit to D4J.
|
||||
* TODO: Class to Object
|
||||
*/
|
||||
Set<Class<?>> getEventHandlers();
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains event manager for plugins event handlers.
|
||||
*/
|
||||
package net.pingex.dcf.modularity.events;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Contains all classes needed to load modules
|
||||
*/
|
||||
package net.pingex.dcf.modularity;
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Main package of DCF, contains everything to make it run !
|
||||
*/
|
||||
package net.pingex.dcf;
|
@ -0,0 +1,203 @@
|
||||
package net.pingex.dcf.util;
|
||||
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import sx.blah.discord.api.IDiscordClient;
|
||||
import sx.blah.discord.handle.obj.IGuild;
|
||||
import sx.blah.discord.handle.obj.IRole;
|
||||
import sx.blah.discord.handle.obj.IUser;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Util class used to parse a String into primitive types.
|
||||
*/
|
||||
public class ArgumentParser
|
||||
{
|
||||
/**
|
||||
* Parses a String into a primitive variable.
|
||||
* Choice between boolean, byte, short, int, long, float, double, String and their respective wrapper types.
|
||||
* @param target Target primitive
|
||||
* @param value Input value
|
||||
* @return `null` if the value is null or the string content is null, or the parsed value.
|
||||
* @throws IllegalArgumentException If the target primitive/class isn't part of the previous list.
|
||||
* @throws NumberFormatException if the cast failed.
|
||||
*/
|
||||
public static Object parse(Class<?> target, String value) throws IllegalArgumentException
|
||||
{
|
||||
if(value == null || value.equalsIgnoreCase("null")) return null;
|
||||
if(Boolean.class == target || Boolean.TYPE == target) return Boolean.parseBoolean(value);
|
||||
if(Byte.class == target || Byte.TYPE == target) return Byte.parseByte(value);
|
||||
if(Short.class == target || Short.TYPE == target) return Short.parseShort(value);
|
||||
if(Integer.class == target || Integer.TYPE == target) return Integer.parseInt(value);
|
||||
if(Long.class == target || Long.TYPE == target) return Long.parseLong(value);
|
||||
if(Float.class == target || Float.TYPE == target) return Float.parseFloat(value);
|
||||
if(Double.class == target || Double.TYPE == target) return Double.parseDouble(value);
|
||||
if(String.class == target) return value;
|
||||
throw new IllegalArgumentException("Unknown target type");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the input arguments to a list of argument of various types.
|
||||
* Use a primitive type for an argument that is required.
|
||||
* Use a wrapper type to an argument that is optional, thereby the said argument can be {@code null}.
|
||||
* Any extra argument not specified in {@code targetTypes} are ignored and will not appear in the output list.
|
||||
*
|
||||
* @param targetTypes A list of types that the {@code arguments} will be checked against.
|
||||
* @param arguments Input arguments to check
|
||||
* @return The parsed arguments whose types matches {@code targetTypes}
|
||||
* @throws ParserException if the parsing failed.
|
||||
*/
|
||||
public static List<Object> parseAll(List<Class<?>> targetTypes, List<String> arguments) throws ParserException
|
||||
{
|
||||
if(targetTypes.size() == 0) throw new ParserException("Nothing to parse.");
|
||||
|
||||
List<Object> outputList = new ArrayList<>();
|
||||
|
||||
for(int i = 0; i < targetTypes.size(); i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Object parsingOutput = parse(targetTypes.get(i), arguments.get(i));
|
||||
if(parsingOutput == null)
|
||||
{
|
||||
if(ClassUtils.isPrimitiveWrapper(targetTypes.get(i)) || targetTypes.get(i) == String.class) outputList.add(null); // Allows null
|
||||
else throw new ParserException("Invalid arguments."); // Disallows null
|
||||
}
|
||||
else outputList.add(parsingOutput);
|
||||
}
|
||||
catch(IndexOutOfBoundsException e) // Not enough arguments supplied
|
||||
{
|
||||
if(ClassUtils.isPrimitiveWrapper(targetTypes.get(i)) || targetTypes.get(i) == String.class) outputList.add(null); // Allows null
|
||||
else throw new ParserException("Not enough arguments supplied."); // Disallows null
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
if(e instanceof NumberFormatException) throw new ParserException("Invalid arguments."); // Cannot be casted because the user fucked up
|
||||
else throw new ParserException("Parsing error, report to dev.", e); // Cannot be casted because the dev fucked up
|
||||
}
|
||||
}
|
||||
|
||||
return outputList;
|
||||
}
|
||||
|
||||
private static Pattern usernamePattern = Pattern.compile("^.*#\\d{4}$");
|
||||
private static Pattern idPattern = Pattern.compile("^\\d{18}$");
|
||||
|
||||
/**
|
||||
* Binds an username or an UID to an actual {@link IUser}.
|
||||
* The following username input formats are allowed:
|
||||
* <ul>
|
||||
* <li>18-digit UID: "123456789012345678"</li>
|
||||
* <li>Username and 4-digit discriminator: "Username#1234"</li>
|
||||
* </ul>
|
||||
*
|
||||
* Unknown formats will cause a {@link ParserException}.
|
||||
*
|
||||
* @param input String to parse
|
||||
* @param client Discord instance to look for actual users
|
||||
* @return A filled Optional when an user was found, an empty one otherwise.
|
||||
* @throws ParserException Unknown format
|
||||
*/
|
||||
public static Optional<IUser> checkParseUsernameOrID(String input, IDiscordClient client) throws ParserException
|
||||
{
|
||||
IUser foundUser = null;
|
||||
|
||||
if(idPattern.matcher(input).matches()) foundUser = client.getUserByID(input); // UID
|
||||
else if(usernamePattern.matcher(input).matches()) // username#1234
|
||||
{
|
||||
for(IUser iUser : client.getUsers()) // Greedy
|
||||
if(input.equalsIgnoreCase(iUser.getName() + "#" + iUser.getDiscriminator()))
|
||||
{
|
||||
foundUser = iUser;
|
||||
break; // Limit greediness
|
||||
}
|
||||
}
|
||||
else throw new ParserException("Invalid argument."); // ?
|
||||
|
||||
return Optional.ofNullable(foundUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a guild or an ID to an actual {@link IGuild}.
|
||||
* The following guild input formats are allowed:
|
||||
* <ul>
|
||||
* <li>18-digit GID: "123456789012345678"</li>
|
||||
* <li>Guild name: "Discord Developers"</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param input String to parse
|
||||
* @param client Discord instance to look for guilds
|
||||
* @return A filled Optional when a guild was found, an empty one otherwise.
|
||||
*/
|
||||
public static Optional<IGuild> checkParseGuildOrID(String input, IDiscordClient client)
|
||||
{
|
||||
IGuild foundGuild = null;
|
||||
|
||||
if(idPattern.matcher(input).matches()) foundGuild = client.getGuildByID(input); // ID
|
||||
else
|
||||
for(IGuild i : client.getGuilds()) // Name
|
||||
if(input.equalsIgnoreCase(i.getName()))
|
||||
{
|
||||
foundGuild = i;
|
||||
break;
|
||||
}
|
||||
|
||||
return Optional.ofNullable(foundGuild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a role or an ID to an actual {@link IRole}.
|
||||
* The following role input formats are allowed:
|
||||
* <ul>
|
||||
* <li>18-digit RID: "123456789012345678"</li>
|
||||
* <li>Role name: "Admin"</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param input String to parse
|
||||
* @param guild Guild to look for roles
|
||||
* @return A filled Optional when a role was found, an empty one otherwise.
|
||||
*/
|
||||
public static Optional<IRole> checkParseRoleOrID(String input, IGuild guild)
|
||||
{
|
||||
IRole foundRole = null;
|
||||
|
||||
if(idPattern.matcher(input).matches()) foundRole = guild.getRoleByID(input); // ID
|
||||
else
|
||||
for(IRole i : guild.getRoles()) // Name
|
||||
if(input.equalsIgnoreCase(i.getName()))
|
||||
{
|
||||
foundRole = i;
|
||||
break;
|
||||
}
|
||||
|
||||
return Optional.ofNullable(foundRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown whenever any parse operation failed.
|
||||
*/
|
||||
public static class ParserException extends RuntimeException
|
||||
{
|
||||
public ParserException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ParserException(Throwable cause)
|
||||
{
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ParserException()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
public ParserException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package net.pingex.dcf.util;
|
||||
|
||||
import net.jodah.failsafe.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import sx.blah.discord.api.IDiscordClient;
|
||||
import sx.blah.discord.handle.obj.IChannel;
|
||||
import sx.blah.discord.handle.obj.IMessage;
|
||||
import sx.blah.discord.util.DiscordException;
|
||||
import sx.blah.discord.util.MissingPermissionsException;
|
||||
import sx.blah.discord.util.RateLimitException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Contains some useful functions to avoid having to rewrite the same code over and over
|
||||
*/
|
||||
public class DiscordInteractionsUtil
|
||||
{
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
private static final Logger LOGGER = LogManager.getLogger(DiscordInteractionsUtil.class);
|
||||
|
||||
/**
|
||||
* Maximum amount of retries for an interaction
|
||||
*/
|
||||
public static final int MAX_GW_RETRIES = 5;
|
||||
|
||||
/**
|
||||
* Delay between retries
|
||||
*/
|
||||
public static final int GW_RETRY_DELAY = 3;
|
||||
|
||||
/**
|
||||
* Retry policy for sending a message
|
||||
*/
|
||||
private static final RetryPolicy MESSAGE_RETRY_POLICY = new RetryPolicy()
|
||||
.retryOn(RateLimitException.class, DiscordException.class)
|
||||
.withDelay(GW_RETRY_DELAY, TimeUnit.SECONDS)
|
||||
.withMaxRetries(MAX_GW_RETRIES)
|
||||
.abortOn(MissingPermissionsException.class);
|
||||
|
||||
/**
|
||||
* Sends a message
|
||||
* @param channel Channel to send the message
|
||||
* @param message Message to send
|
||||
* @return The sent message, or {@code Optional.empty()}
|
||||
*/
|
||||
public static Optional<IMessage> sendMessage(IChannel channel, String message)
|
||||
{
|
||||
LOGGER.debug("Attempting to send a message (error-tolerant operation).");
|
||||
|
||||
SyncFailsafe fs = Failsafe.with(MESSAGE_RETRY_POLICY)
|
||||
.onFailedAttempt(failure -> LOGGER.warn("Failed to send message.", failure))
|
||||
.onRetry((c, f, stats) -> LOGGER.warn("Message sending failure #{}. Retrying.", stats.getExecutions()))
|
||||
.onSuccess((result, context) -> LOGGER.debug("Sent message after {} attempts.", context.getExecutions()))
|
||||
.onAbort(failure -> LOGGER.warn("Aborted sending message.", failure));
|
||||
|
||||
try
|
||||
{
|
||||
return Optional.of((IMessage) fs.get(() -> channel.sendMessage(message)));
|
||||
}
|
||||
catch(FailsafeException e)
|
||||
{
|
||||
LOGGER.error("Failed to send message (exceeded amounts or trials or aborted)", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Policy for a gateway disconnection
|
||||
*/
|
||||
private static final RetryPolicy GW_LOGOUT_POLICY = new RetryPolicy()
|
||||
.retryOn(RateLimitException.class, DiscordException.class)
|
||||
.withDelay(GW_RETRY_DELAY, TimeUnit.SECONDS)
|
||||
.withMaxRetries(MAX_GW_RETRIES);
|
||||
|
||||
/**
|
||||
* Disconnect from the gateway
|
||||
* @param client Client interface
|
||||
*/
|
||||
public static void disconnect(IDiscordClient client)
|
||||
{
|
||||
LOGGER.debug("Attempting to disconnect user #{} from the gateway (error-tolerant operation).", client.getOurUser().getID());
|
||||
|
||||
SyncFailsafe fs = Failsafe.with(GW_LOGOUT_POLICY)
|
||||
.onFailedAttempt(failure -> LOGGER.warn("Failed to disconnect.", failure))
|
||||
.onRetry((result, failure, context) -> LOGGER.warn("Disconnection failure #{}. Retrying.", context.getExecutions()))
|
||||
.onSuccess((result, context) -> LOGGER.info("Disconnected user {} from gateway after {} attempts.", client.getOurUser().getID(), context.getExecutions()));
|
||||
|
||||
try
|
||||
{
|
||||
fs.run(client::logout);
|
||||
}
|
||||
catch(FailsafeException e)
|
||||
{
|
||||
LOGGER.error("Connection #{} failed to disconnect (amount of trials exceeded)", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Some useful classes and functions
|
||||
*/
|
||||
package net.pingex.dcf.util;
|
@ -0,0 +1,13 @@
|
||||
name = LoggingConfig
|
||||
|
||||
appender.console.type = Console
|
||||
appender.console.name = STDOUT
|
||||
appender.console.layout.type = PatternLayout
|
||||
appender.console.layout.pattern = %d{dd/MM/yyyy HH:mm:ss} [%t/%p] %c:%M - %m%n
|
||||
|
||||
rootLogger.level = info
|
||||
rootLogger.appenderRef.stdout.ref = STDOUT
|
||||
|
||||
# DCF out
|
||||
logger.jetty.name = net.pingex.dcf
|
||||
logger.jetty.level = trace
|
Reference in New Issue