pingex
/
DiscordBot
Archived
1
0
Fork 0

Compare commits

..

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

2
.gitignore vendored

@ -69,5 +69,3 @@ gradle-app.setting
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
config.ini
permissions.json

@ -1,8 +1,11 @@
apply plugin: 'idea'
apply plugin: 'java'
apply plugin: "com.zoltu.git-versioning"
group 'net.pingex.discordbot'
sourceCompatibility = 1.8
version '0.1-dev'
apply plugin: 'java'
sourceCompatibility = 1.5
repositories {
mavenCentral()
@ -12,35 +15,7 @@ repositories {
}
}
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "gradle.plugin.com.zoltu.gradle.plugin:git-versioning:2.0.12"
}
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
compile "com.github.austinv11:Discord4j:2.4.5"
compile "org.ini4j:ini4j:0.5.4"
compile "org.slf4j:slf4j-simple:1.7.9"
compile "org.apache.commons:commons-lang3:3.0"
compile "com.google.code.gson:gson:2.6.2"
// LeagueOfDiscordModule
compile "com.robrua:orianna:2.4.3"
}
jar {
manifest {
attributes 'Implementation-Title': 'DiscordBot',
'Main-Class': 'net.pingex.discordbot.DiscordBot'
}
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
}
compile "com.github.austinv11:Discord4j:2.4.2"
}

@ -1,40 +0,0 @@
[general]
; The command prefix which precedes the actual command, ie: "§" for "§command arguments".
; Will fallback to "!" if the prefix is invalid or empty.
commandPrefix = !
; Bot Owner UID, obtainable using /perm:getMyUID
owner = XXXXXXXXXXXXXXXXXX
; This bot name
name = My awesome bot
[discord]
; Change next value to true if the account is a bot, thus logins using a token.
; Don't forget to uncomment token then
isBot = false
email = bot@example.com
password = samplePassword
; token = sampleToken
[plugins]
; Comma-separated list of modules to load
; enable = net.pingex.discordbot.HelloWorldModule, net.pingex.discordbot.AnothaModule, fr.w4rell.discordbot.ThemeSombreModule
enable = net.pingex.discordbot.HelloWorldModule
[permissions]
; The location of the permissions file, defaulting to `permissions.json` if not specified
file = permissions.json
[leagueofdiscord]
; LoL Region to focus requests on. Invalid region will fallback on NA.
region = NA
; The API key Riot gave you. Empty key will disable the module.
; This key serves as an example and is invalid. You can get a key by visiting https://developer.riotgames.com (LoL account required)
apikey = a3cd6695-2174-4ea8-ac97-414f6dfc8826
[botstatus]
; Text channels containing this string sequence as MOTD will receive all bot logs.
; An empty value will disable
botlog_detection = %botlog%

@ -1,22 +1,2 @@
# DiscordBot
## What is this
DiscordBot is just another bot written in Java. So basically it's a modular bot, which means you can create your own plugins to integrate with it. *coming soon*
## Getting started
* You need Java 8. Run `java -version` to check.
* Execute the following:
```
git clone https://git.pingex.net/pingex/DiscordBot.git
cd DiscordBot
./gradlew
./gradlew jar
```
* You can find the compiled jar in `build/libs/DiscordBot-X.Y.Z.jar`.
* Copy `config.example.ini` to `config.ini` and place it in the same directory as the jar. Then tweak it following your needs.
* Then `java -jar DiscordBot-X.Y.Z.jar` to run it.
Notes:
* No plugin will start by default. You need to enable them by specifying their full classname in the config file. (behavior to be changed)
Modular Bot for Discord, coming soon

@ -1,63 +0,0 @@
package net.pingex.dbotm;
import net.pingex.discordbot.AbstractModule;
import net.pingex.discordbot.Configuration;
import sx.blah.discord.api.EventSubscriber;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.handle.impl.events.GuildCreateEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.HTTP429Exception;
import sx.blah.discord.util.MissingPermissionsException;
/**
* Status of the Bot, ie. starting up and so on
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
public class BotStatusModule extends AbstractModule
{
/**
* Whether the bot should broadcast his logs
*/
private boolean enableBotBroadcast = true;
/**
* Constructor doing all the basic stuff, like registering as a Listener to Discord, getting a logger, etc.
* @param client Discord Client instance used to register self.
*/
public BotStatusModule(IDiscordClient client)
{
super(client);
if(!Configuration.exists("botstatus", "botlog_detection") || Configuration.getValue("botstatus", "botlog_detection").isEmpty())
{
logger.warning("Bot won't log anything to Discord channel per configuration value.");
enableBotBroadcast = false;
}
if(!Configuration.exists("general", "name") || Configuration.getValue("general", "name").isEmpty())
Configuration.setValue("general", "name", client.getOurUser().getName());
}
/**
* Broadcast the starting up event to botlog channels.
*/
@EventSubscriber
public void broadcastStartup(GuildCreateEvent event)
{
if(enableBotBroadcast)
for(IChannel channel : event.getGuild().getChannels())
if (channel.getTopic().contains(Configuration.getValue("botstatus", "botlog_detection")))
try
{
channel.sendMessage(Configuration.getValue("general", "name") + " (DiscordBot version " +
(Configuration.class.getPackage().getImplementationVersion() != null ? Configuration.class.getPackage().getImplementationVersion() : "UNKNOWN") +
") started up and joined this server !");
}
catch (MissingPermissionsException | HTTP429Exception | DiscordException e)
{
logger.warning("Failed to send message to channel #" + channel.getID() + ": " + e.getMessage());
}
}
}

