UNPKG

d-bot

Version:

A quirky Discord bot made for single, small, private servers

262 lines (250 loc) 12.5 kB
// Top-typed words var util = require('./../core/util.js'); var messages = require(__base+'core/messages.js'); var discord = require(__base+'core/discord.js'); var Canvas = require('canvas'); var regexgen = require('regexgen'); var requireUncached = require('require-uncached'); const { UnitContext } = requireUncached('./helpers/canvas.js'); var DateFormat = require('dateformat'); var _commands = {}; _commands.words = data => getTopWords(data); _commands.unique = data => getTopWords(data, true); _commands.graph = async function(data) { let graphUsers = data.params.length === 0; let graphChannels = data.paramStr === 'by channel'; let words = graphUsers || graphChannels ? [] : data.params.filter((p, i, a) => a.indexOf(p) === i); // Remove dupes let query = graphUsers || graphChannels ? {} : { $or: words.map(w => ({ content: util.regExpify(w) })) }; let allMessages = await messages.cursor(db => db.cfind(query).sort({ time: 1 })); if(!allMessages) return data.reply(`Couldn't find any messages` + (graphUsers ? '' : ` containing _${data.paramStr}_`)); // TODO: Rewrite using maps // TODO: Use logarithmic scale when appropriate let dailyUsage = {}; words.forEach(w => dailyUsage[w] = []); let firstDate = graphUsers || graphChannels ? new Date(allMessages[0].time) : null, firstDay = graphUsers || graphChannels ? Math.floor(firstDate.getTime() / 8.64e7) : null; for(let { content: text, time, user, channel: channelID } of allMessages) { let day = Math.floor(new Date(time) / 8.64e7 - firstDay); if(graphUsers) { let username = discord.getUsernameFromID(user); if(!username) continue; if(!words.includes(username)) words.push(username); if(!dailyUsage[username]) dailyUsage[username] = []; dailyUsage[username][day] = (dailyUsage[username][day] || 0) + 1; } else if(graphChannels) { if(channelID === '86915384589967360') continue; let guildID = discord.bot.channelGuildMap[channelID]; let channel = guildID && discord.bot.guilds.get(guildID).channels.get(channelID).name; if(!channel) continue; if(!words.includes(channel)) words.push(channel); if(!dailyUsage[channel]) dailyUsage[channel] = []; dailyUsage[channel][day] = (dailyUsage[channel][day] || 0) + 1; } else { words.forEach((word, i) => { let rxMatches = util.getRegExpMatches(text, util.regExpify(word)); if(!rxMatches || rxMatches.length === 0 || !rxMatches[0]) return; if(!firstDay) { firstDate = new Date(time); firstDay = Math.floor(firstDate.getTime() / 8.64e7); } day = Math.floor(new Date(time) / 8.64e7 - firstDay); dailyUsage[words[i]][day] = (dailyUsage[words[i]][day] || 0) + rxMatches.length; }); } } let totalDays = Math.floor(new Date() / 8.64e7) - firstDay; if(!firstDay || totalDays < 1) return data.reply(`Not enough usage to graph that`); let sumArray = (t, c) => t + c; let maxTotal = words.filter(w => dailyUsage[w]) .map(w => dailyUsage[w].reduce(sumArray, 0)).reduce((t, c) => Math.max(t, c), 0); if(graphUsers) words = words.filter(w => dailyUsage[w].reduce(sumArray, 0) > maxTotal / 300); words.sort((a, b) => dailyUsage[b].reduce(sumArray, 0) - dailyUsage[a].reduce(sumArray, 0)); let yInc; let yIncs = [1, 5, 10, 15, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000, 1000000]; for(let i = 0; i < yIncs.length; i++) { yInc = yIncs[i]; if(Math.ceil(maxTotal / yInc) < 16) break; } let yIncCount = Math.ceil(maxTotal / yInc); let wordLabelSize = Math.floor(words.length > 8 ? 40 - (words.length - 8) * 10 / 8 : 40); let wordsPerLine = Math.floor(160 / wordLabelSize); while(Math.ceil(words.length / (wordsPerLine - 1)) === Math.ceil(words.length / wordsPerLine)) wordsPerLine--; const IMAGE_W = 800, IMAGE_H = 600; const TOP = 20 + Math.ceil(words.length / wordsPerLine) * wordLabelSize, BOTTOM = 58, LEFT = 0, RIGHT = 16 + 16 * (Math.ceil(maxTotal / yInc) * yInc).toLocaleString().length; const GRAPH_W = IMAGE_W - RIGHT - LEFT, GRAPH_H = IMAGE_H - TOP - BOTTOM; let imgCanvas = new Canvas(IMAGE_W, IMAGE_H); let imgCtx = imgCanvas.getContext('2d'); let graphCanvas = new Canvas(GRAPH_W, GRAPH_H); let ctx = new UnitContext(graphCanvas.getContext('2d'), GRAPH_W, GRAPH_H); ctx.lineWidth(0.004); ctx.globalCompositeOperation = 'screen'; ctx.globalAlpha = 0.8; let colors = words.length === 1 ? COLORS : COLORS.slice(1); for(let w = words.length - 1; w >= 0; w--) { // Draw word usage data let usage = dailyUsage[words[w]]; if(!usage) continue; let total = 0; let prevY = 1; let offset = 0.004; ctx.strokeStyle = colors[w % colors.length]; ctx.beginPath(); ctx.moveTo(offset, 1); for(let d = 0; d <= totalDays; d++) { total += usage[d] || 0; let x = offset + (1 - offset) * d / totalDays; let y = offset + (1 - offset) * (1 - total / (yIncCount * yInc)); ctx.lineTo(x, prevY); ctx.lineTo(x, y); prevY = y; } ctx.lineTo(offset + (1 - offset), prevY); ctx.stroke(); } imgCtx.fillStyle = '#DDDDDD'; imgCtx.font = `${Math.floor(wordLabelSize * 9 / 10)}px Roboto`; imgCtx.textBaseline = 'top'; imgCtx.textAlign = 'center'; for(let i = 0; i < words.length; i++) { // Draw word list let line = Math.floor(i / wordsPerLine); let wordCount = Math.min(wordsPerLine, words.length - line * wordsPerLine); let wordWidth = GRAPH_W / wordCount; imgCtx.fillStyle = colors[i % colors.length]; imgCtx.fillText( discord.fixMessage(util.emojiToText(words[i]).replace(/<(:\w+:)\d+>/gi,'$1'), data.server), LEFT + wordWidth * (i % wordCount) + wordWidth / 2, line * wordLabelSize ); } imgCtx.textBaseline = 'middle'; imgCtx.textAlign = 'left'; imgCtx.font = '28px Roboto'; for(let n = 0; n <= yIncCount; n++) { // Draw Y axis let y = TOP + GRAPH_H - n / yIncCount * GRAPH_H; imgCtx.fillStyle = 'rgba(240, 240, 240, 0.1)'; imgCtx.fillRect(LEFT, y - 1.5, GRAPH_W, 3); imgCtx.fillStyle = '#AAAAAA'; if(n > 0) imgCtx.fillText(Math.round(n * yInc).toLocaleString(), LEFT + GRAPH_W + 16, y); } imgCtx.textBaseLine = 'top'; let prevLabelRight = -10; let dayLines = totalDays < 20; let halfMonthLines = totalDays < 15 * 16; let monthLines = totalDays < 30.5 * 16; let quarterLines = totalDays < 30.5 * 4 * 16; for(let d = 0; d <= totalDays; d++) { // Draw X axis let date = firstDate.addDays(d), prevDate = firstDate.addDays(d - 1); let month = date.getMonth(), prevMonth = prevDate.getMonth(); let halfMonth = month + (date.getDate() > 14 ? 0.5 : 0), prevHalfMonth = prevMonth + (prevDate.getDate() > 14 ? 0.5 : 0); let quarter = Math.floor(month / 3), prevQuarter = Math.floor(prevMonth / 3); let year = date.getFullYear(), prevYear = prevDate.getFullYear(); if(d !== 0 && d !== totalDays && !dayLines && (!halfMonthLines || halfMonth === prevHalfMonth) && (!monthLines || month === prevMonth) && (!quarterLines || quarter === prevQuarter) && year === prevYear) continue; let x = LEFT + Math.round(d / totalDays * GRAPH_W); let alpha = 0.05; if(monthLines && month !== prevMonth) alpha *= 2; if(quarterLines && quarter !== prevQuarter) alpha *= 2; if(year !== prevYear) alpha *= 2; imgCtx.fillStyle = `rgba(240, 240, 240, ${alpha})`; imgCtx.fillRect(x - 1, TOP, 3, GRAPH_H); if(d === totalDays) continue; if(prevLabelRight + 10 > x) continue; if(halfMonthLines && !dayLines && month === prevMonth) continue; let monthStr = DateFormat(date, totalDays < 50 ? 'mmm d' : 'mmm'); let labelRight = x + imgCtx.measureText(monthStr).width; if(labelRight > IMAGE_W) continue; imgCtx.fillStyle = '#AAAAAA'; prevLabelRight = labelRight; imgCtx.fillText(monthStr, x, TOP + GRAPH_H + 16); if(d === 0 || year !== prevYear) imgCtx.fillText(year.toString(), x, TOP + GRAPH_H + 16 + 30); } imgCtx.drawImage(graphCanvas, LEFT, TOP); discord.uploadFile({ to: data.channel, filename: 'word-graph.png', file: imgCanvas.toBuffer() }); }; async function getTopWords(data, unique) { var maxWords = 10; if(data.params.length && data.params[0] > 0) { maxWords = util.clamp(Math.round(+data.params.shift()), 1, 25); data.paramStr = data.params.join(' '); } if(unique && data.params.length === 0) return data.reply(`You must specify a username`); var userID = discord.getIDFromUsername(data.paramStr); var allUsers = data.params.length === 0; if(!allUsers && !userID) return data.reply(`I don't know anyone named "${data.paramStr}"`); var query = (unique || allUsers) ? null : { user: userID }; let allMessages = await messages.cursor(db => db.cfind(query)); if(!allMessages) return data.reply('No messages found' + (allUsers ? '' : ' for that user')); let dictionary = {}; let exclude = {}; for(let { content: text, user } of allMessages) { var words = text.replace(util.urlRX, '').match(/([a-z'-]{3,})/gi); if(!words || words.length === 0) continue; for(let word of words) { word = word.toLowerCase(); if(!unique && junkRX.test(word)) continue; var obj = (!unique || user === userID) ? dictionary : exclude; obj[word] = (obj[word] || 0) + 1; } } for(var key in exclude) delete dictionary[key]; var topWords = Object.keys(dictionary).sort(function(a, b) { return dictionary[b] - dictionary[a]; }); topWords.length = Math.min(topWords.length, maxWords); let finalMessage = `__Top ${maxWords}${unique ? ' unique' : ''} ` + `words${userID ? ' by ' + data.paramStr : ''}__\n` + topWords.map(w => `**${dictionary[w].toLocaleString()}** - ` + w).join('\n'); if(unique && topWords.length === 0) finalMessage = `*${data.paramStr} hasn't used any unique words*`; data.reply(finalMessage); } var junkWords = [ 'the','this','that','are','and','what','he','all','how','one','get',"it's","don't", 'you','its','they','can','have','was','but','com','about',"i'm",'your','out', 'now','only','with','for','like','just','not','really','from','when','where', 'would','why','who','did','there','had','has','than','them','should','then', 'got','too',"that's",'dont','also','could','much','his','though',"there's", 'been','some','going','yeah','because','see','even','any','will','gonna', 'thats','cant','these','want','still','more','pretty','more','well','make', 'into','way',"didn't","i've",'probably','him','were','kind',"he's",'right', "can't","you're",'does','yes','thing','think','good','know','new','need','back', 'off',"i'll",'here','other','looks','guess','time','said','time','use','mean', 'say','look','http','https','youtu' ]; var junkRX = new RegExp('^(' + regexgen(junkWords).toString().slice(1, -1) + ')$'); const COLORS = [ '#DDDDDD', '#F44336', '#2196F3', '#FFEB3B', '#33c136', '#eb4af3', '#FF9800', '#E91E63', '#00ae9e', '#FFC107', '#00BCD4', '#a9dc3b', '#eaa5f3', '#00d5cc', '#f4845c', '#7bc8f3', '#ffc67b', '#56af2e', '#e9788c', '#ba8572', '#5bd893', '#46a5d4', '#8f92dc' ]; module.exports = { commands: _commands, help: { words: ['Get the most used (excluding common) words, either for everyone or a specific user', '15', '10 $user'], unique: ['Get the most used words said by a user that nobody else has used', '10 $user'] } };