initial commit.

This commit is contained in:
mntmn
2017-04-07 01:29:05 +02:00
commit 7ff2926578
258 changed files with 83743 additions and 0 deletions

View File

@@ -0,0 +1,615 @@
'use strict';
const exec = require('child_process');
const gm = require('gm');
const async = require('async');
const fs = require('fs');
const Models = require('../models/schema');
const uploader = require('../helpers/uploader');
const path = require('path');
const fileExtensionMap = {
".amr" : "audio/AMR",
".ogg" : "audio/ogg",
".aac" : "audio/aac",
".mp3" : "audio/mpeg",
".mpg" : "video/mpeg",
".3ga" : "audio/3ga",
".mp4" : "video/mp4",
".wav" : "audio/wav",
".mov" : "video/quicktime",
".doc" : "application/msword",
".dot" : "application/msword",
".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".dotx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
".docm" : "application/vnd.ms-word.document.macroEnabled.12",
".dotm" : "application/vnd.ms-word.template.macroEnabled.12",
".xls" : "application/vnd.ms-excel",
".xlt" : "application/vnd.ms-excel",
".xla" : "application/vnd.ms-excel",
".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xltx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
".xlsm" : "application/vnd.ms-excel.sheet.macroEnabled.12",
".xltm" : "application/vnd.ms-excel.template.macroEnabled.12",
".xlam" : "application/vnd.ms-excel.addin.macroEnabled.12",
".xlsb" : "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
".ppt" : "application/vnd.ms-powerpoint",
".pot" : "application/vnd.ms-powerpoint",
".pps" : "application/vnd.ms-powerpoint",
".ppa" : "application/vnd.ms-powerpoint",
".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".potx" : "application/vnd.openxmlformats-officedocument.presentationml.template",
".ppsx" : "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
".ppam" : "application/vnd.ms-powerpoint.addin.macroEnabled.12",
".pptm" : "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
".potm" : "application/vnd.ms-powerpoint.template.macroEnabled.12",
".ppsm" : "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
".key" : "application/x-iwork-keynote-sffkey",
".pages" : "application/x-iwork-pages-sffpages",
".numbers" : "application/x-iwork-numbers-sffnumbers",
".ttf" : "application/x-font-ttf"
};
const convertableImageTypes = [
"image/png",
"image/jpeg",
"application/pdf",
"image/jpg",
"image/gif",
"image/tiff",
"image/vnd.adobe.photoshop"];
const convertableVideoTypes = [
"video/quicktime",
"video/3gpp",
"video/mpeg",
"video/mp4",
"video/ogg"];
const convertableAudioTypes = [
"application/ogg",
"audio/AMR",
"audio/3ga",
"audio/wav",
"audio/3gpp",
"audio/x-wav",
"audio/aiff",
"audio/x-aiff",
"audio/ogg",
"audio/mp4",
"audio/x-m4a",
"audio/mpeg",
"audio/mp3",
"audio/x-hx-aac-adts",
"audio/aac"];
function getDuration(localFilePath, callback){
exec.execFile("ffprobe", ["-show_format", "-of", "json", localFilePath], function(error, stdout, stderr) {
var test = JSON.parse(stdout);
callback(parseFloat(test.format.duration));
});
}
function createWaveform(fileName, localFilePath, callback){
var filePathImage = localFilePath + "-" + (new Date().getTime()) + ".png";
getDuration(localFilePath, function(duration){
var totalTime = duration || 1.0;
var pixelsPerSecond = 256.0;
do {
var targetWidth = parseInt(pixelsPerSecond*totalTime, 10);
if (targetWidth>2048) pixelsPerSecond/=2.0;
} while (targetWidth>2048 && pixelsPerSecond>1);
exec.execFile("audiowaveform",
[
"-w",
""+targetWidth,
"--pixels-per-second",
""+parseInt(pixelsPerSecond),
"--background-color", "ffffff00",
"--border-color", "ffffff",
"--waveform-color", "3498db",
"--no-axis-labels",
"-i", localFilePath, "-o", filePathImage
],
{}, function(error, stdout, stderr) {
if(!error) {
callback(null, filePathImage);
} else {
console.log("error:", stdout, stderr);
callback(error, null);
}
});
});
}
function convertVideo(fileName, filePath, codec, callback, progress_callback) {
var ext = path.extname(fileName);
var presetMime = fileExtensionMap[ext];
var newExt = codec == "mp4" ? "mp4" : "ogv";
var convertedPath = filePath + "." + newExt;
console.log("converting", filePath, "to", convertedPath, "progress_cb:",progress_callback);
var convertArgs = (codec == "mp4") ? [
"-i", filePath,
"-threads", "4",
"-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value
"-b:v", "2000k",
"-acodec", "libvo_aacenc",
"-b:a", "96k",
"-vcodec", "libx264",
"-y", convertedPath ]
: [
"-i", filePath,
"-threads", "4",
"-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value
"-b:v", "2000k",
"-acodec", "libvorbis",
"-b:a", "96k",
"-vcodec", "libtheora",
"-y", convertedPath];
var ff = exec.spawn('ffmpeg', convertArgs, {
stdio: [
'pipe', // use parents stdin for child
'pipe', // pipe child's stdout to parent
'pipe'
]
});
ff.stdout.on('data', function (data) {
console.log('[ffmpeg-video] stdout: ' + data);
});
ff.stderr.on('data', function (data) {
console.log('[ffmpeg-video] stderr: ' + data);
if (progress_callback) {
progress_callback(data);
}
});
ff.on('close', function (code) {
console.log('[ffmpeg-video] child process exited with code ' + code);
if (!code) {
console.log("converted", filePath, "to", convertedPath);
callback(null, convertedPath);
} else {
callback(code, null);
}
});
}
function convertAudio(fileName, filePath, codec, callback) {
var ext = path.extname(fileName);
var presetMime = fileExtensionMap[ext];
var newExt = codec == "mp3" ? "mp3" : "ogg";
var convertedPath = filePath + "." + newExt;
console.log("converting audio", filePath, "to", convertedPath);
var convertArgs = (ext == ".aac") ? [ "-i", filePath, "-y", convertedPath ]
: [ "-i", filePath,
"-b:a", "128k",
"-y", convertedPath];
exec.execFile("ffmpeg", convertArgs , {}, function(error, stdout, stderr) {
if(!error){
console.log("converted", filePath, "to", convertedPath);
callback(null, convertedPath);
}else{
console.log(error,stdout, stderr);
callback(error, null);
}
});
}
function createThumbnailForVideo(fileName, filePath, callback) {
var filePathImage = filePath + ".jpg";
exec.execFile("ffmpeg", ["-y", "-i", filePath, "-ss", "00:00:01.00", "-vcodec", "mjpeg", "-vframes", "1", "-f", "image2", filePathImage], {}, function(error, stdout, stderr){
if(!error){
callback(null, filePathImage);
}else{
console.log("error:", stdout, stderr);
callback(error, null);
}
});
}
function getMime(fileName, filePath, callback) {
var ext = path.extname(fileName);
var presetMime = fileExtensionMap[ext];
if (presetMime) {
callback(null, presetMime);
} else {
exec.execFile("file", ["-b","--mime-type", filePath], {}, function(error, stdout, stderr) {
console.log("file stdout: ",stdout);
if (stderr === '' && error == null) {
//filter special chars from commandline
var mime = stdout.replace(/[^a-zA-Z0-9\/\-]/g,'');
callback(null, mime);
} else {
console.log("getMime file error: ", error);
callback(error, null);
}
});
}
}
function resizeAndUpload(a, size, max, fileName, localFilePath, callback) {
if (max>320 || size.width > max || size.height > max) {
var resizedFileName = max + "_"+fileName;
var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + resizedFileName;
var localResizedFilePath = "/tmp/"+resizedFileName;
gm(localFilePath).resize(max, max).autoOrient().write(localResizedFilePath, function (err) {
if(!err) {
uploader.uploadFile(s3Key, "image/jpeg", localResizedFilePath, function(err, url) {
if (err) callback(err);
else{
console.log(localResizedFilePath);
fs.unlink(localResizedFilePath, function (err) {
if (err) {
console.error(err);
callback(null, url);
}
else callback(null, url);
});
}
});
} else {
console.error(err);
callback(err);
}
});
} else {
callback(null, "");
}
}
var resizeAndUploadImage = function(a, mime, size, fileName, fileNameOrg, imageFilePath, originalFilePath, payloadCallback) {
async.parallel({
small: function(callback){
resizeAndUpload(a, size, 320, fileName, imageFilePath, callback);
},
medium: function(callback){
resizeAndUpload(a, size, 800, fileName, imageFilePath, callback);
},
big: function(callback){
resizeAndUpload(a, size, 1920, fileName, imageFilePath, callback);
},
original: function(callback){
var s3Key = "s"+ a.space_id.toString() + "/a" + a._id + "/" + fileNameOrg;
uploader.uploadFile(s3Key, mime, originalFilePath, function(err, url){
callback(null, url);
});
}
}, function(err, results) {
a.state = "idle";
a.mime = mime;
var stats = fs.statSync(originalFilePath);
a.payload_size = stats["size"];
a.payload_thumbnail_web_uri = results.small;
a.payload_thumbnail_medium_uri = results.medium;
a.payload_thumbnail_big_uri = results.big;
a.payload_uri = results.original;
var factor = 320/size.width;
var newBoardSpecs = a.board;
newBoardSpecs.w = Math.round(size.width*factor);
newBoardSpecs.h = Math.round(size.height*factor);
a.board = newBoardSpecs;
a.updated_at = new Date();
a.save(function(err) {
if(err) payloadCallback(err, null);
else {
fs.unlink(originalFilePath, function (err) {
if (err){
console.error(err);
payloadCallback(err, null);
} else {
console.log('successfully deleted ' + originalFilePath);
payloadCallback(null, a);
}
});
}
});
});
};
module.exports = {
convert: function(a, fileName, localFilePath, payloadCallback, progress_callback) {
getMime(fileName, localFilePath, function(err, mime){
console.log("[convert] fn: "+fileName+" local: "+localFilePath+" mime:", mime);
if (!err) {
if (convertableImageTypes.indexOf(mime) > -1) {
gm(localFilePath).size(function (err, size) {
console.log("[convert] gm:", err, size);
if (!err) {
if(mime == "application/pdf") {
var firstImagePath = localFilePath + ".jpeg";
exec.execFile("gs", ["-sDEVICE=jpeg","-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-dFirstPage=1", "-dLastPage=1", "-sOutputFile=" + firstImagePath, "-r90", "-f", localFilePath], {}, function(error, stdout, stderr) {
if(error === null) {
resizeAndUploadImage(a, mime, size, fileName + ".jpeg", fileName, firstImagePath, localFilePath, function(err, a) {
fs.unlink(firstImagePath, function (err) {
payloadCallback(err, a);
});
});
} else {
payloadCallback(error, null);
}
});
} else if(mime == "image/gif") {
//gifs are buggy after convertion, so we should not convert them
var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
uploader.uploadFile(s3Key, "image/gif", localFilePath, function(err, url) {
if(err)callback(err);
else{
console.log(localFilePath);
var stats = fs.statSync(localFilePath);
a.state = "idle";
a.mime = mime;
a.payload_size = stats["size"];
a.payload_thumbnail_web_uri = url;
a.payload_thumbnail_medium_uri = url;
a.payload_thumbnail_big_uri = url;
a.payload_uri = url;
var factor = 320/size.width;
var newBoardSpecs = a.board;
newBoardSpecs.w = Math.round(size.width*factor);
newBoardSpecs.h = Math.round(size.height*factor);
a.board = newBoardSpecs;
a.updated_at = new Date();
a.save(function(err){
if(err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
if (err){
console.error(err);
payloadCallback(err, null);
} else {
console.log('successfully deleted ' + localFilePath);
payloadCallback(null, a);
}
});
}
});
}
});
} else {
resizeAndUploadImage(a, mime, size, fileName, fileName, localFilePath, localFilePath, payloadCallback);
}
} else payloadCallback(err);
});
} else if (convertableVideoTypes.indexOf(mime) > -1) {
async.parallel({
thumbnail: function(callback) {
createThumbnailForVideo(fileName, localFilePath, function(err, created){
console.log("thumbnail created: ", err, created);
if(err) callback(err);
else{
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".jpg" ;
uploader.uploadFile(keyName, "image/jpeg", created, function(err, url){
if (err) callback(err);
else callback(null, url);
});
}
});
},
ogg: function(callback) {
if (mime == "video/ogg") {
callback(null, "org");
} else {
convertVideo(fileName, localFilePath, "ogg", function(err, file) {
if(err) callback(err);
else {
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogv" ;
uploader.uploadFile(keyName, "video/ogg", file, function(err, url){
if (err) callback(err);
else callback(null, url);
});
}
}, progress_callback);
}
},
mp4: function(callback) {
if (mime == "video/mp4") {
callback(null, "org");
} else {
convertVideo(fileName, localFilePath, "mp4", function(err, file) {
if (err) callback(err);
else {
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp4";
uploader.uploadFile(keyName, "video/mp4" ,file, function(err, url) {
if (err) callback(err);
else callback(null, url);
});
}
}, progress_callback);
}
},
original: function(callback){
uploader.uploadFile(fileName, mime, localFilePath, function(err, url){
callback(null, url);
});
}
}, function(err, results){
console.log(err, results);
if (err) payloadCallback(err, a);
else {
a.state = "idle";
a.mime = mime;
var stats = fs.statSync(localFilePath);
a.payload_size = stats["size"];
a.payload_thumbnail_web_uri = results.thumbnail;
a.payload_thumbnail_medium_uri = results.thumbnail;
a.payload_thumbnail_big_uri = results.thumbnail;
a.payload_uri = results.original;
if (mime == "video/mp4") {
a.payload_alternatives = [
{
mime: "video/ogg",
payload_uri: results.ogg
}
];
} else {
a.payload_alternatives = [
{
mime: "video/mp4",
payload_uri: results.mp4
}
];
}
a.updated_at = new Date();
a.save(function(err) {
if (err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
if (err){
console.error(err);
payloadCallback(err, null);
} else {
console.log('successfully deleted ' + localFilePath);
payloadCallback(null, a);
}
});
}
});
}
});
} else if (convertableAudioTypes.indexOf(mime) > -1) {
async.parallel({
ogg: function(callback) {
convertAudio(fileName, localFilePath, "ogg", function(err, file) {
if(err) callback(err);
else {
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogg" ;
uploader.uploadFile(keyName, "audio/ogg", file, function(err, url){
if (err) callback(err);
else callback(null, url);
});
}
});
},
mp3_waveform: function(callback) {
convertAudio(fileName, localFilePath, "mp3", function(err, file) {
if(err) callback(err);
else {
createWaveform(fileName, file, function(err, filePath){
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + "-" + (new Date().getTime()) + ".png";
uploader.uploadFile(keyName, "image/png", filePath, function(err, pngUrl){
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp3" ;
uploader.uploadFile(keyName, "audio/mp3", file, function(err, mp3Url){
if (err) callback(err);
else callback(null, {waveform: pngUrl, mp3: mp3Url});
});
});
});
}
});
},
original: function(callback) {
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
uploader.uploadFile(keyName, mime, localFilePath, function(err, url){
callback(null, url);
});
}
}, function(err, results) {
console.log(err, results);
if (err) payloadCallback(err, a);
else {
a.state = "idle";
a.mime = mime;
var stats = fs.statSync(localFilePath);
a.payload_size = stats["size"];
a.payload_thumbnail_web_uri = results.mp3_waveform.waveform;
a.payload_thumbnail_medium_uri = results.mp3_waveform.waveform;
a.payload_thumbnail_big_uri = results.mp3_waveform.waveform;
a.payload_uri = results.original;
a.payload_alternatives = [
{payload_uri:results.ogg, mime:"audio/ogg"},
{payload_uri:results.mp3_waveform.mp3, mime:"audio/mpeg"}
];
a.updated_at = new Date();
a.save(function(err){
if(err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
if (err){
console.error(err);
payloadCallback(err, null);
} else {
console.log('successfully deleted ' + localFilePath);
payloadCallback(null, a);
}
});
}
});
}
});
} else {
console.log("mime not matched for conversion, storing file");
var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName;
uploader.uploadFile(keyName, mime, localFilePath, function(err, url) {
a.state = "idle";
a.mime = mime;
var stats = fs.statSync(localFilePath);
a.payload_size = stats["size"];
a.payload_uri = url;
a.updated_at = new Date();
a.save(function(err) {
if(err) payloadCallback(err, null);
else {
fs.unlink(localFilePath, function (err) {
payloadCallback(null, a);
});
}
});
});
}
} else {
//there was an error getting mime
payloadCallback(err);
}
});
}
};