@ -1,326 +0,0 @@
package net.pingex.dbotm;
import com.robrua.orianna.api.core.RiotAPI;
import com.robrua.orianna.type.core.champion.ChampionStatus;
import com.robrua.orianna.type.core.common.QueueType;
import com.robrua.orianna.type.core.common.Region;
import com.robrua.orianna.type.core.common.Side;
import com.robrua.orianna.type.core.currentgame.CurrentGame;
import com.robrua.orianna.type.core.currentgame.MasteryRank;
import com.robrua.orianna.type.core.currentgame.Participant;
import com.robrua.orianna.type.core.league.League;
import com.robrua.orianna.type.core.league.LeagueEntry;
import com.robrua.orianna.type.core.staticdata.Champion;
import com.robrua.orianna.type.core.summoner.Summoner;
import com.robrua.orianna.type.exception.APIException;
import net.pingex.discordbot.AbstractModule;
import net.pingex.discordbot.Command;
import net.pingex.discordbot.Configuration;
import net.pingex.discordbot.Controllable;
import org.apache.commons.lang3.StringUtils;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
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 javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.LongStream;
/**
* League of Discord, all the stuff related to League of Legends !
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
@Controllable(name = "lol")
public class LeagueOfDiscordModule extends AbstractModule
{
/**
* Loading Splace Image Unit Width
*/
private static final int LSI_UNIT_WIDTH = 308;
/**
* Loading Splace Image Unit Height
*/
private static final int LSI_UNIT_HEIGHT = 560;
/**
* Time shift when spectating games (3 minutes)
*/
private static final int SPECTATE_TIMESHIFT = 180;
/**
* All keystones ID.
*/
private static final long[] KEYSTONES_ID = {6161, 6162, 6164, 6361, 6362, 6363, 6261, 6262, 6263};
/**
* Time to wait when the Rate Limit has been reached.
*/
private static final int RATELIMIT_SLEEP = 5;
/**
* Constructor doing all the basic stuff, like registering as a Listener to Discord, getting a logger, etc.
* @param client Discord Client instance used to register self.
*/
public LeagueOfDiscordModule(IDiscordClient client)
{
super(client);
// Region config
Region region;
try
{
region = Region.valueOf(Configuration.getValue("leagueofdiscord", "region"));
logger.info("Selected region: " + region);
}
catch (IllegalArgumentException | NullPointerException e)
{
region = Region.NA;
logger.warning("Invalid region selected. Defaulting to NA !");
}
RiotAPI.setRegion(region);
// API Key config
if(Configuration.exists("leagueofdiscord", "apikey"))
RiotAPI.setAPIKey(Configuration.getValue("leagueofdiscord", "apikey"));
else
{
logger.severe("API Key not specified. Module shutdown.");
shutdown();
}
}
/**
* Returns this week F2P champs
* @return A list of the F2P champions, plus a picture with all the splash.
*/
@Command(description = "Returns this week F2P champions.")
public String fwr(MessageReceivedEvent event)
{
Map<Champion, ChampionStatus> champs = RiotAPI.getChampionStatuses(true);
StringBuffer toReturn = new StringBuffer("This week F2P champions:\n");
ArrayList<String> picMatrix = new ArrayList<>();
for(Map.Entry<Champion, ChampionStatus> i : champs.entrySet())
{
toReturn.append("* ").append(i.getKey().getName()).append(" - ").append(i.getKey().getTitle()).append("\n");
picMatrix.add("http://ddragon.leagueoflegends.com/cdn/img/champion/loading/" + i.getKey().getKey() + "_0.jpg");
}
// PICTURE PROCESSING
int lines = (int)Math.floor(picMatrix.size()/5.1+1);
BufferedImage result = new BufferedImage(LSI_UNIT_WIDTH*5, LSI_UNIT_HEIGHT*lines, BufferedImage.TYPE_INT_RGB);
Graphics g = result.createGraphics();
for(int i = 0; i < picMatrix.size(); i++)
{
int linepos = (int)Math.floor(i/5.0);
BufferedImage ins = null;
try { ins = ImageIO.read(new URL(picMatrix.get(i))); }
catch (IOException e) { toReturn.append("IO Error while processing the picture.").toString(); }
g.drawImage(ins, (i-linepos*5)*LSI_UNIT_WIDTH, linepos*LSI_UNIT_HEIGHT, null);
}
try
{
File targetImage = File.createTempFile("DiscordBot", ".jpg");ImageIO.write(result, "jpg", targetImage);
client.getChannelByID(event.getMessage().getChannel().getID()).sendFile(targetImage);
}
catch (IOException e)
{
toReturn.append("IO Error while processing the picture.").toString();
}
catch (DiscordException | HTTP429Exception | MissingPermissionsException e)
{
toReturn.append("Error with Discord while processing the picture.").toString();
}
return toReturn.toString();
}
/**
* Tell cur game info for given player.
* @param summonerName Summoner username
* @return Text-based game info
*/
@Command(description = "Tell the current game info of given summoner", shorthand = "lolcur")
public String currentGame(MessageReceivedEvent event, String summonerName) throws HTTP429Exception, DiscordException, MissingPermissionsException, InterruptedException
{
IMessage loadingMessage = event.getMessage().getChannel().sendMessage("Fetching data from Riot API...");
CurrentGame ginfo = null;
Summoner targetPlayer = null;
// Data-fetching marathon
for(int i=1;; i++)
try
{
ginfo = RiotAPI.getCurrentGame(summonerName);
targetPlayer = RiotAPI.getSummonerByName(summonerName);
if(ginfo == null) return "Summoner is not in a game.";
else break;
}
catch (APIException e)
{
switch(e.getStatus())
{
case NOT_FOUND:
return "Summoner not found.";
case RATE_LIMIT_EXCEEDED:
Thread.sleep(TimeUnit.SECONDS.toMillis(RATELIMIT_SLEEP));
break;
case INTERNAL_SERVER_ERROR:
case SERVICE_UNAVAILABLE:
if(i>=5) return "Command failed. Please try again later.";
break;
default:
throw e;
}
}
// General game info
StringBuffer sb = new StringBuffer("Current game info for " + targetPlayer.getName() + ": ");
String timeStarted = String.format("%02d:%02d", TimeUnit.SECONDS.toMinutes(ginfo.getLength()+SPECTATE_TIMESHIFT), ginfo.getLength()+SPECTATE_TIMESHIFT - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(ginfo.getLength()+SPECTATE_TIMESHIFT)));
sb.append(ginfo.getQueueType()).append(" started ").append(timeStarted).append(" ago.\n\n");
loadingMessage.edit("Game found. Fetching summoners data...");
// Do all requests and store them in a temporary object for easier access
ArrayList<CurrentMatchPlayersData> participantsData = new ArrayList<>();
for(Participant i : ginfo.getParticipants())
{
CurrentMatchPlayersData currentParticipant = new CurrentMatchPlayersData();
currentParticipant.summonerName = i.getSummonerName();
currentParticipant.side = i.getTeam();
currentParticipant.championName = i.getChampion().getName();
currentParticipant.summonerSpell1Name = i.getSummonerSpell1().getName();
currentParticipant.summonerSpell2Name = i.getSummonerSpell2().getName();
// SoloQ Ranked stats
for(int count=1; count>0; count++)
try
{
for(League j : i.getSummoner().getLeagueEntries())
if(j.getQueueType().equals(QueueType.RANKED_SOLO_5x5))
{
LeagueEntry div = j.getParticipantEntry();
currentParticipant.leagueDivision = j.getTier().toString() + " " + div.getDivision();
currentParticipant.LP = div.getLeaguePoints();
currentParticipant.ratio = (int)Math.round((double)div.getWins()/(div.getWins()+div.getLosses())*100);
count = -1;
break;
}
}
catch (APIException e)
{
switch(e.getStatus())
{
case NOT_FOUND: // Silently ignore HTTP404 which is OK and means "Unranked"
count = -1;
break;
case RATE_LIMIT_EXCEEDED:
Thread.sleep(TimeUnit.SECONDS.toMillis(RATELIMIT_SLEEP));
break;
case INTERNAL_SERVER_ERROR:
case SERVICE_UNAVAILABLE:
if(count>=5) currentParticipant.leagueDivision = "(Unavailable)";
break;
default:
throw e;
}
}
// Masteries
for(MasteryRank j : i.getMasteries()) // TODO: May crash ? Wait and see
{
if(LongStream.of(KEYSTONES_ID).anyMatch(x -> x == j.getMasteryID()))
currentParticipant.keystone = j.getMastery().getName();
switch(j.getMastery().getType())
{
case Ferocity:
currentParticipant.masteries[0]+=j.getRank();
break;
case Cunning:
currentParticipant.masteries[1]+=j.getRank();
break;
case Resolve:
currentParticipant.masteries[2]+=j.getRank();
break;
}
}
participantsData.add(currentParticipant);
}
// Get all the max /o/
CurrentMatchPlayersData longestNames = participantsData.stream().max((pd1, pd2) -> Integer.compare(pd1.summonerName.length()+pd1.championName.length(), pd2.summonerName.length()+pd2.championName.length())).get();
int maxSummonerChamp = longestNames.summonerName.length() + longestNames.championName.length() + 3; // Inkl. " ()"
int maxLDLength = participantsData.stream().max((pd1, pd2) -> Integer.compare(pd1.leagueDivision.length(), pd2.leagueDivision.length())).get().leagueDivision.length();
CurrentMatchPlayersData longestSpells = participantsData.stream().max((pd1, pd2) -> Integer.compare(pd1.summonerSpell1Name.length()+pd1.summonerSpell2Name.length(), pd2.summonerSpell1Name.length()+pd2.summonerSpell2Name.length())).get();
int maxSummonerSpell = longestSpells.summonerSpell1Name.length()+longestSpells.summonerSpell2Name.length()+1;
// Format all the things \o\
Consumer<CurrentMatchPlayersData> printer = (x -> {
sb.append("* ").append(StringUtils.rightPad(x.summonerName + " (" + x.championName + ")", maxSummonerChamp)).append(" - ");
sb.append(StringUtils.rightPad(x.leagueDivision, maxLDLength));
sb.append(StringUtils.leftPad(x.leagueDivision.equals("Unranked") ? "" : x.LP + "LP " + x.ratio + "%", 11)).append(" - ");
sb.append(StringUtils.rightPad(x.summonerSpell1Name + "/" + x.summonerSpell2Name, maxSummonerSpell)).append(" - ");
sb.append("[").append(String.format("%02d", x.masteries[0]))
.append("/").append(String.format("%02d", x.masteries[1]))
.append("/").append(String.format("%02d", x.masteries[2])).append("] ");
sb.append(x.keystone).append("\n");
});
// Print all the things \o/
sb.append("Team Blue");
if(!ginfo.getBannedChampions().isEmpty()) // Bans, if any
{
StringJoiner joinedChamps = new StringJoiner("/");
ginfo.getBannedChampions().stream().filter(bc -> bc.getTeam().equals(Side.BLUE)).forEach(bc -> joinedChamps.add(bc.getChampion().getName())); // WOW, MUCH STREAMS, MANY LAMBDA
sb.append(" - Bans: ").append(joinedChamps.toString());
}
sb.append("\n");
participantsData.stream().filter(x -> x.side.equals(Side.BLUE)).forEach(printer);
sb.append("\nTeam Purple");
if(!ginfo.getBannedChampions().isEmpty()) // Bans, if any
{
StringJoiner joinedChamps = new StringJoiner("/");
ginfo.getBannedChampions().stream().filter(bc -> bc.getTeam().equals(Side.PURPLE)).forEach(bc -> joinedChamps.add(bc.getChampion().getName())); // WOW, MUCH STREAMS, MANY LAMBDA
sb.append(" - Bans: ").append(joinedChamps.toString());
}
sb.append("\n");
participantsData.stream().filter(x -> x.side.equals(Side.PURPLE)).forEach(printer);
return sb.toString();
}
private class CurrentMatchPlayersData
{
String summonerName;
Side side;
String championName;
String leagueDivision = "Unranked";
int LP;
int ratio;
String summonerSpell1Name;
String summonerSpell2Name;
int[] masteries = new int[3];
String keystone = "No Keystone";
}
}

