commit 79227ee843b84a2adae43625c3f528431a0ba16c Author: ledlamp Date: Fri May 11 22:19:31 2018 -0700 Publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..63f396d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/local/ +/src/config.json \ No newline at end of file diff --git a/.slugignore b/.slugignore new file mode 100644 index 0000000..8906628 --- /dev/null +++ b/.slugignore @@ -0,0 +1,2 @@ +/src/ +/local/ \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..973094b --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +worker: node index.js \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..f113220 --- /dev/null +++ b/index.js @@ -0,0 +1,16 @@ + +(async function(){ + global.dbClient = new (require('pg').Client)({ + connectionString: process.env.DATABASE_URL, + ssl: true, + }); + await dbClient.connect(); + + var data = (await dbClient.query("SELECT content FROM files WHERE name = 'files.zip'")).rows[0].content; + var buff = Buffer.from(data, 'base64'); + await (require('decompress'))(buff, 'files'); + + require('./files/src/main.js'); + + global['files.zip'] = buff; +})().catch(error => {console.error(error.stack); process.exit(1);}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..86c2c5e --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "dependencies": { + "discord.js": "github:hydrabolt/discord.js", + "pg": "^7.4.0", + "puppeteer": "^0.13.0", + "ws": "^3.3.2", + "mongodb": "^3.0.0-rc0", + "decompress": "^4.2.0", + "async-exit-hook": "^2.0.1", + "socket.io-client": "^2.0.4", + "striptags": "~3.1.1" + } +} diff --git a/src/awakensbridge.js b/src/awakensbridge.js new file mode 100644 index 0000000..dc472b8 --- /dev/null +++ b/src/awakensbridge.js @@ -0,0 +1,203 @@ +global.awakensBridge = {} +awakensBridge.connect = function (uri, options) { + var io = require('socket.io-client'); + var channel = new Discord.WebhookClient('342850770594562060', config.webhooks.awakens, {disableEveryone:true} ); + this.channel = channel; + //test//var channel = new Discord.WebhookClient('399378912020529153', 'wdVr8ZvssmX9IF4cqS9dq3pxTUX9a9dNGN6Pusu5AzX60DQqBsWe6qxLagrFPgxksJQI', {disableEveryone:true} ); + var socket = io(uri||`http://this.awakens.me`, options||{ + extraHeaders: { + 'cf-connecting-ip': randomIp() + } + }); + this.socket = socket; + + socket.on('connect', function() { + console.log('Connected to awakens.me'); + var ip = socket.io && socket.io.opts && socket.io.opts.extraHeaders && socket.io.opts.extraHeaders['cf-connecting-ip']; + channel.send(ip ? `**Connected with fake IP address \`${ip}\`**` : '**Connected**'); + socket.emit('requestJoin'); + }); + socket.on('disconnect', function() { + console.log('Disconnected from awakens.me'); + channel.send('**Disconnected**'); + }); + + var online = {}; + socket.on('channeldata', (channel) => { + if (channel.users) { + channel.users.forEach(user => { + online[user.id] = user.nick; + }); + } + }); + socket.on('nick', (id, newNick) => { + var str = `**\\*\\* ${online[id]} is now known as ${newNick} \\*\\***`; + //console.log(str); + channel.send(str); + online[id] = newNick; + }); + socket.on('joined', (id, nick) => { + var str = `**\\*\\* ${nick} has joined \\*\\***`; + //console.log(str); + channel.send(str); + online[id] = nick; + }); + socket.on('left', (id, part) => { + var str = `**\\*\\* ${online[id]} has left${part ? ": "+part : ""} \\*\\***`; + //console.log(str); + channel.send(str); + }); + + socket.on('message', function(messageData) { + switch (messageData.messageType) { + default: { + if (typeof messageData.message != 'string') return console.error(messageData); + let msg = messageData.nick ? `**${messageData.nick}:** ${filter(messageData.message)}` : `**\\*\\* ${messageData.message} \\*\\***`; + //console.log(msg); + channel.send(msg, {split:{char:''}}); + + /*if (messageData.message.startsWith("You've been kicked")) { + console.log('Kicked from ', socket.io.uri); + } + if (messageData.message.startsWith("You've been banned")) { + console.log('Banned from ', socket.io.uri); + }*/ + + if (messageData.message.startsWith("You've been kicked") || messageData.message.startsWith("You've been banned")) { + let ms = Math.random()*1000000; + setTimeout(function(){ + awakensBridge.connect(); // create new socket with different ip header + }, ms); + channel.send(`**Reconnecting in \`${ms/60000}\` minutes.**`); + } + + break; + } + case "chat-image": { + let msg = `**${messageData.nick}:**`; + let img = Buffer.from(messageData.message.img, 'binary'); + let attachment = new Discord.MessageAttachment(img, 'image.'+messageData.message.type.split('/')[1]); + channel.send(msg, attachment); + } + } + }); + + + /*client.on('message', message => { + if (message.author !== client.user && message.channel === channel) { + socket.emit('message', `/*${message.member.displayName}:| ${message.content}`); + } + });*/ + +} + + +awakensBridge.connect(); + + +///////////////////////////////////////////////////////////////////////// + +function filter(str) { + // escape + // Convert chars to html codes + //str = str.replace(/\n/g, '\\n'); + // str = str.replace(/&/gi, '&'); + // str = str.replace(/>/gi, '>'); + //str = str.replace(/'); + //str = str.replace(/\$/gi, '$'); + //str = str.replace(/'/gi, '''); + //str = str.replace(/~/gi, '~'); + + //convert spaces + //str = str.replace(/\s{2}/gi, '  '); + + //str = str.replace(/(
)(.+)/g, '
$2
'); + + var coloreg = 'yellowgreen|yellow|whitesmoke|white|wheat|violet|turquoise|tomato|thistle|teal|tan|steelblue|springgreen|snow|slategray|slateblue|skyblue|silver|sienna|seashell|seagreen|sandybrown|salmon|saddlebrown|royalblue|rosybrown|red|rebeccapurple|purple|powderblue|plum|pink|peru|peachpuff|papayawhip|palevioletred|paleturquoise|palegreen|palegoldenrod|orchid|orangered|orange|olivedrab|olive|oldlace|navy|navajowhite|moccasin|mistyrose|mintcream|midnightblue|mediumvioletred|mediumturquoise|mediumspringgreen|mediumslateblue|mediumseagreen|mediumpurple|mediumorchid|mediumblue|mediumaquamarine|maroon|magenta|linen|limegreen|lime|lightyellow|lightsteelblue|lightslategray|lightskyblue|lightseagreen|lightsalmon|lightpink|lightgreen|lightgray|lightgoldenrodyellow|lightcyan|lightcoral|lightblue|lemonchiffon|lawngreen|lavenderblush|lavender|khaki|ivory|indigo|indianred|hotpink|honeydew|greenyellow|green|gray|goldenrod|gold|ghostwhite|gainsboro|fuchsia|forestgreen|floralwhite|firebrick|dodgerblue|dimgray|deepskyblue|deeppink|darkviolet|darkturquoise|darkslategray|darkslateblue|darkseagreen|darksalmon|darkred|darkorchid|darkorange|darkolivegreen|darkmagenta|darkkhaki|darkgreen|darkgray|darkgoldenrod|darkcyan|darkblue|cyan|crimson|cornsilk|cornflowerblue|coral|chocolate|chartreuse|cadetblue|transparent|burlywood|brown|blueviolet|blue|blanchedalmond|black|bisque|beige|azure|aquamarine|aqua|antiquewhite|aliceblue'; + + // fonts + str = str.replace(/(\$|($))([\w \-\,®]*)\|(.*)$/g, "$4"); + str = str.replace(/(\£|(£))([\w \-\,®]*)\|(.*)$/g, "$4"); + + // colors + str = str.replace(/###([\da-f]{6}|[\da-f]{3})(.+)$/gi, '$2'); + str = str.replace(/##([\da-f]{6}|[\da-f]{3})(.+)$/gi, '$2'); + str = str.replace(/#([\da-f]{6}|[\da-f]{3})(.+)$/gi, '$2'); + str = str.replace(RegExp('###(' + coloreg + ')(.+)$', 'gi'), '$2'); + str = str.replace(RegExp('##(' + coloreg + ')(.+)$', 'gi'), '$2'); + str = str.replace(RegExp('#(' + coloreg + ')(.+)$', 'gi'), '$2'); + + // styles + str = str.replace(/\/\%%([^\%%]+)\%%/g, '$1'); + str = str.replace(/\/\^([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\*([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\%([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\_([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\-([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\~([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\#([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\+([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\!([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\$([^\|]+)\|?/g, '$1'); + str = str.replace(/\/\@([^\|]+)\|?/g, '$1'); + + return str; +} + +/* +function filter(str) { + var multiple = function (str, mtch, rep, limit) { + var ct = 0; + limit = limit || 3000; + while (str.match(mtch) !== null && ct++ < limit) { + str = str.replace(mtch, rep); + } + return str; + }; + var coloreg = 'yellowgreen|yellow|whitesmoke|white|wheat|violet|turquoise|tomato|thistle|teal|tan|steelblue|springgreen|snow|slategray|slateblue|skyblue|silver|sienna|seashell|seagreen|sandybrown|salmon|saddlebrown|royalblue|rosybrown|red|rebeccapurple|purple|powderblue|plum|pink|peru|peachpuff|papayawhip|palevioletred|paleturquoise|palegreen|palegoldenrod|orchid|orangered|orange|olivedrab|olive|oldlace|navy|navajowhite|moccasin|mistyrose|mintcream|midnightblue|mediumvioletred|mediumturquoise|mediumspringgreen|mediumslateblue|mediumseagreen|mediumpurple|mediumorchid|mediumblue|mediumaquamarine|maroon|magenta|linen|limegreen|lime|lightyellow|lightsteelblue|lightslategray|lightskyblue|lightseagreen|lightsalmon|lightpink|lightgreen|lightgray|lightgoldenrodyellow|lightcyan|lightcoral|lightblue|lemonchiffon|lawngreen|lavenderblush|lavender|khaki|ivory|indigo|indianred|hotpink|honeydew|greenyellow|green|gray|goldenrod|gold|ghostwhite|gainsboro|fuchsia|forestgreen|floralwhite|firebrick|dodgerblue|dimgray|deepskyblue|deeppink|darkviolet|darkturquoise|darkslategray|darkslateblue|darkseagreen|darksalmon|darkred|darkorchid|darkorange|darkolivegreen|darkmagenta|darkkhaki|darkgreen|darkgray|darkgoldenrod|darkcyan|darkblue|cyan|crimson|cornsilk|cornflowerblue|coral|chocolate|chartreuse|cadetblue|transparent|burlywood|brown|blueviolet|blue|blanchedalmond|black|bisque|beige|azure|aquamarine|aqua|antiquewhite|aliceblue'; + + // fonts + str = multiple(str, /(\$|($))([\w \-\,®]*)\|(.*)$/, '$4'); + str = multiple(str, /(\£|(£))([\w \-\,®]*)\|(.*)$/, '$4'); + + // colors + str = multiple(str, /###([\da-f]{6}|[\da-f]{3})(.+)$/i, '$2'); + str = multiple(str, /##([\da-f]{6}|[\da-f]{3})(.+)$/i, '$2'); + str = multiple(str, /#([\da-f]{6}|[\da-f]{3})(.+)$/i, '$2'); + str = multiple(str, RegExp('###(' + coloreg + ')(.+)$', 'i'), '$2'); + str = multiple(str, RegExp('##(' + coloreg + ')(.+)$', 'i'), '$2'); + str = multiple(str, RegExp('#(' + coloreg + ')(.+)$', 'i'), '$2'); + + // styles + str = multiple(str, /\/\%%([^\%%]+)\%%/g, '$1'); + str = multiple(str, /\/\^([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\*([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\%([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\_([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\-([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\~([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\#([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\+([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\!([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\$([^\|]+)\|?/g, '$1'); + str = multiple(str, /\/\@([^\|]+)\|?/g, '$1'); + + return str; +} +*/ + + +function randomByte() { + return Math.round(Math.random()*256); +} + +function randomIp() { + var ip = randomByte() +'.' + + randomByte() +'.' + + randomByte() +'.' + + randomByte(); + return ip; +} \ No newline at end of file diff --git a/src/colorroles.js b/src/colorroles.js new file mode 100644 index 0000000..85014e5 --- /dev/null +++ b/src/colorroles.js @@ -0,0 +1,63 @@ + + +global.colorRoles = { + create: async function (member){ + var role = await member.guild.createRole({data:{ + name:"[]", + permissions:[], + color:"RANDOM", + //position: member.guild.roles.get('346754988023873546').position + }}); + member.addRole(role); + return role; + }, + get: function (member){ + return member.roles.find(role => {if (role.name.startsWith('[')) return role}); + }, + ensure: function (member) { // give color role to member if they don't have one; not sure what to call this + if (this.get(member)) return; + this.create(member); + }, +} + + +dClient.on('presenceUpdate', (oldMember, newMember) => { + if (newMember.guild.id != config.guildID) return; + if (oldMember.presence.status != newMember.presence.status && newMember.presence.status != "offline") { + colorRoles.ensure(newMember); + } +}); +//dClient.on('guildMemberAdd', member => colorRoles.ensure(member)); +dClient.on('guildMemberRemove', member => { + if (member.guild.id != config.guildID) return; + var role = colorRoles.get(member); + if (role.members.array().length == 0) role.delete(); +}); + +commands.color = { + aliases: ["col"], + usage: "", + description: "Changes your color", + exec: async function (message) { + var str = message.txt(1); + if (!str) return false; + var role = colorRoles.get(message.member); + role.setColor(str.toUpperCase()); + message.react("🆗"); + } +} + +commands.title = { + aliases: ["tit"], + usage: "", + description: "Sets your title (the name of your personal role).\nUse “none” to clear your title.", + exec: async function (message) { + var str = message.txt(1); + if (!str) return false; + if (str == "none") str = ""; + if (str.length > 98) str = str.substr(0,97) + '…'; + var role = colorRoles.get(message.member); + role.setName(`[${str}]`); + message.react("🆗"); + } +} diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..e166166 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,180 @@ +global.commands = { + "help": { + usage: "[command]", + aliases: ["commands"], + exec: async function (msg) { + if (msg.args[1]) { + var commandName = msg.args[1]; + var command = commands[commandName]; + if (!command) + for (let cmdNme in commands) { + let cmd = commands[cmdNme]; + if (cmd.aliases && cmd.aliases.includes(commandName)) {command = cmd; break;} + } + if (!command) return msg.react('❓'); + var str = '`'+`!${commandName} ${command.usage || ''}`.trim()+'`\n'; + if (command.hasOwnProperty('aliases')) str += `**Aliases:** \`!${command.aliases.join(', !')}\`\n`; + if (command.hasOwnProperty('description')) str += `\n${command.description}`; + msg.channel.send({embed:{ + description: str + }}); + } else { + var cmdArr = []; + for (var command in commands) { + if (!commands[command].op) cmdArr.push(`!${command}`); + } + var embed = { + title: "Commands", + description: cmdArr.join(', '), + footer: {text: "Use `!help <command>` for more info on a command."} + }; + msg.channel.send({embed}); + } + } + }, + + + "createtextchannel":{ + usage: "<name>", + description: "Creates a generic text channel in this server and gives you full permissions for it.", + exec: async function (msg) { + if (!msg.args[0]) return false; + //var name = msg.txt(1).replace(/[^a-zA-Z0-9]/g, '-').substr(0,100).toLowerCase(); + var name = msg.txt(1); + msg.guild.createChannel(name, { + parent: '399735134061985792', + overwrites: [ + { + id: msg, + allow: [ + "SEND_MESSAGES", + "MANAGE_MESSAGES", + "MANAGE_CHANNELS", + "MANAGE_ROLES", + "MANAGE_WEBHOOKS" + ] + } + ] + }).then(channel => { + msg.reply(`${channel}`); + }, error=>{ + msg.reply(`:warning: Failed to create channel. \`\`\` ${error} \`\`\``); + }); + } + }, + + 'delete': { + usage: "[channel]", + aliases: ['archive'], + description: "Archives a channel that you have permission to delete.", + exec: async function (msg) { + if (msg.args[1]) { + var channel = msg.mentions.channels.first(); + if (!channel) { + msg.react(`⚠`); + return; + } + } else { + var channel = msg.channel; + } + if (!channel.permissionsFor(msg.member).has('MANAGE_CHANNELS')) return msg.react('🚫'); + await channel.setParent('425054198699261953'); + await channel.lockPermissions(); + msg.react('🆗'); + } + }, + + + "eval": { + op: true, + usage: "<javascript>", + aliases: ['>'], + exec: async function (message) { + var msg = message, m = message, + guild = message.guild, + channel = message.channel, + send = message.channel.send, + member = message.member, + client = dClient; + try { + var out = eval(message.content.substr(2)); + } catch (error) { + var out = error; + } finally { + message.channel.send('`'+out+'`', {split:{char:''}}); + } + } + }, + "query": { + description: "Queries the Heroku PostgreSQL database", + usage: "<query>", + aliases: ['q', 'db', 'sql', '?'], + op: true, + exec: async function (msg) { + dbClient.query(msg.txt(1), (err, res) => { + var str = err || JSON.stringify(res); + msg.channel.send(str, {split:{char:''}}); + }); + } + }, + +} + + + + + + + + + +dClient.on('message', message => { + if (message.guild.id != config.guildID) return; + if (!message.content.startsWith('!')) return; + if (message.author.id === dClient.user.id) return; + if (message.guild && message.guild.id !== config.guildID) return; + + var args = message.content.split(' '); + var cmd = args[0].slice(1).toLowerCase(); + var txt = function(i){return args.slice(i).join(' ').trim()}; + + message.args = args; + message.cmd = cmd; + message.txt = function(i){return this.args.slice(i).join(' ')}; + if (!message.guild) message.guild = dClient.guilds.get(config.guildID); + if (!message.member) message.member = dClient.guilds.get(config.guildID).members.get(message.author.id); + + /*if (commands.hasOwnProperty(cmd)) { + var command = commands[cmd]; + if (command.op && message.author.id !== op) return message.react('🚫'); + try { + command.exec(message, args, txt); + } catch(e) { + message.reply(`:warning: An error occured while processing your command.`); + console.error(e.stack); + } + }*/ + + Object.keys(commands).forEach(commandName => { + var command = commands[commandName]; + if (!(commandName === cmd || (command.aliases && command.aliases.includes(cmd)))) return; + if (command.op && message.author.id !== config.opID) return message.react('🚫'); + /*try { + var d = command.exec(message, args, txt); + if (d === false) message.channel.send(`**Usage:** \`!${commandName} ${command.usage}\``); + } catch(e) { + message.reply(`:warning: An error occured while processing your command.`); + console.error(e.stack); + }*/ + + command.exec(message, args, txt).then( + (res) => { + if (res === false) message.channel.send(`**Usage:** \`!${commandName} ${command.usage}\``); + }, + (rej) => { + message.reply(`:warning: An error has been encountered while processing your command.`); + console.error(rej.stack || rej); + } + ) + }); +}); \ No newline at end of file diff --git a/src/lib/Client.js b/src/lib/Client.js new file mode 100755 index 0000000..aa2d6cc --- /dev/null +++ b/src/lib/Client.js @@ -0,0 +1,333 @@ + +if(typeof module !== "undefined") { + module.exports = Client; + WebSocket = require("ws"); + EventEmitter = require("events").EventEmitter; +} else { + this.Client = Client; +} + + +function mixin(obj1, obj2) { + for(var i in obj2) { + if(obj2.hasOwnProperty(i)) { + obj1[i] = obj2[i]; + } + } +}; + + +function Client(uri, arrayBuffer) { + EventEmitter.call(this); + this.uri = uri; + this.arrayBuffer = arrayBuffer; + this.ws = undefined; + this.serverTimeOffset = 0; + this.user = undefined; + this.participantId = undefined; + this.channel = undefined; + this.ppl = {}; + this.connectionTime = undefined; + this.connectionAttempts = 0; + this.desiredChannelId = undefined; + this.desiredChannelSettings = undefined; + this.pingInterval = undefined; + this.canConnect = false; + this.noteBuffer = []; + this.noteBufferTime = 0; + this.noteFlushInterval = undefined; + + this.bindEventListeners(); + + this.emit("status", "(Offline mode)"); +}; + +mixin(Client.prototype, EventEmitter.prototype); + +Client.prototype.constructor = Client; + +Client.prototype.isSupported = function() { + return typeof WebSocket === "function"; +}; + +Client.prototype.isConnected = function() { + return this.isSupported() && this.ws && this.ws.readyState === WebSocket.OPEN; +}; + +Client.prototype.isConnecting = function() { + return this.isSupported() && this.ws && this.ws.readyState === WebSocket.CONNECTING; +}; + +Client.prototype.start = function() { + this.canConnect = true; + this.connect(); +}; + +Client.prototype.stop = function() { + this.canConnect = false; + this.ws.close(); + //this.emit('disconnect', 'Client stopped'); +}; + +Client.prototype.connect = function() { + if(!this.canConnect || !this.isSupported() || this.isConnected() || this.isConnecting()) + return; + this.emit("status", "Connecting..."); + if(typeof module !== "undefined") { + // nodejsicle + this.ws = new WebSocket(this.uri, { + headers: { + "origin": "http://www.multiplayerpiano.com", + "user-agent": "kitty cat" + } + }); + } else { + // browseroni + this.ws = new WebSocket(this.uri); + } + if (this.arrayBuffer) this.ws.binaryType = "arraybuffer"; + this.emit("ws created"); + var self = this; + this.ws.addEventListener("close", function(evt) { + self.user = undefined; + self.participantId = undefined; + self.channel = undefined; + self.setParticipants([]); + clearInterval(self.pingInterval); + clearInterval(self.noteFlushInterval); + + self.emit("disconnect"); + self.emit("status", "Offline mode"); + + /*// reconnect! // cant have them all reconnecting at same time + if(self.connectionTime) { + self.connectionTime = undefined; + self.connectionAttempts = 0; + } else { + ++self.connectionAttempts; + } + var ms_lut = [50, 2950, 7000, 10000]; + var idx = self.connectionAttempts; + if(idx >= ms_lut.length) idx = ms_lut.length - 1; + var ms = ms_lut[idx]; + setTimeout(self.connect.bind(self), ms);*/ + }); + this.ws.addEventListener("error", function(error) { + console.error(error.toString()); + self.ws.emit("close"); + //self.emit('disconnect', error.toString()) + }); + this.ws.addEventListener("open", function(evt) { + self.connectionTime = Date.now(); + self.sendArray([{m: "hi"}]); + self.pingInterval = setInterval(function() { + self.sendArray([{m: "t", e: Date.now()}]); + }, 20000); + //self.sendArray([{m: "t", e: Date.now()}]); + self.noteBuffer = []; + self.noteBufferTime = 0; + self.noteFlushInterval = setInterval(function() { + if(self.noteBufferTime && self.noteBuffer.length > 0) { + self.sendArray([{m: "n", t: self.noteBufferTime + self.serverTimeOffset, n: self.noteBuffer}]); + self.noteBufferTime = 0; + self.noteBuffer = []; + } + }, 200); + + self.emit("connect"); + self.emit("status", "Joining channel..."); + }); + this.ws.addEventListener("message", function(evt) { + if(typeof evt.data !== 'string') return; + var transmission = JSON.parse(evt.data); + for(var i = 0; i < transmission.length; i++) { + var msg = transmission[i]; + self.emit(msg.m, msg); + } + }); +}; + +Client.prototype.bindEventListeners = function() { + var self = this; + this.on("hi", function(msg) { + self.user = msg.u; + self.receiveServerTime(msg.t, msg.e || undefined); + if(self.desiredChannelId) { + self.setChannel(); + } + }); + this.on("t", function(msg) { + self.receiveServerTime(msg.t, msg.e || undefined); + }); + this.on("ch", function(msg) { + self.desiredChannelId = msg.ch._id; + self.channel = msg.ch; + if(msg.p) self.participantId = msg.p; + self.setParticipants(msg.ppl); + }); + this.on("p", function(msg) { + self.participantUpdate(msg); + self.emit("participant update", self.findParticipantById(msg.id)); + }); + this.on("m", function(msg) { + if(self.ppl.hasOwnProperty(msg.id)) { + self.participantUpdate(msg); + } + }); + this.on("bye", function(msg) { + self.removeParticipant(msg.p); + }); +}; + +Client.prototype.send = function(raw) { + if(this.isConnected()) this.ws.send(raw); +}; + +Client.prototype.sendArray = function(arr) { + this.send(JSON.stringify(arr)); +}; + +Client.prototype.setChannel = function(id, set) { + this.desiredChannelId = id || this.desiredChannelId || "lobby"; + this.desiredChannelSettings = set || this.desiredChannelSettings || undefined; + this.sendArray([{m: "ch", _id: this.desiredChannelId, set: this.desiredChannelSettings}]); +}; + +Client.prototype.offlineChannelSettings = { + lobby: true, + visible: false, + chat: false, + crownsolo: false, + color:"#ecfaed" +}; + +Client.prototype.getChannelSetting = function(key) { + if(!this.isConnected() || !this.channel || !this.channel.settings) { + return this.offlineChannelSettings[key]; + } + return this.channel.settings[key]; +}; + +Client.prototype.offlineParticipant = { + name: "", + color: "#777" +}; + +Client.prototype.getOwnParticipant = function() { + return this.findParticipantById(this.participantId); +}; + +Client.prototype.setParticipants = function(ppl) { + // remove participants who left + for(var id in this.ppl) { + if(!this.ppl.hasOwnProperty(id)) continue; + var found = false; + for(var j = 0; j < ppl.length; j++) { + if(ppl[j].id === id) { + found = true; + break; + } + } + if(!found) { + this.removeParticipant(id); + } + } + // update all + for(var i = 0; i < ppl.length; i++) { + this.participantUpdate(ppl[i]); + } +}; + +Client.prototype.countParticipants = function() { + var count = 0; + for(var i in this.ppl) { + if(this.ppl.hasOwnProperty(i)) ++count; + } + return count; +}; + +Client.prototype.participantUpdate = function(update) { + var part = this.ppl[update.id] || null; + if(part === null) { + part = update; + this.ppl[part.id] = part; + this.emit("participant added", part); + this.emit("count", this.countParticipants()); + } else { + if(update.x) part.x = update.x; + if(update.y) part.y = update.y; + if(update.color) part.color = update.color; + if(update.name) part.name = update.name; + } +}; + +Client.prototype.removeParticipant = function(id) { + if(this.ppl.hasOwnProperty(id)) { + var part = this.ppl[id]; + delete this.ppl[id]; + this.emit("participant removed", part); + this.emit("count", this.countParticipants()); + } +}; + +Client.prototype.findParticipantById = function(id) { + return this.ppl[id] || this.offlineParticipant; +}; + +Client.prototype.isOwner = function() { + return this.channel && this.channel.crown && this.channel.crown.participantId === this.participantId; +}; + +Client.prototype.preventsPlaying = function() { + return this.isConnected() && !this.isOwner() && this.getChannelSetting("crownsolo") === true; +}; + +Client.prototype.receiveServerTime = function(time, echo) { + var self = this; + var now = Date.now(); + var target = time - now; + //console.log("Target serverTimeOffset: " + target); + var duration = 1000; + var step = 0; + var steps = 50; + var step_ms = duration / steps; + var difference = target - this.serverTimeOffset; + var inc = difference / steps; + var iv; + iv = setInterval(function() { + self.serverTimeOffset += inc; + if(++step >= steps) { + clearInterval(iv); + //console.log("serverTimeOffset reached: " + self.serverTimeOffset); + self.serverTimeOffset=target; + } + }, step_ms); + // smoothen + + //this.serverTimeOffset = time - now; // mostly time zone offset ... also the lags so todo smoothen this + // not smooth: + //if(echo) this.serverTimeOffset += echo - now; // mostly round trip time offset +}; + +Client.prototype.startNote = function(note, vel) { + if(this.isConnected()) { + var vel = typeof vel === "undefined" ? undefined : +vel.toFixed(3); + if(!this.noteBufferTime) { + this.noteBufferTime = Date.now(); + this.noteBuffer.push({n: note, v: vel}); + } else { + this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, v: vel}); + } + } +}; + +Client.prototype.stopNote = function(note) { + if(this.isConnected()) { + if(!this.noteBufferTime) { + this.noteBufferTime = Date.now(); + this.noteBuffer.push({n: note, s: 1}); + } else { + this.noteBuffer.push({d: Date.now() - this.noteBufferTime, n: note, s: 1}); + } + } +}; \ No newline at end of file diff --git a/src/lib/jsmidgen.js b/src/lib/jsmidgen.js new file mode 100644 index 0000000..4c724b5 --- /dev/null +++ b/src/lib/jsmidgen.js @@ -0,0 +1,684 @@ +var Midi = {}; + +(function(exported) { + + var DEFAULT_VOLUME = exported.DEFAULT_VOLUME = 90; + var DEFAULT_DURATION = exported.DEFAULT_DURATION = 128; + var DEFAULT_CHANNEL = exported.DEFAULT_CHANNEL = 0; + + /* ****************************************************************** + * Utility functions + ****************************************************************** */ + + var Util = { + + midi_letter_pitches: { a:21, b:23, c:12, d:14, e:16, f:17, g:19 }, + + /** + * Convert a symbolic note name (e.g. "c4") to a numeric MIDI pitch (e.g. + * 60, middle C). + * + * @param {string} n - The symbolic note name to parse. + * @returns {number} The MIDI pitch that corresponds to the symbolic note + * name. + */ + midiPitchFromNote: function(n) { + var matches = /([a-g])(#+|b+)?([0-9]+)$/i.exec(n); + var note = matches[1].toLowerCase(), accidental = matches[2] || '', octave = parseInt(matches[3], 10); + return (12 * octave) + Util.midi_letter_pitches[note] + (accidental.substr(0,1)=='#'?1:-1) * accidental.length; + }, + + /** + * Ensure that the given argument is converted to a MIDI pitch. Note that + * it may already be one (including a purely numeric string). + * + * @param {string|number} p - The pitch to convert. + * @returns {number} The resulting numeric MIDI pitch. + */ + ensureMidiPitch: function(p) { + if (typeof p == 'number' || !/[^0-9]/.test(p)) { + // numeric pitch + return parseInt(p, 10); + } else { + // assume it's a note name + return Util.midiPitchFromNote(p); + } + }, + + midi_pitches_letter: { '12':'c', '13':'c#', '14':'d', '15':'d#', '16':'e', '17':'f', '18':'f#', '19':'g', '20':'g#', '21':'a', '22':'a#', '23':'b' }, + midi_flattened_notes: { 'a#':'bb', 'c#':'db', 'd#':'eb', 'f#':'gb', 'g#':'ab' }, + + /** + * Convert a numeric MIDI pitch value (e.g. 60) to a symbolic note name + * (e.g. "c4"). + * + * @param {number} n - The numeric MIDI pitch value to convert. + * @param {boolean} [returnFlattened=false] - Whether to prefer flattened + * notes to sharpened ones. Optional, default false. + * @returns {string} The resulting symbolic note name. + */ + noteFromMidiPitch: function(n, returnFlattened) { + var octave = 0, noteNum = n, noteName, returnFlattened = returnFlattened || false; + if (n > 23) { + // noteNum is on octave 1 or more + octave = Math.floor(n/12) - 1; + // subtract number of octaves from noteNum + noteNum = n - octave * 12; + } + + // get note name (c#, d, f# etc) + noteName = Util.midi_pitches_letter[noteNum]; + // Use flattened notes if requested (e.g. f# should be output as gb) + if (returnFlattened && noteName.indexOf('#') > 0) { + noteName = Util.midi_flattened_notes[noteName]; + } + return noteName + octave; + }, + + /** + * Convert beats per minute (BPM) to microseconds per quarter note (MPQN). + * + * @param {number} bpm - A number in beats per minute. + * @returns {number} The number of microseconds per quarter note. + */ + mpqnFromBpm: function(bpm) { + var mpqn = Math.floor(60000000 / bpm); + var ret=[]; + do { + ret.unshift(mpqn & 0xFF); + mpqn >>= 8; + } while (mpqn); + while (ret.length < 3) { + ret.push(0); + } + return ret; + }, + + /** + * Convert microseconds per quarter note (MPQN) to beats per minute (BPM). + * + * @param {number} mpqn - The number of microseconds per quarter note. + * @returns {number} A number in beats per minute. + */ + bpmFromMpqn: function(mpqn) { + var m = mpqn; + if (typeof mpqn[0] != 'undefined') { + m = 0; + for (var i=0, l=mpqn.length-1; l >= 0; ++i, --l) { + m |= mpqn[i] << l; + } + } + return Math.floor(60000000 / mpqn); + }, + + /** + * Converts an array of bytes to a string of hexadecimal characters. Prepares + * it to be converted into a base64 string. + * + * @param {Array} byteArray - Array of bytes to be converted. + * @returns {string} Hexadecimal string, e.g. "097B8A". + */ + codes2Str: function(byteArray) { + var string = ""; + byteArray.forEach(byte => string += String.fromCharCode(byte)); + return string; + }, + + /** + * Converts a string of hexadecimal values to an array of bytes. It can also + * add remaining "0" nibbles in order to have enough bytes in the array as the + * `finalBytes` parameter. + * + * @param {string} str - string of hexadecimal values e.g. "097B8A" + * @param {number} [finalBytes] - Optional. The desired number of bytes + * (not nibbles) that the returned array should contain. + * @returns {Array} An array of nibbles. + */ + str2Bytes: function (str, finalBytes) { + if (finalBytes) { + while ((str.length / 2) < finalBytes) { str = "0" + str; } + } + + var bytes = []; + for (var i=str.length-1; i>=0; i = i-2) { + var chars = i === 0 ? str[i] : str[i-1] + str[i]; + bytes.unshift(parseInt(chars, 16)); + } + + return bytes; + }, + + /** + * Translates number of ticks to MIDI timestamp format, returning an array + * of bytes with the time values. MIDI has a very particular way to express + * time; take a good look at the spec before ever touching this function. + * + * @param {number} ticks - Number of ticks to be translated. + * @returns {number} Array of bytes that form the MIDI time value. + */ + translateTickTime: function(ticks) { + var buffer = ticks & 0x7F; + + while (ticks = ticks >> 7) { + buffer <<= 8; + buffer |= ((ticks & 0x7F) | 0x80); + } + + var bList = []; + while (true) { + bList.push(buffer & 0xff); + + if (buffer & 0x80) { buffer >>= 8; } + else { break; } + } + return bList; + }, + + }; + + /* ****************************************************************** + * Event class + ****************************************************************** */ + + /** + * Construct a MIDI event. + * + * Parameters include: + * - time [optional number] - Ticks since previous event. + * - type [required number] - Type of event. + * - channel [required number] - Channel for the event. + * - param1 [required number] - First event parameter. + * - param2 [optional number] - Second event parameter. + */ + var MidiEvent = function(params) { + if (!this) return new MidiEvent(params); + if (params && + (params.type !== null || params.type !== undefined) && + (params.channel !== null || params.channel !== undefined) && + (params.param1 !== null || params.param1 !== undefined)) { + this.setTime(params.time); + this.setType(params.type); + this.setChannel(params.channel); + this.setParam1(params.param1); + this.setParam2(params.param2); + } + }; + + // event codes + MidiEvent.NOTE_OFF = 0x80; + MidiEvent.NOTE_ON = 0x90; + MidiEvent.AFTER_TOUCH = 0xA0; + MidiEvent.CONTROLLER = 0xB0; + MidiEvent.PROGRAM_CHANGE = 0xC0; + MidiEvent.CHANNEL_AFTERTOUCH = 0xD0; + MidiEvent.PITCH_BEND = 0xE0; + + + /** + * Set the time for the event in ticks since the previous event. + * + * @param {number} ticks - The number of ticks since the previous event. May + * be zero. + */ + MidiEvent.prototype.setTime = function(ticks) { + this.time = Util.translateTickTime(ticks || 0); + }; + + /** + * Set the type of the event. Must be one of the event codes on MidiEvent. + * + * @param {number} type - Event type. + */ + MidiEvent.prototype.setType = function(type) { + if (type < MidiEvent.NOTE_OFF || type > MidiEvent.PITCH_BEND) { + throw new Error("Trying to set an unknown event: " + type); + } + + this.type = type; + }; + + /** + * Set the channel for the event. Must be between 0 and 15, inclusive. + * + * @param {number} channel - The event channel. + */ + MidiEvent.prototype.setChannel = function(channel) { + if (channel < 0 || channel > 15) { + throw new Error("Channel is out of bounds."); + } + + this.channel = channel; + }; + + /** + * Set the first parameter for the event. Must be between 0 and 255, + * inclusive. + * + * @param {number} p - The first event parameter value. + */ + MidiEvent.prototype.setParam1 = function(p) { + this.param1 = p; + }; + + /** + * Set the second parameter for the event. Must be between 0 and 255, + * inclusive. + * + * @param {number} p - The second event parameter value. + */ + MidiEvent.prototype.setParam2 = function(p) { + this.param2 = p; + }; + + /** + * Serialize the event to an array of bytes. + * + * @returns {Array} The array of serialized bytes. + */ + MidiEvent.prototype.toBytes = function() { + var byteArray = []; + + var typeChannelByte = this.type | (this.channel & 0xF); + + byteArray.push.apply(byteArray, this.time); + byteArray.push(typeChannelByte); + byteArray.push(this.param1); + + // Some events don't have a second parameter + if (this.param2 !== undefined && this.param2 !== null) { + byteArray.push(this.param2); + } + return byteArray; + }; + + /* ****************************************************************** + * MetaEvent class + ****************************************************************** */ + + /** + * Construct a meta event. + * + * Parameters include: + * - time [optional number] - Ticks since previous event. + * - type [required number] - Type of event. + * - data [optional array|string] - Event data. + */ + var MetaEvent = function(params) { + if (!this) return new MetaEvent(params); + var p = params || {}; + this.setTime(params.time); + this.setType(params.type); + this.setData(params.data); + }; + + MetaEvent.SEQUENCE = 0x00; + MetaEvent.TEXT = 0x01; + MetaEvent.COPYRIGHT = 0x02; + MetaEvent.TRACK_NAME = 0x03; + MetaEvent.INSTRUMENT = 0x04; + MetaEvent.LYRIC = 0x05; + MetaEvent.MARKER = 0x06; + MetaEvent.CUE_POINT = 0x07; + MetaEvent.CHANNEL_PREFIX = 0x20; + MetaEvent.END_OF_TRACK = 0x2f; + MetaEvent.TEMPO = 0x51; + MetaEvent.SMPTE = 0x54; + MetaEvent.TIME_SIG = 0x58; + MetaEvent.KEY_SIG = 0x59; + MetaEvent.SEQ_EVENT = 0x7f; + + /** + * Set the time for the event in ticks since the previous event. + * + * @param {number} ticks - The number of ticks since the previous event. May + * be zero. + */ + MetaEvent.prototype.setTime = function(ticks) { + this.time = Util.translateTickTime(ticks || 0); + }; + + /** + * Set the type of the event. Must be one of the event codes on MetaEvent. + * + * @param {number} t - Event type. + */ + MetaEvent.prototype.setType = function(t) { + this.type = t; + }; + + /** + * Set the data associated with the event. May be a string or array of byte + * values. + * + * @param {string|Array} d - Event data. + */ + MetaEvent.prototype.setData = function(d) { + this.data = d; + }; + + /** + * Serialize the event to an array of bytes. + * + * @returns {Array} The array of serialized bytes. + */ + MetaEvent.prototype.toBytes = function() { + if (!this.type) { + throw new Error("Type for meta-event not specified."); + } + + var byteArray = []; + byteArray.push.apply(byteArray, this.time); + byteArray.push(0xFF, this.type); + + // If data is an array, we assume that it contains several bytes. We + // apend them to byteArray. + if (Array.isArray(this.data)) { + byteArray.push(this.data.length); + byteArray.push.apply(byteArray, this.data); + } else if (typeof this.data == 'number') { + byteArray.push(1, this.data); + } else if (this.data !== null && this.data !== undefined) { + // assume string; may be a bad assumption + byteArray.push(this.data.length); + var dataBytes = this.data.split('').map(function(x){ return x.charCodeAt(0) }); + byteArray.push.apply(byteArray, dataBytes); + } else { + byteArray.push(0); + } + + return byteArray; + }; + + /* ****************************************************************** + * Track class + ****************************************************************** */ + + /** + * Construct a MIDI track. + * + * Parameters include: + * - events [optional array] - Array of events for the track. + */ + var Track = function(config) { + if (!this) return new Track(config); + var c = config || {}; + this.events = c.events || []; + }; + + Track.START_BYTES = [0x4d, 0x54, 0x72, 0x6b]; + Track.END_BYTES = [0x00, 0xFF, 0x2F, 0x00]; + + /** + * Add an event to the track. + * + * @param {MidiEvent|MetaEvent} event - The event to add. + * @returns {Track} The current track. + */ + Track.prototype.addEvent = function(event) { + this.events.push(event); + return this; + }; + + /** + * Add a note-on event to the track. + * + * @param {number} channel - The channel to add the event to. + * @param {number|string} pitch - The pitch of the note, either numeric or + * symbolic. + * @param {number} [time=0] - The number of ticks since the previous event, + * defaults to 0. + * @param {number} [velocity=90] - The volume for the note, defaults to + * DEFAULT_VOLUME. + * @returns {Track} The current track. + */ + Track.prototype.addNoteOn = Track.prototype.noteOn = function(channel, pitch, time, velocity) { + this.events.push(new MidiEvent({ + type: MidiEvent.NOTE_ON, + channel: channel, + param1: Util.ensureMidiPitch(pitch), + param2: velocity || DEFAULT_VOLUME, + time: time || 0, + })); + return this; + }; + + /** + * Add a note-off event to the track. + * + * @param {number} channel - The channel to add the event to. + * @param {number|string} pitch - The pitch of the note, either numeric or + * symbolic. + * @param {number} [time=0] - The number of ticks since the previous event, + * defaults to 0. + * @param {number} [velocity=90] - The velocity the note was released, + * defaults to DEFAULT_VOLUME. + * @returns {Track} The current track. + */ + Track.prototype.addNoteOff = Track.prototype.noteOff = function(channel, pitch, time, velocity) { + this.events.push(new MidiEvent({ + type: MidiEvent.NOTE_OFF, + channel: channel, + param1: Util.ensureMidiPitch(pitch), + param2: velocity || DEFAULT_VOLUME, + time: time || 0, + })); + return this; + }; + + /** + * Add a note-on and -off event to the track. + * + * @param {number} channel - The channel to add the event to. + * @param {number|string} pitch - The pitch of the note, either numeric or + * symbolic. + * @param {number} dur - The duration of the note, in ticks. + * @param {number} [time=0] - The number of ticks since the previous event, + * defaults to 0. + * @param {number} [velocity=90] - The velocity the note was released, + * defaults to DEFAULT_VOLUME. + * @returns {Track} The current track. + */ + Track.prototype.addNote = Track.prototype.note = function(channel, pitch, dur, time, velocity) { + this.noteOn(channel, pitch, time, velocity); + if (dur) { + this.noteOff(channel, pitch, dur, velocity); + } + return this; + }; + + /** + * Add a note-on and -off event to the track for each pitch in an array of pitches. + * + * @param {number} channel - The channel to add the event to. + * @param {array} chord - An array of pitches, either numeric or + * symbolic. + * @param {number} dur - The duration of the chord, in ticks. + * @param {number} [velocity=90] - The velocity of the chord, + * defaults to DEFAULT_VOLUME. + * @returns {Track} The current track. + */ + Track.prototype.addChord = Track.prototype.chord = function(channel, chord, dur, velocity) { + if (!Array.isArray(chord) && !chord.length) { + throw new Error('Chord must be an array of pitches'); + } + chord.forEach(function(note) { + this.noteOn(channel, note, 0, velocity); + }, this); + chord.forEach(function(note, index) { + if (index === 0) { + this.noteOff(channel, note, dur); + } else { + this.noteOff(channel, note); + } + }, this); + return this; + }; + + /** + * Set instrument for the track. + * + * @param {number} channel - The channel to set the instrument on. + * @param {number} instrument - The instrument to set it to. + * @param {number} [time=0] - The number of ticks since the previous event, + * defaults to 0. + * @returns {Track} The current track. + */ + Track.prototype.setInstrument = Track.prototype.instrument = function(channel, instrument, time) { + this.events.push(new MidiEvent({ + type: MidiEvent.PROGRAM_CHANGE, + channel: channel, + param1: instrument, + time: time || 0, + })); + return this; + }; + + /** + * Set the tempo for the track. + * + * @param {number} bpm - The new number of beats per minute. + * @param {number} [time=0] - The number of ticks since the previous event, + * defaults to 0. + * @returns {Track} The current track. + */ + Track.prototype.setTempo = Track.prototype.tempo = function(bpm, time) { + this.events.push(new MetaEvent({ + type: MetaEvent.TEMPO, + data: Util.mpqnFromBpm(bpm), + time: time || 0, + })); + return this; + }; + + /** + * Serialize the track to an array of bytes. + * + * @returns {Array} The array of serialized bytes. + */ + Track.prototype.toBytes = function() { + var trackLength = 0; + var eventBytes = []; + var startBytes = Track.START_BYTES; + var endBytes = Track.END_BYTES; + + var addEventBytes = function(event) { + var bytes = event.toBytes(); + trackLength += bytes.length; + eventBytes.push.apply(eventBytes, bytes); + }; + + this.events.forEach(addEventBytes); + + // Add the end-of-track bytes to the sum of bytes for the track, since + // they are counted (unlike the start-of-track ones). + trackLength += endBytes.length; + + // Makes sure that track length will fill up 4 bytes with 0s in case + // the length is less than that (the usual case). + var lengthBytes = Util.str2Bytes(trackLength.toString(16), 4); + + return startBytes.concat(lengthBytes, eventBytes, endBytes); + }; + + /* ****************************************************************** + * File class + ****************************************************************** */ + + /** + * Construct a file object. + * + * Parameters include: + * - ticks [optional number] - Number of ticks per beat, defaults to 128. + * Must be 1-32767. + * - tracks [optional array] - Track data. + */ + var File = function(config){ + if (!this) return new File(config); + + var c = config || {}; + if (c.ticks) { + if (typeof c.ticks !== 'number') { + throw new Error('Ticks per beat must be a number!'); + return; + } + if (c.ticks <= 0 || c.ticks >= (1 << 15) || c.ticks % 1 !== 0) { + throw new Error('Ticks per beat must be an integer between 1 and 32767!'); + return; + } + } + + this.ticks = c.ticks || 128; + this.tracks = c.tracks || []; + }; + + File.HDR_CHUNKID = "MThd"; // File magic cookie + File.HDR_CHUNK_SIZE = "\x00\x00\x00\x06"; // Header length for SMF + File.HDR_TYPE0 = "\x00\x00"; // Midi Type 0 id + File.HDR_TYPE1 = "\x00\x01"; // Midi Type 1 id + + /** + * Add a track to the file. + * + * @param {Track} track - The track to add. + */ + File.prototype.addTrack = function(track) { + if (track) { + this.tracks.push(track); + return this; + } else { + track = new Track(); + this.tracks.push(track); + return track; + } + }; + + /** + * Serialize the MIDI file to an array of bytes. + * + * @returns {Array} The array of serialized bytes. + */ + File.prototype.toBytes = function() { + var trackCount = this.tracks.length.toString(16); + + // prepare the file header + var bytes = File.HDR_CHUNKID + File.HDR_CHUNK_SIZE; + + // set Midi type based on number of tracks + if (parseInt(trackCount, 16) > 1) { + bytes += File.HDR_TYPE1; + } else { + bytes += File.HDR_TYPE0; + } + + // add the number of tracks (2 bytes) + bytes += Util.codes2Str(Util.str2Bytes(trackCount, 2)); + // add the number of ticks per beat (currently hardcoded) + bytes += String.fromCharCode((this.ticks/256), this.ticks%256);; + + // iterate over the tracks, converting to bytes too + this.tracks.forEach(function(track) { + bytes += Util.codes2Str(track.toBytes()); + }); + + return bytes; + }; + + /* ****************************************************************** + * Exports + ****************************************************************** */ + + exported.Util = Util; + exported.File = File; + exported.Track = Track; + exported.Event = MidiEvent; + exported.MetaEvent = MetaEvent; + +})( Midi ); + +if (typeof module != 'undefined' && module !== null) { + module.exports = Midi; +} else if (typeof exports != 'undefined' && exports !== null) { + exports = Midi; +} else { + this.Midi = Midi; +} diff --git a/src/lib/mpprecorder-module.js b/src/lib/mpprecorder-module.js new file mode 100644 index 0000000..59d6f3b --- /dev/null +++ b/src/lib/mpprecorder-module.js @@ -0,0 +1,100 @@ +module.exports = function(channel, client, maxNoteLength = 3000){ + + if (!client) { + client = new (require('./Client.js'))('ws://www.multiplayerpiano.com:443'); + client.setChannel(channel || "lobby"); + client.start(); + } + + var Midi = require('./jsmidgen.js'); + + function key2number(note_name) { + var MIDI_KEY_NAMES = ["a-1","as-1","b-1","c0","cs0","d0","ds0","e0","f0","fs0","g0","gs0","a0","as0","b0","c1","cs1","d1","ds1","e1","f1","fs1","g1","gs1","a1","as1","b1","c2","cs2","d2","ds2","e2","f2","fs2","g2","gs2","a2","as2","b2","c3","cs3","d3","ds3","e3","f3","fs3","g3","gs3","a3","as3","b3","c4","cs4","d4","ds4","e4","f4","fs4","g4","gs4","a4","as4","b4","c5","cs5","d5","ds5","e5","f5","fs5","g5","gs5","a5","as5","b5","c6","cs6","d6","ds6","e6","f6","fs6","g6","gs6","a6","as6","b6","c7"]; + var MIDI_TRANSPOSE = -12; + var note_number = MIDI_KEY_NAMES.indexOf(note_name); + if (note_number == -1) return; + note_number = note_number + 9 - MIDI_TRANSPOSE; + return note_number; + } + + + var midiFile = new Midi.File(); + var startTime = Date.now(); + var players = {}; + function addPlayer(participant) { + players[participant._id] = { + track: midiFile.addTrack(), + lastNoteTime: startTime, + keys: {} + } + var track_name = `${participant.name} ${participant._id}`; + players[participant._id].track.addEvent(new Midi.MetaEvent({type: Midi.MetaEvent.TRACK_NAME, data: track_name })); + } + + function addNote(note_name, vel, participant, isStopNote) { + if (!players.hasOwnProperty([participant._id])) addPlayer(participant); + var player = players[participant._id]; + if (!player.keys.hasOwnProperty(note_name)) player.keys[note_name] = {}; + var playerkey = player.keys[note_name]; + + var note_number = key2number(note_name); + if (!note_number) return; + + var time_ms = Date.now() - player.lastNoteTime; + var time_ticks = time_ms * 0.256; // 0.256 ticks per millisecond, based on 128 ticks per beat and 120 beats per minute + var midiVel = vel * 127; + + //player.track[isStopNote ? 'addNoteOff' : 'addNoteOn'](0, note_number, time_ticks, midiVel); // easy way + // but we need to maintain proper on/off order and limit note lengths for it to (dis)play properly in all midi players etc + if (isStopNote) { + if (!playerkey.isPressed) return; + player.track.addNoteOff(0, note_number, time_ticks, midiVel); + playerkey.isPressed = false; + clearTimeout(playerkey.timeout); + } else { + if (playerkey.isPressed) { + player.track.addNoteOff(0, note_number, time_ticks, midiVel); + player.track.addNoteOn(0, note_number, 0, midiVel); + clearTimeout(playerkey.timeout); + } else { + player.track.addNoteOn(0, note_number, time_ticks, midiVel); + } + playerkey.isPressed = true; + playerkey.timeout = setTimeout(addNote, maxNoteLength, note_name, vel, participant, true); + } + + player.lastNoteTime = Date.now(); + } + + + function save(){ + var file = midiFile.toBytes(); + midiFile = new Midi.File(); + players = {}; + startTime = Date.now(); + return file; + } + + + + client.on("n", function (msg) { + var DEFAULT_VELOCITY = 0.5; + var TIMING_TARGET = 1000; + var t = msg.t - client.serverTimeOffset + TIMING_TARGET - Date.now(); + var participant = client.findParticipantById(msg.p); + msg.n.forEach(note => { + var ms = t + (note.d || 0); + if (ms < 0) ms = 0; else if (ms > 10000) return; + if (note.s) { + setTimeout(addNote, ms, note.n, undefined, participant, true); + } else { + var vel = parseFloat(note.v) || DEFAULT_VELOCITY; + if (vel < 0) vel = 0; else if (vel > 1) vel = 1; + setTimeout(addNote, ms, note.n, vel, participant, false); + } + }); + }); + + + return {client, save, startTime}; +} \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100755 index 0000000..d33b029 --- /dev/null +++ b/src/main.js @@ -0,0 +1,57 @@ +global.exitHook = require('async-exit-hook'); +global.Discord = require('discord.js'); +global.fs = require('fs'); +global.config = require('./config.json'); +global.dClient = new Discord.Client({ disableEveryone: true }); + +console._log = console.log; +console.log = function(){ + console._log.apply(console, arguments); + log2discord(arguments); +} +console._error = console.error; +console.error = function(){ + console._error.apply(console, arguments); + log2discord(arguments); +} +console.warn = console.error; +console.info = console.log; + +var webhook = new Discord.WebhookClient('405445543536623627', config.webhooks.console); +function log2discord(str){ + str = Array.from(str); + str = str.map(require('util').inspect); + str = str.join(' '); + webhook.send(str, {split:{char:''}}); +} + +process.on('unhandledRejection', (reason, promise) => { + console.error(promise); +}); +process.on('uncaughtException', error => { + console.error(error.stack); +}); + + +(require('mongodb').MongoClient).connect(process.env.MONGODB_URI).then(client=>{ + global.mdbClient = client; + dClient.login(config.testmode ? config.test_token : config.token); +}); + +dClient.once('ready', () => { + console.log('Discord Client Ready'); + + require('./commands.js'); + require('./colorroles.js'); + require('./mppbridger.js'); + require('./owopbridge.js'); + //require('./awakensbridge.js'); + require('./screenshotter.js'); + require('./misc.js'); + + // backup + dClient.channels.get('394962139644690432').send(new Discord.MessageAttachment(global['files.zip'], 'files.zip')); + delete global['files.zip']; +}); +dClient.on('error', console.error); +dClient.on('warn', console.warn); \ No newline at end of file diff --git a/src/misc.js b/src/misc.js new file mode 100644 index 0000000..694b9dd --- /dev/null +++ b/src/misc.js @@ -0,0 +1,28 @@ + + +// join/leave +(function(){ + var webhook = new Discord.WebhookClient('404736784354770958', config.webhooks.welcome); + dClient.on('guildMemberAdd', member => { + webhook.send(`${member} joined.`, {username: member.user.username, avatarURL: member.user.displayAvatarURL(), disableEveryone:true}); + }); + dClient.on('guildMemberRemove', member => { + webhook.send(`${member.user.tag} left.`, {username: member.user.username, avatarURL: member.user.displayAvatarURL(), disableEveryone:true}); + }); +})(); + + +// view deleted channels +(function(){ + var vcid = '425060452129701889'; + var rid = '425060792455397376'; + dClient.on('voiceStateUpdate', (oldMember, newMember) => { + if (oldMember.voiceChannelID != vcid && newMember.voiceChannelID == vcid) { + // member joined the channel + newMember.addRole(newMember.guild.roles.get(rid)); + } else if (oldMember.voiceChannelID == vcid && newMember.voiceChannelID != vcid) { + // member left the channel + newMember.removeRole(newMember.guild.roles.get(rid)); + } + }); +})(); \ No newline at end of file diff --git a/src/mppbridger.js b/src/mppbridger.js new file mode 100644 index 0000000..b7dc228 --- /dev/null +++ b/src/mppbridger.js @@ -0,0 +1,473 @@ +var Client = require('./lib/Client.js'); +global.clients = {}; + + + + + + + + +/*function reconnectClients() { + for (let site in clients) { + site = clients[site]; + let i = 0; + for (let client in site) { + client = site[client]; + if (client.reconnectTimeout) clearTimeout(client.reconnectTimeout); + client.reconnectTimeout = setTimeout(()=>{ + client.connect(); + client.reconnectTimeout = undefined; + }, i += 2000); + } + } +} //TODO BETTAH*/ + + +global.clientConnector = { + queue: [], + enqueue: function(client) { + if (this.queue.includes(client)) return; + this.queue.push(client); + }, + interval: setInterval(function(){ + var client = clientConnector.queue.shift(); + if (client) client.connect(); + }, 2000) +} + + + +global.createMPPbridge = function (room, DiscordChannelID, site = 'MPP', webhookID, webhookToken) { + var DiscordChannel = dClient.channels.get(DiscordChannelID); + if (!DiscordChannel) return console.error(`Couldn't bridge ${site} ${room} because Discord Channel ${DiscordChannelID} is missing!`); + if (webhookID && webhookToken) var webhook = new Discord.WebhookClient(webhookID, webhookToken, {disableEveryone:true}); + + var msgBuffer = []; + function _dSend(msg, embed) { + if (webhook && !config.testmode) { + let username = gClient.channel && gClient.channel._id || room; + if (username.length > 32) username = username.substr(0,31) + '…'; + else if (username.length < 2) username = undefined; + webhook.send(msg, {username, embed, split:{char:''}}).catch(e => { + console.error(e); + DiscordChannel.send(msg, {embed, split:{char:''}}).catch(console.error); + }); + } + else DiscordChannel.send(msg, {embed, split:{char:''}}).catch(console.error); + } + function dSend(msg) { + msgBuffer.push(msg); + } + setInterval(()=>{ + if (msgBuffer.length == 0) return; + _dSend(msgBuffer.join('\n')); + msgBuffer = []; + }, 2000); //TODO make changeable + + const gClient = site == "MPP" ? new Client("ws://www.multiplayerpiano.com:443") : site == "WOPP" ? new Client("ws://ourworldofpixels.com:1234", true) : site == "MPT" ? new Client("ws://23.95.115.204:8080", true) : site == "VFDP" ? new Client("ws://www.visualfiredev.com:8080") : undefined; + if (!gClient) return console.error(`Invalid site ${site}`); + gClient.setChannel(/*(site == "MPP" && room == "lobby") ? "lolwutsecretlobbybackdoor" : */room); + gClient.canConnect = true; + clientConnector.enqueue(gClient); + + + + var isConnected = false; + gClient.on('connect', () => { + console.log(`Connected to room ${room} of ${site} server`); + dSend(`**Connected**`); // TODO say what room it actually connected to ? + gClient.sendArray([{m: "userset", set: {name: config.mppname}}]) + isConnected = true; + }); + gClient.on('disconnect', () => { + if (isConnected) { + console.log(`Disconnected from room ${room} of ${site} server`); + dSend(`**Disconnected**`); + isConnected = false; + } + clientConnector.enqueue(gClient); + }); + /*gClient.on('status', status => { + console.log(`[${site}] [${room}] ${status}`); + });*/ + + let lastCh = room; + gClient.on('ch', msg => { + if (lastCh && msg.ch._id !== lastCh) { + dSend(`**Channel changed from \`${lastCh}\` to \`${msg.ch._id}\`**`); + console.log(`[${site}][${room}] Channel changed from ${lastCh} to ${msg.ch._id}`); + lastCh = msg.ch._id; + } + (async function(){ + // catch dropped crown + if (msg.ch.crown && !msg.ch.crown.hasOwnProperty('participantId')) { + gClient.sendArray([{m:'chown', id: gClient.getOwnParticipant().id}]); // if possible + var avail_time = msg.ch.crown.time + 15000 - gClient.serverTimeOffset; + var ms = avail_time - Date.now(); + setTimeout(()=> gClient.sendArray([{m:'chown', id: gClient.getOwnParticipant().id}]) , ms); + } + // transfer crown to owner + if (msg.ppl && msg.ch.crown && msg.ch.crown.participantId == gClient.getOwnParticipant().id) { + var res = await dbClient.query("SELECT owner_mpp__id FROM bridges WHERE mpp_room = $1 AND site = $2;", [room, site]); + if (res.rows.length == 0) return; + var owner = res.rows[0].owner_mpp__id; + if (!owner) return; + msg.ppl.some(part => { + if (part._id == owner) { + gClient.sendArray([{m:'chown', id: part.id}]); + return true; + } else return false; + }); + } + })(); + }); + + // MPP to Discord + gClient.on('a', msg => { + if (msg.p._id == gClient.getOwnParticipant()._id) return; + var id = msg.p._id.substr(0,6); + var name = msg.p.name.replace(/discord.gg\//g, 'discord.gg\\/'); + var str = `\`${id}\` **${name}:** ${msg.a}`; + str = str.replace(/<@/g, "<\\@"); + dSend(str); + }); + + // Discord to MPP + dClient.on('message', message => { + if (message.channel.id !== DiscordChannelID || message.author.bot || message.content.startsWith('!')) return; + var str = message.cleanContent; + var arr = []; + if (str.startsWith('/') || str.startsWith('\\')) { + arr.push({m:"a", message: + `⤹ ${message.member.displayName}` + }); + } else str = message.member.displayName + ': ' + str; + if (str.startsWith('\\')) str = str.slice(1); + if (message.attachments.first()) str += ' '+message.attachments.first().url; + if (str.length > 512) str = str.substr(0,511) + '…'; + arr.push({m:"a", message:str}); + gClient.sendArray(arr); + }); + + /*// announce join/leave + gClient.on('participant added', participant => { + dSend(`**\`${participant._id.substr(0,4)}\` ${participant.name} entered the room.**`); + }); + gClient.on('participant removed', participant => { + dSend(`**\`${participant._id.substr(0,4)}\` ${participant.name} left the room.**`); + });*/ // too spammy + + + /*// autoban banned participants + gClient.on('participant added', part => { + if (PS.banned_ppl.hasOwnProperty(part._id) && gClient.isOwner()) + gClient.sendArray([{m: "kickban", _id: part._id, ms: 60*60*1000}]); + });*/ + + + gClient.on('notification', async msg => { + + // show notification + _dSend(undefined, { + title: msg.title, + description: msg.text || msg.html + }); + + // ban handling + if (msg.text && (msg.text.startsWith('Banned from') || msg.text.startsWith('Currently banned from'))) { + // Banned from "{room}" for {n} minutes. + // Currently banned from "{room}" for {n} minutes. + let arr = msg.text.split(' '); + arr.pop(); + let minutes = arr.pop(); + + gClient.stop(); + setTimeout(()=>{ + gClient.setChannel(room); + gClient.start(); + }, minutes*60*1000+3000); + dSend(`**Attempting to rejoin in ${minutes} minutes.**`); + } + }); + + + // addons + gClient.on('participant update', function(participant){ + nameCollector.collect(participant); + }); + //if (site == 'MPP' && room == 'lobby') MPPrecorder.start(gClient);//TODO too much memory + + // collect data + (async function(){ + var filename = `${site} ${room} .txt`.replace(/\//g, ':'); + var size = 0; + var startDate = new Date(); + gClient.on('ws created', function(){ + gClient.ws.addEventListener('message', msg => { + var data = msg.data; + if (data instanceof ArrayBuffer) data = Buffer.from(data).toString('base64'); + var line = `${Date.now()} ${data}\n`; + size += line.length; + fs.appendFile(filename, line, ()=>{}); + if (size > 8000000) {save(); size = 0;} + }); + }); + async function save(callback){ + console.log(`saving data recording`, filename) + fs.readFile(filename, (err, file) => { + if (err) return console.error(err); + require('zlib').gzip(file, async function(err, gzip){ + if (err) return console.error(err); + var attachmentName = `${site} ${room} raw data recording from ${startDate.toISOString()} to ${new Date().toISOString()} .txt.gz`; + await DiscordChannel.send(new Discord.MessageAttachment(gzip, attachmentName)); + fs.writeFileSync(filename, ''); + size = 0; + startDate = new Date(); + console.log(`saved raw data recording`, attachmentName); + if (callback) callback(); + }); + }); + } + exitHook(callback => { + save(()=>callback()); + }); + gClient.dataCollectorSave = function(){save()}; // test + })(); + + if (!clients[site]) clients[site] = {}; + clients[site][room] = gClient; + + + + /*// EXPERIMENTAL + gClient.on('ls', msg => { + msg.u.forEach(async r => { + if (clients[site][r._id]) return; + for (let client of clientConnector.queue) {if (client.desiredChannelId == r._id) return} // ugg + var discordChannelName = r._id.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(); + var categoryID = '409079939501916160'; + var channel = await dClient.guilds.get(config.guildID).createChannel(discordChannelName, {parent: categoryID}); + channel.setTopic(`Bridged to http://www.multiplayerpiano.com/${encodeURIComponent(r._id)}`); + var webhook = await channel.createWebhook('Webhook'); + createMPPbridge(r._id, channel.id, site, webhook.id, webhook.token); + }) + })*/ +} + + + + + + + + + + + +global.nameCollector = { + collection: mdbClient.db('heroku_jrtfvpd9').collection('ppl'), + collect: async function (participant) { + if (config.testmode) return; + if (participant.name == "Anonymous" || participant.name == "Anonymoose") return; + + var newMsg = function(continued){ + var str = `__**${participant._id}**__${continued ? ' (continued)' : ''}\n${participant.name}`; + return dClient.channels.get('379738469511069698').send(str); + } + + var document = await this.collection.findOne({_id: participant._id}); + + if (document) { + // update person + if (document.names.includes(participant.name)) return; + document.names.push(participant.name); + this.collection.updateOne({_id: participant._id}, {$set:{names: document.names}}); + + let message = await dClient.channels.get('379738469511069698').messages.fetch(document.discord_msg_id); + try { + await message.edit(message.content + ', ' + participant.name); + } catch(e) { + let message = await newMsg(true); + this.collection.updateOne({_id: participant._id}, {$set:{discord_msg_id: message.id}}); + } + } else { + // add new person + let message = await newMsg(); + nameCollector.collection.insertOne({ + _id: participant._id, + discord_msg_id: message.id, + names: [participant.name] + }); + } + } +}; + + + +global.MPPrecorder = { + start: function(client) { + var recorder = (require('./lib/mpprecorder-module.js'))(undefined, client); + this.save = async function () { + var startDate = new Date(recorder.startTime), endDate = new Date(); + var filename = `www.multiplayerpiano.com lobby recording from ${startDate.toISOString()} to ${endDate.toISOString()} .mid.gz`; + var file = recorder.save(); + file = Buffer.from(file, 'binary'); + file = require('zlib').gzipSync(file); + var attachment = new Discord.MessageAttachment(file, filename); + await dClient.channels.get('394967426133000193').send(attachment); + } + this.interval = setInterval(() => { + this.save(); + }, 10*60*1000); + exitHook(callback => { + this.save().then(callback); + }); + } +}; + + + + + + + +// start +(async function () { + var res = await dbClient.query('SELECT * FROM bridges;'); + + var sites = {}; + res.rows.forEach(row => { + if (row.disabled) return; + if (!sites[row.site]) sites[row.site] = []; + sites[row.site].push(row); + }); + + for (let site in sites) { + let arr = sites[site]; + arr.sort((a, b) => {return a.position - b.position}); + let i = 0; + arr.forEach(bridge => { + setTimeout(function(){ + createMPPbridge(bridge.mpp_room, bridge.discord_channel_id, bridge.site, bridge.webhook_id, bridge.webhook_token, bridge.owner_mpp__id); + }, i); + i = i + 2000; + }); + } +})(); + + + + + + + +// commands + +commands.bridge = { + usage: "<MPP room>", + description: "Creates a bridge to the specified MPP room.", + exec: async function (msg) { + var site = 'MPP'; + var room = msg.txt(1); + if (!room) return false; + var existingBridge = (await dbClient.query('SELECT * FROM bridges WHERE mpp_room = $1;', [room])).rows[0]; + if (existingBridge && !existingBridge.disabled) return msg.reply(`${site} room ${room} is already bridged.`); + else if ((existingBridge && existingBridge.disabled) || config.disabledRooms.includes(room)) return msg.reply(`You cannot bridge this room.`); + var discordChannelName = room.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase(); + var categoryID = '360557444952227851'; + var channel = await dClient.guilds.get(config.guildID).createChannel(discordChannelName, {parent: categoryID}); + channel.setTopic(`Bridged to http://www.multiplayerpiano.com/${encodeURIComponent(room)}`); + var webhook = await channel.createWebhook('Webhook'); + createMPPbridge(room, channel.id, site, webhook.id, webhook.token); + dbClient.query('INSERT INTO bridges (site, mpp_room, discord_channel_id, webhook_id, webhook_token, owner_discord_user_id) VALUES ($1, $2, $3, $4, $5, $6)', [ + site, room, channel.id, webhook.id, webhook.token, msg.author.id, + ]); + msg.reply(`${site} room ${room} is now bridged to ${channel}.`); + } +}; + +commands.unbridge = { + usage: "[MPP Room]", + description: "Deletes a bridge to the specified MPP room.", + exec: async function (msg) { + var bridge = (await dbClient.query("SELECT * FROM bridges WHERE mpp_room = $1 OR discord_channel_id = $2", [msg.txt(1), msg.channel.id])).rows[0]; + if (!bridge) { + //msg.react('⚠️'); + msg.reply(`That room is not bridged. Make sure you type the MPP room name correctly.`); + return; + } + if (bridge.disabled) { + msg.reply(`That room has already been unbridged.`); + return; + } + if (!(bridge.owner_discord_user_id == msg.author.id || msg.author.id == config.opID)) { + //msg.react('🚫'); + msg.reply(`You do not own that bridge.`); + return; + } + clients.MPP[bridge.mpp_room].stop(); + var channel = dClient.channels.get(bridge.discord_channel_id) + await channel.setParent('425054341725159424'); + await channel.lockPermissions(); + await dbClient.query("UPDATE bridges SET disabled = 'true' WHERE mpp_room = $1", [bridge.mpp_room]); + msg.reply(`${bridge.mpp_room} has been unbridged.`); + } +} + +commands.chown = { + usage: "<'mpp'/'discord'> <Discord User ID or mention, or MPP _id>", + description: "Changes the MPP or Discord owner of a private bridge. The first argument must be either `mpp` or `discord`.", + aliases: ['changeowner', 'setowner'], + exec: async function (msg) { + if (msg.args.length < 3 || !['mpp','discord'].includes(msg.args[1])) return false; + var res = await dbClient.query('SELECT * FROM bridges WHERE discord_channel_id = $1;', [msg.channel.id]); + if (res.rows.length == 0) return msg.react('🚫'); + var bridge = res.rows[0]; + if (!(bridge.owner_discord_user_id == msg.author.id || msg.author.id == config.opID)) return msg.react('🚫'); + + if (msg.args[1] == 'discord') { + let selectedUser = dClient.users.get(msg.args[2]) || msg.mentions.users.first(); + if (!selectedUser) return msg.react('⚠️'); + msg.channel.overwritePermissions(selectedUser, { + MANAGE_CHANNELS: true, + MANAGE_ROLES: true, + MANAGE_WEBHOOKS: true, + MANAGE_MESSAGES: true + }); + let po = msg.channel.permissionOverwrites.find('id', msg.author.id); + if (po) po.delete(); + await dbClient.query('UPDATE bridges SET owner_discord_user_id = $1 WHERE discord_channel_id = $2;', [selectedUser.id, msg.channel.id]); + msg.channel.send(`Ownership of ${msg.channel} has been transferred to ${selectedUser}`); + } else if (msg.args[1] == 'mpp') { + let _id = msg.args[2]; + await dbClient.query('UPDATE bridges SET owner_mpp__id = $1 WHERE discord_channel_id = $2;', [_id, msg.channel.id]); + msg.channel.send(`MPP user \`${_id}\` has been assigned as owner of the MPP room, and the crown will be transferred to them whenever possible.`); + //todo give crown if owner there + } + } +}; + +commands.list = { + description: "Lists online participants", + aliases: ['ppl', 'online'], + exec: async function (message) { + var row = (await dbClient.query("SELECT mpp_room, site FROM bridges WHERE discord_channel_id = $1;", [message.channel.id])).rows[0]; + if (!row) { + //message.react('🚫'); + message.reply(`Use this in a bridged room to see who is at the other side.`); + return; + } + var ppl = clients[row.site][row.mpp_room].ppl; + + var numberOfPpl = Object.keys(ppl).length; + var str = `__**Participants Online (${numberOfPpl})**__\n`; + var names = []; + for (let person in ppl) { + person = ppl[person]; + names.push(`\`${person._id.substr(0,4)}\` ${person.name}`); + } + str += names.join(', '); + message.channel.send(str, {split:{char:''}}); + } +}; \ No newline at end of file diff --git a/src/owopbridge.js b/src/owopbridge.js new file mode 100644 index 0000000..37e7765 --- /dev/null +++ b/src/owopbridge.js @@ -0,0 +1,108 @@ +var striptags = require('striptags'); +function createOWOPbridge(dClient, channelID, webhookID, webhookToken, OWOPworld = 'main', OWOPnick = '[Discord]'){ + var webhook = new Discord.WebhookClient(webhookID, webhookToken, {disableEveryone:true}); + var WebSocket = require('ws'); + var socket; + var canConnect = true; + function connect() { + if (!canConnect) return; + var myId; + socket = new WebSocket("ws://ourworldofpixels.com:443/"); + socket.binaryType = "arraybuffer"; + + var pingInterval; + socket.addEventListener('open', () => { + console.log('[OWOP] ws open'); + joinWorld(OWOPworld); + sendMessage('/nick '+OWOPnick); + pingInterval = setInterval(sendCursorActivity, 1000*60*5); + webhook.send('**Connected**'); + }); + socket.addEventListener('close', () => { + console.log('[OWOP] ws close'); + clearInterval(pingInterval); + setTimeout(connect, 10000); + webhook.send('**Disconnected**'); + }); + socket.addEventListener('error', console.error); + socket.addEventListener('message', msg => { + if (!myId) myId = extractId(msg.data); + if (typeof msg.data != "string") return; + if (myId && (msg.data.startsWith(`[${myId}]`) || msg.data.startsWith(myId))) return; + webhook.send(striptags(msg.data)); + }); + } connect(); + + dClient.on('message', message => { + if (message.channel.id != channelID) return; + var str = `${message.member.displayName}: ${message.cleanContent}`; + if (message.attachments.first()) str += ' ' + message.attachments.first().url; + if (str.length > 128) str = str.substr(0,127) + '…'; + sendMessage(str); + }); + + + + function joinWorld(name) { + var nstr = stoi(name, 24); + var array = new ArrayBuffer(nstr[0].length + 2); + var dv = new DataView(array); + for (var i = nstr[0].length; i--;) { + dv.setUint8(i, nstr[0][i]); + } + dv.setUint16(nstr[0].length, 1337, true); + socket.send(array); + return nstr[1]; + } + function stoi(string, max) { + var ints = []; + var fstring = ""; + string = string.toLowerCase(); + for (var i = 0; i < string.length && i < max; i++) { + var charCode = string.charCodeAt(i); + if (charCode < 123 && charCode > 96 || charCode < 58 && charCode > 47 || charCode == 95 || charCode == 46) { + fstring += String.fromCharCode(charCode); + ints.push(charCode); + } + } + return [ints, fstring]; + } + + function sendMessage(str) { + if (socket && socket.readyState == WebSocket.OPEN) + socket.send(str + String.fromCharCode(10)); + } + + function sendCursorActivity() { // thx kit + var arb = new ArrayBuffer(12); + var dv = new DataView(arb); + dv.setInt32(0, 0, true); // x + dv.setInt32(4, 0, true); // y + dv.setUint8(8, 0); // r + dv.setUint8(9, 0); // g + dv.setUint8(10, 0); // b + dv.setUint8(11, "cursor"); // tool + socket.send(arb); + } + + function extractId(arb) { + var dv = new DataView(arb); + var type = dv.getUint8(0); + if (type != 0) return null; + var _id = dv.getUint32(1, true); + webhook.send(`**ID is \`${_id}\`**`); + return _id; + } + + return { + socket, + start: function(){canConnect = true; connect();}, + stop: function(){canConnect = false; socket.close();} + } +} +global.createOWOPbridge = createOWOPbridge; + +//global.OWOPbridge = createOWOPbridge(dClient, '398613291817238548', '398613329439883275', config.webhooks.owop); + + + diff --git a/src/screenshotter.js b/src/screenshotter.js new file mode 100644 index 0000000..a05db7d --- /dev/null +++ b/src/screenshotter.js @@ -0,0 +1,25 @@ +global.screenshotter = { + capture: async function () { + console.log('Starting screen captures'); + var puppeteer = require('puppeteer'); + var browser = await puppeteer.launch({args:['--no-sandbox']}); + var page = await browser.newPage(); + await page.setViewport({width: 1440, height: 900}); + await page.goto('http://www.multiplayerpiano.com/lobby'); + await new Promise(resolve => setTimeout(resolve, 5000)); + var screenshot = await page.screenshot({type: 'png'}); + var filename = `Screenshot of www.multiplayerpiano.com/lobby @ ${new Date().toISOString()}.png`; + var attachment = new Discord.MessageAttachment(screenshot, filename); + await dClient.channels.get('383773548810076163').send(attachment); + await page.goto('http://ourworldofpixels.com'); + await page.evaluate(function(){OWOP.camera.zoom = 1;}); + await new Promise(resolve => setTimeout(resolve, 5000)); + var screenshot = await page.screenshot({type: 'png'}); + var filename = `Screenshot of ourworldofpixels.com/main @ ${new Date().toISOString()}.png`; + var attachment = new Discord.MessageAttachment(screenshot, filename); + await dClient.channels.get('399079481161023492').send(attachment); + await browser.close(); + console.log('Finished screen captures'); + }, + interval: setInterval(()=>{screenshotter.capture();}, 1000*60*60) +}; \ No newline at end of file