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

1
Procfile Normal file
View file

@ -0,0 +1 @@
web: node app.js

9
README.md Normal file
View file

@ -0,0 +1,9 @@
HackMD 0.2.7
===
This is a realtime collaborative markdown notes on all platforms.
But still in early stage, feel free to fork or contribute to it.
Thanks for your using.
License under MIT.

253
app.js Normal file
View file

@ -0,0 +1,253 @@
//app
//external modules
var connect = require('connect');
var express = require('express');
var toobusy = require('toobusy-js');
var ejs = require('ejs');
var passport = require('passport');
var methodOverride = require('method-override');
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var compression = require('compression')
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
//core
var config = require("./config.js");
var User = require("./lib/user.js");
var auth = require("./lib/auth.js");
var response = require("./lib/response.js");
//server setup
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io').listen(server);
var port = process.env.PORT || config.testport;
// connect to the mongodb
if (config.debug)
mongoose.connect(config.mongodbstring);
else
mongoose.connect(process.env.MONGOLAB_URI);
//others
var db = require("./lib/db.js");
var realtime = require("./lib/realtime.js");
//methodOverride
app.use(methodOverride('_method'));
// create application/json parser
var jsonParser = bodyParser.json();
// create application/x-www-form-urlencoded parser
var urlencodedParser = bodyParser.urlencoded({
extended: false
});
//compression
app.use(compression());
//session
app.use(session({
name: config.sessionname,
secret: config.sessionsecret,
resave: false, //don't save session if unmodified
saveUninitialized: true, //don't create session until something stored
cookie: {
maxAge: new Date(Date.now() + config.sessionlife),
expires: new Date(Date.now() + config.sessionlife),
},
maxAge: new Date(Date.now() + config.sessionlife),
store: new MongoStore({
mongooseConnection: mongoose.connection,
touchAfter: config.sessiontouch
},
function (err) {
console.log(err);
})
}));
//middleware which blocks requests when we're too busy
app.use(function (req, res, next) {
if (toobusy()) {
response.errorServiceUnavailable(res);
} else {
next();
}
});
//passport
app.use(passport.initialize());
app.use(passport.session());
//serialize and deserialize
passport.serializeUser(function (user, done) {
//console.log('serializeUser: ' + user._id);
done(null, user._id);
});
passport.deserializeUser(function (id, done) {
User.model.findById(id, function (err, user) {
//console.log(user)
if (!err) done(null, user);
else done(err, null);
})
});
//routes
//static files
app.use('/', express.static(__dirname + '/public'));
//template files
app.set('views', __dirname + '/public');
//set render engine
app.engine('html', ejs.renderFile);
//get index
app.get("/", function (req, res, next) {
res.render("index.html");
});
//get status
app.get("/status", function (req, res, next) {
realtime.getStatus(function (data) {
res.end(JSON.stringify(data));
});
});
//facebook auth
app.get('/auth/facebook',
passport.authenticate('facebook'),
function (req, res) {});
//facebook auth callback
app.get('/auth/facebook/callback',
passport.authenticate('facebook', {
failureRedirect: '/'
}),
function (req, res) {
res.redirect('/');
});
//twitter auth
app.get('/auth/twitter',
passport.authenticate('twitter'),
function (req, res) {});
//twitter auth callback
app.get('/auth/twitter/callback',
passport.authenticate('twitter', {
failureRedirect: '/'
}),
function (req, res) {
res.redirect('/');
});
//github auth
app.get('/auth/github',
passport.authenticate('github'),
function (req, res) {});
//github auth callback
app.get('/auth/github/callback',
passport.authenticate('github', {
failureRedirect: '/'
}),
function (req, res) {
res.redirect('/');
});
//dropbox auth
app.get('/auth/dropbox',
passport.authenticate('dropbox-oauth2'),
function (req, res) {});
//dropbox auth callback
app.get('/auth/dropbox/callback',
passport.authenticate('dropbox-oauth2', {
failureRedirect: '/'
}),
function (req, res) {
res.redirect('/');
});
//logout
app.get('/logout', function (req, res) {
if (config.debug && req.session.passport.user)
console.log('user logout: ' + req.session.passport.user);
req.logout();
res.redirect('/');
});
//get history
app.get('/history', function (req, res) {
if (req.isAuthenticated()) {
User.model.findById(req.session.passport.user, function (err, user) {
if (err) {
console.log('read history failed: ' + err);
} else {
var history = [];
if (user.history)
history = JSON.parse(user.history);
res.send({
history: history
});
}
});
} else {
response.errorForbidden(res);
}
});
//post history
app.post('/history', urlencodedParser, function (req, res) {
if (req.isAuthenticated()) {
if (config.debug)
console.log('SERVER received history from [' + req.session.passport.user + ']: ' + req.body.history);
User.model.findById(req.session.passport.user, function (err, user) {
if (err) {
console.log('write history failed: ' + err);
} else {
user.history = req.body.history;
user.save(function (err) {
if (err) {
console.log('write user history failed: ' + err);
} else {
if (config.debug)
console.log("write user history success: " + user._id);
};
});
}
});
res.end();
} else {
response.errorForbidden(res);
}
});
//get me info
app.get('/me', function (req, res) {
if (req.isAuthenticated()) {
User.model.findById(req.session.passport.user, function (err, user) {
if (err) {
console.log('read me failed: ' + err);
} else {
var profile = JSON.parse(user.profile);
res.send({
status: 'ok',
name: profile.displayName || profile.username
});
}
});
} else {
res.send({
status: 'forbidden'
});
}
});
//get new note
app.get("/new", response.newNote);
//get features
app.get("/features", response.showFeatures);
//get note by id
app.get("/:noteId", response.showNote);
//note actions
app.get("/:noteId/:action", response.noteActions);
//socket.io secure
io.use(realtime.secure);
//socket.io heartbeat
io.set('heartbeat interval', config.heartbeatinterval);
io.set('heartbeat timeout', config.heartbeattimeout);
//socket.io connection
io.sockets.on('connection', realtime.connection);
//listen
server.listen(port, function () {
console.log('Server listening at port %d', port);
});