@ -1,35 +0,0 @@
package net.pingex.discordbot;
import sx.blah.discord.api.IDiscordClient;
import java.util.logging.Logger;
/**
* Defines a basic bot module
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
public abstract class AbstractModule
{
protected Logger logger;
protected IDiscordClient client;
/**
* Constructor doing all the basic stuff, like registering as a Listener to Discord, getting a logger, etc.
* @param client Discord Client instance used to register self.
*/
public AbstractModule(IDiscordClient client)
{
ModulesRegistry.register(this);
client.getDispatcher().registerListener(this);
logger = Logger.getLogger(this.getClass().getName());
this.client = client;
}
/**
* Disable this module.
*/
public void shutdown()
{
ModulesRegistry.unregister(this);
}
}

@ -1,41 +0,0 @@
package net.pingex.discordbot;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a Command as callable from the Discord chat.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Command
{
/**
* Shortened command
*/
String shorthand() default "";
/**
* Default permission for this command
*/
DefaultPermission permission() default DefaultPermission.EVERYONE;
/**
* Description of the command
*/
String description() default "No description provided for this command.";
/**
* Arguments that are required, or not. `true` for a required field, `false` for a not required one.
*/
boolean[] required() default {};
/**
* Should the output be embraced with "```" ?
*/
boolean codeOutput() default true;
}

