const PollDB = require("../models/polls"); const Embed = require("./embed"); const Canvas = require('canvas'); const format = `${(new Date().getMonth() + 1) < 10 ? `0${new Date().getMonth() + 1}` : new Date().getMonth() + 1}/${new Date().getDate()}/${new Date().getFullYear()} ${new Date().getHours()}:${(new Date().getMinutes() < 10 ? '0' : '') + new Date().getMinutes()}`; class Polls { constructor({ time, client, name, options, users, avatars, votes, owner, lang }) { this.client = client; this.time = time; if (votes) this.votes = votes; else this.votes = options.name.length === 2 ? [0, 0] : options.name.length === 3 ? [0, 0, 0] : options.name.length === 4 ? [0, 0, 0, 0] : options.name.length === 5 ? [0, 0, 0, 0, 0] : options.name.length === 6 ? [0, 0, 0, 0, 0, 0] : options.name.length === 7 ? [0, 0, 0, 0, 0, 0, 0] : options.name.length === 8 ? [0, 0, 0, 0, 0, 0, 0, 0] : options.name.length === 9 ? [0, 0, 0, 0, 0, 0, 0, 0, 0] : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; this.users = users || []; this.avatars = avatars || []; this.options = { name: name.name, description: name.description }; this.voteOptions = options; this.owner = owner; this.lang = lang; this.size = { canvas: options.name.length === 2 ? 200 : options.name.length === 3 ? 250 : options.name.length === 4 ? 300 : options.name.length === 5 ? 350 : options.name.length === 6 ? 400 : options.name.length === 7 ? 450 : options.name.length === 8 ? 500 : options.name.length === 9 ? 550 : 600, bar: options.name.length === 2 ? 150 : options.name.length === 3 ? 200 : options.name.length === 4 ? 250 : options.name.length === 5 ? 300 : options.name.length === 6 ? 350 : options.name.length === 7 ? 400 : options.name.length === 8 ? 450 : options.name.length === 9 ? 500 : 550 }; } async start(message, poll) { this.client.polls.set(message.id, { poll, messageId: message.id, users: this.users, owner: this.owner, lang: this.lang }) setTimeout(async () => { if (!this.client.polls.get(message.id)) return; await this.update(); message.edit({ embeds: [new Embed().setMedia(await this.client.Uploader.upload(poll.canvas.toBuffer(), `Poll.png`)).setColor(`#F24646`)], content: this.client.translate.get(this.lang, "Functions.poll.end") }).catch(() => { }) this.client.polls.delete(message.id); await PollDB.findOneAndDelete({ messageId: message.id }); }, this.time); if (this.time < 0) return; await (new PollDB({ owner: this.owner, channelId: message.channelId, messageId: message.id, avatars: this.avatars, users: this.users, votes: this.votes, name: this.options.name, desc: this.options.description, options: this.voteOptions, time: this.time, lang: this.lang, now: Date.now(), }).save()); } textHeight(text, ctx, m) { let metrics = m || ctx.measureText(text); return metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; } roundRect(ctx, x, y, width, height, radius, fill, stroke) { // Credit to https://stackoverflow.com/users/227299/juan-mendes if (typeof stroke === 'undefined') { stroke = true; } if (typeof radius === 'undefined') { radius = 5; } if (typeof radius === 'number') { radius = { tl: radius, tr: radius, br: radius, bl: radius }; } else { var defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 }; for (var side in defaultRadius) { radius[side] = radius[side] || defaultRadius[side]; } } ctx.beginPath(); ctx.moveTo(x + radius.tl, y); ctx.lineTo(x + width - radius.tr, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr); ctx.lineTo(x + width, y + height - radius.br); ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height); ctx.lineTo(x + radius.bl, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl); ctx.lineTo(x, y + radius.tl); ctx.quadraticCurveTo(x, y, x + radius.tl, y); ctx.closePath(); if (fill) { ctx.fill(); } if (stroke) { ctx.stroke(); } } async update() { let roundRect = this.roundRect; let textHeight = this.textHeight; var width = 600, height = this.size.canvas, padding = 10; const canvas = Canvas.createCanvas(width, height); this.canvas = canvas; const ctx = this.canvas.getContext('2d'); this.ctx = ctx; let name = this.options.name?.length > 70 ? this.options?.name.slice(0, 67) + "..." : this.options.name; var nameHeight = textHeight(name, ctx); let description = this.options.description.length > 80 ? this.options.description.slice(0, 77) + "..." : this.options.description; var descHeight = textHeight(description, ctx); ctx.fillStyle = "#23272A"; roundRect(ctx, 0, 0, width, height, 5, true, false); // background ctx.fillStyle = "#4E535A"; ctx.font = `normal 12px Sans-Serif`; ctx.fillText(name, padding, padding + 2 + nameHeight / 2); // name ctx.fillStyle = "#FFFFFF"; ctx.font = `normal 17px Sans-Serif`; ctx.fillText(description, padding, padding + 15 + nameHeight + descHeight / 2); // description var headerHeight = padding + descHeight + nameHeight + 15; var dataWidth = width - padding * 2; var barHeight = 40; var votes = this.votes; var names = this.voteOptions.name; this.drawVoteBars(ctx, dataWidth - 20, barHeight, votes, { pad: padding, hHeight: headerHeight }, names); await this.drawFooter(ctx, padding, padding + headerHeight + barHeight * 2 + 20, width, height, padding, this.avatars); } async addVote(option, user, avatar, id) { if (this.avatars.length === 6) this.avatars.shift(); this.avatars.push(avatar); this.votes[option]++; await PollDB.findOneAndUpdate({ messageId: id }, { $push: { users: user, avatars: avatar }, $inc: { [`votes.${option}`]: 1 } }); await this.update(); return this.canvas; } async removeVote(option, user, avatar, id) { this.avatars.splice(this.avatars.indexOf(avatar), 1); this.votes[option]--; await PollDB.findOneAndUpdate({ messageId: id }, { $pull: { users: user, avatars: avatar }, $inc: { [`votes.${option}`]: -1 } }); await this.update(); return this.canvas; } drawVoteBars(ctx, width, height, votes, vars, names, vote) { let roundRect = this.roundRect; let textHeight = this.textHeight; let padding = vars.pad; let headerHeight = vars.hHeight; let sum = votes.reduce((prev, curr) => prev + curr); let percentages = votes.map((v) => Math.floor(v / (sum / 100) * 10) / 10); ctx.save(); ctx.translate(padding, padding + headerHeight); var barPadding = 5; percentages.forEach((percentage, i) => { if (!percentage) percentage = 0; let paddingLeft = (vote != undefined) ? 30 : 0; ctx.fillStyle = "#2C2F33"; let y = (height + 10) * i; roundRect(ctx, 20, y, width, height, 5, true, false); // full bar if (vote == i || percentage) { ctx.fillStyle = "#A52F05"; } else { ctx.fillStyle = "#24282B"; } // percentage display roundRect(ctx, 20, y, width * (votes[i] / (sum / 100) / 100), height, 5, true, false); ctx.fillStyle = "#4E535A"; let h = textHeight(i + 1, ctx); ctx.fillText(i + 1, 0, y + height / 2 + h / 2); ctx.fillStyle = "#FFFFFF"; // Option names h = textHeight(names[i], ctx); ctx.fillText(names[i].length > 65 ? names[i].slice(0, 62) + "..." : names[i], 30 + paddingLeft, y + 13 + h); if (vote != undefined) { ctx.strokeStyle = "#FFFFFF"; // selection circle ctx.fillStyle = "#717cf4"; ctx.beginPath(); ctx.arc(35, y + 10 + h * 0.75, 6, 0, 2 * Math.PI); ctx.closePath(); ctx.stroke(); if (vote == i) { ctx.beginPath(); ctx.arc(35, y + 10 + h * 0.75, 3, 0, 2 * Math.PI); ctx.closePath(); ctx.fill(); } } ctx.fillStyle = "#2C2F33"; // percentage and vote count background let metrics = ctx.measureText(percentage + "% (" + votes[i] + ")"); let w = metrics.width; h = textHeight(percentage + "% (" + votes[i] + ")", ctx, metrics); y = y + (height - h - barPadding * 2) + barPadding * 2.6; if (vote == i || vote == undefined) roundRect(ctx, width - barPadding - w - 3, y - h - 4, w + 5, h + 12, 5, true, false); ctx.fillStyle = "#A52F05"; // percentage and vote count ctx.fillText(percentage + "% (" + votes[i] + ")", width - barPadding - w, y); }); ctx.restore(); } async drawFooter(ctx, x, y, width, height, padding, users) { ctx.save(); ctx.translate(10, this.size.bar); var rad = 18; ctx.fillStyle = "#4E535A"; ctx.lineWidth = 2; ctx.strokeStyle = "#4E535A"; ctx.beginPath(); ctx.moveTo(0 - padding, 0); ctx.lineTo(width, 0); ctx.stroke(); let votes = (this.votes.reduce((p, c) => p + c) == 1) ? `${this.votes.reduce((p, c) => p + c)} vote` : `${this.votes.reduce((p, c) => p + c)} votes`; let metrics = ctx.measureText(votes); let h = this.textHeight(votes, ctx, metrics); ctx.fillText(votes, 5, rad + h); // Avatars var pos = rad * users.length + 10 + metrics.width; var yPos = 6; users.reverse(); for (let i = 0; i < users.length; i++) { ctx.beginPath(); let user = users[i]; const a = Canvas.createCanvas(rad * 2, rad * 2); const context = a.getContext("2d"); context.beginPath(); context.arc(rad, rad, rad, 0, Math.PI * 2, true); context.closePath(); context.clip(); const avatar = await Canvas.loadImage(user); context.drawImage(avatar, 0, 0, rad * 2, rad * 2); ctx.drawImage(a, pos, yPos); ctx.closePath(); pos -= rad; } // Date let date = format; metrics = ctx.measureText(date); h = this.textHeight(date, ctx, metrics); ctx.fillText(date, width - 15 - metrics.width, rad + h); ctx.restore(); } } module.exports = Polls;