First commit, version 0.2.7

This commit is contained in:
Wu Cheng-Han
2015-05-04 15:53:29 +08:00
parent 61eb11d23c
commit 4b0ca55eb7
1379 changed files with 173000 additions and 0 deletions

49
lib/auth.js Normal file
View File

@@ -0,0 +1,49 @@
//auth
//external modules
var passport = require('passport');
var FacebookStrategy = require('passport-facebook').Strategy;
var TwitterStrategy = require('passport-twitter').Strategy;
var GithubStrategy = require('passport-github').Strategy;
var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
//core
var User = require('./user.js')
var config = require('../config.js')
function callback(accessToken, refreshToken, profile, done) {
//console.log(profile.displayName || profile.username);
User.findOrNewUser(profile.id, profile, function (err, user) {
if (err || user == null) {
console.log('auth callback failed: ' + err);
} else {
if(config.debug && user)
console.log('user login: ' + user._id);
done(null, user);
}
});
}
//facebook
module.exports = passport.use(new FacebookStrategy({
clientID: config.facebook.clientID,
clientSecret: config.facebook.clientSecret,
callbackURL: config.domain + config.facebook.callbackPath
}, callback));
//twitter
passport.use(new TwitterStrategy({
consumerKey: config.twitter.consumerKey,
consumerSecret: config.twitter.consumerSecret,
callbackURL: config.domain + config.twitter.callbackPath
}, callback));
//github
passport.use(new GithubStrategy({
clientID: config.github.clientID,
clientSecret: config.github.clientSecret,
callbackURL: config.domain + config.github.callbackPath
}, callback));
//dropbox
passport.use(new DropboxStrategy({
clientID: config.dropbox.clientID,
clientSecret: config.dropbox.clientSecret,
callbackURL: config.domain + config.dropbox.callbackPath
}, callback));

146
lib/db.js Normal file
View File

@@ -0,0 +1,146 @@
//db
//external modules
var pg = require('pg');
var fs = require('fs');
var util = require('util');
//core
var config = require("../config.js");
//public
var db = {
readFromFile: readFromDB,
saveToFile: saveToFile,
newToDB: newToDB,
readFromDB: readFromDB,
saveToDB: saveToDB,
countFromDB: countFromDB
};
function getDBClient() {
if (config.debug)
return new pg.Client(config.postgresqlstring);
else
return new pg.Client(process.env.DATABASE_URL);
}
function readFromFile(callback) {
fs.readFile('hackmd', 'utf8', function (err, data) {
if (err) throw err;
callback(data);
});
}
function saveToFile(doc) {
fs.writeFile('hackmd', doc, function (err) {
if (err) throw err;
});
}
var updatequery = "UPDATE notes SET title='%s', content='%s', update_time=NOW() WHERE id='%s';";
var insertquery = "INSERT INTO notes (id, owner, content) VALUES ('%s', '%s', '%s');";
var insertifnotexistquery = "INSERT INTO notes (id, owner, content) \
SELECT '%s', '%s', '%s' \
WHERE NOT EXISTS (SELECT 1 FROM notes WHERE id='%s') RETURNING *;";
var selectquery = "SELECT * FROM notes WHERE id='%s';";
var countquery = "SELECT count(*) FROM notes;";
function newToDB(id, owner, body, callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
callback(err, null);
return console.error('could not connect to postgres', err);
}
var newnotequery = util.format(insertquery, id, owner, body);
//console.log(newnotequery);
client.query(newnotequery, function (err, result) {
if (err) {
callback(err, null);
return console.error("new note to db failed: " + err);
} else {
if (config.debug)
console.log("new note to db success");
callback(null, result);
client.end();
}
});
});
}
function readFromDB(id, callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
callback(err, null);
return console.error('could not connect to postgres', err);
}
var readquery = util.format(selectquery, id);
//console.log(readquery);
client.query(readquery, function (err, result) {
if (err) {
callback(err, null);
return console.error("read from db failed: " + err);
} else {
//console.log(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db", null);
} else {
console.log("read from db success");
callback(null, result);
client.end();
}
}
});
});
}
function saveToDB(id, title, data, callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
callback(err, null);
return console.error('could not connect to postgres', err);
}
var savequery = util.format(updatequery, title, data, id);
//console.log(savequery);
client.query(savequery, function (err, result) {
if (err) {
callback(err, null);
return console.error("save to db failed: " + err);
} else {
if (config.debug)
console.log("save to db success");
callback(null, result);
client.end();
}
});
});
}
function countFromDB(callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
callback(err, null);
return console.error('could not connect to postgres', err);
}
client.query(countquery, function (err, result) {
if (err) {
callback(err, null);
return console.error("count from db failed: " + err);
} else {
//console.log(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db", null);
} else {
console.log("count from db success");
callback(null, result);
client.end();
}
}
});
});
}
module.exports = db;