@ -1,274 +0,0 @@
package net.pingex.discordbot;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import sx.blah.discord.api.EventSubscriber;
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.HTTP429Exception;
import sx.blah.discord.util.MissingPermissionsException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.*;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class parses and dispatch Discord commands
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
class CommandDispatcher
{
private Logger logger;
/**
* Singleton instance
*/
private static final CommandDispatcher INSTANCE = new CommandDispatcher();
/**
* Contains all available commands, built using `rebuildCommandList()`
*/
private Map<String, InvokableMethod> commandList;
/**
* Contains all the shorthanded commands
*/
private Map<String, String> shortList;
/**
* Thread pool used to invoke commands
*/
private ExecutorService threadPool;
/**
* Basic Constructor, automatically rebuilds command list from the modules registry
*/
private CommandDispatcher()
{
logger = Logger.getLogger(this.getClass().getName());
rebuildCommandList();
threadPool = Executors.newCachedThreadPool(new BasicThreadFactory.Builder().namingPattern("CommandDispatcher-%d").build()); // TODO: threadPool.shutdown()
if(!Configuration.exists("general", "commandPrefix") || Configuration.getValue("general", "commandPrefix").isEmpty())
{
logger.warning("Invalid command prefix detected, falling back to \"!\"");
Configuration.setValue("general", "commandPrefix", "!");
}
}
/**
* Gets the unique instance of `CommandDispatcher`
* @return The current instance of `CommandDispatcher`
*/
public static CommandDispatcher getInstance()
{
return INSTANCE;
}
/**
* Fired when receiving a message
* @param event Event from Discord API
*/
@EventSubscriber
public void onMessageReceivedEvent(MessageReceivedEvent event)
{
// Command matcher
Matcher m = Pattern.compile("^" + Configuration.getValue("general", "commandPrefix") + "([:\\w]+)(?: (.*))?$").matcher(event.getMessage().getContent());
if(!m.matches()) return; // We don't need to go further if it's not even a command
String fullCommand = m.group(1).toLowerCase();
// Arg splitter
Matcher am = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher((m.group(2) != null) ? m.group(2) : "");
ArrayList<String> args = new ArrayList<>();
while(am.find()) args.add(am.group(1).replace("\"", ""));
// Shorthand matcher
if(!fullCommand.contains(":") && shortList.containsKey(fullCommand))
fullCommand = shortList.get(fullCommand);
String commandAnswer = null;
Object[] parsedArray = null;
// Conditions
if(commandList.containsKey(fullCommand))
{
logger.info("Command invoked (" + event.getMessage().getAuthor().getName() + "#" + event.getMessage().getAuthor().getDiscriminator() + "): " + fullCommand);
InvokableMethod foundMethod = commandList.get(fullCommand);
parsedArray = new Object[foundMethod.getMethod().getParameterCount()];
parsedArray[0] = event;
// To be redone, maybe ?
Boolean canOverrideRun = PermissionsModule.getInstance().canRun(event.getMessage().getGuild(), event.getMessage().getAuthor(), fullCommand);
if(!foundMethod.getMethod().getAnnotation(Command.class).permission().eval(event))
commandAnswer = "Permission denied.";
if(canOverrideRun != null)
commandAnswer = canOverrideRun ? null : "Permission denied.";
if(commandAnswer == null)
{
if(foundMethod.getMethod().getParameterCount()-1 >= args.size())
{
for(int i=1; i < foundMethod.getMethod().getParameterCount(); i++)
{
try
{
parsedArray[i] = parse(foundMethod.getMethod().getParameterTypes()[i], args.size() > i-1 ? args.get(i-1) : null);
} catch (IllegalArgumentException e)
{
commandAnswer = "Invalid arguments. Call `" + Configuration.getValue("general", "commandPrefix") + "help " + fullCommand + "` for help.";
}
// Try to know if the field is required (or not) from the Command annotation
boolean isRequired = true;
try { isRequired = foundMethod.getMethod().getAnnotation(Command.class).required()[i-1]; } catch (ArrayIndexOutOfBoundsException e) {}
// if(ArgumentIsNull && (IsRequired || ArgumentIsRealPrimitiveOrObject))
if(parsedArray[i] == null && (isRequired ||
!(ClassUtils.isPrimitiveWrapper(foundMethod.getMethod().getParameterTypes()[i]) || foundMethod.getMethod().getParameterTypes()[i] == String.class)))
{
commandAnswer = "Invalid arguments. Call `" + Configuration.getValue("general", "commandPrefix") + "help " + fullCommand + "` for help.";
break;
}
}
}
else
commandAnswer = "Invalid arguments. Call `" + Configuration.getValue("general", "commandPrefix") + "help " + fullCommand + "` for help.";
}
}
else
commandAnswer = "Unknown command. Call `" + Configuration.getValue("general", "commandPrefix") + "list` to see all available commands.";
// Run command
if(commandAnswer == null)
{
String finalFullCommand = fullCommand;
Object[] finalParsedArray = parsedArray;
threadPool.submit(() -> {
try
{
String ans = (String) commandList.get(finalFullCommand).invoke(finalParsedArray);
if(ans != null) event.getMessage().getChannel().sendMessage(commandList.get(finalFullCommand).getMethod().getAnnotation(Command.class).codeOutput() ? "```\n" + ans + "\n```" : ans);
} catch (IllegalArgumentException | IllegalAccessException e)
{
logger.severe("Couldn't call target method (" + e.getClass().getName() + "): " + e.getMessage());
} catch (InvocationTargetException e)
{
logger.severe("An error occurred inside the target command: " + e.getCause().getMessage());
e.getCause().printStackTrace();
} catch (MissingPermissionsException | HTTP429Exception | DiscordException e)
{
logger.warning("Couldn't reply to command (" + e.getClass().getName() + "): " + e.getMessage());
}
});
}
else // Error answer
try
{
event.getMessage().getChannel().sendMessage("```\n" + commandAnswer + "\n```");
} catch (MissingPermissionsException | HTTP429Exception | DiscordException e)
{
logger.warning("Couldn't reply to command (" + e.getClass().getName() + "): " + e.getMessage());
}
}
/**
* Rebuilds command list using the module registry.
* - It looks for methods with @Command annotation inside class with @Controllable annotation
* - Check his prototype (should be `method(IDiscordClient, ...):String`)
* - Add usable methods in his local registry, using InvokableMethod
* @see InvokableMethod
* @see ModulesRegistry
*/
public void rebuildCommandList()
{
logger.info("Rebuilding command list...");
ArrayList<AbstractModule> registry = ModulesRegistry.getRegistry();
commandList = new TreeMap<>();
shortList = new TreeMap<>();
int count = 0;
int invalidCount = 0;
for(AbstractModule i : registry)
if(i.getClass().isAnnotationPresent(Controllable.class))
for(Method j : i.getClass().getDeclaredMethods())
if(j.isAnnotationPresent(Command.class))
{
String id = i.getClass().getAnnotation(Controllable.class).name() + ":" + j.getName().toLowerCase();
if(j.getParameterCount() >= 1 && j.getParameterTypes()[0] == MessageReceivedEvent.class && j.getReturnType() == String.class)
if(!commandList.containsKey(id))
{
commandList.put(id, new InvokableMethod(j, i));
count++;
// Eventual command shorthands
if(!j.getAnnotation(Command.class).shorthand().isEmpty())
if(!shortList.containsKey(j.getAnnotation(Command.class).shorthand().toLowerCase()))
shortList.put(j.getAnnotation(Command.class).shorthand().toLowerCase(), id);
else
logger.warning("Conflicting shorthand for command " + id);
}
else
{
logger.warning("Conflicting command " + id);
invalidCount++;
}
else
{
logger.warning("Command [" + id + "]: incorrect function prototype, thus won't be added to the command list.");
invalidCount++;
}
}
logger.info("Found " + count + " valid commands and " + invalidCount + " invalid commands.");
logger.info("... Done");
}
/**
* Parses a String into a primitive variable. Choice between boolean, byte, short, int, long, float, double, string
* @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
*/
public static Object parse(Class<?> target, String value) throws IllegalArgumentException
{
if(value == null || value.equals("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");
}
/**
* Gets the command list
* @return The current command list
*/
public Map<String, InvokableMethod> getCommandList()
{
return commandList;
}
/**
* Gets the shorthands list
* @return The current short list
*/
public Map<String, String> getShortList()
{
return shortList;
}
}

@ -1,100 +0,0 @@
package net.pingex.discordbot;
import org.ini4j.Ini;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.logging.Logger;
/**
* Contains configuration keys for the whole app. Also parses a .ini configuration file.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
public final class Configuration
{
private static Logger LOGGER = Logger.getLogger(Configuration.class.getName());
/**
* Main Configuration Datastore
*/
private static Ini datastore = new Ini();
/**
* Load keys from a file to the datastore
* @param path Relative or absolute path to the config file
* @throws IOException
*/
public static void loadConfiguration(String path) throws IOException
{
LOGGER.info("Loading configuration file: " + path);
datastore.load(new File(path));
}
/**
* Request a value from the datastore
* @param section Section of the datastore
* @param key Key wanted
* @return A String representation of the value, `null` if the key doesn't exist
*/
public static String getValue(String section, String key)
{
return getValue(section, key, String.class);
}
/**
* Request a value from the datastore
* @param section Section of the datastore
* @param key Key wanted
* @param returnType Method will return a variable of this type
* @param <T> Method will return a variable of this type
* @return A <T> representation of the value, `null` if the key doesn't exist
*/
public static<T> T getValue(String section, String key, Class<T> returnType)
{
return datastore.get(section, key, returnType);
}
/**
* Return all the values associated with this key
* @param section Section of the datastore
* @param key Key wanted
* @return A List of String containing all the values
*/
public static List<String> getMultiValue(String section, String key)
{
return datastore.get(section).getAll(key);
}
/**
* Change a key value. If value is null or an empty String, then the key will be removed from the datastore.
* Please note this method doesn't support multivalue keys.
* @param section Section of the datastore
* @param key Key to change
* @param value New key value
*/
public static void setValue(String section, String key, String value)
{
if(value == null || value.isEmpty())
datastore.remove(section, key);
else
{
if(datastore.get(section, key) == null)
datastore.add(section, key, value);
else
datastore.get(section).replace(key, value);
}
}
/**
* Say if the key exists in the datastore
* @param section Section of the datastore
* @param key Key to research
* @return `true` if the key exists, `false` otherwise
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean exists(String section, String key)
{
return (datastore.get(section, key) != null);
}
}

@ -1,34 +0,0 @@
package net.pingex.discordbot;
/**
* Exception thrown when an issue involving Configuration occurs.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
public class ConfigurationException extends Exception
{
public ConfigurationException(String message)
{
super(message);
}
protected ConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
{
super(message, cause, enableSuppression, writableStackTrace);
}
public ConfigurationException(Throwable cause)
{
super(cause);
}
public ConfigurationException(String message, Throwable cause)
{
super(message, cause);
}
public ConfigurationException()
{
super();
}
}

@ -1,21 +0,0 @@
package net.pingex.discordbot;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a module as Controllable, ie which contains commands callable from the Discord chat.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controllable
{
/**
* Name of the module, ie the `module` part in `!module:command`
*/
String name();
}

