Added D4J, configured logging. Also basic event handler.

keep-around/d31701866686f66088b78de2e29736ae36e55a68
Pingex aka Raphaël 9 years ago
parent 6bc355b9ac
commit dd30fbecdc

@ -7,16 +7,28 @@ sourceCompatibility = 1.8
repositories {
mavenCentral()
jcenter()
maven {
url "https://jitpack.io"
}
}
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'
runtime group: 'commons-beanutils', name: 'commons-beanutils', version: '1.9.2'
// Discord Gateway API
compile group: 'com.github.austinv11', name: 'Discord4j', version: '2.4.9'
// Util
compile group: 'net.jodah', name: 'failsafe', version: '0.8.3'
testCompile group: 'junit', name: 'junit', version: '4.11'
}

@ -1,2 +1,10 @@
# Bot display name
general.bot_name = DCF-enabled Bot
general.bot_name = DCF Bot
# Initial connection token...
discord.token = tokenGoesHere
# ... or username/password combination if you don't use bot mode
# comment out discord.token if using us/pw tuple
#discord.username = email
#discord.password = superSecretPassword

@ -1,14 +1,19 @@
package net.pingex.dcf;
import net.pingex.dcf.core.Configuration;
import net.pingex.dcf.core.GatewayConnectionsManager;
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);
/**
@ -16,10 +21,16 @@ public class DiscordCommandableFramework
*/
public static void main(String[] args)
{
LOGGER.info("Hello World");
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");
LOGGER.info("I'm " + Configuration.BOT_NAME);
// Set up initial connection
ClientBuilder builder = new ClientBuilder();
if(Configuration.isConnectionToken()) builder.withToken(Configuration.CONNECTION_TOKEN);
else builder.withLogin(Configuration.CONNECTION_USERNAME, Configuration.CONNECTION_PASSWORD);
GatewayConnectionsManager.getInstance().registerConnection(builder);
}
}

@ -19,6 +19,10 @@ 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);
/**
@ -57,8 +61,31 @@ public class Configuration
/**
* Bot display name
*/
public static String BOT_NAME = "DCF-enabled Bot";
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;
/**
* Tells if the bot is configured to connect using a token or an username/password tuple.
* @return
*/
public static boolean isConnectionToken()
{
return store.containsKey("discord.token");
}
/**
* Load config keys
@ -66,5 +93,18 @@ public class Configuration
public static void init()
{
BOT_NAME = store.getString("general.bot_name", BOT_NAME);
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");
}
}
}

@ -0,0 +1,80 @@
package net.pingex.dcf.core;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import sx.blah.discord.api.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);
/**
* Singleton instance
*/
private static CoreEventsHandler INSTANCE = new CoreEventsHandler();
/**
* Get current instance
*/
public static CoreEventsHandler getInstance()
{
return INSTANCE;
}
/**
* Basic constructor, enforcing singleton
*/
private CoreEventsHandler()
{}
@EventSubscriber
public void onReady(ReadyEvent event)
{
LOGGER.info("Connection with user #" + event.getClient().getOurUser().getID() + " ready.");
}
@EventSubscriber
public void onDiscordDisconnected(DiscordDisconnectedEvent event)
{
LOGGER.warn("Discord connection with user #" + event.getClient().getOurUser().getID() + "lost (" + event.getReason().toString() + "). Reconnecting");
}
@EventSubscriber
public void onMessageSend(MessageSendEvent event)
{
LOGGER.trace("Sent message to channel #" + event.getMessage().getChannel().getID() + ".");
}
@EventSubscriber
public void onMention(MentionEvent event)
{
LOGGER.trace("Received mention from channel #" + event.getMessage().getChannel().getID() + ".");
}
@EventSubscriber
public void onMessageReceived(MessageReceivedEvent event)
{
LOGGER.trace("Received message from channel #" + event.getMessage().getChannel().getID() + ".");
}
@EventSubscriber
public void onGuildUnavailable(GuildUnavailableEvent event)
{
LOGGER.warn("Guild #" + event.getGuild().getID() + " is unavailable.");
}
@EventSubscriber
public void onGuildCreate(GuildCreateEvent event)
{
LOGGER.info("Joined guild #" + event.getGuild().getID() + ".");
}
}

