Added Anilist Api

- moved graphql schemas and queries to lib/graphql
- added anilistApiLib that contains functions to access the anilist graphql api
- added graphql queries to lib/graphql to access data on anilist
- added global ~anime command that returns information to an anime
- modified help command so that it shows command categories for global and server commands
- moved global command registration to lib/cmd
pull/51/head
Trivernis 6 years ago
parent aeaebb4f33
commit 42ee8cc4c5

122
bot.js

@ -8,7 +8,6 @@ const Discord = require("discord.js"),
args = require('args-parser')(process.argv),
waterfall = require('promise-waterfall'),
sqliteAsync = require('./lib/sqliteAsync'),
globcommands = require('./commands/globalcommands.json'),
authToken = args.token || config.api.botToken,
prefix = args.prefix || config.prefix || '~',
gamepresence = args.game || config.presence;
@ -47,6 +46,7 @@ class Bot {
*/
async initServices() {
logger.verbose('Registering cleanup function');
utils.Cleanup(() => {
for (let gh in Object.values(this.guildHandlers))
if (gh instanceof guilding.GuildHandler)
@ -61,6 +61,7 @@ class Bot {
this.maindb.close();
});
await this.initializeDatabase();
if (config.webservice && config.webservice.enabled)
await this.initializeWebserver();
logger.verbose('Registering commands');
@ -76,6 +77,7 @@ class Bot {
async start() {
await this.client.login(authToken);
logger.debug("Logged in");
if (this.webServer) {
this.webServer.start();
logger.info(`WebServer runing on port ${this.webServer.port}`);
@ -92,6 +94,7 @@ class Bot {
logger.verbose('Connecting to main database');
this.maindb = new sqliteAsync.Database('./data/main.db');
await this.maindb.init();
await this.maindb.run(`${utils.sql.tableExistCreate} presences (
${utils.sql.pkIdSerial},
text VARCHAR(255) UNIQUE NOT NULL
@ -161,109 +164,9 @@ class Bot {
* registeres global commands
*/
registerCommands() {
// useless test command
cmd.createGlobalCommand(prefix, globcommands.utils.say, (msg, argv, args) => {
return args.join(' ');
});
// adds a presence that will be saved in the presence file and added to the rotation
cmd.createGlobalCommand(prefix, globcommands.utils.addpresence, async (msg, argv, args) => {
let p = args.join(' ');
this.presences.push(p);
await this.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]);
return `Added Presence \`${p}\``;
});
// shuts down the bot after destroying the client
cmd.createGlobalCommand(prefix, globcommands.utils.shutdown, async (msg) => {
try {
await msg.reply('Shutting down...');
logger.debug('Destroying client...');
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await this.client.destroy();
logger.debug('Exiting server...');
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await this.webServer.stop();
logger.debug(`Exiting Process...`);
process.exit(0);
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
});
// forces a presence rotation
cmd.createGlobalCommand(prefix, globcommands.utils.rotate, () => {
try {
this.client.clearInterval(this.rotator);
this.rotatePresence();
this.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration);
} catch (error) {
logger.warn(error.message);
}
});
// ping command that returns the ping attribute of the client
cmd.createGlobalCommand(prefix, globcommands.info.ping, () => {
return `Current average ping: \`${this.client.ping} ms\``;
});
// returns the time the bot is running
cmd.createGlobalCommand(prefix, globcommands.info.uptime, () => {
let uptime = utils.getSplitDuration(this.client.uptime);
return new Discord.RichEmbed().setDescription(`
**${uptime.days}** days
**${uptime.hours}** hours
**${uptime.minutes}** minutes
**${uptime.seconds}** seconds
**${uptime.milliseconds}** milliseconds
`).setTitle('Uptime');
});
// returns the numbe of guilds, the bot has joined
cmd.createGlobalCommand(prefix, globcommands.info.guilds, () => {
return `Number of guilds: \`${this.client.guilds.size}\``;
});
cmd.createGlobalCommand(prefix, globcommands.utils.createUser, (msg, argv) => {
return new Promise((resolve, reject) => {
if (msg.guild) {
resolve("It's not save here! Try again via PM.");
} else if (argv.username && argv.scope) {
logger.debug(`Creating user entry ${argv.username}, scope: ${argv.scope}`);
this.webServer.createUser(argv.username, argv.password, argv.scope, false).then((token) => {
resolve(`Created entry
username: ${argv.username},
scope: ${argv.scope},
token: ${token}
`);
}).catch((err) => reject(err.message));
}
});
});
cmd.createGlobalCommand(prefix, globcommands.info.about, () => {
return new Discord.RichEmbed()
.setTitle('About')
.setDescription(globcommands.info.about.response.about_creator)
.addField('Icon', globcommands.info.about.response.about_icon);
});
cmd.createGlobalCommand(prefix, globcommands.utils.bugreport, () => {
return new Discord.RichEmbed()
.setTitle('Where to report a bug?')
.setDescription(globcommands.utils.bugreport.response.bug_report);
});
cmd.registerUtilityCommands(prefix, this);
cmd.registerInfoCommands(prefix, this);
cmd.registerAnilistApiCommands(prefix);
}
/**
@ -292,15 +195,15 @@ class Bot {
this.client.on('ready', () => {
logger.info(`logged in as ${this.client.user.tag}!`);
this.client.user.setPresence({
game: {
name: gamepresence, type: "PLAYING"
}, status: 'online'
})
.catch((err) => {
if (err)
logger.warn(err.message);
});
}).catch((err) => {
if (err)
logger.warn(err.message);
});
});
this.client.on('message', async (msg) => {
@ -336,6 +239,7 @@ class Bot {
this.client.on('voiceStateUpdate', async (oldMember, newMember) => {
let gh = await this.getGuildHandler(newMember.guild, prefix);
if (newMember.user === this.client.user) {
if (newMember.voiceChannel)
gh.dj.updateChannel(newMember.voiceChannel);

@ -80,8 +80,21 @@
"guilds": {
"name": "guilds",
"permission": "owner",
"description": "Answers with the number of guilds the bot has joined",
"description": "Answers with the number of guilds the bot has joined.",
"category": "Info"
}
},
"api": {
"AniList": {
"animeSearch": {
"name": "anime",
"permission": "all",
"description": "Answers the anime found for that name on AniList.",
"category": "AniList",
"response": {
"not_found": "The Anime was not found :("
}
}
}
}
}

@ -0,0 +1,103 @@
const fetch = require('node-fetch'),
fsx = require('fs-extra'),
queryPath = './lib/graphql/AnilistApi',
alApiEndpoint = 'https://graphql.anilist.co';
/**
* Return a graphql query read from a file from a configured path.
* @param name
* @returns {Promise<*>}
*/
async function getGraphqlQuery(name) {
return await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'});
}
/**
* Post a query read from a file to the configured graphql endpoint and return the data.
* @param queryName
* @param queryVariables
* @returns {Promise<any>}
*/
function postGraphqlQuery(queryName, queryVariables) {
return new Promise(async (resolve, reject) => {
fetch(alApiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
query: await getGraphqlQuery(queryName),
variables: queryVariables
})
}).then(async (response) => {
let json = await response.json();
return response.ok ? json: Promise.reject(json);
}).then((data) => resolve(data.data)).catch((err) => reject(err));
});
}
/**
* Get an anime by id.
* @param id
* @returns {Promise<any>}
*/
exports.getAnimeById = async function(id) {
let data = await postGraphqlQuery('AnimeById', {id: id});
if (data.Media)
return data.Media;
else
return null;
};
/**
* Get a manga by id.
* @param id
* @returns {Promise<any>}
*/
exports.getMangaById = async function(id) {
let data = await postGraphqlQuery('MangaById', {id: id});
if (data.Media)
return data.Media;
else
return null;
};
/**
* Search for a media entry by name and return it.
* @param name
* @returns {Promise<any>}
*/
exports.searchMediaByName = async function(name) {
let data = await postGraphqlQuery('MediaSearchByName', {name: name});
if (data.Media)
return data.Media;
else
return null;
};
/**
* Search for an anime by name and get it by id.
* @param name
* @returns {Promise<*>}
*/
exports.searchAnimeByName = async function(name) {
let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'ANIME'});
if (data && data.Media && data.Media.id)
return await exports.getAnimeById(data.Media.id);
else
return null;
};
/**
* Search for a manga by name and get it by id.
* @param name
* @returns {Promise<*>}
*/
exports.searchMangaByName = async function(name) {
let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'MANGA'}).data;
if (data && data.Media && data.Media.id)
return await postGraphqlQuery('MangaById', {id: data.Media.id});
else
return null;
};

