Compare commits

..

90 Commits
v0.0 ... master

Author SHA1 Message Date
Pingex aka Raphaël 9b47800da4 perm:audit improvment. 9 years ago
Pingex aka Raphaël f8134a4f8b Permissions commands are now registered. 9 years ago
Pingex aka Raphaël 561828f223 Set user & set role permission commands.
Permissions setting are only available for the current guild, as it may compromise other's guild permissions if messed up with.
9 years ago
Pingex aka Raphaël 00d4961819 Fix: Audits with fake contexts crash whenever a command uses GUILD_OWNER 9 years ago
Pingex aka Raphaël 65d5972fa5 Dump roles ID command. 9 years ago
Pingex aka Raphaël 5033de0355 IRole parser. 9 years ago
Pingex aka Raphaël ee4e1e0c84 New permissions checking command 9 years ago
Pingex aka Raphaël 56ca209148 Discord4J 2.7.0 support 9 years ago
Pingex aka Raphaël b1a59cbb15 Added IGuild to Context 9 years ago
Pingex aka Raphaël 34af04a7b0 ICommandExecutor now uses a Context instead of a MRE+Arguments. 9 years ago
Pingex aka Raphaël a38e9e7ce7 Moved permissions package to commands package. 9 years ago
Pingex aka Raphaël 6d4ac31d79 Effective removal of the old permissions system. 9 years ago
Pingex aka Raphaël b505e79d2a Effective removal of annotated commands. 9 years ago
Pingex aka Raphaël 38188d941d Deprecated AnnotatedCommand.
I cannot support AnnotatedCommand with command options. Sorry !
9 years ago
Pingex aka Raphaël 299b417478 Deprecated DefaultPermission command internal. 9 years ago
Pingex aka Raphaël a40088ecfe Private parser attribute in CH. 9 years ago
Pingex aka Raphaël dd6403dc17 Removed old permissions handling on CommandHandler. 9 years ago
Pingex aka Raphaël d8b76f3d01 New permissions API. Deprecated old API until complete removal. 9 years ago
Pingex aka Raphaël a5938c6c0c Simpler basic checks injection and a more verbose audit debug message.
The audit debug message now displays subaudits name and results.
9 years ago
Pingex aka Raphaël 7bc0409d8c Added new constructor & subaudit existence to AuditResult. 9 years ago
Pingex aka Raphaël 3c354c0f00 Migrated CH to the new audit system. 9 years ago
Pingex aka Raphaël e97cff9f68 Removed hardcoded ScopeOptions 9 years ago
Pingex aka Raphaël 0c9db2814d Scope checking option implementation. 9 years ago
Pingex aka Raphaël ee2c6cb38c New ScopeOption class for options framework.
Also moved the scope enum to ScopeOption class. Refactored every occurence of the scope enum in the project for transitionning purpose.
9 years ago
Pingex aka Raphaël f4fe2c5a6a Moved ICommandOption to a separate package. 9 years ago
Pingex aka Raphaël 23b8b9e883 New options parameter to command. 9 years ago
Pingex aka Raphaël cf25b259d5 Aliases are now unmodifiable. 9 years ago
Pingex aka Raphaël afc102d49e Checks are now automatically added on runtime. 9 years ago
Pingex aka Raphaël 86a68de6ab First 2 basic checks 9 years ago
Pingex aka Raphaël 9d6ccc3344 More context 9 years ago
Pingex aka Raphaël 17185123e4 Audit main work #1. 9 years ago
Pingex aka Raphaël 913ff8d573 AuditResult builder and enum adjustements. 9 years ago
Pingex aka Raphaël e1c38102f8 Quick refactor #2. 9 years ago
Pingex aka Raphaël a754f46337 Quick refactor 9 years ago
Pingex aka Raphaël c7268fe850 Changed array to an ordered list for further audit troubleshooting.
Also rephrased some descriptions.
9 years ago
Pingex aka Raphaël b4ee8f84a1 Typo 9 years ago
Pingex aka Raphaël 8e5dec8aa6 Audits preliminary work 9 years ago
Pingex aka Raphaël c2ad1056d9 Introducing Command Scopes.
Command Scopes allows to reject/accept an invoked command before it gets executed based on the originating channel.
9 years ago
Pingex aka Raphaël b935767eec Deprecated 9 years ago
Pingex aka Raphaël 18c3309047 Feels better this way 9 years ago
Pingex aka Raphaël 85364cf910 Refactored GCM part 2/2. Also removed unused imports in the same class. 9 years ago
Pingex aka Raphaël dc67379a1f Removed multiple connections (broken) support (p1/2). 9 years ago
Pingex aka Raphaël 53597f4b7f build edits (2nd attempt) 9 years ago
Pingex aka Raphaël 1a40085f16 build file adjustements 9 years ago
Pingex aka Raphaël f5022a9cee Dep fix 9 years ago
Pingex aka Raphaël c1dc471bbe D4J 2.6.1 support 9 years ago
Pingex aka Raphaël ce877a9f0b Minor change 9 years ago
Pingex aka Raphaël 6fc5fad4dd Disabled command check 9 years ago
Pingex aka Raphaël d317018666 jar versioning 9 years ago
Pingex aka Raphaël 95d60c3e95 Restricted commands are now filtered. 9 years ago
Pingex aka Raphaël adc51ba1ca Changed internal:usage behavior. 9 years ago
Pingex aka Raphaël e9d6f249d8 Refactored arguments checks, also added guild argument for perm:canrun. 9 years ago
Pingex aka Raphaël e4e126aea8 Removed unnecessary imports. 9 years ago
Pingex aka Raphaël 1b89180c86 `perm:setuser`, also adjusted `perm:canrun`. 9 years ago
Pingex aka Raphaël fa2668e0c2 Auto username/UID parser, and small change in parse function. 9 years ago
Pingex aka Raphaël 44f7dcbfa2 Catch DB problems 9 years ago
Pingex aka Raphaël 82e709c2e2 Permissions Provider implementation. 9 years ago
Pingex aka Raphaël 5a3ac763eb Permissions package, the following + canRun command 9 years ago
Pingex aka Raphaël 38ccb6e9f6 Permissions framework + stub DB 9 years ago
Pingex aka Raphaël 073d2455cb Added general.bot_owner config key. 9 years ago
Pingex aka Raphaël 82c081cd3c Minor changes. 9 years ago
Pingex aka Raphaël eb8ec72406 Internal commands and command parser. 9 years ago
Pingex aka Raphaël 60c12e3290 Case-insensitive command requests. 9 years ago
Pingex aka Raphaël 975eab204d Small changes 9 years ago
Pingex aka Raphaël 73f350aacc Async Command handling. 9 years ago
Pingex aka Raphaël dea35f59a6 Small change in "Command not found" message. 9 years ago
Pingex aka Raphaël 150d8bd52b Run commands ! 9 years ago
Pingex aka Raphaël 519af58989 Forgot to also pass MRE into ICommandExecutor. 9 years ago
Pingex aka Raphaël f5fbd49b12 Aliases were always conflicting 9 years ago
Pingex aka Raphaël 5b0aede82d Hooked up to the system. 9 years ago
Pingex aka Raphaël a4e493e577 Missing unregister 9 years ago
Pingex aka Raphaël 279d8aae13 Plugin <=> CommandSystem interaction. 9 years ago
Pingex aka Raphaël 2b0238dff1 Command and Command bank. 9 years ago
Pingex aka Raphaël cb9767f8bd Whoops, forgot to move ParserException. 9 years ago
Pingex aka Raphaël dac18daa8e Refactor: moved parser in a separate package. 9 years ago
Pingex aka Raphaël 01057f8492 Commands, step 1: parsing and handling. 9 years ago
Pingex aka Raphaël 6d9f31f592 Data storage token 9 years ago
Pingex aka Raphaël d4785ade27 Typo fix 9 years ago
Pingex aka Raphaël 0eac4cd14a Static GCM 9 years ago
Pingex aka Raphaël c5b33ad415 package-info for modularity.events 9 years ago
Pingex aka Raphaël 0088bc59c2 Plugins event handling through IWithEventHandlers. 9 years ago
Pingex aka Raphaël 72d4a3b54e Duplicate plugin ID fix. 9 years ago
Pingex aka Raphaël f33b8e1249 Static CEH 9 years ago
Pingex aka Raphaël 193383a60c D4J 2.5.1 9 years ago
Pingex aka Raphaël 3c2e6694ad Quick edits. 9 years ago
Pingex aka Raphaël fcef6deeeb Mostly working plugin loader. 9 years ago
Pingex aka Raphaël 107bf41a76 Basic modularity system: plugin preloading. 9 years ago
Pingex aka Raphaël 10376954f9 Small changes. 9 years ago
Pingex aka Raphaël dd30fbecdc Added D4J, configured logging. Also basic event handler. 9 years ago
Pingex aka Raphaël 6bc355b9ac Configuration and loggging 9 years ago

4
.gitignore vendored

@ -68,3 +68,7 @@ gradle-app.setting
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties # gradle/wrapper/gradle-wrapper.properties
dcf.properties
plugins/
data/

@ -1,14 +1,93 @@
group 'net.pingex' group 'net.pingex'
version '0.1'
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: "com.zoltu.git-versioning"
sourceCompatibility = 1.8 sourceCompatibility = 1.8
repositories { repositories {
mavenCentral() 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 { 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' 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