@ -1,62 +0,0 @@
package net.pingex.discordbot;
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
import java.util.function.Predicate;
/**
* Enum describing the default behavior of a command when no specific permission override is given.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
public enum DefaultPermission
{
/**
* Everyone can call the command
*/
EVERYONE(e -> true),
/**
* Only the owner of the guild where command was run can run the command
*/
GUILD_OWNER(e -> e.getMessage().getGuild().getOwnerID().equals(e.getMessage().getAuthor().getID())),
/**
* Only the bot owner can run the command. Bot owner can be defined in the configuration file
*/
BOT_OWNER(e -> e.getMessage().getAuthor().getID().equals(Configuration.getValue("general", "owner"))),
/**
* Either BOT_OWNER or GUILD_OWNER
*/
ANY_OWNER(e -> e.getMessage().getGuild().getOwnerID().equals(e.getMessage().getAuthor().getID())
|| e.getMessage().getAuthor().getID().equals(Configuration.getValue("general", "owner"))),
/**
* Nobody can run the command
*/
NONE(e -> false);
/**
* The predicate for each enum value defined above
*/
private Predicate<MessageReceivedEvent> predicate;
/**
* Quick constructor, no further explanation needed
* @param p Input Predicate
*/
DefaultPermission(Predicate<MessageReceivedEvent> p)
{
predicate = p;
}
/**
* Evaluates the input MRE against the predicate of the said enum value
* @param e The input event, containing all needed methods to evaluate the predicate
* @return What Predicate.test() returns
*/
boolean eval(MessageReceivedEvent e)
{
return predicate.test(e);
}
}

@ -1,167 +0,0 @@
package net.pingex.discordbot;
import sx.blah.discord.api.ClientBuilder;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.api.IListener;
import sx.blah.discord.handle.impl.events.ReadyEvent;
import sx.blah.discord.util.DiscordException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.logging.Logger;
/**
* Main Entry Point
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
class DiscordBot
{
private static final Logger LOGGER = Logger.getLogger(DiscordBot.class.getName());
/**
* Discord Client instance
*/
private IDiscordClient client;
/**
* Constructor of the singleton
* @param client Discord client instance used to Bootstrap the whole program
*/
private DiscordBot(IDiscordClient client)
{
this.client = client;
LOGGER.info("Successfully logged in !");
this.client.getDispatcher().registerListener((IListener<ReadyEvent>) event ->
LOGGER.info("Logged in as \"" + event.getClient().getOurUser().getName() + "#" + event.getClient().getOurUser().getDiscriminator() + "\" (#" + event.getClient().getOurUser().getID() + ")"));
enableModules();
this.client.getDispatcher().registerListener(CommandDispatcher.getInstance());
}
private void enableModules()
{
LOGGER.info("Enabling modules");
// Hardcoded internal modules
new InternalCommandsModule(client);
new PermissionsModule(client);
if(!Configuration.exists("plugins", "enable") || Configuration.getValue("plugins", "enable").isEmpty())
{
LOGGER.warning("Key \"plugins/enable\" doesn't exist in Configuration. No module will be enabled.");
return;
}
for(String i : Configuration.getValue("plugins", "enable").split(","))
{
i = i.trim();
try
{
Class<?> clazz = Class.forName(i);
if(!AbstractModule.class.isAssignableFrom(clazz))
throw new InvalidModuleException(i + " isn't a valid module.");
clazz.getConstructor(IDiscordClient.class).newInstance(client);
} catch (ClassNotFoundException e)
{
LOGGER.warning("Class " + i + " wasn't found, thus won't be loaded.");
} catch (InvalidModuleException e)
{
LOGGER.warning(e.getMessage());
} catch (NoSuchMethodException e)
{
LOGGER.warning("No valid Constructor for " + i + " was found.");
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e)
{
LOGGER.warning("An error occured while creating an instance of " + i);
e.printStackTrace();
}
}
}
// ====================================================
// Static functions: main, config loader, etc.
// ====================================================
/**
* Entry point
* @param args Arguments passed to java
*/
public static void main(String[] args)
{
// SUPER DEBUG 9000
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println("Uncaught thread exception - " + e.getMessage());
e.printStackTrace(System.out);
System.err.println("Uncaught thread exception - " + e.getMessage());
e.printStackTrace(System.err);
});
try
{
// SET UP
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [%2$s/%4$s] %5$s%6$s%n");
LOGGER.info("Hello World, starting up");
LOGGER.info("DiscordBot version " + (DiscordBot.class.getPackage().getImplementationVersion() != null ? DiscordBot.class.getPackage().getImplementationVersion() : "UNKNOWN"));
// LOAD CONFIGURATION
try
{
Configuration.loadConfiguration("config.ini"); // TODO: pass configuration file using commandline argument
} catch (IOException e)
{
LOGGER.severe("Could not load Configuration, reason: " + e.getMessage());
System.exit(10);
}
LOGGER.info("Logging in to Discord");
try
{
loginToDiscord();
} catch (ConfigurationException e)
{
LOGGER.severe("There is a problem with the Configuration: " + e.getMessage());
System.exit(9);
} catch (DiscordException e)
{
LOGGER.severe(e.getMessage());
System.exit(2);
}
}
catch (Throwable e)
{
System.out.println("Uncaught exception - " + e.getMessage());
e.printStackTrace(System.out);
System.err.println("Uncaught exception - " + e.getMessage());
e.printStackTrace(System.err);
}
}
/**
* Discord login subroutine. Called by `main()` once
* @return Instance of DiscordBot if login was successful
* @throws ConfigurationException When token/email/password field in config.ini isn't filled
* @throws DiscordException
*/
private static DiscordBot loginToDiscord() throws ConfigurationException, DiscordException
{
ClientBuilder builder = new ClientBuilder();
if(Configuration.getValue("discord", "isBot", boolean.class))
{
if(!Configuration.exists("discord", "token"))
throw new ConfigurationException("No login token defined");
builder.withToken(Configuration.getValue("discord", "token"));
}
else
{
if(!Configuration.exists("discord", "email") || !Configuration.exists("discord", "password"))
throw new ConfigurationException("Can't login: Either the email or the password wasn't provided.");
builder.withLogin(Configuration.getValue("discord", "email"), Configuration.getValue("discord", "password"));
}
return new DiscordBot(builder.login());
}
}