61
helpers/mailer.js Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
var swig = require('swig');
var AWS = require('aws-sdk');
module.exports = {
sendMail: (to_email, subject, body, options) => {
if (!options) {
options = {};
}
// FIXME
const teamname = options.teamname || "My Open Spacedeck"
const from = teamname + ' <support@example.org>';
let reply_to = [from];
if (options.reply_to) {
reply_to = [options.reply_to];
}
let plaintext = body;
if (options.action && options.action.link) {
plaintext+="\n"+options.action.link+"\n\n";
}
const htmlText = swig.renderFile('./views/emails/action.html', {
text: body.replace(/(?:\n)/g, '<br />'),
options: options
});
if (process.env.NODE_ENV === 'development') {
console.log("Email: to " + to_email + " in production.\nreply_to: " + reply_to + "\nsubject: " + subject + "\nbody: \n" + htmlText + "\n\n plaintext:\n" + plaintext);
} else {
AWS.config.update({region: 'eu-west-1'});
var ses = new AWS.SES();
ses.sendEmail( {
Source: from,
Destination: { ToAddresses: [to_email] },
ReplyToAddresses: reply_to,
Message: {
Subject: {
Data: subject
},
Body: {
Text: {
Data: plaintext,
},
Html: {
Data: htmlText
}
}
}
}, function(err, data) {
if(err) console.log('Email not sent:', err);
else console.log("Email sent.");
});
}
}
};