50
config.js Normal file
View file

@ -0,0 +1,50 @@
//config
var path = require('path');
var config = {
debug: true,
version: '0.2.7',
domain: 'http://localhost:3000',
testport: '3000',
//path
tmppath: "./tmp/",
defaultnotepath: path.join(__dirname, '/public', "default.md"),
defaultfeaturespath: path.join(__dirname, '/public', "features.md"),
hackmdpath: path.join(__dirname, '/public/views', "index.ejs"),
errorpath: path.join(__dirname, '/public/views', "error.ejs"),
prettypath: path.join(__dirname, '/public/views', 'pretty.ejs'),
//db string
postgresqlstring: "postgresql://localhost:5432/hackmd",
mongodbstring: "mongodb://localhost/hackmd",
//constants
featuresnotename: "features",
sessionname: 'please set this',
sessionsecret: 'please set this',
sessionlife: 14 * 24 * 60 * 60 * 1000, //14 days
sessiontouch: 1 * 3600, //1 hour
heartbeatinterval: 5000,
heartbeattimeout: 10000,
//auth
facebook: {
clientID: 'get yourself one',
clientSecret: 'get yourself one',
callbackPath: '/auth/facebook/callback'
},
twitter: {
consumerKey: 'get yourself one',
consumerSecret: 'get yourself one',
callbackPath: '/auth/twitter/callback'
},
github: {
clientID: 'get yourself one',
clientSecret: 'get yourself one',
callbackPath: '/auth/github/callback'
},
dropbox: {
clientID: 'get yourself one',
clientSecret: 'get yourself one',
callbackPath: '/auth/dropbox/callback'
}
};
module.exports = config;

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;

45
package.json Normal file
View file

@ -0,0 +1,45 @@
{
"name": "hackmd",
"version": "0.2.7",
"description": "Realtime collaborative markdown notes on all platforms.",
"main": "server.js",
"author": "jackymaxj",
"private": true,
"license": "MIT",
"dependencies": {
"async": "^0.9.0",
"body-parser": "^1.12.3",
"cheerio": "^0.19.0",
"compression": "^1.4.3",
"connect": "3.x",
"connect-mongo": "^0.8.1",
"cookie": "0.1.2",
"cookie-parser": "1.3.3",
"ejs": "^1.0.0",
"emojify.js": "^1.0.1",
"express": "4.x",
"express-session": "^1.11.1",
"highlight.js": "^8.4.0",
"html": "0.0.7",
"jsdom-nogyp": "^0.8.3",
"lz-string": "1.3.6",
"markdown-pdf": "^5.2.0",
"marked": "^0.3.3",
"method-override": "^2.3.2",
"mongoose": "^4.0.2",
"node-uuid": "^1.4.3",
"passport": "^0.2.1",
"passport-dropbox-oauth2": "^0.1.6",
"passport-facebook": "^2.0.0",
"passport-github": "^0.1.5",
"passport-twitter": "^1.0.3",
"pg": "4.x",
"randomcolor": "^0.2.0",
"shortid": "2.1.3",
"socket.io": "1.3.5",
"toobusy-js": "^0.4.1"
},
"engines": {
"node": "0.10.x"
}
}

45
public/GFM.md Normal file
View file

