From 0600cf230f342b9d382b09bd93ea4ef89ee2b1c6 Mon Sep 17 00:00:00 2001 From: TheEssem Date: Thu, 5 Nov 2020 15:40:18 -0600 Subject: [PATCH] Caption now supports more unicode characters, more api work, fixed multiple bugs --- .env.example | 2 ++ api/index.js | 65 +++++++++++++++++++++++++++++++++++++---- api/job.js | 28 ++++++++++++++++++ events/guildCreate.js | 4 +-- events/messageCreate.js | 6 +++- events/ready.js | 2 +- natives/caption.cc | 4 +-- servers.json | 8 +++++ utils/image.js | 31 ++++++++++++++------ utils/imagedetect.js | 14 ++++++--- utils/soundplayer.js | 60 ++++++++++++++++++++----------------- 11 files changed, 174 insertions(+), 50 deletions(-) create mode 100644 api/job.js create mode 100644 servers.json diff --git a/.env.example b/.env.example index 60102ad..b112402 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,8 @@ TEMPDIR= # Set this to true if you're using PM2 to manage the bot PMTWO=false +# Set this to true if you want to use the image API +API=false # Enable/disable Twitter bot (true/false) TWITTER=false diff --git a/api/index.js b/api/index.js index f26ed01..70d3a30 100644 --- a/api/index.js +++ b/api/index.js @@ -1,11 +1,14 @@ require("dotenv").config(); const magick = require("../utils/image.js"); +const Job = require("./job.js"); const { version } = require("../package.json"); const express = require("express"); const execPromise = require("util").promisify(require("child_process").exec); const app = express(); const port = 3000; +const jobs = new Map(); + app.get("/", (req, res) => { res.send(`esmBot v${version}`); }); @@ -22,19 +25,71 @@ app.post("/run", express.json(), async (req, res, next) => { return res.sendStatus(400); } object.type = type.split("/")[1]; - if (object.type !== "gif" && object.onlyGIF) return res.send("nogif"); + if (object.type !== "gif" && object.onlyGIF) return res.send({ + status: "nogif" + }); + object.delay = object.delay ? object.delay : 0; + } + + const id = Math.random().toString(36).substring(2, 15); + if (object.type === "gif" && !object.delay) { const delay = (await execPromise(`ffprobe -v 0 -of csv=p=0 -select_streams v:0 -show_entries stream=r_frame_rate ${object.path}`)).stdout.replace("\n", ""); object.delay = (100 / delay.split("/")[0]) * delay.split("/")[1]; } - - const data = await magick.run(object, true); - res.contentType(type ? type : "png"); - res.send(data); + const job = new Job(object); + jobs.set(id, job); + res.send({ + id: id, + status: "queued" + }); + job.run(); } catch (e) { next(e); } }); +app.get("/status", (req, res) => { + if (!req.query.id) return res.sendStatus(400); + const job = jobs.get(req.query.id); + if (!job) return res.sendStatus(400); + const timeout = setTimeout(function() { + job.removeAllListeners(); + return res.send({ + id: req.query.id, + status: job.status + }); + }, 10000); + job.once("data", function() { + clearTimeout(timeout); + res.send({ + id: req.query.id, + status: job.status + }); + //jobs.delete(req.query.id); + }); + job.on("error", function(e) { + clearTimeout(timeout); + res.status(500); + res.send({ + id: req.query.id, + status: job.status, + error: e + }); + jobs.delete(req.query.id); + }); +}); + +app.get("/image", (req, res) => { + if (!req.query.id) return res.sendStatus(400); + const job = jobs.get(req.query.id); + if (!job) return res.sendStatus(400); + if (!job.data) return res.sendStatus(400); + if (job.error) return; + jobs.delete(req.query.id); + res.contentType(job.options.type ? job.options.type : "png"); + return res.send(job.data); +}); + app.listen(port, () => { console.log(`Started image API on port ${port}.`); }); \ No newline at end of file diff --git a/api/job.js b/api/job.js new file mode 100644 index 0000000..efc04fa --- /dev/null +++ b/api/job.js @@ -0,0 +1,28 @@ +const { EventEmitter } = require("events"); +const magick = require("../utils/image.js"); + +class Job extends EventEmitter { + constructor(options) { + super(); + this.options = options; + this.status = "queued"; + this.data = null; + this.error = null; + } + + run() { + this.status = "processing"; + magick.run(this.options, true).then(data => { + this.status = "success"; + this.data = data; + return this.emit("data", data, this.options.type); + }).catch(e => { + this.status = "error"; + this.error = e; + return this.emit("error", e); + }); + return; + } +} + +module.exports = Job; diff --git a/events/guildCreate.js b/events/guildCreate.js index ac0615e..dfb47f4 100644 --- a/events/guildCreate.js +++ b/events/guildCreate.js @@ -1,11 +1,10 @@ const db = require("../utils/database.js"); const logger = require("../utils/logger.js"); const misc = require("../utils/misc.js"); -const client = require("../utils/client.js"); // run when the bot is added to a guild module.exports = async (guild) => { - logger.log("info", `[GUILD JOIN] ${guild.name} (${guild.id}) added the bot. Owner: ${client.users.get(guild.ownerID).username}#${client.users.get(guild.ownerID).discriminator} (${guild.ownerID})`); + logger.log("info", `[GUILD JOIN] ${guild.name} (${guild.id}) added the bot.`); const guildDB = new db.guilds({ id: guild.id, tags: misc.tagDefaults, @@ -15,4 +14,5 @@ module.exports = async (guild) => { tagsDisabled: false }); await guildDB.save(); + return guildDB; }; diff --git a/events/messageCreate.js b/events/messageCreate.js index 558cd1f..08f37c6 100644 --- a/events/messageCreate.js +++ b/events/messageCreate.js @@ -3,6 +3,7 @@ const client = require("../utils/client.js"); const database = require("../utils/database.js"); const logger = require("../utils/logger.js"); const collections = require("../utils/collections.js"); +const guildCreate = require("./guildCreate.js"); const commands = [...collections.aliases.keys(), ...collections.commands.keys()]; // run when someone sends a message @@ -24,7 +25,10 @@ module.exports = async (message) => { if (!valid) return; // prefix can be a mention or a set of special characters - const guildDB = message.channel.guild ? await database.guilds.findOne({ id: message.channel.guild.id }).lean().exec() : null; + let guildDB = message.channel.guild ? await database.guilds.findOne({ id: message.channel.guild.id }).lean().exec() : null; + if (message.channel.guild && !guildDB) { + guildDB = await guildCreate(message.channel.guild); + } // there's a bit of a workaround here due to member.mention not accounting for both mention types const prefix = message.channel.guild ? (message.content.startsWith(message.channel.guild.members.get(client.user.id).mention) ? `${message.channel.guild.members.get(client.user.id).mention} ` : (message.content.startsWith(`<@${client.user.id}>`) ? `<@${client.user.id}> ` : guildDB.prefix)) : ""; diff --git a/events/ready.js b/events/ready.js index 3ca74af..3ac1392 100644 --- a/events/ready.js +++ b/events/ready.js @@ -30,7 +30,7 @@ module.exports = async () => { tagsDisabled: false }); await newGuild.save(); - } else if (guildDB) { + } else { if (!guildDB.warns) { logger.log(`Creating warn object for guild ${id}...`); guildDB.set("warns", {}); diff --git a/natives/caption.cc b/natives/caption.cc index a5ff904..283aa51 100644 --- a/natives/caption.cc +++ b/natives/caption.cc @@ -23,10 +23,10 @@ class CaptionWorker : public Napi::AsyncWorker { Image caption_image(Geometry(query), Color("white")); caption_image.fillColor("black"); caption_image.alpha(true); - caption_image.font("./assets/caption.otf"); + caption_image.font("Futura"); caption_image.fontPointsize(width / 10); caption_image.textGravity(Magick::CenterGravity); - caption_image.read("caption:" + caption); + caption_image.read("pango:" + caption); caption_image.extent(Geometry(width, caption_image.rows() + (width / 10)), Magick::CenterGravity); coalesceImages(&coalesced, frames.begin(), frames.end()); diff --git a/servers.json b/servers.json new file mode 100644 index 0000000..380d230 --- /dev/null +++ b/servers.json @@ -0,0 +1,8 @@ +{ + "lava": [ + { "id": "1", "host": "localhost", "port": 2333, "password": "youshallnotpass" } + ], + "image": [ + "http://localhost:3000" + ] +} \ No newline at end of file diff --git a/utils/image.js b/utils/image.js index 211574f..b03df0f 100644 --- a/utils/image.js +++ b/utils/image.js @@ -4,32 +4,45 @@ const { promisify } = require("util"); const AbortController = require("abort-controller"); const fileType = require("file-type"); const execPromise = promisify(require("child_process").exec); +const servers = require("../servers.json").image; const formats = ["image/jpeg", "image/png", "image/webp", "image/gif"]; exports.run = async (object, fromAPI = false) => { if (process.env.API === "true" && !fromAPI) { - const req = await fetch(`${process.env.API_URL}/run`, { + const currentServer = servers[Math.floor(Math.random() * servers.length)]; + const req = await fetch(`${currentServer}/run`, { method: "POST", body: JSON.stringify(object), headers: { "Content-Type": "application/json" } }); - const buffer = await req.buffer(); - console.log(buffer.toString()); - if (buffer.toString() === "nogif") return { + const json = await req.json(); + if (json.status === "nogif") return { buffer: "nogif", type: null }; - return { - buffer: buffer, - type: req.headers.get("content-type").split("/")[1] - }; + let data; + while (!data) { + const statusReq = await fetch(`${currentServer}/status?id=${json.id}`); + const statusJSON = await statusReq.json(); + if (statusJSON.status === "success") { + const imageReq = await fetch(`${currentServer}/image?id=${json.id}`); + data = { + buffer: await imageReq.buffer(), + type: imageReq.headers.get("content-type").split("/")[1] + }; + } else if (statusJSON.status === "error") { + throw new Error(statusJSON.error); + } + } + return data; } else { let type; if (!fromAPI && object.path) { - type = (object.type ? object.type : await this.getType(object.path)).split("/")[1]; + const newType = (object.type ? object.type : await this.getType(object.path)); + type = newType ? newType.split("/")[1] : "png"; if (type !== "gif" && object.onlyGIF) return { buffer: "nogif", type: null diff --git a/utils/imagedetect.js b/utils/imagedetect.js index 9f59229..99ead32 100644 --- a/utils/imagedetect.js +++ b/utils/imagedetect.js @@ -1,4 +1,5 @@ const fetch = require("node-fetch"); +const execPromise = require("util").promisify(require("child_process").exec); // gets the proper image paths const getImage = async (image, image2, gifv = false) => { @@ -8,10 +9,15 @@ const getImage = async (image, image2, gifv = false) => { path: image }; if (gifv) { - if (image2.includes("tenor.com") && process.env.TENOR !== "") { - const data = await fetch(`https://api.tenor.com/v1/gifs?ids=${image2.split("-").pop()}&key=${process.env.TENOR}`); - const json = await data.json(); - payload.path = json.results[0].media[0].gif.url; + if (image2.includes("tenor.com")) { + if (process.env.TENOR !== "") { + const data = await fetch(`https://api.tenor.com/v1/gifs?ids=${image2.split("-").pop()}&key=${process.env.TENOR}`); + const json = await data.json(); + payload.path = json.results[0].media[0].gif.url; + } else { + const delay = (await execPromise(`ffprobe -v 0 -of csv=p=0 -select_streams v:0 -show_entries stream=r_frame_rate ${image}`)).stdout.replace("\n", ""); + payload.delay = (100 / delay.split("/")[0]) * delay.split("/")[1]; + } } else if (image2.includes("giphy.com")) { payload.path = `https://media0.giphy.com/media/${image2.split("-").pop()}/giphy.gif`; } else if (image2.includes("imgur.com")) { diff --git a/utils/soundplayer.js b/utils/soundplayer.js index 4c9f687..f6c642a 100644 --- a/utils/soundplayer.js +++ b/utils/soundplayer.js @@ -6,7 +6,7 @@ const moment = require("moment"); require("moment-duration-format"); const { Manager } = require("@lavacord/eris"); -const nodes = require("../lavanodes.json"); +const nodes = require("../servers.json").lava; exports.players = new Map(); @@ -51,7 +51,8 @@ exports.play = async (sound, message, music = false) => { if (!message.channel.guild.members.get(client.user.id).permission.has("voiceConnect") || !message.channel.permissionsOf(client.user.id).has("voiceConnect")) return client.createMessage(message.channel.id, `${message.author.mention}, I can't join this voice channel!`); const voiceChannel = message.channel.guild.channels.get(message.member.voiceState.channelID); if (!voiceChannel.permissionsOf(client.user.id).has("voiceConnect")) return client.createMessage(message.channel.id, `${message.author.mention}, I don't have permission to join this voice channel!`); - if (!music && this.manager.voiceStates.has(message.channel.guild.id) && this.players.get(message.channel.guild.id).type === "music") return client.createMessage(message.channel.id, `${message.author.mention}, I can't play a sound effect while playing music!`); + const player = this.players.get(message.channel.guild.id); + if (!music && this.manager.voiceStates.has(message.channel.guild.id) && (player && player.type === "music")) return client.createMessage(message.channel.id, `${message.author.mention}, I can't play a sound effect while playing music!`); const node = this.manager.idealNodes[0]; const { tracks } = await fetch(`http://${node.host}:${node.port}/loadtracks?identifier=${sound}`, { headers: { Authorization: node.password } }).then(res => res.json()); const oldQueue = this.queues.get(voiceChannel.guild.id); @@ -59,11 +60,16 @@ exports.play = async (sound, message, music = false) => { if (music) { this.queues.set(voiceChannel.guild.id, oldQueue ? [...oldQueue, tracks[0].track] : [tracks[0].track]); } - const connection = await this.manager.join({ - guild: voiceChannel.guild.id, - channel: voiceChannel.id, - node: node.id - }); + let connection; + if (player) { + connection = player; + } else { + connection = await this.manager.join({ + guild: voiceChannel.guild.id, + channel: voiceChannel.id, + node: node.id + }); + } if (oldQueue && music) { client.createMessage(message.channel.id, `${message.author.mention}, your tune has been added to the queue!`); @@ -101,7 +107,7 @@ exports.nextSong = async (message, connection, track, info, music, voiceChannel, }); await connection.play(track); this.players.set(voiceChannel.guild.id, { player: connection, type: music ? "music" : "sound", host: message.author.id, voiceChannel: voiceChannel, originalChannel: message.channel }); - if (inQueue) { + if (inQueue && connection.listeners("error").length === 0) { connection.on("error", (error) => { if (playingMessage.channel.messages.get(playingMessage.id)) playingMessage.delete(); this.manager.leave(voiceChannel.guild.id); @@ -111,24 +117,26 @@ exports.nextSong = async (message, connection, track, info, music, voiceChannel, logger.error(error); }); } - connection.on("end", async (data) => { - if (data.reason === "REPLACED") return; - const queue = this.queues.get(voiceChannel.guild.id); - const newQueue = queue ? queue.slice(1) : []; - this.queues.set(voiceChannel.guild.id, newQueue); - if (newQueue.length === 0) { - this.manager.leave(voiceChannel.guild.id); - connection.destroy(); - this.players.delete(voiceChannel.guild.id); - this.queues.delete(voiceChannel.guild.id); - if (music) await client.createMessage(message.channel.id, "🔊 The current voice channel session has ended."); - if (playingMessage.channel.messages.get(playingMessage.id)) await playingMessage.delete(); - } else { - const track = await fetch(`http://${connection.node.host}:${connection.node.port}/decodetrack?track=${encodeURIComponent(newQueue[0])}`, { headers: { Authorization: connection.node.password } }).then(res => res.json()); - this.nextSong(message, connection, newQueue[0], track, music, voiceChannel, true); - if (playingMessage.channel.messages.get(playingMessage.id)) await playingMessage.delete(); - } - }); + if (connection.listeners("end").length === 0) { + connection.on("end", async (data) => { + if (data.reason === "REPLACED") return; + const queue = this.queues.get(voiceChannel.guild.id); + const newQueue = queue ? queue.slice(1) : []; + this.queues.set(voiceChannel.guild.id, newQueue); + if (newQueue.length === 0) { + this.manager.leave(voiceChannel.guild.id); + connection.destroy(); + this.players.delete(voiceChannel.guild.id); + this.queues.delete(voiceChannel.guild.id); + if (music) await client.createMessage(message.channel.id, "🔊 The current voice channel session has ended."); + if (playingMessage.channel.messages.get(playingMessage.id)) await playingMessage.delete(); + } else { + const track = await fetch(`http://${connection.node.host}:${connection.node.port}/decodetrack?track=${encodeURIComponent(newQueue[0])}`, { headers: { Authorization: connection.node.password } }).then(res => res.json()); + this.nextSong(message, connection, newQueue[0], track, music, voiceChannel, true); + if (playingMessage.channel.messages.get(playingMessage.id)) await playingMessage.delete(); + } + }); + } }; exports.stop = async (message) => {