60
lib/note.js Normal file
View File

@@ -0,0 +1,60 @@
//note
//external modules
var LZString = require('lz-string');
var marked = require('marked');
var cheerio = require('cheerio');
//others
var db = require("./db.js");
//public
var note = {
checkNoteIdValid: checkNoteIdValid,
checkNoteExist: checkNoteExist,
getNoteTitle: getNoteTitle
};
function checkNoteIdValid(noteId) {
try {
//console.log(noteId);
var id = LZString.decompressFromBase64(noteId);
if (!id) return false;
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
var result = id.match(uuidRegex);
if (result && result.length == 1)
return true;
else
return false;
} catch (err) {
console.error(err);
return false;
}
}
function checkNoteExist(noteId) {
try {
//console.log(noteId);
var id = LZString.decompressFromBase64(noteId);
db.readFromDB(id, function (err, result) {
if (err) return false;
return true;
});
} catch (err) {
console.error(err);
return false;
}
}
//get title
function getNoteTitle(body) {
var $ = cheerio.load(marked(body));
var h1s = $("h1");
var title = "";
if (h1s.length > 0)
title = h1s.first().text();
else
title = "Untitled";
return title;
}
module.exports = note;

392
lib/realtime.js Normal file
View File

@@ -0,0 +1,392 @@
//realtime
//external modules
var cookie = require('cookie');
var cookieParser = require('cookie-parser');
var url = require('url');
var async = require('async');
var LZString = require('lz-string');
var shortId = require('shortid');
var randomcolor = require("randomcolor");
//core
var config = require("../config.js");
//others
var db = require("./db.js");
var Note = require("./note.js");
var User = require("./user.js");
//public
var realtime = {
secure: secure,
connection: connection,
getStatus: getStatus
};
function secure(socket, next) {
try {
var handshakeData = socket.request;
if (handshakeData.headers.cookie) {
handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionname], config.sessionsecret);
if (handshakeData.cookie[config.sessionname] == handshakeData.sessionID) {
next(new Error('AUTH failed: Cookie is invalid.'));
}
} else {
next(new Error('AUTH failed: No cookie transmitted.'));
}
if (config.debug)
console.log("AUTH success cookie: " + handshakeData.sessionID);
next();
} catch (ex) {
next(new Error("AUTH failed:" + JSON.stringify(ex)));
}
}
//actions
var users = {};
var notes = {};
var updater = setInterval(function () {
async.each(Object.keys(notes), function (key, callback) {
var note = notes[key];
if (note.isDirty) {
if (config.debug)
console.log("updater found dirty note: " + key);
var title = Note.getNoteTitle(LZString.decompressFromBase64(note.body));
db.saveToDB(key, title, note.body,
function (err, result) {});
note.isDirty = false;
}
callback();
}, function (err) {
if (err) return console.error('updater error', err);
});
}, 5000);
function getStatus(callback) {
db.countFromDB(function (err, data) {
if (err) return console.log(err);
var regusers = 0;
var distinctregusers = 0;
var distinctaddresses = [];
Object.keys(users).forEach(function (key) {
var value = users[key];
if(value.login)
regusers++;
var found = false;
for (var i = 0; i < distinctaddresses.length; i++) {
if (value.address == distinctaddresses[i]) {
found = true;
break;
}
}
if (!found)
distinctaddresses.push(value.address);
if(!found && value.login)
distinctregusers++;
});
User.getUserCount(function (err, regcount) {
if (err) {
console.log('get status failed: ' + err);
return;
}
if (callback)
callback({
onlineNotes: Object.keys(notes).length,
onlineUsers: Object.keys(users).length,
distinctOnlineUsers: distinctaddresses.length,
notesCount: data.rows[0].count,
registeredUsers: regcount,
onlineRegisteredUsers: regusers,
distinctOnlineRegisteredUsers: distinctregusers
});
});
});
}
function getNotenameFromSocket(socket) {
var hostUrl = url.parse(socket.handshake.headers.referer);
var notename = hostUrl.pathname.split('/')[1];
if (notename == config.featuresnotename) {
return notename;
}
if (!Note.checkNoteIdValid(notename)) {
socket.emit('info', {
code: 404
});
return socket.disconnect();
}
notename = LZString.decompressFromBase64(notename);
return notename;
}
function emitOnlineUsers(socket) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var users = [];
Object.keys(notes[notename].users).forEach(function (key) {
var user = notes[notename].users[key];
if (user)
users.push({
id: user.id,
color: user.color,
cursor: user.cursor
});
});
notes[notename].socks.forEach(function (sock) {
sock.emit('online users', {
count: notes[notename].socks.length,
users: users
});
});
}
var isConnectionBusy = false;
var connectionSocketQueue = [];
var isDisconnectBusy = false;
var disconnectSocketQueue = [];
function finishConnection(socket, notename) {
notes[notename].users[socket.id] = users[socket.id];
notes[notename].socks.push(socket);
emitOnlineUsers(socket);
socket.emit('refresh', {
body: notes[notename].body
});
//clear finished socket in queue
for (var i = 0; i < connectionSocketQueue.length; i++) {
if (connectionSocketQueue[i].id == socket.id)
connectionSocketQueue.splice(i, 1);
}
//seek for next socket
isConnectionBusy = false;
if (connectionSocketQueue.length > 0)
startConnection(connectionSocketQueue[0]);
if (config.debug) {
console.log('SERVER connected a client to [' + notename + ']:');
console.log(JSON.stringify(users[socket.id]));
//console.log(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
});
}
}
function startConnection(socket) {
if (isConnectionBusy) return;
isConnectionBusy = true;
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (!notes[notename]) {
db.readFromDB(notename, function (err, data) {
if (err) {
socket.emit('info', {
code: 404
});
socket.disconnect();
//clear err socket in queue
for (var i = 0; i < connectionSocketQueue.length; i++) {
if (connectionSocketQueue[i].id == socket.id)
connectionSocketQueue.splice(i, 1);
}
isConnectionBusy = false;
return console.error(err);
}
var body = data.rows[0].content;
notes[notename] = {
socks: [],
body: body,
isDirty: false,
users: {}
};
finishConnection(socket, notename);
});
} else {
finishConnection(socket, notename);
}
}
function disconnect(socket) {
if (isDisconnectBusy) return;
isDisconnectBusy = true;
if (config.debug) {
console.log("SERVER disconnected a client");
console.log(JSON.stringify(users[socket.id]));
}
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (users[socket.id]) {
delete users[socket.id];
}
if (notes[notename]) {
delete notes[notename].users[socket.id];
var index = notes[notename].socks.indexOf(socket);
if (index > -1) {
notes[notename].socks.splice(index, 1);
}
if (Object.keys(notes[notename].users).length <= 0) {
var title = Note.getNoteTitle(LZString.decompressFromBase64(notes[notename].body));
db.saveToDB(notename, title, notes[notename].body,
function (err, result) {
delete notes[notename];
if (config.debug) {
//console.log(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
});
}
});
}
}
emitOnlineUsers(socket);
//clear finished socket in queue
for (var i = 0; i < disconnectSocketQueue.length; i++) {
if (disconnectSocketQueue[i].id == socket.id)
disconnectSocketQueue.splice(i, 1);
}
//seek for next socket
isDisconnectBusy = false;
if (disconnectSocketQueue.length > 0)
disconnect(disconnectSocketQueue[0]);
if (config.debug) {
//console.log(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
});
}
}
function connection(socket) {
users[socket.id] = {
id: socket.id,
address: socket.handshake.address,
'user-agent': socket.handshake.headers['user-agent'],
otk: shortId.generate(),
color: randomcolor({
luminosity: 'light'
}),
cursor: null,
login: false
};
connectionSocketQueue.push(socket);
startConnection(socket);
//when a new client coming or received a client refresh request
socket.on('refresh', function (body_) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (config.debug)
console.log('SERVER received [' + notename + '] data updated: ' + socket.id);
if (notes[notename].body != body_) {
notes[notename].body = body_;
notes[notename].isDirty = true;
}
});
socket.on('user status', function (data) {
if(data)
users[socket.id].login = data.login;
});
socket.on('online users', function () {
emitOnlineUsers(socket);
});
socket.on('version', function () {
socket.emit('version', config.version);
});
socket.on('cursor focus', function (data) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
var out = {
id: socket.id,
color: users[socket.id].color,
cursor: data
};
sock.emit('cursor focus', out);
}
});
});
socket.on('cursor activity', function (data) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
var out = {
id: socket.id,
color: users[socket.id].color,
cursor: data
};
sock.emit('cursor activity', out);
}
});
});
socket.on('cursor blur', function () {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
users[socket.id].cursor = null;
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
var out = {
id: socket.id
};
if (sock != socket) {
sock.emit('cursor blur', out);
}
}
});
});
//when a new client disconnect
socket.on('disconnect', function () {
disconnectSocketQueue.push(socket);
disconnect(socket);
});
//when received client change data request
socket.on('change', function (op) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
op = LZString.decompressFromBase64(op);
if (op)
op = JSON.parse(op);
if (config.debug)
console.log('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
switch (op.origin) {
case '+input':
case '+delete':
case 'paste':
case 'cut':
case 'undo':
case 'redo':
case 'drag':
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
if (config.debug)
console.log('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + JSON.stringify(op));
sock.emit('change', LZString.compressToBase64(JSON.stringify(op)));
}
});
break;
}
});
}
module.exports = realtime;