@ -1,133 +0,0 @@
package net.pingex.discordbot;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
import java.util.Map;
/**
* Internal commands of the bot, such as `help`, and so on
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
@Controllable(name="internal")
class InternalCommandsModule extends AbstractModule
{
private static final int LINES_PER_PAGE = 10;
/**
* Constructor doing all the basic stuff, like registering as a Listener to Discord, getting a logger, etc.
* @param client Discord Client instance used to register self.
*/
public InternalCommandsModule(IDiscordClient client)
{
super(client);
}
/**
* Scans and print the usage of a command
* @param command Full command, ie `module:command`
* @return The man of this command
*/
@Command(shorthand = "help", description = "Gives the usage of a command.")
public String help(MessageReceivedEvent event, String command)
{
// Shorthand and lower-case matcher
if(!command.contains(":") && CommandDispatcher.getInstance().getShortList().containsKey(command.toLowerCase()))
command = CommandDispatcher.getInstance().getShortList().get(command.toLowerCase());
else
command = command.toLowerCase();
if(!CommandDispatcher.getInstance().getCommandList().containsKey(command)) return "Command not found.";
InvokableMethod matchingMethod = CommandDispatcher.getInstance().getCommandList().get(command);
Map<String, String> shorthands = CommandDispatcher.getInstance().getShortList();
StringBuffer toReturn = new StringBuffer();
toReturn.append(Configuration.getValue("general", "commandPrefix")).append(command).append(" - ");
toReturn.append(matchingMethod.getMethod().getAnnotation(Command.class).description());
// Shorthand
if(shorthands.containsValue(command))
{
toReturn.append(" (shorthand: `");
for(Map.Entry<String, String> i : shorthands.entrySet())
if(i.getValue().equals(command))
{
toReturn.append(i.getKey());
break;
}
toReturn.append("`)");
}
toReturn.append("\nUsage: ").append(Configuration.getValue("general", "commandPrefix")).append(command);
for(int i=1; i < matchingMethod.getMethod().getParameterCount(); i++)
{
// Try to know if the field is required (or not) from the Command annotation
String brackets = "<>";
try { if(!matchingMethod.getMethod().getAnnotation(Command.class).required()[i-1]) brackets = "[]"; } catch (ArrayIndexOutOfBoundsException e) {}
toReturn.append(" ").append(brackets.charAt(0)).append(matchingMethod.getMethod().getParameterTypes()[i].getSimpleName()).append(brackets.charAt(1));
}
return toReturn.toString();
}
/**
* List all commands registered
* @param page Page number
* @return String representation of this command
*/
@Command(shorthand = "list", description = "List all commands available to you.", required = {false})
public String list(MessageReceivedEvent event, Integer page)
{
// TODO: Show only available commands
Map<String, InvokableMethod> commands = CommandDispatcher.getInstance().getCommandList();
Map<String, String> shorthands = CommandDispatcher.getInstance().getShortList();
if(page == null) page = 1;
int pagesCount = (int) Math.ceil(commands.size()/(double)LINES_PER_PAGE);
if(page > pagesCount || page <= 0)
return "Page #" + page + " doesn't exist. " + pagesCount + " pages are available.";
StringBuffer toReturn = new StringBuffer("List of commands ");
toReturn.append("(page ").append(page).append("/").append(pagesCount).append(") ");
toReturn.append("- Call `" + Configuration.getValue("general", "commandPrefix") + "help command` for individual help and usage.\n");
int maxNameLength = 0, maxDescLength = 0;
for(Map.Entry<String, InvokableMethod> i : commands.entrySet())
{
// Maximum name length
if(i.getKey().length() > maxNameLength)
maxNameLength = i.getKey().length();
// Maximum desc length
if(i.getValue().getMethod().getAnnotation(Command.class).description().length() > maxDescLength)
maxDescLength = i.getValue().getMethod().getAnnotation(Command.class).description().length();
}
int pos = 1;
for(Map.Entry<String, InvokableMethod> i : commands.entrySet())
{
if(pos > page*LINES_PER_PAGE-LINES_PER_PAGE && pos <= page*LINES_PER_PAGE)
{
toReturn.append("* ").append(String.format("%1$-" + (maxNameLength+1) + "s", i.getKey()));
toReturn.append(String.format("%1$-" + (maxDescLength+1) + "s", i.getValue().getMethod().getAnnotation(Command.class).description()));
// Shorthand
if(shorthands.containsValue(i.getKey()))
{
toReturn.append(" (shorthand: ");
for(Map.Entry<String, String> j : shorthands.entrySet())
if(j.getValue().equals(i.getKey()))
{
toReturn.append(j.getKey());
break;
}
toReturn.append(")");
}
toReturn.append("\n");
}
pos++;
}
return toReturn.toString();
}
}