@ -0,0 +1,152 @@
package net.pingex.dcf.core;
import net.pingex.dcf.util.DiscordInteractionsUtil;
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 sx.blah.discord.util.HTTP429Exception;
import sx.blah.discord.util.MessageBuilder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Storage of all active connection to Discord gateway
*/
public class GatewayConnectionsManager
{
/**
* Main datastore
*/
private Set<IDiscordClient> connectionsDatastore;
/**
* List of registered listeners for each connection, as we can't access registered listeners in IDC dispatcher.
*/
private Map<IDiscordClient, Set<Object>> registeredListeners;
/**
* Singleton unique instance
*/
private static final GatewayConnectionsManager INSTANCE = new GatewayConnectionsManager();
/**
* Logger
*/
private static final Logger LOGGER = LogManager.getLogger(GatewayConnectionsManager.class);
/**
* Get singleton instance
*/
public static GatewayConnectionsManager getInstance()
{
return INSTANCE;
}
/**
* Private constructor, to enforce singleton
*/
private GatewayConnectionsManager()
{
connectionsDatastore = new HashSet<>();
registeredListeners = new HashMap<>();
}
/**
* Registers a connection and login automatically
* @param builder Filled and builder ready for log in
*/
public void registerConnection(ClientBuilder builder)
{
LOGGER.info("Registering new connection");
try
{
IDiscordClient builtConnection = builder.login();
connectionsDatastore.add(builtConnection);
builtConnection.getDispatcher().registerListener(CoreEventsHandler.getInstance()); // Register the core event handler independently from the events package
//updateListeners(builtConnection, null); // TODO: EventRegistry
}
catch (DiscordException e)
{
LOGGER.warn("Failed to login to Discord.", e);
}
}
/**
* Logout and unregister specified connection
* @param client Target connection
*/
public void unregisterConnection(IDiscordClient client)
{
LOGGER.info("Unregistering connection with user #" + client.getOurUser().getID());
DiscordInteractionsUtil.disconnect(client);
connectionsDatastore.remove(client);
registeredListeners.remove(client);
}
/**
* Update listeners for target connection
* @param target Target conection, must be registered
* @param refListeners Reference listeners list
*/
public void updateListeners(IDiscordClient target, Set<Object> refListeners)
{
LOGGER.debug("Updating listeners for target " + target.getOurUser().getID());
if(!connectionsDatastore.contains(target)) return;
if(!registeredListeners.containsKey(target)) registeredListeners.put(target, new HashSet<>());
Set<Object> currentListeners = registeredListeners.get(target);
// Case 1: item is not registered in IDC
long toRegister = refListeners.stream().filter(item -> !currentListeners.contains(item)).count();
refListeners.stream().filter(item -> !currentListeners.contains(item)).forEach(item ->
{
target.getDispatcher().registerListener(item);
currentListeners.add(item);
});
// Case 2: item is registered, but shouldn't be
long toUnregister = currentListeners.stream().filter(item -> !refListeners.contains(item)).count();
currentListeners.stream().filter(item -> !refListeners.contains(item)).forEach(item ->
{
target.getDispatcher().unregisterListener(item);
currentListeners.remove(item);
});
LOGGER.debug("Registered " + toRegister + " listeners and unregistered " + toUnregister + " listeners.");
}
/**
* Update all connections listeners
* @param refListeners Reference listeners list
*/
public void updateAllListeners(Set<Object> refListeners)
{
for(IDiscordClient i : connectionsDatastore) updateListeners(i, refListeners);
}
/**
* Reconnect a disconnected gateway connection
* @param target Disconnected connection
*/
public void reconnect(IDiscordClient target)
{
if(!connectionsDatastore.contains(target)) return;
try
{
target.login();
}
catch (DiscordException e)
{
LOGGER.warn("User #" + target.getOurUser().getID() + " failed to reconnect to the gateway.", e);
}
}
}

@ -0,0 +1,101 @@
package net.pingex.dcf.util;
import net.jodah.failsafe.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.HTTP429Exception;
import sx.blah.discord.util.MissingPermissionsException;
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(HTTP429Exception.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(HTTP429Exception.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);
}
}
}

@ -5,5 +5,11 @@ 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
rootLogger.level = trace
rootLogger.appenderRef.stdout.ref = STDOUT
# Mute jetty garbage
logger.jetty.name = org.eclipse.jetty
logger.jetty.level = info
logger.d4j.name = sx.blah.discord
logger.d4j.level = info