64
helpers/phantom.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict';
require('../models/schema');
var config = require('config');
var phantom = require('node-phantom-simple');
module.exports = {
// type = "pdf" or "png"
takeScreenshot: function(space,type,on_success,on_error) {
var spaceId = space._id;
var space_url = config.get("endpoint")+"/api/spaces/"+spaceId+"/html";
var export_path = "/tmp/"+spaceId+"."+type;
var timeout = 5000;
if (type=="pdf") timeout = 30000;
space_url += "?api_token="+config.get("phantom_api_secret");
console.log("[space-screenshot] url: "+space_url);
console.log("[space-screenshot] export_path: "+export_path);
var on_success_called = false;
var on_exit = function(exit_code) {
if (exit_code>0) {
console.log("phantom abnormal exit for url "+space_url);
if (!on_success_called && on_error) {
on_error();
}
}
};
phantom.create({ path: require('phantomjs-prebuilt').path }, function (err, browser) {
return browser.createPage(function (err, page) {
console.log("page created, opening ",space_url);
if (type=="pdf") {
var psz = {
width: space.advanced.width+"px",
height: space.advanced.height+"px"
};
page.set('paperSize', psz);
}
page.set('settings.resourceTimeout',timeout);
page.set('settings.javascriptEnabled',false);
return page.open(space_url, function (err,status) {
page.render(export_path, function() {
on_success_called = true;
if (on_success) {
on_success(export_path);
}
page.close();
browser.exit();
});
});
});
}, {
onExit: on_exit
});
}
};