@ -1,34 +0,0 @@
package net.pingex.discordbot;
/**
* Exception thrown when trying to load a module which isn't one.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
class InvalidModuleException extends Exception
{
public InvalidModuleException(String message)
{
super(message);
}
protected InvalidModuleException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
{
super(message, cause, enableSuppression, writableStackTrace);
}
public InvalidModuleException(Throwable cause)
{
super(cause);
}
public InvalidModuleException(String message, Throwable cause)
{
super(message, cause);
}
public InvalidModuleException()
{
super();
}
}

@ -1,36 +0,0 @@
package net.pingex.discordbot;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Helper class which defines a pair of Method/Object used for direct invoking.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
class InvokableMethod
{
private Method method;
private Object object;
public InvokableMethod(Method m, Object o)
{
method = m;
object = o;
}
public Object invoke(Object... args) throws InvocationTargetException, IllegalAccessException, IllegalArgumentException
{
return method.invoke(object, args);
}
public Method getMethod()
{
return method;
}
public Object getObject()
{
return object;
}
}

@ -1,50 +0,0 @@
package net.pingex.discordbot;
import java.util.ArrayList;
import java.util.logging.Logger;
/**
* Class which keeps track of all known loaded modules
*/
class ModulesRegistry
{
/**
* Contains all `AbstractModule` instances created.
*/
private static ArrayList<AbstractModule> datastore = new ArrayList<>();
/**
* Logger
*/
private static Logger LOGGER = Logger.getLogger(ModulesRegistry.class.getName());
/**
* Action to register a loaded module
* @param toRegister Module to register
*/
public static void register(AbstractModule toRegister)
{
LOGGER.info("Registering module " + toRegister.getClass().getName());
datastore.add(toRegister);
}
/**
* Remove the target module from the list (if it crashed for example)
* @param target Module to remove
*/
public static void unregister(AbstractModule target)
{
LOGGER.info("Unregistering module " + target.getClass().getName());
datastore.remove(target);
}
/**
* Returns of the whole registry
* @return A clone of the whole registry, to avoid damaging the original registry
*/
@SuppressWarnings("unchecked")
public static ArrayList<AbstractModule> getRegistry()
{
return (ArrayList<AbstractModule>) datastore.clone();
}
}