@ -5,7 +5,8 @@ const Discord = require('discord.js'),
args = require('args-parser')(process.argv),
config = require('../config.json'),
gcmdTempl = require('../commands/globalcommands'),
scmdTempl = require('../commands/servercommands');
scmdTempl = require('../commands/servercommands'),
utils = require('./utils');
let logger = require('winston'),
globCommands = {};
@ -34,34 +35,8 @@ exports.Servant = class {
return 'Command not found :(';
} else {
let helpEmbed = new Discord.RichEmbed()
.setTitle('Commands')
.setDescription('Create a sequence of commands with `;` (semicolon).')
.setTimestamp();
let globHelp = '';
Object.entries(globCommands).sort().forEach(([key, value]) => {
if (value.role !== 'owner' || checkPermission(msg, 'owner'))
globHelp += `\`${key}\` \t`;
});
helpEmbed.addField('Global Commands', globHelp);
let categories = [];
let catCommands = {};
Object.entries(this.commands).sort().forEach(([key, value]) => {
if (value.role !== 'owner' || checkPermission(msg, 'owner'))
if (!categories.includes(value.category)) {
categories.push(value.category);
catCommands[value.category] = `\`${key}\` \t`;
} else {
catCommands[value.category] += `\`${key}\` \t`;
}
});
for (let cat of categories)
helpEmbed.addField(cat, catCommands[cat]);
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
return helpEmbed;
let allCommands = {...globCommands, ...this.commands};
return createHelpEmbed(allCommands, msg, prefix);
}
});
@ -217,7 +192,7 @@ exports.parseMessage = function (msg) {
* Initializes the module by creating a help command
*/
exports.init = function (prefix) {
logger.verbose("Created help command");
logger.verbose("Creating help command...");
this.createGlobalCommand((prefix || config.prefix), gcmdTempl.utils.help, (msg, kwargs) => {
if (kwargs.command) {
let cmd = kwargs.command;
@ -231,25 +206,19 @@ exports.init = function (prefix) {
.addField('Permission Role', globCommands[cmd].role || 'all');
} else {
let helpEmbed = new Discord.RichEmbed()
.setTitle('Global Commands')
.setDescription('Create a sequence of commands with `;` (semicolon).')
.setTimestamp();
let description = '';
Object.entries(globCommands).sort().forEach(([key, value]) => {
if (value.role === 'owner' && checkPermission(msg, 'owner'))
description += `\`${key}\` \t`;
else if (value.role !== 'owner')
description += `\`${key}\` \t`;
});
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
helpEmbed.setDescription(description);
return helpEmbed;
return createHelpEmbed(globCommands, msg, prefix);
}
});
};
/**
* Processes commands for command series.
* @param cmd
* @param msg
* @param content
* @param returnFunction
* @returns {function(): *}
*/
function processCommand(cmd, msg, content, returnFunction) {
let argvars = content.match(/(?<= )\S+/g) || [];
let kwargs = {};
@ -306,6 +275,37 @@ function parseGlobalCommand(msg) {
}
}
/**
* Creates a rich embed that contains help for all commands in the commands object
* @param commands {Object}
* @param msg {module:discord.js.Message}
* @param prefix {String}
* @returns {module:discord.js.RichEmbed}
*/
function createHelpEmbed(commands, msg, prefix) {
let helpEmbed = new Discord.RichEmbed()
.setTitle('Commands')
.setDescription('Create a sequence of commands with `;` (semicolon).')
.setTimestamp();
let categories = [];
let catCommands = {};
Object.entries(commands).sort().forEach(([key, value]) => {
if (value.role !== 'owner' || checkPermission(msg, 'owner'))
if (!categories.includes(value.category)) {
categories.push(value.category);
catCommands[value.category] = `\`${key}\` \t`;
} else {
catCommands[value.category] += `\`${key}\` \t`;
}
});
for (let cat of categories)
helpEmbed.addField(cat, catCommands[cat]);
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
return helpEmbed;
}
/**
* @param msg
* @param rolePerm {String}
@ -323,3 +323,177 @@ function checkPermission(msg, rolePerm) {
return false;
}
/**
* Registers the bot's utility commands
* @param prefix
* @param bot - the instance of the bot that called
*/
exports.registerUtilityCommands = function(prefix, bot) {
// responde with the commands args
exports.createGlobalCommand(prefix, gcmdTempl.utils.say, (msg, argv, args) => {
return args.join(' ');
});
// adds a presence that will be saved in the presence file and added to the rotation
exports.createGlobalCommand(prefix, gcmdTempl.utils.addpresence, async (msg, argv, args) => {
let p = args.join(' ');
this.presences.push(p);
await bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]);
return `Added Presence \`${p}\``;
});
// shuts down the bot after destroying the client
exports.createGlobalCommand(prefix, gcmdTempl.utils.shutdown, async (msg) => {
try {
await msg.reply('Shutting down...');
logger.debug('Destroying client...');
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await bot.client.destroy();
logger.debug('Exiting server...');
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await bot.webServer.stop();
logger.debug(`Exiting Process...`);
process.exit(0);
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
});
// forces a presence rotation
exports.createGlobalCommand(prefix, gcmdTempl.utils.rotate, () => {
try {
bot.client.clearInterval(this.rotator);
bot.rotatePresence();
bot.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration);
} catch (error) {
logger.warn(error.message);
}
});
exports.createGlobalCommand(prefix, gcmdTempl.utils.createUser, (msg, argv) => {
return new Promise((resolve, reject) => {
if (msg.guild) {
resolve("It's not save here! Try again via PM.");
} else if (argv.username && argv.scope) {
logger.debug(`Creating user entry ${argv.username}, scope: ${argv.scope}`);
bot.webServer.createUser(argv.username, argv.password, argv.scope, false).then((token) => {
resolve(`Created entry
username: ${argv.username},
scope: ${argv.scope},
token: ${token}
`);
}).catch((err) => reject(err.message));
}
});
});
exports.createGlobalCommand(prefix, gcmdTempl.utils.bugreport, () => {
return new Discord.RichEmbed()
.setTitle('Where to report a bug?')
.setDescription(gcmdTempl.utils.bugreport.response.bug_report);
});
};
/**
* Registers the bot's info commands
* @param prefix {String}
* @param bot {Object}
*/
exports.registerInfoCommands = function(prefix, bot) {
// ping command that returns the ping attribute of the client
exports.createGlobalCommand(prefix, gcmdTempl.info.ping, () => {
return `Current average ping: \`${bot.client.ping} ms\``;
});
// returns the time the bot is running
exports.createGlobalCommand(prefix, gcmdTempl.info.uptime, () => {
let uptime = utils.getSplitDuration(bot.client.uptime);
return new Discord.RichEmbed().setDescription(`
**${uptime.days}** days
**${uptime.hours}** hours
**${uptime.minutes}** minutes
**${uptime.seconds}** seconds
**${uptime.milliseconds}** milliseconds
`).setTitle('Uptime');
});
// returns the number of guilds, the bot has joined
exports.createGlobalCommand(prefix, gcmdTempl.info.guilds, () => {
return `Number of guilds: \`${bot.client.guilds.size}\``;
});
// returns information about the bot
exports.createGlobalCommand(prefix, gcmdTempl.info.about, () => {
return new Discord.RichEmbed()
.setTitle('About')
.setDescription(gcmdTempl.info.about.response.about_creator)
.addField('Icon', gcmdTempl.info.about.response.about_icon);
});
};
/**
* Registers all commands that use the anilist api.
* @param prefix {String}
*/
exports.registerAnilistApiCommands = function(prefix) {
const anilistApi = require('./anilistApiLib');
// returns the anime found for the name
exports.createGlobalCommand(prefix, gcmdTempl.api.AniList.animeSearch, async (msg, kwargs, argv) => {
try {
let animeData = await anilistApi.searchAnimeByName(argv.join(' '));
if (animeData) {
let response = new Discord.RichEmbed()
.setTitle(animeData.title.romaji)
.setDescription(animeData.description.replace(/<\/?.*?>/g, ''))
.setThumbnail(animeData.coverImage.large)
.setURL(animeData.siteUrl)
.setColor(animeData.coverImage.color)
.addField('Genres', animeData.genres.join(', '))
.setTimestamp();
if (animeData.studios.studioList.length > 0)
response.addField(animeData.studios.studioList.length === 1? 'Studio' : 'Studios', animeData.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`));
response.addField('Scoring', `**Average Score:** ${animeData.averageScore}
**Favourites:** ${animeData.favourites}`);
if (animeData.episodes)
response.addField('Episodes', animeData.episodes);
response.addField('Season', animeData.season);
if (animeData.startDate.day)
response.addField('Start Date', `
${animeData.startDate.day}.${animeData.startDate.month}.${animeData.startDate.year}`);
if (animeData.nextAiringEpisode)
response.addField('Next Episode', `**Episode** ${animeData.nextAiringEpisode.episode}
**Airing at:** ${new Date(animeData.nextAiringEpisode.airingAt*1000).toUTCString()}`);
if (animeData.endDate.day)
response.addField('End Date', `
${animeData.endDate.day}.${animeData.endDate.month}.${animeData.endDate.year}`);
return response;
} else {
return gcmdTempl.api.AniList.animeSearch.response.not_found;
}
} catch (err) {
if (err.message) {
logger.warn(err.message);
logger.debug(err.stack);
} else {
logger.debug(JSON.stringify(err));
}
return gcmdTempl.api.AniList.animeSearch.response.not_found;
}
});
};

@ -0,0 +1,47 @@
query ($id: Int) {
Media (id: $id, type: ANIME) {
id
title {
romaji
english
native
}
status
startDate {
year
month
day
}
endDate {
year
month
day
}
format
season
episodes
duration
genres
siteUrl
coverImage {
large
medium
color
}
description(asHtml: false)
averageScore
favourites
studios(isMain: true) {
studioList: nodes {
id
name
siteUrl
}
}
nextAiringEpisode {
id
airingAt
episode
}
}
}

@ -0,0 +1,53 @@
query ($id: Int) {
Media (id: $id, type: MANGA) {
id
title {
romaji
english
native
}
status
startDate {
year
month
day
}
endDate {
year
month
day
}
format
chapters
volumes
genres
siteUrl
coverImage {
large
medium
color
}
staff {
edges {
node {
id
name {
first
last
native
}
image {
large
medium
}
language
siteUrl
}
role
}
}
description(asHtml: false)
averageScore
favourites
}
}

@ -0,0 +1,11 @@
query ($name: String, $type: MediaType) {
Media (search: $name, type: $type) {
id
title {
romaji
english
native
}
type
}
}

@ -23,10 +23,13 @@ exports.WebServer = class {
this.app = express();
this.server = null;
this.port = port;
this.schema = buildSchema(fs.readFileSync('./web/graphql/schema.graphql', 'utf-8'));
this.schema = buildSchema(fs.readFileSync('./lib/graphql/schema.gql', 'utf-8'));
this.root = {};
}
/**
* Configures express by setting properties and middleware.
*/
configureExpress() {
this.app.set('view engine', 'pug');
this.app.set('trust proxy', 1);

@ -1,6 +1,6 @@
{
"name": "discordbot",
"version": "1.0.0",
"version": "0.9.1",
"scripts": {
"start": "node bot.js",
"test": "mocha --exit",
@ -23,6 +23,7 @@
"graphql": "14.1.1",
"js-md5": "0.7.3",
"js-sha512": "0.8.0",
"node-fetch": "^2.3.0",
"node-sass": "4.11.0",
"opusscript": "0.0.6",
"promise-waterfall": "0.1.0",

Loading…
Cancel
Save