61
helpers/redis.js Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
const RedisConnection = require('ioredis');
const websockets = require('./websockets');
module.exports = {
connectRedis(){
const redisHost = process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost';
this.connection = new RedisConnection(6379, redisHost);
},
sendMessage(action, model, attributes, channelId) {
const data = JSON.stringify({
channel_id: channelId,
action: action,
model: model,
object: attributes
});
this.connection.publish('updates', data);
},
logIp(ip, cb) {
this.connection.incr("ip_"+ ip, (err, socketCounter) => {
cb();
});
},
rateLimit(namespace, ip, cb) {
const key = "limit_"+ namespace + "_"+ ip;
const redis = this.connection;
redis.get(key, (err, count)=> {
if (count) {
if(count < 150) {
redis.incr(key, (err, newCount) => {
if (newCount==150) {
// limit
}
cb(true);
});
} else {
cb(false);
}
} else {
redis.set(key, 1, (err, count) => {
redis.expire(key, 1800, (err, expResult) => {
cb(true);
});
});
}
});
},
isOnlineInSpace(user, space, cb) {
this.connection.smembers("space_" + space._id.toString(), function(err, list) {
if (err) cb(err);
else {
var users = list.filter(function(item) {
return user._id.toString() === item;
});
cb(null, (users.length > 0));
}
});
}
};

