Code:
package com.runeavion.core.network.discord;
import com.runeavion.util.ThreadUtil;
import sx.blah.discord.handle.obj.IChannel;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Discord API by Discord4J. Implemented and designed by Tyrant
*
* @author Tyrant
*/
public class DiscordAPI {
/**
* The responsible communicator for communication between this API and the discord client
*/
public static final DiscordCommunicator DISCORD_COMMUNICATOR = new DiscordCommunicator();
/**
* Indicates if we have reached a connection to the discord client.
*/
public static final AtomicBoolean CONNECTED = new AtomicBoolean();
/**
* The main community channel for default communication
*/
public static final long MAIN_COMMUNITY_CHANNEL_ID = YOUR_CHANNEL_ID;
/**
* The thread that this API will be depending on.
* Please notice that this isn't what the dispatchers will run on, since the API
* provides us with its own executor. This is used for the starting up of this API (since we block)
* and for the future, we will convert into a blocking-queue supportive and have a proper queue
* of registered events
*/
private static final Executor DISCORD_EXECUTOR =
Executors.newSingleThreadScheduledExecutor(ThreadUtil.create("DiscordExecutorThread"));
/**
* The time it takes out before we timeout the connection with the discord server
*/
private static final long CONNECTION_TIMEOUT = TimeUnit.MINUTES.toMillis(5);
/**
* Start the discord API
*/
public static void start() {
DISCORD_EXECUTOR.execute(() -> {
try {
long start = System.nanoTime();
//attempt to login
DISCORD_COMMUNICATOR.login();
//block until we are connected. we're doing this concurrently so we're fine with locking
while (!DISCORD_COMMUNICATOR.getClient().isReady()) {
//TODO add some sort of shutdown if connection failed after a while
if (System.nanoTime() - start >= CONNECTION_TIMEOUT) {
throw new IllegalStateException("Discord API took too long to connect.");
}
}
} finally {
DISCORD_COMMUNICATOR.dispatch();
}
});
}
//static references
/**
* Message to default channel
*/
public static void message(String message) {
message(DISCORD_COMMUNICATOR.getDefaultChannel(), message);
}
/**
* Send message to a channel
*/
public static void message(IChannel channel, String message) {
queue(args -> channel.sendMessage(message));
}
/**
* TODO: Create a queueing system
*/
public static void queue(DiscordEvent event) {
if (!CONNECTED.get()) {
throw new IllegalStateException("Discord API isn't online");
}
DISCORD_EXECUTOR.execute(event::action);
}
}
Code:
package com.runeavion.core.network.discord;
import com.runeavion.Config;
import com.runeavion.core.network.discord.commands.DiscordCommandDispatcher;
import com.runeavion.core.network.discord.commands.DiscordCommandFactory;
import sx.blah.discord.api.ClientBuilder;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.handle.obj.IChannel;
/**
* The client whom acts as a pipeline of communication between the discord client to this API
*
* @author Tyrant
* @date 3/6/2018
*/
public class DiscordCommunicator {
/**
* The client builder.
*/
private final ClientBuilder builder;
/**
* The discord client.
*/
private final IDiscordClient client;
/**
* The default channel to communicate on
*/
private IChannel defaultChannel;
public DiscordCommunicator() {
builder = new ClientBuilder().withToken(YOUR_DISCORD_TOKEN);
client = builder.build();
}
public DiscordCommunicator login() {
if (client.isLoggedIn()) {
throw new IllegalStateException("Discord API already logged in.");
}
client.login();
return this;
}
public IChannel channel(long id) {
for (IChannel channel : client.getChannels()) {
if (channel != null && channel.getLongID() == id)
return channel;
}
return null;
}
/**
* Dispatch every event when this is ready
*/
public DiscordCommunicator dispatch() {
try {
defaultChannel = channel(DiscordAPI.MAIN_COMMUNITY_CHANNEL_ID);
//TODO: reflection find any dispatcher and register
client.getDispatcher().registerListener(new DiscordCommandDispatcher());
} finally {
DiscordCommandFactory.init();
DiscordAPI.CONNECTED.set(true);
}
return this;
}
public IDiscordClient getClient() {
return client;
}
public ClientBuilder getBuilder() {
return builder;
}
public IChannel getDefaultChannel() {
return defaultChannel;
}
}
Code:
package com.runeavion.core.network.discord;
/**
* A dispatcher that can be registered
*
* @author Tyrant
* @date 3/7/2018
*/
public interface DiscordDispatcher {
}
Code:
package com.runeavion.core.network.discord;
/**
* An event that happens through the discord client
*
* @author Tyrant
* @date 3/6/2018
*/
public interface DiscordEvent {
/**
* Do action with potentially given arguments
*/
void action(Object... args);
}
commands package
Code:
package com.runeavion.core.network.discord.commands;
import com.runeavion.core.network.discord.DiscordEvent;
/**
* A discord command event
*
* @author Tyrant
*/
public interface DiscordCommand extends DiscordEvent {
}
Code:
package com.runeavion.core.network.discord.commands;
import com.runeavion.core.network.discord.DiscordAPI;
import com.runeavion.core.network.discord.DiscordDispatcher;
import sx.blah.discord.api.events.EventSubscriber;
import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IUser;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.MissingPermissionsException;
/**
* Discord command dispatcher
*
* @author Tyrant
* @date 3/7/2018
*/
public class DiscordCommandDispatcher implements DiscordDispatcher {
@EventSubscriber
public void OnMesageEvent(MessageReceivedEvent event) throws DiscordException, MissingPermissionsException {
IMessage message = event.getMessage();
long channelId = event.getChannel().getLongID();
IUser user = event.getAuthor();
String input = message.getContent();
//get the channel we got this command from or else the default channel
IChannel channel = DiscordAPI.DISCORD_COMMUNICATOR.channel(channelId);
if (channel == null) {
channel = DiscordAPI.DISCORD_COMMUNICATOR.getDefaultChannel();
}
if (input.startsWith("::")) {
input = input.substring(2);//start after ::
DiscordCommandFactory.handleCommand(channel, user, input);
}
}
}
Code:
package com.runeavion.core.network.discord.commands;
import com.runeavion.core.network.discord.DiscordAPI;
import org.reflections.Reflections;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.handle.obj.IUser;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
/**
* The command system
*
* @author Tyrant
* @date 3/6/2018
*/
public class DiscordCommandFactory {
private static final Logger LOGGER = Logger.getLogger(DiscordCommand.class.getName());
/**
* The map that stores every command where <code>key</code> represents the name of the command
* and the <code>value</code> represents the functional command.
*/
private static final Map<String, DiscordCommand> COMMANDS = new HashMap<>();
/**
* The directory for the package-parent of all commands
*/
private static final String COMMANDS_IMPL_DIRECTORY = "com.runeavion.core.network.discord.commands.impl";
/**
* The message you receive when an error occurred while executing the command (Unless {@link DiscordCommandSettings#syntaxPrefix()} is specified)
*/
private static final String DEFAULT_SYNTAX_ERROR_MESSAGE = "Error executing command. Input: {INPUT}";
/**
* The initial method to load commands, loads them using reflections, hence,
* must ensure classes being the same instance of {@link DiscordCommand}
*/
public static void init() {
COMMANDS.clear();
Set<Class<?>> annotated = new Reflections(COMMANDS_IMPL_DIRECTORY).getTypesAnnotatedWith(DiscordCommandSettings.class);
for (Class<?> aClass : annotated) {
Arrays.asList(aClass.getAnnotation(DiscordCommandSettings.class).accessInputs()).forEach($it -> {
try {
COMMANDS.put($it.toLowerCase(), ((DiscordCommand) aClass.newInstance()));
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
});
}
LOGGER.info("Loaded: " + COMMANDS.size() + " discord commands");
}
//must run on a debug mode on your IDE (or jrebel)
public static void reload() {
int commands = COMMANDS.size();
init();
LOGGER.info("Reloaded " + (COMMANDS.size() - commands) + " commands.");
}
public static void handleCommand(IChannel channel, IUser user, String input) {
//(advance this by disallowing this to run through the game thread)
System.out.println("Input: " + input + "; " + user + " " + channel.getName() + " Thread: " + Thread.currentThread().getName());
//fetches the command by the parser
DiscordCommand command = COMMANDS.get(input);
//check if command exists
if (command == null) {
DiscordAPI.message(channel, "The command ::" + input + " is invalid");
return;
} else {
final DiscordCommandSettings annotation = command.getClass().getAnnotation(DiscordCommandSettings.class);
if (annotation != null) {
try {
//check if player is privileged
if (annotation.privilege().getPosition() <= 0) {
command.action(channel, user, input);
return;
} else {
for (IRole role : user.getRolesForGuild(channel.getGuild())) {
if (role.getPosition() >= annotation.privilege().getPosition()) {
command.action(channel, user, input);
return;
}
}
}
//player has insufficient rights to use this command
DiscordAPI.message(channel, "You do not have the permissions to use this command.");
return;
} catch (Exception e) {
e.printStackTrace();
if (!annotation.syntaxPrefix().isEmpty()) {
DiscordAPI.message(channel, annotation.syntaxPrefix());
return;
} else {
DiscordAPI.message(channel, DEFAULT_SYNTAX_ERROR_MESSAGE.replace("{INPUT}", input));
return;
}
}
}
}
DiscordAPI.message(channel, DEFAULT_SYNTAX_ERROR_MESSAGE.replace("{INPUT}", input));
}
}
Code:
package com.runeavion.core.network.discord.commands;
import netscape.security.Privilege;
import sx.blah.discord.handle.obj.IRole;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Discord command settings
* @author Tyrant
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DiscordCommandSettings {
/**
* The String inputs that may access this command
*/
@Nonnull
String[] accessInputs();
/**
* A short line explaining what is the purpose/usage of this command
*/
@Nonnull
String info();
/**
* The lowest {@link IRole#getPosition()} for using this command
*/
@Nullable
DiscordPrivilege privilege();
/**
* What message should be displayed on exception (e.g wrong format usage) when using this command.
* If no input is included, then will show default error.
*/
@Nullable
String syntaxPrefix();
}
Code:
package com.runeavion.core.network.discord.commands;
/**
* A privilege
*
* @author Tyrant
* @date 3/7/2018
*/
public enum DiscordPrivilege {
EVERYBODY(-1);
private final int position;
DiscordPrivilege(int position) {
this.position = position;
}
public int getPosition() {
return position;
}
}
some command tests; obviously just examples and code shouldn't be used (probably)
Code:
package com.runeavion.core.network.discord.commands.impl;
import com.runeavion.core.network.discord.DiscordAPI;
import com.runeavion.core.network.discord.commands.DiscordCommand;
import com.runeavion.core.network.discord.commands.DiscordCommandSettings;
import com.runeavion.core.network.discord.commands.DiscordPrivilege;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
/**
* A test command
*
* @author Tyrant
* @date 3/7/2018
*/
@DiscordCommandSettings(accessInputs = "test", info = "Test command", privilege = DiscordPrivilege.EVERYBODY, syntaxPrefix = "No no no... use it as ::test")
public class TestDiscordCommand implements DiscordCommand {
@Override
public void action(Object... args) {
IChannel channel = (IChannel) args[0];
IUser user = (IUser) args[1];
String input = (String) args[2];
DiscordAPI.message(channel,"Hello there, " + user.getDisplayName(channel.getGuild()) + "... :smile:");
}
}
Code:
package com.runeavion.core.network.discord.commands.impl;
import com.runeavion.core.network.discord.DiscordAPI;
import com.runeavion.core.network.discord.commands.DiscordCommand;
import com.runeavion.core.network.discord.commands.DiscordCommandSettings;
import com.runeavion.core.network.discord.commands.DiscordPrivilege;
import com.runeavion.util.RandomUtils;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
/**
* A test command
*
* @author Tyrant
* @date 3/7/2018
*/
@DiscordCommandSettings(accessInputs = "roll", info = "Roll between 1-10", privilege = DiscordPrivilege.EVERYBODY, syntaxPrefix = "")
public class RollDiscordCommand implements DiscordCommand {
@Override
public void action(Object... args) {
IChannel channel = (IChannel) args[0];
IUser user = (IUser) args[1];
DiscordAPI.message(channel, user.getDisplayName(channel.getGuild()) + " rolled a " + ThreadLocalRandom.current().nextInt(10));
}
}