@ -0,0 +1,45 @@
GitHub Flavored Markdown
========================
Everything from markdown plus GFM features:
## URL autolinking
Underscores_are_allowed_between_words.
asdasdasd
## Strikethrough text
GFM adds syntax to strikethrough text, which is missing from standard Markdown.
~~Mistaken text.~~
~~**works with other fomatting**~~
~~spans across
lines~~
## Fenced code blocks (and syntax highlighting)
```javascript
for (var i = 0; i < items.length; i++) {
console.log(items[i], i); // log them
}
```
## Task Lists
- [ ] Incomplete task list item
- [x] **Completed** task list item
## A bit of GitHub spice
* SHA: be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User@SHA ref: mojombo@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User/Project@SHA: mojombo/god@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* \#Num: #1
* User/#Num: mojombo#1
* User/Project#Num: mojombo/god#1
See http://github.github.com/github-flavored-markdown/.

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

101
public/css/bootstrap-social.css vendored Executable file
View file

@ -0,0 +1,101 @@
/*
* Social Buttons for Bootstrap
*
* Copyright 2013-2014 Panayiotis Lipiridis
* Licensed under the MIT License
*
* https://github.com/lipis/bootstrap-social
*/
.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}
.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg :first-child{line-height:45px;width:45px;font-size:1.8em}
.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm :first-child{line-height:28px;width:28px;font-size:1.4em}
.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs :first-child{line-height:20px;width:20px;font-size:1.2em}
.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}
.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg :first-child{line-height:45px;width:45px;font-size:1.8em}
.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm :first-child{line-height:28px;width:28px;font-size:1.4em}
.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs :first-child{line-height:20px;width:20px;font-size:1.2em}
.btn-social-icon :first-child{border:none;text-align:center;width:100% !important}
.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}
.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}
.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}
.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:hover,.btn-adn:focus,.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}
.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}
.btn-adn.disabled,.btn-adn[disabled],fieldset[disabled] .btn-adn,.btn-adn.disabled:hover,.btn-adn[disabled]:hover,fieldset[disabled] .btn-adn:hover,.btn-adn.disabled:focus,.btn-adn[disabled]:focus,fieldset[disabled] .btn-adn:focus,.btn-adn.disabled:active,.btn-adn[disabled]:active,fieldset[disabled] .btn-adn:active,.btn-adn.disabled.active,.btn-adn[disabled].active,fieldset[disabled] .btn-adn.active{background-color:#d87a68;border-color:rgba(0,0,0,0.2)}
.btn-adn .badge{color:#d87a68;background-color:#fff}
.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover,.btn-bitbucket:focus,.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}
.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}
.btn-bitbucket.disabled,.btn-bitbucket[disabled],fieldset[disabled] .btn-bitbucket,.btn-bitbucket.disabled:hover,.btn-bitbucket[disabled]:hover,fieldset[disabled] .btn-bitbucket:hover,.btn-bitbucket.disabled:focus,.btn-bitbucket[disabled]:focus,fieldset[disabled] .btn-bitbucket:focus,.btn-bitbucket.disabled:active,.btn-bitbucket[disabled]:active,fieldset[disabled] .btn-bitbucket:active,.btn-bitbucket.disabled.active,.btn-bitbucket[disabled].active,fieldset[disabled] .btn-bitbucket.active{background-color:#205081;border-color:rgba(0,0,0,0.2)}
.btn-bitbucket .badge{color:#205081;background-color:#fff}
.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover,.btn-dropbox:focus,.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}
.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}
.btn-dropbox.disabled,.btn-dropbox[disabled],fieldset[disabled] .btn-dropbox,.btn-dropbox.disabled:hover,.btn-dropbox[disabled]:hover,fieldset[disabled] .btn-dropbox:hover,.btn-dropbox.disabled:focus,.btn-dropbox[disabled]:focus,fieldset[disabled] .btn-dropbox:focus,.btn-dropbox.disabled:active,.btn-dropbox[disabled]:active,fieldset[disabled] .btn-dropbox:active,.btn-dropbox.disabled.active,.btn-dropbox[disabled].active,fieldset[disabled] .btn-dropbox.active{background-color:#1087dd;border-color:rgba(0,0,0,0.2)}
.btn-dropbox .badge{color:#1087dd;background-color:#fff}
.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover,.btn-facebook:focus,.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}
.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}
.btn-facebook.disabled,.btn-facebook[disabled],fieldset[disabled] .btn-facebook,.btn-facebook.disabled:hover,.btn-facebook[disabled]:hover,fieldset[disabled] .btn-facebook:hover,.btn-facebook.disabled:focus,.btn-facebook[disabled]:focus,fieldset[disabled] .btn-facebook:focus,.btn-facebook.disabled:active,.btn-facebook[disabled]:active,fieldset[disabled] .btn-facebook:active,.btn-facebook.disabled.active,.btn-facebook[disabled].active,fieldset[disabled] .btn-facebook.active{background-color:#3b5998;border-color:rgba(0,0,0,0.2)}
.btn-facebook .badge{color:#3b5998;background-color:#fff}