diff --git a/README.md b/README.md index 867c15f2..79e99872 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Xiao is a Discord bot coded in JavaScript with 7. Run `npm i -g pm2` to install PM2. 8. Run `pm2 start Xiao.js --name xiao` to run the bot. -## Commands (333) +## Commands (334) ### Utility: * **eval:** Executes JavaScript code. @@ -159,6 +159,7 @@ Xiao is a Discord bot coded in JavaScript with * **azur-lane:** Responds with information on an Azur Lane ship. * **book:** Searches Google Books for a book. * **bulbapedia:** Searches Bulbapedia for your query. +* **character:** Searches AniList for your query, getting character results. * **danbooru:** Responds with an image from Danbooru, with optional query. * **define:** Defines a word. * **derpibooru:** Responds with an image from Derpibooru. diff --git a/commands/search/anime.js b/commands/search/anime.js index 78462b8d..382a288d 100644 --- a/commands/search/anime.js +++ b/commands/search/anime.js @@ -18,11 +18,15 @@ const resultGraphQL = stripIndents` english userPreferred } - coverImage { large } + coverImage { + large + medium + } startDate { year } - description + description(asHtml: false) season type + siteUrl status episodes isAdult @@ -30,6 +34,18 @@ const resultGraphQL = stripIndents` } } `; +const seasons = { + WINTER: 'Winter', + SPRING: 'Spring', + SUMMER: 'Summer', + FALL: 'Fall' +}; +const statuses = { + FINISHED: 'Finished', + RELEASING: 'Releasing', + NOT_YET_RELEASED: 'Unreleased', + CANCELLED: 'Cancelled' +}; module.exports = class AnimeCommand extends Command { constructor(client) { @@ -58,14 +74,14 @@ module.exports = class AnimeCommand extends Command { const embed = new MessageEmbed() .setColor(0x02A9FF) .setAuthor('AniList', 'https://i.imgur.com/iUIRC7v.png', 'https://anilist.co/') - .setURL(`https://anilist.co/anime/${anime.id}`) - .setThumbnail(anime.coverImage.large || null) + .setURL(anime.siteUrl) + .setThumbnail(anime.coverImage.large || anime.coverImage.medium || null) .setTitle(anime.title.english || anime.title.userPreferred) - .setDescription(anime.description ? shorten(anime.description.replace(/(
)+/g, '\n')) : 'No description.') - .addField('❯ Status', anime.status, true) - .addField('❯ Episodes', anime.episodes, true) - .addField('❯ Season', `${anime.season} ${anime.startDate.year}`, true) - .addField('❯ Average Score', `${anime.meanScore}/100`, true); + .setDescription(anime.description ? shorten(anime.description) : 'No description.') + .addField('❯ Status', statuses[anime.status], true) + .addField('❯ Episodes', anime.episodes || '???', true) + .addField('❯ Season', anime.season ? `${seasons[anime.season]} ${anime.startDate.year}` : '???', true) + .addField('❯ Average Score', anime.meanScore ? `${anime.meanScore}/100` : '???', true); return msg.embed(embed); } catch (err) { return msg.reply(`Oh no, an error occurred: \`${err.message}\`. Try again later!`); diff --git a/commands/search/character.js b/commands/search/character.js new file mode 100644 index 00000000..839d31c4 --- /dev/null +++ b/commands/search/character.js @@ -0,0 +1,105 @@ +const Command = require('../../structures/Command'); +const { MessageEmbed } = require('discord.js'); +const request = require('node-superfetch'); +const { stripIndents } = require('common-tags'); +const { shorten } = require('../../util/Util'); +const searchGraphQL = stripIndents` + query ($search: String) { + characters: Page (perPage: 1) { + results: characters (search: $search) { id } + } + } +`; +const resultGraphQL = stripIndents` + query ($id: Int!) { + Character (id: $id) { + id + name { native } + image { + large + medium + } + description(asHtml: false) + siteUrl + media(page: 1, perPage: 10) { + edges { + node { + title { + english + userPreferred + } + type + siteUrl + } + } + } + } + } +`; +const types = { + ANIME: 'Anime', + MANGA: 'Manga' +}; + +module.exports = class CharacterCommand extends Command { + constructor(client) { + super(client, { + name: 'character', + aliases: ['anilist-character', 'anime-character', 'manga-character', 'manga-char', 'ani-char', 'char'], + group: 'search', + memberName: 'character', + description: 'Searches AniList for your query, getting character results.', + clientPermissions: ['EMBED_LINKS'], + args: [ + { + key: 'query', + prompt: 'What character would you like to search for?', + type: 'string' + } + ] + }); + } + + async run(msg, { query }) { + try { + const id = await this.search(query); + if (!id) return msg.say('Could not find any results.'); + const character = await this.fetchCharacter(id); + const embed = new MessageEmbed() + .setColor(0x02A9FF) + .setAuthor('AniList', 'https://i.imgur.com/iUIRC7v.png', 'https://anilist.co/') + .setURL(character.siteUrl) + .setThumbnail(character.image.large || character.image.medium || null) + .setTitle(character.name.native) + .setDescription(character.description ? shorten(character.description) : 'No description.') + .addField('❯ Appearances', character.edges.map(edge => { + const title = edge.node.title.english || edge.node.title.userPreferred; + return `[${title} (${types[edge.node.type]})](${edge.node.siteUrl})`; + }).join(', ')); + return msg.embed(embed); + } catch (err) { + return msg.reply(`Oh no, an error occurred: \`${err.message}\`. Try again later!`); + } + } + + async search(query) { + const { body } = await request + .post('https://graphql.anilist.co/') + .send({ + variables: { search: query }, + query: searchGraphQL + }); + if (!body.data.characters.results.length) return null; + return body.data.characters.results[0].id; + } + + async fetchCharacter(id) { + const { body } = await request + .post('https://graphql.anilist.co/') + .send({ + variables: { id }, + query: resultGraphQL + }); + return body.data.Character; + } +}; diff --git a/commands/search/manga.js b/commands/search/manga.js index 05c5c938..3d3f44aa 100644 --- a/commands/search/manga.js +++ b/commands/search/manga.js @@ -20,7 +20,8 @@ const resultGraphQL = stripIndents` } coverImage { large } startDate { year } - description + description(asHtml: false) + siteUrl type status volumes @@ -30,6 +31,12 @@ const resultGraphQL = stripIndents` } } `; +const statuses = { + FINISHED: 'Finished', + RELEASING: 'Releasing', + NOT_YET_RELEASED: 'Unreleased', + CANCELLED: 'Cancelled' +}; module.exports = class MangaCommand extends Command { constructor(client) { @@ -58,14 +65,14 @@ module.exports = class MangaCommand extends Command { const embed = new MessageEmbed() .setColor(0x02A9FF) .setAuthor('AniList', 'https://i.imgur.com/iUIRC7v.png', 'https://anilist.co/') - .setURL(`https://anilist.co/manga/${manga.id}`) - .setThumbnail(manga.coverImage.large || null) + .setURL(manga.siteUrl) + .setThumbnail(manga.coverImage.large || manga.coverImage.medium || null) .setTitle(manga.title.english || manga.title.userPreferred) - .setDescription(manga.description ? shorten(manga.description.replace(/(
)+/g, '\n')) : 'No description.') - .addField('❯ Status', manga.status, true) + .setDescription(manga.description ? shorten(manga.description) : 'No description.') + .addField('❯ Status', statuses[manga.status], true) .addField('❯ Chapters / Volumes', `${manga.chapters || '???'}/${manga.volumes || '???'}`, true) - .addField('❯ Year', manga.startDate.year, true) - .addField('❯ Average Score', `${manga.meanScore}/100`, true); + .addField('❯ Year', manga.startDate.year || '???', true) + .addField('❯ Average Score', manga.meanScore ? `${manga.meanScore}/100` : '???', true); return msg.embed(embed); } catch (err) { return msg.reply(`Oh no, an error occurred: \`${err.message}\`. Try again later!`); diff --git a/package.json b/package.json index f222e9d0..5bb54f28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xiao", - "version": "101.2.1", + "version": "101.3.0", "description": "Your personal server companion.", "main": "Xiao.js", "scripts": {