@ -1,369 +0,0 @@
package net.pingex.discordbot;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import net.pingex.discordbot.json.permissions.Guild;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.handle.impl.events.MessageReceivedEvent;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.handle.obj.IUser;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.HTTP429Exception;
import sx.blah.discord.util.MissingPermissionsException;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
/**
* Permissions modules.
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
@Controllable(name="perm")
class PermissionsModule extends AbstractModule
{
/**
* Main permissions table
*/
private HashMap<String, Guild> permissions;
/**
* Gson instance for json (de)serialisation
*/
private static Gson gson = new GsonBuilder().setPrettyPrinting().create();
/**
* Current instance of this module, for easier access
*/
private static PermissionsModule INSTANCE = null;
/**
* Get the current instance of this module
* @return This instance
*/
public static PermissionsModule getInstance()
{
return INSTANCE;
}
/**
* Constructor doing all the basic stuff, like registering as a Listener to Discord, getting a logger, etc.
* @param client Discord Client instance used to register self.
*/
public PermissionsModule(IDiscordClient client)
{
super(client);
INSTANCE = this;
if(!Configuration.exists("permissions", "file") || Configuration.getValue("permissions", "file").isEmpty())
{
logger.warning("Permissions file location not specified, defaulting to `permissions.json`");
Configuration.setValue("permissions", "file", "permissions.json");
}
reloadPermissions();
}
/**
* Gives the UID of the user passed as arguments
* @param name The current name of the user
* @param discriminator His discriminator
* @return The ID of the target user
*/
@Command(description = "Returns the Unique Identifier of the target user.", permission = DefaultPermission.ANY_OWNER)
public String getUID(MessageReceivedEvent event, String name, String discriminator)
{
if(!discriminator.matches("^\\d{4}$")) return "Invalid discriminator.";
for(IUser i : event.getMessage().getGuild().getUsers())
if(i.getName().equals(name) && i.getDiscriminator().equals(discriminator))
return i.getID();
return "User not found";
}
/**
* Gets caller's UID
* @return Caller's UID
*/
@Command(description = "Returns your own Unique Identifier")
public String getMyUID(MessageReceivedEvent event)
{
return event.getMessage().getAuthor().getID();
}
/**
* Give GID of the named role
* @param name The role current name
* @return The GID of the role
*/
@Command(description = "Returns the Unique Identifier of the targer role.", permission = DefaultPermission.ANY_OWNER)
public String getGID(MessageReceivedEvent event, String name)
{
for(IRole i : event.getMessage().getGuild().getRoles())
if(i.getName().equals(name))
return i.getID();
return "Role not found";
}
/**
* Dump all Roles and their GID
* Note: this commands answers the dump via PM to avoid spam and the @everyone role to be publicly displayed thus notify everyone in the guild.
* @return The whole dump, formatted as a displayable String
*/
@Command(description = "Gives ALL roles and GID, for debugging purposes.", permission = DefaultPermission.BOT_OWNER)
public String dumpGID(MessageReceivedEvent event)
{
StringBuffer buffer = new StringBuffer("Dumping Roles for Guild ").append(event.getMessage().getGuild().getName()).append("\n");
for(IRole i : event.getMessage().getGuild().getRoles())
buffer.append(i.getName()).append("\t").append(i.getID()).append("\n");
// PM the IDs
// TODO: Remove when PM answers are implemented
try
{
client.getOrCreatePMChannel(event.getMessage().getAuthor()).sendMessage(buffer.toString());
} catch (MissingPermissionsException | HTTP429Exception | DiscordException e)
{
logger.warning("Fail to send the result");
e.printStackTrace();
}
return "Check your PM for the command result";
}
/**
* Dump all Users and their UID
* Note: this commands answers the dump via PM to avoid spam.
* @return The whole dump, formatted as a displayable String
*/
@Command(description = "Gives ALL users and UID, for debugging purposes.", permission = DefaultPermission.BOT_OWNER)
public String dumpUID(MessageReceivedEvent event)
{
StringBuffer buffer = new StringBuffer("Dumping all users for Guild ").append(event.getMessage().getGuild().getName()).append("\n");
for(IUser i : event.getMessage().getGuild().getUsers())
buffer.append(i.getName()).append("#").append(i.getDiscriminator()).append("\t").append(i.getID()).append("\n");
// PM the IDs
// TODO: Remove when PM answers are implemented
try
{
client.getOrCreatePMChannel(event.getMessage().getAuthor()).sendMessage(buffer.toString());
} catch (MissingPermissionsException | HTTP429Exception | DiscordException e)
{
logger.warning("Fail to send the result");
e.printStackTrace();
}
return "Check your PM for the command result";
}
/**
* Reload permissions from file
* @return Nothing
*/
@Command(description = "Reloads the perm file, discarding all unsaved changes.", permission = DefaultPermission.BOT_OWNER)
public String reload(MessageReceivedEvent event)
{
reloadPermissions();
return null;
}
/**
* Save permissions to file
* @return Nothing
*/
@Command(description = "Saves the perm files.", permission = DefaultPermission.ANY_OWNER)
public String save(MessageReceivedEvent event)
{
writePermissions();
return null;
}
/**
* Sets an user permission
* @param command The target *full* command, shorthands doesn't work
* @param user The user's UID
* @param target `true` to allow execution of the command, `false` to deny it, `null` to remove the rule
* @return
*/
@Command(description = "Sets an user permission.", permission = DefaultPermission.ANY_OWNER, required = {true, true, false})
public String setUser(MessageReceivedEvent event, String command, String user, Boolean target)
{
if(!command.matches("^\\w+:\\w+$")) return "Invalid command.";
if(!user.matches("^\\d{18}$")) return "Invalid user.";
if(!permissions.containsKey(event.getMessage().getGuild().getID()))
permissions.put(event.getMessage().getGuild().getID(), new Guild(new HashMap<>()));
Guild g = permissions.get(event.getMessage().getGuild().getID());
if(!g.commands.containsKey(command))
g.commands.put(command, new net.pingex.discordbot.json.permissions.Command(new HashMap<>(), new HashMap<>()));
net.pingex.discordbot.json.permissions.Command c = g.commands.get(command);
if(target == null) c.users.remove(user);
else c.users.put(user, target);
return "OK";
}
/**
* Sets a role permission
* @param command The target *full* command, shorthands doesn't work
* @param group The role's GID
* @param target `true` to allow execution of the command, `false` to deny it, `null` to remove the rule
* @return
*/
@Command(description = "Sets a role permission.", permission = DefaultPermission.ANY_OWNER, required = {true, true, false})
public String setRole(MessageReceivedEvent event, String command, String group, Boolean target)
{
if(!command.matches("^\\w+:\\w+$")) return "Invalid command.";
if(!group.matches("^\\d{18}$")) return "Invalid group.";
if(!permissions.containsKey(event.getMessage().getGuild().getID()))
permissions.put(event.getMessage().getGuild().getID(), new Guild(new HashMap<>()));
Guild g = permissions.get(event.getMessage().getGuild().getID());
if(!g.commands.containsKey(command))
g.commands.put(command, new net.pingex.discordbot.json.permissions.Command(new HashMap<>(), new HashMap<>()));
net.pingex.discordbot.json.permissions.Command c = g.commands.get(command);
if(target == null) c.roles.remove(group);
else c.roles.put(group, target);
return "OK";
}
/**
* Dump all permissions defined for the current guild
* @return Displayable String of the permissions
*/
@Command(description = "Dump all permission for this Guild.", permission = DefaultPermission.ANY_OWNER)
public String dumpPermissions(MessageReceivedEvent event)
{
StringBuffer buffer = new StringBuffer("Dumping all permissions for Guild ").append(event.getMessage().getGuild().getName()).append("\n\n```");
if(!permissions.containsKey(event.getMessage().getGuild().getID()))
return "No permissions set for Guild";
Guild g = permissions.get(event.getMessage().getGuild().getID());
for(Map.Entry<String, net.pingex.discordbot.json.permissions.Command> i : g.commands.entrySet())
{
buffer.append("========== COMMAND ").append(i.getKey()).append(" ==========\n");
// USERS
buffer.append("----- Users -----\n");
for(Map.Entry<String, Boolean> j : i.getValue().users.entrySet())
{
buffer.append(j.getKey()).append("\t").append(j.getValue()).append("\n");
}
buffer.append("\n");
// GROUPS
buffer.append("----- Roles -----\n");
for(Map.Entry<String, Boolean> j : i.getValue().roles.entrySet())
{
buffer.append(j.getKey()).append("\t").append(j.getValue()).append("\n");
}
buffer.append("\n\n");
}
buffer.append("```");
return buffer.toString();
}
/**
* Says if the permissions overrider will allow the target user to run the target command
* @param user User name
* @param discriminator His discriminator
* @param command The target command
* @return `YES` if he can run it, `NO` if he can't, `N/A` if the decision relies on the default behavior
*/
@Command(description = "Says if the target user can run said command. Ignores default behavior.", permission = DefaultPermission.ANY_OWNER)
public String canRun(MessageReceivedEvent event, String user, String discriminator, String command)
{
if(!discriminator.matches("^\\d{4}$")) return "Invalid discriminator.";
IUser target = null;
for(IUser i : event.getMessage().getGuild().getUsers())
if(i.getName().equals(user) && i.getDiscriminator().equals(discriminator))
{
target = i;
break;
}
if(target == null)
return "User not found in this Guild.";
Boolean toReturn = canRun(event.getMessage().getGuild(), target, command);
return toReturn == null ? "N/A" : (toReturn ? "YES" : "NO");
}
/**
* Says if the permissions overrider will allow the target user to run the target command. To be run internally
* @param guild The current guild
* @param user The target user
* @param command The target command
* @return `true` is he can run the command, `false` if he can't, `null` if the overrider has nothing to say
*/
public Boolean canRun(IGuild guild, IUser user, String command)
{
// Return null if command or guild aren't mentioned in the permissions file
if(!permissions.containsKey(guild.getID()) || !permissions.get(guild.getID()).commands.containsKey(command)) return null;
net.pingex.discordbot.json.permissions.Command c = permissions.get(guild.getID()).commands.get(command);
// User matching
if(c.users.containsKey(user.getID()))
return c.users.get(user.getID());
// Role matching
for(IRole i : user.getRolesForGuild(guild))
{
if(c.roles.containsKey(i.getID()))
return c.roles.get(i.getID());
}
return null;
}
/**
* Reloads the permissions file. Discards the current permissions cache
*/
private void reloadPermissions()
{
logger.info("Reloading permissions");
try
{
FileReader reader = new FileReader(Configuration.getValue("permissions", "file"));
permissions = gson.fromJson(reader, new TypeToken<HashMap<String, Guild>>(){}.getType());
reader.close();
}
catch (FileNotFoundException e)
{
logger.info("File doesn't exist, using default permissions");
permissions = new HashMap<>();
}
catch (IOException e)
{
logger.severe("I/O error while reading permissions file");
e.printStackTrace();
}
}
/**
* Write the current HashMap to file.
*/
private void writePermissions()
{
logger.info("Writing permissions");
try
{
FileWriter writer = new FileWriter(Configuration.getValue("permissions", "file"));
writer.write(gson.toJson(permissions));
writer.close();
}
catch (IOException e)
{
logger.severe("I/O error while writing permissions file");
e.printStackTrace();
}
}
}

@ -1,25 +0,0 @@
package net.pingex.discordbot.json.permissions;
import java.util.HashMap;
/**
* A command's permissions list
*/
public class Command
{
/**
* Users
*/
public HashMap<String, Boolean> users;
/**
* Roles
*/
public HashMap<String, Boolean> roles;
public Command(HashMap<String, Boolean> users, HashMap<String, Boolean> roles)
{
this.users = users;
this.roles = roles;
}
}

@ -1,19 +0,0 @@
package net.pingex.discordbot.json.permissions;
import java.util.HashMap;
/**
* Json representation of a guild, contains an HashMap of commands
*/
public class Guild
{
/**
* The list of commands which have permission overrides defined.
*/
public HashMap<String, Command> commands;
public Guild(HashMap<String, Command> commands)
{
this.commands = commands;
}
}

@ -0,0 +1,14 @@
package net.pingex.discordbot;
/**
* Main Entry Point
* @version 0.1-dev
* @author Raphael "Pingex" NAAS
*/
public class DiscordBot
{
public static void main(String[] args)
{
System.out.println("Hello World");
}
}