211
lib/response.js Normal file
View File

@@ -0,0 +1,211 @@
//response
//external modules
var ejs = require('ejs');
var fs = require('fs');
var path = require('path');
var uuid = require('node-uuid');
var markdownpdf = require("markdown-pdf");
var LZString = require('lz-string');
//core
var config = require("../config.js");
//others
var db = require("./db.js");
var Note = require("./note.js");
//public
var response = {
errorForbidden: function (res) {
res.status(403).send("Forbidden, oh no.")
},
errorNotFound: function (res) {
responseError(res, "404", "Not Found", "oops.")
},
errorInternalError: function (res) {
responseError(res, "500", "Internal Error", "wtf.")
},
errorServiceUnavailable: function (res) {
res.status(503).send("I'm busy right now, try again later.")
},
newNote: newNote,
showFeatures: showFeatures,
showNote: showNote,
noteActions: noteActions
};
function responseError(res, code, detail, msg) {
res.writeHead(code, {
'Content-Type': 'text/html'
});
var content = ejs.render(fs.readFileSync(config.errorpath, 'utf8'), {
cache: !config.debug,
filename: config.errorpath,
code: code,
detail: detail,
msg: msg
});
res.write(content);
res.end();
}
function responseHackMD(res) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
var content = ejs.render(fs.readFileSync(config.hackmdpath, 'utf8'), {
cache: !config.debug,
filename: config.hackmdpath
});
res.write(content);
res.end();
}
function newNote(req, res, next) {
var newId = uuid.v4();
var body = fs.readFileSync(config.defaultnotepath, 'utf8');
body = LZString.compressToBase64(body);
var owner = null;
if (req.isAuthenticated()) {
owner = req.session.passport.user;
}
db.newToDB(newId, owner, body, function (err, result) {
if (err) {
responseError(res, "500", "Internal Error", "wtf.");
return;
}
res.redirect("/" + LZString.compressToBase64(newId));
});
}
function showFeatures(req, res, next) {
db.readFromDB(config.featuresnotename, function (err, data) {
if (err) {
var body = fs.readFileSync(config.defaultfeaturespath, 'utf8');
body = LZString.compressToBase64(body);
db.newToDB(config.featuresnotename, null, body, function (err, result) {
if (err) {
responseError(res, "500", "Internal Error", "wtf.");
return;
}
responseHackMD(res);
});
} else {
responseHackMD(res);
}
});
}
function showNote(req, res, next) {
var noteId = req.params.noteId;
if (!Note.checkNoteIdValid(noteId)) {
responseError(res, "404", "Not Found", "oops.");
return;
}
responseHackMD(res);
}
function actionPretty(req, res, noteId) {
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = data.rows[0].content;
var template = config.prettypath;
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'));
var origin = "//" + req.headers.host;
var html = compiled({
url: origin,
body: body
});
var buf = html;
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8',
'Cache-Control': 'private',
'Content-Length': buf.length
});
res.end(buf);
});
}
function actionDownload(req, res, noteId) {
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
var title = Note.getNoteTitle(body);
res.writeHead(200, {
'Content-Type': 'text/markdown; charset=UTF-8',
'Cache-Control': 'private',
'Content-disposition': 'attachment; filename=' + title + '.md',
'Content-Length': body.length
});
res.end(body);
});
}
function actionPDF(req, res, noteId) {
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
var title = Note.getNoteTitle(body);
if (!fs.existsSync(config.tmppath)) {
fs.mkdirSync(config.tmppath);
}
var path = config.tmppath + Date.now() + '.pdf';
markdownpdf().from.string(body).to(path, function () {
var stream = fs.createReadStream(path);
var filename = title;
// Be careful of special characters
filename = encodeURIComponent(filename);
// Ideally this should strip them
res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
res.setHeader('Cache-Control', 'private');
res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
stream.pipe(res);
fs.unlink(path);
});
});
}
function noteActions(req, res, next) {
var noteId = req.params.noteId;
if (noteId != config.featuresnotename) {
if (!Note.checkNoteIdValid(noteId)) {
responseError(res, "404", "Not Found", "oops.");
return;
}
noteId = LZString.decompressFromBase64(noteId);
if (!noteId) {
responseError(res, "404", "Not Found", "oops.");
return;
}
}
var action = req.params.action;
switch (action) {
case "pretty":
actionPretty(req, res, noteId);
break;
case "download":
actionDownload(req, res, noteId);
break;
case "pdf":
actionPDF(req, res, noteId);
break;
default:
if (noteId != config.featuresnotename)
res.redirect('/' + LZString.compressToBase64(noteId));
else
res.redirect('/' + noteId);
break;
}
}
module.exports = response;

