This commit is contained in:
ledlamp 2018-05-11 22:19:31 -07:00
commit 79227ee843
16 changed files with 2288 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/local/
/src/config.json

2
.slugignore Normal file
View File

@ -0,0 +1,2 @@
/src/
/local/

1
Procfile Normal file
View File

@ -0,0 +1 @@
worker: node index.js

16
index.js Normal file
View File

@ -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);});

13
package.json Normal file
View File

@ -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"
}
}

203
src/awakensbridge.js Normal file
View File

@ -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(/</gi, '&lt;');
//str = str.replace(/"/gi, '&quot;');
//str = str.replace(/#/gi, '&#35;');
//str = str.replace(/\\n/g, '<br>');
//str = str.replace(/\$/gi, '&#36;');
//str = str.replace(/'/gi, '&#39;');
//str = str.replace(/~/gi, '&#126;');
//convert spaces
//str = str.replace(/\s{2}/gi, ' &nbsp;');
//str = str.replace(/(<br>)(.+)/g, '<div style="display:block;padding-left:3.5em;">$2</div>');
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(/(\$|(&#36;))([\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, /(\$|(&#36;))([\w \-\,®]*)\|(.*)$/, '$4');
str = multiple(str, /(\£|(£))([\w \-\,®]*)\|(.*)$/, '$4');
// colors
str = multiple(str, /&#35;&#35;&#35;([\da-f]{6}|[\da-f]{3})(.+)$/i, '$2');
str = multiple(str, /&#35;&#35;([\da-f]{6}|[\da-f]{3})(.+)$/i, '$2');
str = multiple(str, /&#35;([\da-f]{6}|[\da-f]{3})(.+)$/i, '$2');
str = multiple(str, RegExp('&#35;&#35;&#35;(' + coloreg + ')(.+)$', 'i'), '$2');
str = multiple(str, RegExp('&#35;&#35;(' + coloreg + ')(.+)$', 'i'), '$2');
str = multiple(str, RegExp('&#35;(' + 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, /\/\&#126;([^\|]+)\|?/g, '$1');
str = multiple(str, /\/\&#35;([^\|]+)\|?/g, '$1');
str = multiple(str, /\/\+([^\|]+)\|?/g, '$1');
str = multiple(str, /\/\!([^\|]+)\|?/g, '$1');
str = multiple(str, /\/\&#36;([^\|]+)\|?/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;
}

63
src/colorroles.js Normal file
View File

@ -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: "<ColorResolvable>",
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: "<title>",
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("🆗");
}
}

180
src/commands.js Normal file
View File

@ -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);
}
)
});
});

333
src/lib/Client.js Executable file
View File

@ -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});
}
}
};

684
src/lib/jsmidgen.js Normal file
View File

@ -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;
}

View File

@ -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};
}

57
src/main.js Executable file
View File

@ -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);

28
src/misc.js Normal file
View File

@ -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));
}
});
})();

473
src/mppbridger.js Normal file
View File

@ -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:''}});
}
};

108
src/owopbridge.js Normal file
View File

@ -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);

25
src/screenshotter.js Normal file
View File

@ -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)
};