149
helpers/space-render.js Normal file
View File

@@ -0,0 +1,149 @@
var fs = require('fs');
var cheerio = require("cheerio");
var artifact_vector_render = require("../public/javascripts/vector-render.js");
global.render_vector_shape = artifact_vector_render.render_vector_shape;
global.render_vector_drawing = artifact_vector_render.render_vector_drawing;
var artifact_view_model = require("../public/javascripts/spacedeck_board_artifacts.js").SpacedeckBoardArtifacts;
var template = fs.readFileSync("views/partials/space-isolated.html");
var dom = cheerio.load(template);
var compiled_js = "";
function emit(str,indent) {
var spaces="";
for (var i=0; i<indent; i++) spaces+=" ";
compiled_js+=spaces+str;
}
function compile_expr(v) {
v=v.replace(/'/g,"\\'");
v=v.replace(/[\r\n]/g," ");
f=/\{([^\|\{]+)\|([^\}]+)\}/.exec(v);
if (f) {
v=v.replace(f[1]+"|"+f[2],f[2]+"("+f[1]+")");
}
// replace braces
v=v.replace(/\{\{\{?/g,"'+");
v=v.replace(/\}\}\}?/g,"+'");
return v;
}
var iterators = 0;
function walk(n,indent) {
if (n.type == "tag") {
//console.log("n: ",n.type,n.name,n.attribs);
}
var braces = 0;
if (n.type == "text") {
if (n.data.match(/[a-zA-Z0-9\{]+/)) {
emit("h+='"+compile_expr(n.data)+"';",indent);
}
}
else if (n.type == "tag") {
var attrs = [];
var keys = Object.keys(n.attribs);
for (var i=0; i<keys.length; i++) {
var k = keys[i];
var v = n.attribs[k];
if (k.substring(0,2) == "v-") {
// vue attribute
if (k.match("v-if")) {
var test = emit("if ("+v+") {",indent);
braces++;
indent++;
}
else if (k.match("v-repeat")) {
var parts = v.split("|")[0].split(":");
var left = parts[0].replace(/ /g,"");
var right = parts[1].replace(/ /g,"");
iterators++;
emit("for (var i"+iterators+"=0;i"+iterators+"<"+right+".length;i"+iterators+"++) {",indent);
emit("var "+left+"="+right+"[i"+iterators+"];",indent+1);
braces++;
indent++;
}
} else {
v=compile_expr(v);
attrs.push(k+"=\""+v+"\"");
}
}
emit("h+='<"+n.name+" "+attrs.join(" ")+">';",indent);
for (var i=0; i<n.children.length; i++) {
walk(n.children[i],indent);
}
emit("h+='</"+n.name+">';", indent);
for (var i=braces; i>0; i--) {
indent--;
emit("}",indent);
}
}
}
function render_space_as_html(space, artifacts) {
if (!compiled_js.length) {
walk(dom("#space")[0],0);
//console.log("compiled template: \n"+compiled_js);
}
// --------
var mouse_state = "idle";
var active_tool = "pointer";
var active_space = space;
var active_space_artifacts = artifacts;
var bounds_zoom = 1;
var bounds_margin_horiz = 0;
var bounds_margin_vert = 0;
var viewport_zoom = 1;
// --------
var editing_artifact_id = null;
var urls_to_links = function(html) {
return html;
}
artifact_view_model.selected_artifacts_dict = {};
for (var i=0; i<active_space_artifacts.length; i++) {
var a = active_space_artifacts[i];
artifact_view_model.update_board_artifact_viewmodel(a);
if (!a.description) a.description = "";
if (!a.title) a.title = "";
}
var h="";
try {
eval(compiled_js);
} catch (e) {
console.error("error rendering space "+space._id+" as html: "+e);
}
var style="html, body, #space { overflow: visible !important; }\n";
style+=".wrapper { border: none !important; }\n";
h='<html>\n<head>\n<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,700,600,800,300|Montserrat:400,700|EB+Garamond|Vollkorn|Fire+Sans|Lato|Roboto|Source+Code+Pro|Ubuntu|Raleway|Playfair+Display|Crimson+Text" rel="stylesheet" type="text/css">\n<link type="text/css" rel="stylesheet" href="https://fast.fonts.net/cssapi/ee1a3484-4d98-4f9f-9f55-020a7b37f3c5.css"/>\n<link rel="stylesheet" href="/stylesheets/style.css"><style>'+style+'</style>\n</head>\n<body id="main">\n'+h+"\n</html>\n";
return h;
}
exports.render_space_as_html = render_space_as_html;

64
helpers/uploader.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict';
var AWS = require('aws-sdk');
AWS.config.region = 'eu-central-1';
var fs = require('fs');
var config = require('config');
module.exports = {
removeFile: (path, callback) => {
const s3 = new AWS.S3({
region: 'eu-central-1'
});
const bucket = config.get("storage_bucket");
s3.deleteObject({
Bucket: bucket, Key: path
}, (err, res) => {
if (err){
console.error(err);
callback(err);
}else {
callback(null, res);
}
});
},
uploadFile: function(fileName, mime, localFilePath, callback) {
if (typeof(localFilePath)!="string") {
callback({error:"missing path"}, null);
return;
}
console.log("[s3] uploading", localFilePath, " to ", fileName);
const bucket = config.get("storage_bucket");
const fileStream = fs.createReadStream(localFilePath);
fileStream.on('error', function (err) {
if (err) {
console.error(err);
callback(err);
}
});
fileStream.on('open', function () {
// FIXME
var s3 = new AWS.S3({
region: 'eu-central-1'
});
s3.putObject({
Bucket: bucket,
Key: fileName,
ContentType: mime,
Body: fileStream
}, function (err) {
if (err){
console.error(err);
callback(err);
}else {
const url = "https://"+ config.get("storage_cdn") + "/" + fileName;
console.log("[s3]" + localFilePath + " to " + url);
callback(null, url);
}
});
});
}
};

291
helpers/websockets.js Normal file
View File

@@ -0,0 +1,291 @@
'use strict';
require('../models/schema');
const WebSocketServer = require('ws').Server;
const Redis = require('ioredis');
const async = require('async');
const _ = require("underscore");
const mongoose = require("mongoose");
const crypto = require('crypto');
module.exports = {
startWebsockets: function(server){
this.setupSubscription();
this.state = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost');
if(!this.current_websockets){
this.current_websockets = [];
}
const wss = new WebSocketServer({ server:server, path: "/socket" });
wss.on('connection', function(ws) {
this.state.incr("socket_id", function(err, socketCounter) {
const socketId = "socket_" + socketCounter + "_" + crypto.randomBytes(64).toString('hex').substring(0,8);
const serverScope = this;
ws.on('message', function(msgString){
const socket = this;
const msg = JSON.parse(msgString);
if(msg.action == "auth"){
const token = msg.auth_token;
const editorName = msg.editor_name;
const editorAuth = msg.editor_auth;
const spaceId = msg.space_id;
Space.findOne({"_id": spaceId}).populate('creator').exec((err, space) => {
if (space) {
const upgradeSocket = function() {
if (token) {
User.findBySessionToken(token, function(err, user) {
if (err) {
console.error(err, user);
} else {
if (user) {
serverScope.addUserInSpace(user._id, space, ws, function(err){
serverScope.addLocalUser(user._id, ws);
console.log("[websockets] user " + user.email + " online in space " + space._id);
});
}
}
});
} else {
const anonymousUserId = space._id + "-" + editorName;
if(space.access_mode == "private" && space.edit_hash != editorAuth){
console.error("closing websocket: unauthed.");
ws.send(JSON.stringify({error: "auth_failed"}));
// ws.close();
return;
}
serverScope.addUserInSpace(anonymousUserId, space, ws, function(err){
serverScope.addLocalUser(anonymousUserId, ws);
console.log("[websockets] anonymous user " + anonymousUserId + " online in space " + space._id);
});
}
};
if (!ws.id) {
ws['id'] = socketId;
try {
ws.send(JSON.stringify({"action": "init", "channel_id": socketId}));
} catch (e) {
console.log("ws.send error: "+e);
}
}
if (ws.space_id) {
serverScope.removeUserInSpace(ws.space_id, ws, function(err) {
upgradeSocket();
});
} else {
upgradeSocket();
}
} else {
ws.send(JSON.stringify({error: "space not found"}));
ws.close();
return;
}
});
} else if (msg.action == "cursor" || msg.action == "viewport" || msg.action=="media") {
msg.space_id = socket.space_id;
msg.from_socket_id = socket.id;
serverScope.state.publish('cursors', JSON.stringify(msg));
}
});
ws.on('close', function(evt) {
console.log("websocket closed: ", ws.id, ws.space_id);
const spaceId = ws.space_id;
serverScope.removeUserInSpace(spaceId, ws, function(err) {
this.removeLocalUser(ws, function(err) {
}.bind(this));
}.bind(this));
}.bind(this));
ws.on('error', function(ws, err) {
console.error(err, res);
}.bind(this));
}.bind(this));
}.bind(this));
},
setupSubscription: function() {
this.cursorSubscriber = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost');
this.cursorSubscriber.subscribe(['cursors', 'users', 'updates'], function (err, count) {
console.log("[redis] websockets to " + count + " topics." );
});
this.cursorSubscriber.on('message', function (channel, rawMessage) {
const msg = JSON.parse(rawMessage);
const spaceId = msg.space_id;
const websockets = this.current_websockets;
if(channel === "updates") {
for(let i=0;i<websockets.length;i++) {
const ws = websockets[i];
if(ws.readyState === 1) {
ws.send(JSON.stringify(msg));
}
}
} else if(channel === "users") {
const usersList = msg.users;
if (usersList) {
for(let i=0;i<usersList.length;i++) {
const activeUser = usersList[i];
let user_id;
if (activeUser._id) {
user_id = activeUser._id;
} else {
user_id = spaceId + "-" + (activeUser.nickname||"anonymous");
}
for (let a=0; a < websockets.length; a++) {
const ws = websockets[a];
if(ws.readyState === 1){
if(ws.space_id == spaceId) {
ws.send(JSON.stringify({"action": "status_update", space_id: spaceId, users: usersList}));
} else {
//console.log("space id not matching", spaceId, ws.space_id);
}
} else {
// FIXME SHOULD CLEANUP SOCKET HERE
console.error("socket in wrong state", ws.readyState);
if(ws.readyState == 3) {
this.removeLocalUser(ws, (err) => {
console.log("old websocket removed");
});
}
}
}
}
} else {
console.error("userlist undefined for websocket");
}
} else if(channel === "cursors") {
const socketId = msg.from_socket_id;
for (let i=0;i<websockets.length;i++) {
const ws = websockets[i];
if (ws.readyState === 1) {
if (ws.space_id && spaceId) {
if ((ws.space_id == spaceId) && (ws.id !== socketId)) {
ws.send(JSON.stringify(msg));
}
} else {
console.log("space id not set, ignoring");
}
}
}
}
}.bind(this));
},
addLocalUser: function(username, ws) {
if (ws.added) {
return;
}
ws.added = true;
this.current_websockets.push(ws);
},
removeLocalUser: function(ws, cb) {
const idx = this.current_websockets.indexOf(ws);
if(idx > -1) {
this.removed_items = this.current_websockets.splice(idx, 1);
console.log("removed local socket, current online on this process: ", this.current_websockets.length);
} else {
console.log("websocket not found to remove");
}
this.state.del(ws.id, function(err, res) {
if (err) console.error(err, res);
else {
this.removeUserInSpace(ws.space_id, ws, (err) => {
console.log("removed user from space list");
this.distributeUsers(ws.space_id);
})
if(cb)
cb(err);
}
}.bind(this));
},
addUserInSpace: function(username, space, ws, cb) {
console.log("[websockets] user "+username+" in "+space.access_mode +" space " + space._id + " with socket " + ws.id);
this.state.set(ws.id, username, function(err, res) {
if(err) console.error(err, res);
else {
this.state.sadd("space_" + space._id, ws.id, function(err, res) {
if(err) cb(err);
else {
ws['space_id'] = space._id.toString();
this.distributeUsers(ws.space_id);
if(cb)
cb();
}
}.bind(this));
}
}.bind(this));
},
removeUserInSpace: function(spaceId, ws, cb) {
this.state.srem("space_" + spaceId, ws.id, function(err, res) {
if (err) cb(err);
else {
console.log("[websockets] socket "+ ws.id + " went offline in space " + spaceId);
this.distributeUsers(spaceId);
ws['space_id'] = null;
if (cb)
cb();
}
}.bind(this));
},
distributeUsers: function(spaceId) {
if(!spaceId)
return;
this.state.smembers("space_" + spaceId, function(err, list) {
async.map(list, function(item, callback) {
this.state.get(item, function(err, userId) {
console.log(item, "->", userId);
callback(null, userId);
});
}.bind(this), function(err, userIds) {
const uniqueUserIds = _.unique(userIds);
const validUserIds = _.filter(uniqueUserIds, function(uId) {
return mongoose.Types.ObjectId.isValid(uId);
});
const nonValidUserIds = _.filter(uniqueUserIds, function(uId) {
return (uId !== null && !mongoose.Types.ObjectId.isValid(uId));
});
const anonymousUsers = _.map(nonValidUserIds, function(nonValidId) {
const realNickname = nonValidId.slice(nonValidId.indexOf("-")+1);
return {nickname: realNickname, email: null, avatar_thumbnail_uri: null };
});
User.find({"_id" : { "$in" : validUserIds }}, { "nickname" : 1 , "email" : 1, "avatar_thumbnail_uri": 1 }, function(err, users) {
if (err)
console.error(err);
else {
const allUsers = users.concat(anonymousUsers);
const strUsers = JSON.stringify({users: allUsers, space_id: spaceId});
this.state.publish("users", strUsers);
}
}.bind(this));
}.bind(this));
}.bind(this));
}
};