const _ = require('lodash'); const express = require('express'); const Discord = require('discord.js'); const { CanvasEx, ImageEx } = require('./imageex'); const twemoji = require('./twemoji'); const app = express(); const config = require('./config.default.json'); const filters = require('./filters.js'); const logger = require('loglevel'); logger.setLevel(process.env.LOGLEVEL || 'info'); try { _.extend(config, require(`./${process.env.CONFIG || 'config'}`)); // eslint-disable-line global-require } catch (err) { logger.error('No config.json found!'); } function all(x, c) { _.isArray(x) ? _.each(x, c) : c(x); } const { templates } = config; _.each(templates, (template, templateName) => { const data = templates[templateName]; all(data, templatePart => { templatePart.image = new ImageEx(templatePart.src); }); }); // drawing: we keep the image fixed in its default position and draw the template on top/below it // calculates the x or y position of the template to be drawn // size = width or height of the template/image // anchor = the corresponding anchor config function calculatePosition(scale, anchor, imageSize) { if (anchor.absolute) { return anchor.offset; } return imageSize * anchor.position / 100 - anchor.offset * scale; } // global variable, can be used to get the previous template calculations let previousCalculation; // eslint-disable-line no-unused-vars function getNumericAnchor(anchor, imgWidth, imgHeight) { // eslint-disable-line no-unused-vars return _.mapValues(anchor, dimension => _.mapValues(dimension, value => (Number.isFinite(value) ? Number(value) : eval(value)))); // eslint-disable-line no-eval } function render(template, img, size, flipH) { let imgWidth = img.width; let imgHeight = img.height; if (size && size.height) { imgHeight = size.height; if (!size.width) imgWidth = imgWidth * size.height / img.height; } if (size && size.width) { imgWidth = size.width; if (!size.height) imgHeight = imgHeight * size.width / img.width; } logger.debug('Drawing template: ', template); const anchor = getNumericAnchor(template.anchor, imgWidth, imgHeight); logger.debug('Numeric anchor: ', anchor); const xScale = imgWidth / anchor.x.size; const yScale = imgHeight / anchor.y.size; const templateScale = Math.max(0, Math.min(10, Math.max(xScale || 0, yScale || 0))); let templateOffsetX; let templateOffsetY; templateOffsetX = calculatePosition(templateScale, anchor.x, imgWidth); templateOffsetY = calculatePosition(templateScale, anchor.y, imgHeight); logger.debug('xScale', xScale); logger.debug('yScale', yScale); logger.debug('templateOffsetX', templateOffsetX); logger.debug('templateOffsetY', templateOffsetY); let imageOffsetX = 0; let imageOffsetY = 0; let resultingWidth = imgWidth; // start with the image boundaries as defined by the image let resultingHeight = imgHeight; if (templateOffsetX < 0) { resultingWidth -= templateOffsetX; imageOffsetX = -templateOffsetX; templateOffsetX = 0; } if (templateOffsetY < 0) { resultingHeight -= templateOffsetY; imageOffsetY = -templateOffsetY; templateOffsetY = 0; } if (templateOffsetX + template.image.width * templateScale > resultingWidth) { resultingWidth = templateOffsetX + template.image.width * templateScale; } if (templateOffsetY + template.image.height * templateScale > resultingHeight) { resultingHeight = templateOffsetY + template.image.height * templateScale; } previousCalculation = { templateOffsetX, templateOffsetY, resultingWidth, resultingHeight, xScale, yScale, templateScale }; const toDraw = [{ z: 1, image: img, x: flipH ? resultingWidth - imageOffsetX - imgWidth : imageOffsetX, y: imageOffsetY, h: imgHeight, w: imgWidth, name: 'image' }, { z: template.z || 0, image: template.image, x: templateOffsetX, y: templateOffsetY, h: template.image.height * templateScale, w: template.image.width * templateScale, name: `template ${template.src}`, flipH, attributes: template.attributes, filter: filters[template.filter] }].sort((u, v) => u.z - v.z); logger.debug('To draw:', toDraw); let canvas = new CanvasEx(resultingWidth, resultingHeight); for (let i = 0; i < toDraw.length; ++i) { const subject = toDraw[i]; logger.debug(`Drawing ${subject.name}${subject.flipH ? ' (flipped)' : ''}`); try { const transform = {}; if (subject.flipH) { transform.translate = [resultingWidth, 0]; transform.scale = [-1, 1]; } if (subject.filter) { canvas = subject.filter(canvas, subject.image, subject.x, subject.y, { width: subject.w, height: subject.h, transform, attributes: subject.attributes }); } else { canvas.drawImage(subject.image, subject.x, subject.y, { width: subject.w, height: subject.h, transform, attributes: subject.attributes }); } } catch (err) { logger.error(err); throw new Error(JSON.stringify({ status: 400, error: 'Invalid template' })); } } // return the image and cache it return (canvas); } app.get('/debug/frame/', async (req, res) => { try { const img = new ImageEx(req.query.url); await img.loaded; logger.debug(img.frames); return img.frames[req.query.num].canvas.pngStream().pipe(res); } catch (err) { logger.error(err); return res.status(400).end(err.message); } }); app.get('/', async (req, res) => { try { var templateList = []; for (var key in templates) { if(templates.hasOwnProperty(key)) { //to be safe templateList.push(key); } } logger.debug(templateList); res.setHeader('Content-Type', 'application/json') return res.end(JSON.stringify(templateList)); } catch (err) { logger.error(err); return res.status(400).end(err.message); } }); app.get('/:templateName/', async (req, res) => { if (!templates[req.params.templateName]) return res.status(404).end(); try { if (!/^https?:/.test(req.query.url)) { return res.status(400).end('Invalid url!'); } let direction = req.query.reverse === 'true' ? '\\' : '/'; logger.debug('Got command ', direction, req.params.templateName, direction === '\\' ? 'flipped' : 'not flipped', req.query.url); result = new ImageEx(req.query.url); await result.loaded; // eslint-disable-line no-await-in-loop const templateData = templates[req.params.templateName]; all(templateData, template => { // eslint-disable-line no-loop-func result = render(template, result, null, direction === '\\'); }); return result.export(res); } catch (err) { logger.error(err); return res.status(400).end({'error': err.message}); } }); app.listen(config.http.port, config.http.host, () => { logger.info(`Beebot app listening on ${(config.http.host === null ? 'port ' : config.http.host + ':')}${config.http.port}!`); }); // Discord stuff const client = new Discord.Client({ autoReconnect: true }); // manage roles permission is required const invitelink = `https://discordapp.com/oauth2/authorize?client_id=${ config.discord.client_id}&scope=bot&permissions=0`; /* const authlink = `https://discordapp.com/oauth2/authorize?client_id=${ config.discord.client_id}&scope=email`; */ logger.info(`Bot invite link: ${invitelink}`); client.login(config.discord.token).catch(error => { if (error) { logger.error("Couldn't login: ", error.toString()); } }); const discordAvatarRegex = /(https:\/\/cdn.discordapp.com\/avatars\/\w+\/\w+\.(\w+)\?size=)(\w+)/; async function findEmoji(message) { // find a user mention if (message.mentions.users.size > 0) { const mentionedUser = message.mentions.users.first(); const mentionedMember = message.guild.members[mentionedUser.id]; let avatarUrl = mentionedUser.displayAvatarURL; const avatarMatch = discordAvatarRegex.exec(avatarUrl); if (avatarMatch) { // const ext = avatarMatch[2]; avatarUrl = `${avatarMatch[1]}128`; } return { name: mentionedMember ? mentionedMember.displayName : mentionedUser.username, id: mentionedUser.id, url: avatarUrl, ext: avatarUrl.indexOf('.gif') >= 0 ? 'gif' : 'png' }; } const str = message.cleanContent; // find a discord emote const discordEmote = /<(a?):(\w+):(\d+)>/g.exec(str); if (discordEmote) { const ext = discordEmote[1] === 'a' ? 'gif' : 'png'; return { name: discordEmote[2], id: discordEmote[3], url: `https://cdn.discordapp.com/emojis/${discordEmote[3]}.${ext}`, ext }; } // find a unicode emoji let unicodeEmoji; twemoji.parse(str, (name, emoji) => { if (unicodeEmoji) return false; unicodeEmoji = { name, id: name, url: `${emoji.base + emoji.size}/${name}${emoji.ext}`, ext: emoji.ext }; return false; }); if (unicodeEmoji) return unicodeEmoji; return null; } function reverseString(str) { return str.split('').reverse().join(''); } const commands = Object.keys(templates).map(x => `/${x}`).join(', '); const otherCommands = { invite: `Invite link: <${invitelink}>`, help: `Available commands: ${commands}.\nUse \\\\ to flip the template horizontally.${config.discord.public ? "\nInvite link: <" + invitelink + ">" : ""}`, beebot: `Available commands: ${commands}.\nUse \\\\ to flip the template horizontally.${config.discord.public ? "\nInvite link: <" + invitelink + ">" : ""}` }; client.on('ready', () => { client.user.setPresence({ game: { name: 'BBaper' }, status: 'online' }); }); client.on('message', async message => { let commandParsed = /^([/\\])(\w+)\b/.exec(message.cleanContent); if (commandParsed) { const [, direction, command] = commandParsed; if (otherCommands[command]) { const text = otherCommands[command]; message.channel.send(direction === '\\' ? reverseString(text) : text); return; } } if (message.cleanContent[0] === '/' || message.cleanContent[0] === '\\') { const messageSplit = message.cleanContent.split(' '); const emoji = await findEmoji(message); let result = null; let count = 0; try { if (emoji) { let { name } = emoji; for (let i = 0; i < messageSplit.length && count < 4; ++i) { commandParsed = /^([/\\])(\w+)\b/.exec(messageSplit[i]); if (commandParsed) { const [, direction, command] = commandParsed; if (templates[command]) { logger.debug('Got command ', direction, command, direction === '\\' ? 'flipped' : 'not flipped', emoji); count++; name += command; if (result === null) { result = new ImageEx(emoji.url); await result.loaded; // eslint-disable-line no-await-in-loop } const templateData = templates[command]; all(templateData, template => { // eslint-disable-line no-loop-func result = render(template, result, null, direction === '\\'); }); } } else if (i === 0) return; } if (result) { const attachment = await result.toBuffer(); const messageOptions = { files: [ { attachment, name: `${name}.${emoji.ext}` } ] }; logger.debug('Sending message with result:', result); await message.channel.send('', messageOptions).then(() => { logger.debug('Message sent!'); }).catch(err => { logger.error('Message sending failed:', err); }); } } } catch (err) { logger.error(err); } } }); process.on('uncaughtException', exception => { logger.error(exception); });