Compare commits

..

No commits in common. 'master' and 'v0.0' have entirely different histories.
master ... v0.0

4
.gitignore vendored

@ -68,7 +68,3 @@ 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,93 +1,14 @@
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
}
}
}

@ -1,17 +0,0 @@
# 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 = 'dcf' rootProject.name = 'dcm'

@ -1,51 +0,0 @@
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();
}
}

@ -1,219 +0,0 @@
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;
}
}

@ -1,125 +0,0 @@
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);
}
}
}

@ -1,141 +0,0 @@
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();
}
}

@ -1,122 +0,0 @@
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;
}
}

@ -1,10 +0,0 @@
package net.pingex.dcf.commands;
/**
* The body of a command.
*/
@FunctionalInterface
public interface ICommandExecutor
{
void execute(Context context) throws Throwable;
}

@ -1,16 +0,0 @@
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();
}

@ -1,185 +0,0 @@
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());
}
}

@ -1,97 +0,0 @@
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);
}
}

@ -1,187 +0,0 @@
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();
}
}

@ -1,31 +0,0 @@
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();
}

@ -1,37 +0,0 @@
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;
}
}

@ -1,43 +0,0 @@
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;
}
}

@ -1,6 +0,0 @@
/**
* 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;

@ -1,18 +0,0 @@
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();
}

@ -1,74 +0,0 @@
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);
}
}
}

@ -1,4 +0,0 @@
/**
* This package contains everything related to command options.
*/
package net.pingex.dcf.commands.options;

@ -1,4 +0,0 @@
/**
* Contains everything related to commands parsing and executing.
*/
package net.pingex.dcf.commands;

@ -1,47 +0,0 @@
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;
}
}

@ -1,32 +0,0 @@
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;
}

@ -1,17 +0,0 @@
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);
}
}

@ -1,4 +0,0 @@
/**
* Contains command parsers.
*/
package net.pingex.dcf.commands.parser;

@ -1,80 +0,0 @@
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);
}
}
}

@ -1,101 +0,0 @@
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();
}
}

@ -1,46 +0,0 @@
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);
}

@ -1,86 +0,0 @@
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;
}
}

@ -1,267 +0,0 @@
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());
}
}

@ -1,46 +0,0 @@
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;
}
}

@ -1,57 +0,0 @@
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;
}
}

@ -1,61 +0,0 @@
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;
}
}

@ -1,69 +0,0 @@
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;
}
}

@ -1,4 +0,0 @@
/**
* This package contains all sub-checks for PermissionCheck.
*/
package net.pingex.dcf.commands.permissions.audit;

@ -1,4 +0,0 @@
/**
* This package contains everything related to permissions and the ability for an user to run something.
*/
package net.pingex.dcf.commands.permissions;

@ -1,141 +0,0 @@
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.");
}
}

@ -1,118 +0,0 @@
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)
{
}
}

@ -1,75 +0,0 @@
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);
}
}

@ -1,4 +0,0 @@
/**
* Very core components: configuration, handlers, and so on
*/
package net.pingex.dcf.core;

@ -1,30 +0,0 @@
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();
}

@ -1,29 +0,0 @@
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.";
}

@ -1,310 +0,0 @@
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);
}
}

@ -1,32 +0,0 @@
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
}

@ -1,110 +0,0 @@
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());
}
}

@ -1,97 +0,0 @@
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());
}
}

@ -1,17 +0,0 @@
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();
}

@ -1,4 +0,0 @@
/**
* Contains event manager for plugins event handlers.
*/
package net.pingex.dcf.modularity.events;

@ -1,4 +0,0 @@
/**
* Contains all classes needed to load modules
*/
package net.pingex.dcf.modularity;

@ -1,4 +0,0 @@
/**
* Main package of DCF, contains everything to make it run !
*/
package net.pingex.dcf;

@ -1,203 +0,0 @@
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);
}
}
}

@ -1,101 +0,0 @@
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);
}
}
}

@ -1,4 +0,0 @@
/**
* Some useful classes and functions
*/
package net.pingex.dcf.util;

@ -1,13 +0,0 @@
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