Implemented basic AniList api features

- changed anilist query files
- added autocropping to ExtendedRichEmbed
- updated Readme
pull/51/head
Trivernis 6 years ago
parent e74fa83ed3
commit ff9d78566b

@ -63,6 +63,7 @@ At the moment the bot can...
- [x] ...log stuff in a database
- [x] ...execute multiple commands as a sequence
- [x] ...save command sequences with a given name
- [x] ...query AniList
- [ ] ...transform into a cow
Presences
@ -76,15 +77,19 @@ Command Sequences
A command sequence is a single message with several commands seperated by a semicolon.
In a sequence the command can be ommitted if it is the same as the previous one.
That means you can add several videos to the queue and shuffle it afterwards with the sequence
`~play [video1]; [video2]; [video3]; ~shuffle`.
`~play [video1] && ~play [video2]; ~play [video3] && ~shuffle`.
A command sequence can be saved with `~savecmd [sequence] [commandname]`.
In this case the semicolon must be escaped with a backslash so it won't get interpreted as a seperate command.
A command sequence can be saved with `~savecmd [commandname] [sequence]`.
In this case the semicolon must be escaped with a backslash so it won't get interpreted as a seperate command. You can also escape sequences with `~play "whatever &&; you want"` (doublequotes). Command sequences with `&&` are executed in serial while command sequences with `;` are executed in parallel.
A saved command can be executed with `~execute [commandname]`.
References
---
You can test a running version of the bot. [Invite bot server](https://discordapp.com/oauth2/authorize?client_id=374703138575351809&scope=bot&permissions=1983380544)
Ideas
---
- command replies saved in file (server specific file and global file)
- reddit api
- anilist api
- othercoolstuff api

@ -226,7 +226,7 @@ class ExtendedRichEmbed extends Discord.RichEmbed {
* @returns {ExtendedRichEmbed}
*/
addNonemptyField(name, content) {
if (name && name.length > 0 && content)
if (name && name.length > 0 && content && content.length > 0)
this.addField(name, content);
return this;
}
@ -241,6 +241,29 @@ class ExtendedRichEmbed extends Discord.RichEmbed {
this.addNonemptyField(name, value);
return this;
}
/**
* Sets the description by shortening the value string to a fitting length for discord.
* @param value
*/
setDescription(value) {
let croppedValue = value.substring(0, 1024);
if (croppedValue.length < value.length)
croppedValue = croppedValue.replace(/\n.*$/g, '');
super.setDescription(croppedValue);
}
/**
* Sets the field by shortening the value stirn to a fitting length for discord.
* @param name
* @param value
*/
addField(name, value) {
let croppedValue = value.substring(0, 1024);
if (croppedValue.length < value.length)
croppedValue = croppedValue.replace(/\n.*$/g, '');
super.addField(name, croppedValue);
}
}
// -- exports -- //

@ -277,7 +277,7 @@ class MusicPlayer {
}
queue(args) {
let queue = this.dj.queue.map((x) => {
let queue = this.musicPlayer.queue.map((x) => {
return {
id: generateID(['Media', x.url]),
name: x.title,
@ -301,7 +301,7 @@ class MusicPlayer {
}
get paused() {
return this.musicPlayer.disp? this.dj.disp.paused : false;
return this.musicPlayer.disp? this.musicPlayer.disp.paused : false;
}
get queueCount() {

@ -1,15 +1,26 @@
const fetch = require('node-fetch'),
fsx = require('fs-extra'),
yaml = require('js-yaml'),
queryPath = './lib/api/graphql/AnilistApi',
alApiEndpoint = 'https://graphql.anilist.co';
async function getFragments() {
let fragments = await fsx.readFile(`${queryPath}/Fragments.yaml`, {encoding: 'utf-8'});
return yaml.safeLoad(fragments);
}
/**
* Return a graphql query read from a file from a configured path.
* @param name
* @returns {Promise<String>}
*/
async function getGraphqlQuery(name) {
return await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'});
let query = await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'});
let fragments = await getFragments();
for (let [key, value] of Object.entries(fragments))
if (query.includes(`...${key}`))
query += '\n' + value;
return query;
}
/**
@ -27,7 +38,7 @@ function postGraphqlQuery(queryName, queryVariables) {
'Accept': 'application/json'
},
body: JSON.stringify({
query: await getGraphqlQuery(queryName),
query: (await getGraphqlQuery(queryName)),
variables: queryVariables
})
}).then(async (response) => {
@ -39,65 +50,128 @@ function postGraphqlQuery(queryName, queryVariables) {
/**
* Get an anime by id.
* @param id
* @param id {Number}
* @param withStaff {Boolean} Include Staff information?
* @param withMetadata {Boolean} Include Metadata?
* @returns {Promise<JSON>}
*/
exports.getAnimeById = async function(id) {
let data = await postGraphqlQuery('AnimeById', {id: id});
if (data.Media)
async function getAnimeById(id, withStaff, withMoreData) {
let data = await postGraphqlQuery('AnimeQuery',
{id: id, withStaff: withStaff, withMoreData: withMoreData});
if (data && data.Media)
return data.Media;
else
return null;
};
}
/**
* Get a manga by id.
* @param id
* @param id {Number}
* @param withStaff {Boolean} Include Staff information?
* @param withMoreData {Boolean} Include Metadata?
* @returns {Promise<JSON>}
*/
exports.getMangaById = async function(id) {
let data = await postGraphqlQuery('MangaById', {id: id});
if (data.Media)
async function getMangaById(id, withStaff, withMoreData) {
let data = await postGraphqlQuery('MangaQuery',
{id: id, withStaff: withStaff, withMoreData: withMoreData});
if (data && data.Media)
return data.Media;
else
return null;
};
}
/**
* Search for a media entry by name and return it.
* @param name
* @returns {Promise<JSON>}
* Returns a staff member by id.
* @param id {Number}
* @returns {Promise<*>}
*/
exports.searchMediaByName = async function(name) {
let data = await postGraphqlQuery('MediaSearchByName', {name: name});
if (data.Media)
return data.Media;
async function getStaffById(id) {
let data = await postGraphqlQuery('StaffQuery', {id: id});
if (data && data.Staff)
return data.Staff;
else
return null;
};
}
/**
* Returns a character by id.
* @param id {Number}
* @returns {Promise<*>}
*/
async function getCharacterById(id) {
let data = await postGraphqlQuery('CharacterQuery', {id: id});
if (data && data.Character)
return data.Character;
else
return null;
}
/**
* Search for an anime by name and get it by id.
* @param name
* @param name {String}
* @param withStaff {Boolean} Include Staff information?
* @param withMoreData {Boolean} Include Metadata?
* @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);
async function searchAnimeByName(name, withStaff, withMoreData) {
let data = await postGraphqlQuery('AnimeQuery',
{name: name, withStaff: withStaff, withMoreData: withMoreData});
if (data && data.Media)
return data.Media;
else
return null;
};
}
/**
* Search for a manga by name and get it by id.
* @param name
* @param name {String}
* @param withStaff {Boolean} Include Staff information?
* @param withMoreData {Boolean} Include Metadata?
* @returns {Promise<*>}
*/
async function searchMangaByName(name, withStaff, withMoreData) {
let data = await postGraphqlQuery('MangaQuery',
{name: name, withStaff: withStaff, withMoreData: withMoreData});
if (data && data.Media)
return data.Media;
else
return null;
}
/**
* Search for a staff member by name and get information.
* @param name {String} The name of the staff member
* @returns {Promise<*>}
*/
exports.searchMangaByName = async function(name) {
let data = await postGraphqlQuery('MediaSearchByName', {name: name, type: 'MANGA'});
if (data && data.Media && data.Media.id)
return await exports.getMangaById(data.Media.id);
async function searchStaffByName(name) {
let data = await postGraphqlQuery('StaffQuery', {name: name});
if (data && data.Staff)
return data.Staff;
else
return null;
};
}
/**
* Seach for a character by name and get information.
* @param name {String} Character Name
* @returns {Promise<*>}
*/
async function searchCharacterByName(name) {
let data = await postGraphqlQuery('CharacterQuery', {name: name});
if (data && data.Character)
return data.Character;
else
return null;
}
// exports
Object.assign(exports, {
getAnimeById: getAnimeById,
getMangaById: getMangaById,
getStaffById: getStaffById,
getCharacterById: getCharacterById,
searchAnimeByName: searchAnimeByName,
searchMangaByName: searchMangaByName,
searchStaffByName: searchStaffByName,
searchCharacterByName: searchCharacterByName
});

@ -1,47 +0,0 @@
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,22 @@
query AnimeData($name: String, $id: Int, $withStaff: Boolean = false, $withMoreData: Boolean = true) {
Media (id: $id, search: $name, type: ANIME) {
...mediaMetadata
...mediaAdditionalMetadata @include(if: $withMoreData)
...staffFields @include(if: $withStaff)
season @include(if: $withMoreData)
episodes @include(if: $withMoreData)
duration @include(if: $withMoreData)
studios(isMain: true) @include(if: $withMoreData) {
studioList: nodes {
id
name
siteUrl
}
}
nextAiringEpisode @include(if: $withMoreData) {
id
airingAt
episode
}
}
}

@ -0,0 +1,27 @@
query ($name: String, $id: Int) {
Character(search: $name, id: $id) {
id
name {
first
last
native
}
description
image {
large
medium
}
siteUrl
media {
edges {
characterRole
voiceActors(language: JAPANESE) {
...staffMetadata
}
node {
...mediaMetadata
}
}
}
}
}

@ -1,12 +1,27 @@
query ($id: Int) {
Media (id: $id, type: MANGA) {
mediaMetadata: |
fragment mediaMetadata on Media {
id
siteUrl
title {
romaji
english
native
}
coverImage {
large
medium
color
}
}
mediaAdditionalMetadata: |
fragment mediaAdditionalMetadata on Media {
status
description(asHtml: false)
format
genres
averageScore
favourites
startDate {
year
month
@ -17,37 +32,32 @@ query ($id: Int) {
month
day
}
format
chapters
volumes
genres
siteUrl
coverImage {
}
staffMetadata: |
fragment staffMetadata on Staff {
id
name {
first
last
native
}
image {
large
medium
color
}
language
siteUrl
}
staffFields: |
fragment staffFields on Media {
staff {
edges {
node {
id
name {
first
last
native
}
image {
large
medium
}
language
siteUrl
...staffMetadata
}
role
}
}
description(asHtml: false)
averageScore
favourites
}
}

@ -0,0 +1,9 @@
query MangaData($name: String, $id: Int, $withStaff: Boolean = false, $withMoreData: Boolean = true) {
Media (id: $id, search: $name, type: MANGA) {
...mediaMetadata
...mediaAdditionalMetadata @include(if: $withMoreData)
...staffFields @include(if: $withStaff)
chapters @include(if: $withMoreData)
volumes @include(if: $withMoreData)
}
}

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

@ -0,0 +1,57 @@
query StaffData($name: String, $id: Int) {
Staff(id: $id, search: $name) {
id
name {
first
last
native
}
language
image {
large
medium
}
staffMedia(page: 0, perPage: 10) {
edges {
node {
id
title {
romaji
english
native
}
siteUrl
}
characters {
id
name {
first
last
}
siteUrl
image {
large
medium
}
}
staffRole
}
}
characters(page: 0, perPage: 10) {
nodes {
id
name {
first
last
}
siteUrl
image {
large
medium
}
}
}
description(asHtml: false)
siteUrl
}
}

@ -1,23 +1,58 @@
anime_search:
name: anime
name: alAnime
permission: all
usage: anime [search query]
usage: alAnime [search query]
description: >
Searches AniList.co for the anime title and returns information about
it if there is a result.
Searches [AniList.co](https://anilist.co) for the anime *title* or *id* and returns information about
it if there is a result. The staff members are not included because the message would grow too big.
category: AniList
response:
not_found: >
I couldn't find the anime you were searching for :(
anime_staff_search:
name: alAnimeStaff
permission: all
usage: alAnimeStaff [search query]
description: >
Searches [AniList.co](https://anilist.co) for the anime *title* or *id* and returns all staff members.
category: AniList
response:
not_found: >
I couldn't find the anime you were searching for :(
manga_search:
name: manga
name: alManga
permission: all
usage: manga [search query]
usage: alManga [search query]
description: >
Searches AniList.co for the manga title and returns information about
Searches [AniList.co](https://anilist.co) for the manga *title* or *id* and returns information about
it if there is a result.
category: AniList
response:
not_found: >
I couldn't find the manga you were searching for :(
staff_search:
name: alStaff
permission: all
usage: alStaff [search query]
description: >
Searches [AniList.co](https://anilist.co) for the staff member *name* or *id* and returns information about
the member aswell as roles in media.
category: AniList
response:
not_found: >
I couldn't find the staff member you were searching for :(
character_search:
name: alCharacter
permission: all
usage: alCharacter [search query]
description: >
Searches [AniList.co](https://anilist.co) for the character *name* or *id* and returns information about
the character aswell as media roles.
category: AniList
response:
not_found: >
I couldn't find the character member you were searching for :(

@ -2,6 +2,21 @@ const cmdLib = require('../../CommandLib'),
anilistApi = require('../../api/AnilistApi'),
location = './lib/commands/AnilistApiCommands';
/**
* Returns a string for a name.
* @param nameNode {String} The AniList name node in format {first, last, native}
*/
function getNameString(nameNode) {
let name = '';
if (nameNode.first)
name = nameNode.first;
if (nameNode.last)
name += ' ' + nameNode.last;
if (name.length === 0)
name = nameNode.native;
return name;
}
class RichMediaInfo extends cmdLib.ExtendedRichEmbed {
/**
@ -10,16 +25,22 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed {
*/
constructor(mediaInfo) {
super(mediaInfo.title.romaji);
this.setDescription(mediaInfo.description.replace(/<\/?.*?>/g, ''))
.setThumbnail(mediaInfo.coverImage.large)
this.setThumbnail(mediaInfo.coverImage.large || mediaInfo.coverImage.medium)
.setURL(mediaInfo.siteUrl)
.setColor(mediaInfo.coverImage.color)
.setFooter('Provided by AniList.co');
.setFooter('Powered by AniList.co');
if (mediaInfo.description)
this.setDescription(mediaInfo.description
.replace(/<\/?.*?>/g, '')
.replace(/~!.*?!~/g, '')
.replace(/\n\n\n/g, ''));
let fields = {
'Genres': mediaInfo.genres.join(' '),
'Genres': mediaInfo.genres? mediaInfo.genres.join(' ') : null,
'Studios': mediaInfo.studios? mediaInfo.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`) : null,
'Scoring': `**AverageScore**: ${mediaInfo.averageScore}\n**Favourites**${mediaInfo.favourites}`,
'Scoring': mediaInfo.averageScore? `**AverageScore**: ${mediaInfo.averageScore}\n**Favourites:** ${mediaInfo.favourites}`: null,
'Episodes': mediaInfo.episodes,
'Volumes': mediaInfo.volumes,
'Chapters': mediaInfo.chapters,
'Duration': null,
'Season': mediaInfo.season,
'Status': mediaInfo.status,
@ -27,18 +48,109 @@ class RichMediaInfo extends cmdLib.ExtendedRichEmbed {
};
if (mediaInfo.duration)
fields['Episode Duration'] = `${mediaInfo.duration} min`;
if (mediaInfo.startDate.day)
if (mediaInfo.startDate && mediaInfo.startDate.day)
fields['Start Date'] = `${mediaInfo.startDate.day}.${mediaInfo.startDate.month}.${mediaInfo.startDate.year}`;
if (mediaInfo.nextAiringEpisode) {
let epInfo = mediaInfo.nextAiringEpisode;
fields['Next Episode'] = `**Episode** ${epInfo.episode}\n**Airing at:** ${new Date(epInfo.airingAt * 1000).toUTCString()}`;
}
if (mediaInfo.endDate.day)
if (mediaInfo.endDate && mediaInfo.endDate.day)
fields['End Date'] = `${mediaInfo.endDate.day}.${mediaInfo.endDate.month}.${mediaInfo.endDate.year}`;
if (mediaInfo.staff && mediaInfo.staff.edges) {
let staffContent = mediaInfo.staff.edges.map((x) => {
let url = x.node.siteUrl;
let name = getNameString(x.node.name);
return `**${x.role}:** [${name}](${url})`;
});
let staffFieldValue = staffContent.join('\n');
if (staffFieldValue.length > 1024) {
let staffValues = [];
let currentValue = '';
for (let staffLine of staffContent) {
let concatValue = currentValue + '\n' + staffLine;
if (concatValue.length > 1024) {
staffValues.push(currentValue);
currentValue = staffLine;
} else {
currentValue = concatValue;
}
}
staffValues.push(currentValue);
for (let i = 0; i < staffValues.length; i++)
fields[`Staff part ${i + 1}`] = staffValues[i];
} else {
fields['Staff'] = staffFieldValue;
}
}
this.addFields(fields);
}
}
class RichStaffInfo extends cmdLib.ExtendedRichEmbed {
/**
* A Rich Embed with informatin about an AniList staff member.
* @param staffInfo
*/
constructor(staffInfo) {
super(getNameString(staffInfo.name));
this.setThumbnail(staffInfo.image.large || staffInfo.image.medium)
.setURL(staffInfo.siteUrl);
let fields = {
'Language': staffInfo.language
};
if (staffInfo.staffMedia && staffInfo.staffMedia.edges)
fields['Staff Media Roles (first 10)'] = staffInfo.staffMedia.edges.map(x => {
let node = x.node;
let title = node.title.romaji;
let url = node.siteUrl;
return `[**${title}**](${url}): ${x.staffRole}`;
}).join('\n');
if (staffInfo.characters && staffInfo.characters.nodes)
fields['Staff Character Roles (first 10)'] = staffInfo.characters.nodes.map(x => {
let name = getNameString(x.name);
let url = x.siteUrl;
return `[${name}](${url})`;
}).join('\n');
this.addFields(fields);
}
}
class RichCharacterInfo extends cmdLib.ExtendedRichEmbed {
/**
* A RichEmbed with information about an AniList character.
* @param characterInfo {Object}
*/
constructor(characterInfo) {
super(getNameString(characterInfo.name));
this.setURL(characterInfo.siteUrl)
.setThumbnail(characterInfo.image.large || characterInfo.image.medium);
if (characterInfo.description)
this.setDescription(characterInfo.description
.replace(/<\/?.*?>/g, '')
.replace(/~!.*?!~/g, '')
.replace(/\n\n\n/g, ''));
if (characterInfo.media && characterInfo.media.edges)
this.addNonemptyField(
'Media Appeareance',
characterInfo.media.edges.map(x => {
let media = x.node;
let informationString = `**[${media.title.romaji}](${media.siteUrl})**: ${x.characterRole}`;
if (x.voiceActors && x.voiceActors.length > 0)
informationString += ` voice by ${x.voiceActors.map(y => {
`[${getNameString(y.name)}](${y.siteUrl})`;
}).join(', ')}`;
return informationString;
}).join('\n')
);
}
}
// -- initialize -- //
/**
@ -54,38 +166,129 @@ class AniListCommandModule extends cmdLib.CommandModule {
async register(commandHandler) {
await this._loadTemplate();
let animeSearch = new cmdLib.Command(
this.template.anime_search,
new cmdLib.Answer(async (m, k, s) => {
try {
let animeData = await anilistApi.searchAnimeByName(s);
let animeData = {};
if (/^\d+$/.test(s))
animeData = await anilistApi.getAnimeById(s, false, true);
else
animeData = await anilistApi.searchAnimeByName(s, false, true);
this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`);
return new RichMediaInfo(animeData);
} catch (err) {
if (err.message) {
this._logger.verbose(err.message);
this._logger.silly(err.stack);
} else if (err.errors) {
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
}
return this.template.anime_search.response.not_found;
}
})
);
let animeStaffSearch = new cmdLib.Command(
this.template.anime_staff_search,
new cmdLib.Answer(async (m, k, s) => {
try {
let animeData = {};
if (/^\d+$/.test(s))
animeData = await anilistApi.getAnimeById(s, true, false);
else
animeData = await anilistApi.searchAnimeByName(s, true, false);
this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`);
return new RichMediaInfo(animeData);
} catch (err) {
if (err.message)
if (err.message) {
this._logger.verbose(err.message);
return this.template.anime_search.not_found;
this._logger.silly(err.stack);
} else if (err.errors) {
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
}
return this.template.anime_staff_search.response.not_found;
}
}));
})
);
let mangaSearch = new cmdLib.Command(
this.template.manga_search,
new cmdLib.Answer(async (m, k, s) => {
try {
let mangaData = await anilistApi.searchMangaByName(s);
let mangaData = {};
if (/^\d+$/.test(s))
mangaData = await anilistApi.getMangaById(s, true, true);
else
mangaData= await anilistApi.searchMangaByName(s, true, true);
this._logger.silly(`Manga Query returned ${JSON.stringify(mangaData)}`);
return new RichMediaInfo(mangaData);
} catch (err) {
if (err.message)
if (err.message) {
this._logger.verbose(err.message);
this._logger.silly(err.stack);
} else if (err.errors) {
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
}
return this.template.manga_search.response.not_found;
}
})
);
let staffSearch = new cmdLib.Command(
this.template.staff_search,
new cmdLib.Answer(async (m, k, s) => {
try {
let staffData = {};
if (/^\d+$/.test(s))
staffData = await anilistApi.getStaffById(s);
else
staffData = await anilistApi.searchStaffByName(s);
this._logger.silly(`Staff Query returned ${JSON.stringify(staffData)}`);
return new RichStaffInfo(staffData);
} catch (err) {
if (err.message) {
this._logger.verbose(err.message);
this._logger.silly(err.stack);
} else if (err.errors) {
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
}
return this.template.staff_search.response.not_found;
}
})
);
let characterSearch = new cmdLib.Command(
this.template.character_search,
new cmdLib.Answer(async (m, k, s) => {
try {
let characterData = {};
if (/^\d+$/.test(s))
characterData = await anilistApi.getCharacterById(s);
else
characterData = await anilistApi.searchCharacterByName(s);
this._logger.silly(`Character Query returned ${JSON.stringify(characterData)}`);
return new RichCharacterInfo(characterData)
} catch (err) {
if (err.message) {
this._logger.verbose(err.message);
return this.template.manga_search.not_found;
this._logger.silly(err.stack);
} else if (err.errors) {
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
}
return this.template.character_search.response.not_found;
}
})
);
// registering commands
commandHandler.registerCommand(animeSearch)
.registerCommand(mangaSearch);
commandHandler
.registerCommand(animeSearch)
.registerCommand(mangaSearch)
.registerCommand(staffSearch)
.registerCommand(animeStaffSearch)
.registerCommand(characterSearch);
}
}

@ -140,7 +140,6 @@ function queryGuildStatus(guildId) {
}
}
}
config
}`;
postQuery(query).then((res) => {
let guild = res.data.client.guilds[0];

Loading…
Cancel
Save