// Copyright 2019 Campbell Crowley. All rights reserved. // Author: Campbell Crowley (dev@campbellcrowley.com) const SubModule = require('./subModule.js'); const https = require('https'); const auth = require('../auth.js'); /** * @classdesc Handles fetching images and GIFs from various APIs. * @class * @augments SubModule */ class Images extends SubModule { /** * @description Handles fetching images and GIFs from various APIs. */ constructor() { super(); /** @inheritdoc */ this.myName = 'Images'; /** * @description Cached responses from recent requests to Imgur's API. Mapped * by request path. * @private * @type {object} * @default */ this._imgurCache = {}; /** * @description Cached responses from recent requests to Giphy's API. Mapped * by request path. * @private * @type {object} * @default */ this._giphyCache = {}; /** * @description Maximum amount of time to cache a request to Imgur before we * purge it. * @private * @type {number} * @default 1 Week */ this._imgurCacheDuration = 7 * 24 * 60 * 60 * 1000; /** * @description Maximum amount of time to cache a request to Giphy before we * purge it. * @private * @type {number} * @default 1 Day */ this._giphyCacheDuration = 1 * 24 * 60 * 60 * 1000; this._commandCookie = this._commandCookie.bind(this); this._commandImgur = this._commandImgur.bind(this); this._commandGiphy = this._commandGiphy.bind(this); } /** @inheritdoc */ initialize() { this.command.on( ['imgur', 'image', 'png', 'jpg', 'jpeg'], this._commandImgur); this.command.on(['giphy', 'gif', 'giph'], this._commandGiphy); this.command.on(['cookie', 'cookies'], this._commandCookie); } /** @inheritdoc */ shutdown() { this.command.removeListener('imgur'); this.command.removeListener('giphy'); this.command.removeListener('cookie'); } /** * @description Fetch the default host information for making a request to * imgur. * @public * @static * @returns {object} Object to pass to https request. */ static get imgurHost() { return { protocol: 'https:', host: 'api.imgur.com', path: '/3/', method: 'GET', headers: { 'User-Agent': require('./common.js').ua, 'Authorization': 'Client-ID ' + auth.imgurID, 'Content-Type': 'application/json', }, }; } /** * @description Fetch the default host information for making a request to * giphy. * @public * @static * @returns {object} Object to pass to https request. */ static get giphyHost() { return { protocol: 'https:', host: 'api.giphy.com', path: '/v1/', method: 'GET', headers: { 'User-Agent': require('./common.js').ua, 'Content-Type': 'application/json', }, }; } /** * @description Performs a get request to Imgur's API at the given path, and * caches the result before returning it through the callback. If a cached * response is available that isn't too old, that will immediately be returned * instead. * @private * @param {string} path The full API path after the domain and API version. * Includes query parameters. * @param {Function} cb Callback once completed or failed. First parameter is * optional error, second is otherwise the parsed JSON object from Imgur. */ _imgurGet(path, cb) { if (this._imgurCache[path]) { if (Date.now() - this._imgurCache[path].timestamp < this._imgurCacheDuration) { cb(null, this._imgurCache[path].data); return; } else { // update timestamp to prevent multiple requests at the same time before // the first completes. this._imgurCache[path].timestamp = Date.now(); } } const host = Images.imgurHost; host.path += path; const req = https.request(host, (res) => { let content = ''; res.on('data', (chunk) => content += chunk); res.on('end', () => { if (res.statusCode == 200) { try { content = JSON.parse(content); this._imgurCache[path] = {data: content, timestamp: Date.now()}; } catch (err) { this.error('Imgur Unable to parse response to ' + path); console.error(err); cb(err); return; } cb(null, content); } else { this.error('Imgur ' + res.statusCode + ': ' + content); console.error(host); cb(new Error('Imgur Bad Response')); } }); }); req.end(); } /** * @description Performs a get request to Giphy's API at the given path, and * caches the result before returning it through the callback. If a cached * response is available that isn't too old, that will immediately be returned * instead. * @private * @param {string} path The full API path after the domain and API version. * Includes query parameters. * @param {Function} cb Callback once completed or failed. First parameter is * optional error, second is otherwise the parsed JSON object from Imgur. */ _giphyGet(path, cb) { if (this._giphyCache[path]) { if (Date.now() - this._giphyCache[path].timestamp < this._giphyCacheDuration) { cb(null, this._giphyCache[path].data); return; } else { // update timestamp to prevent multiple requests at the same time before // the first completes. this._giphyCache[path].timestamp = Date.now(); } } const host = Images.giphyHost; host.path += path; const req = https.request(host, (res) => { let content = ''; res.on('data', (chunk) => content += chunk); res.on('end', () => { if (res.statusCode == 200) { try { content = JSON.parse(content); this._giphyCache[path] = {data: content, timestamp: Date.now()}; } catch (err) { this.error('Giphy Unable to parse response to ' + path); console.error(err); cb(err); return; } cb(null, content); } else { this.error('Giphy ' + res.statusCode + ': ' + content); console.error(host); cb(new Error('Giphy Bad Response')); } }); }); req.end(); } /** * @description Searches Imgur for the given query. * * @private * @type {commandHandler} * @param {Discord~Message} msg Message that triggered command. * @param {number} [pages=1] Number of potential pages to view. * @listens Command#imgur */ _commandImgur(msg, pages = 1) { const query = '?q=' + encodeURIComponent(msg.text + ' ext:png OR jpg OR gif'); const page = Math.floor(Math.random() * pages); const path = 'gallery/search/top/all/' + page + query; this._imgurGet(path, (err, content) => { if (!err) { const list = content.data; if (list.length == 0) { this.common.reply(msg, 'Failed to search for ' + msg.text.trim()); return; } const image = list[Math.floor(Math.random() * list.length)]; const url = image.images ? image.images[0].link : image.link; const embed = new this.Discord.EmbedBuilder({title: image.title}); embed.setFooter({text: image.link}); if (image.description) { embed.setDescription(image.description); } else if ( image.images && image.images[0] && image.images[0].description) { embed.setDescription(image.images[0].description); } embed.setURL(image.link); embed.setImage(url); msg.channel.send({embeds: [embed]}) .catch( () => this.common.reply( msg, 'Failed to send reply.', 'Am I able to embed images and links?')); } }); } /** * @description Searches Giphy for the given query. * * @private * @type {commandHandler} * @param {Discord~Message} msg Message that triggered command. * @param {number} [pages=1] Number of potential pages to view. * @param {number} [count=20] Number of results per each page. * @listens Command#giphy */ _commandGiphy(msg, pages = 1, count = 20) { const page = Math.floor(Math.random() * pages); const query = '?api_key=' + auth.giphyKey + '&q=' + encodeURIComponent(msg.text) + '&limit=' + count + '&offset=' + (page * count); const path = 'gifs/search' + query; this._giphyGet(path, (err, content) => { if (!err) { const list = content.data; if (list.length == 0) { this.common.reply(msg, 'Failed to search for ' + msg.text.trim()); return; } const image = list[Math.floor(Math.random() * list.length)]; const url = image.images.original.url; const embed = new this.Discord.EmbedBuilder({title: image.title}); embed.setFooter({text: image.url}); embed.setURL(image.url); embed.setImage(url); msg.channel.send({embeds: [embed]}) .catch( () => this.common.reply( msg, 'Failed to send reply.', 'Am I able to embed images and links?')); } }); } /** * Replies with a picture of a cookie. * * @private * @type {commandHandler} * @param {Discord~Message} msg Message that triggered command. * @listens Command#cookie * @listens Command#cookies */ _commandCookie(msg) { msg.text = 'cookies'; this._commandGiphy(msg, 100); } } module.exports = new Images();