83
lib/user.js Normal file
View File

@@ -0,0 +1,83 @@
//user
//external modules
var mongoose = require('mongoose');
//core
var config = require("../config.js");
// create a user model
var model = mongoose.model('user', {
id: String,
profile: String,
history: String,
created: Date
});
//public
var user = {
model: model,
findUser: findUser,
newUser: newUser,
findOrNewUser: findOrNewUser,
getUserCount: getUserCount
};
function getUserCount(callback) {
model.count(function(err, count){
if(err) callback(err, null);
else callback(null, count);
});
}
function findUser(id, callback) {
model.findOne({
id: id
}, function (err, user) {
if (err) {
console.log('find user failed: ' + err);
callback(err, null);
}
if (!err && user != null) {
callback(null, user);
} else {
console.log('find user failed: ' + err);
callback(err, null);
};
});
}
function newUser(id, profile, callback) {
var user = new model({
id: id,
profile: JSON.stringify(profile),
created: Date.now()
});
user.save(function (err) {
if (err) {
console.log('new user failed: ' + err);
callback(err, null);
} else {
console.log("new user success: " + user.id);
callback(null, user);
};
});
}
function findOrNewUser(id, profile, callback) {
findUser(id, function(err, user) {
if(err || user == null) {
newUser(id, profile, function(err, user) {
if(err) {
console.log('find or new user failed: ' + err);
callback(err, null);
} else {
callback(null, user);
}
});
} else {
callback(null, user);
}
});
}
module.exports = user;