Browse Source

Jump to 0.3.1

pull/16/head
Wu Cheng-Han 6 years ago
parent
commit
10c9811fc5
49 changed files with 2332 additions and 444 deletions
  1. +38
    -20
      README.md
  2. +39
    -28
      app.js
  3. +0
    -0
      backups/.keep
  4. +1
    -1
      config.js
  5. +11
    -0
      hackmd
  6. +7
    -6
      lib/auth.js
  7. +18
    -17
      lib/db.js
  8. +2
    -1
      lib/logger.js
  9. +147
    -6
      lib/note.js
  10. +234
    -94
      lib/realtime.js
  11. +158
    -19
      lib/response.js
  12. +7
    -6
      lib/temp.js
  13. +6
    -5
      lib/user.js
  14. +0
    -0
      logs/.keep
  15. +5
    -2
      package.json
  16. +19
    -0
      processes.json
  17. +16
    -1
      public/css/cover.css
  18. +171
    -1
      public/css/extra.css
  19. +22
    -2
      public/css/index.css
  20. +6
    -0
      public/css/site.css
  21. +25
    -9
      public/index.html
  22. +117
    -13
      public/js/cover.js
  23. +230
    -63
      public/js/extra.js
  24. +0
    -8
      public/js/fb.js
  25. +2
    -1
      public/js/history.js
  26. +444
    -97
      public/js/index.js
  27. +78
    -6
      public/js/pretty.js
  28. +7
    -6
      public/js/syncscroll.js
  29. +1
    -1
      public/vendor/codemirror/addon/comment/continuecomment.js
  30. +2
    -2
      public/vendor/codemirror/codemirror.min.js
  31. +4
    -2
      public/vendor/codemirror/lib/codemirror.js
  32. +1
    -0
      public/vendor/highlight-js/github-gist.min.css
  33. +1
    -1
      public/vendor/highlight-js/github.min.css
  34. +2
    -2
      public/vendor/highlight-js/highlight.min.js
  35. +2
    -0
      public/vendor/inlineAttachment/codemirror.inline-attachment.js
  36. +1
    -1
      public/vendor/inlineAttachment/inline-attachment.js
  37. +33
    -6
      public/vendor/jquery-textcomplete/jquery.textcomplete.js
  38. +8
    -0
      public/vendor/jquery.mousewheel.min.js
  39. +121
    -0
      public/vendor/md-toc.js
  40. +1
    -0
      public/vendor/md5.min.js
  41. +125
    -0
      public/vendor/showup/showup.css
  42. +87
    -0
      public/vendor/showup/showup.js
  43. +87
    -1
      public/views/body.ejs
  44. +5
    -0
      public/views/foot.ejs
  45. +4
    -4
      public/views/head.ejs
  46. +9
    -3
      public/views/header.ejs
  47. +27
    -8
      public/views/pretty.ejs
  48. +1
    -1
      run.sh
  49. +0
    -0
      tmp/.keep

+ 38
- 20
README.md View File

@ -1,52 +1,70 @@
HackMD 0.2.9
HackMD 0.3.1
===
HackMD is a realtime collaborative markdown notes on all platforms.
Inspired by Hackpad, but more focusing on speed and flexibility.
Still in early stage, feel free to fork or contribute to this.
HackMD is a realtime collaborative markdown notes on all platforms.
Inspired by Hackpad, but more focusing on speed and flexibility.
Still in early stage, feel free to fork or contribute to this.
Thanks for your using! :smile:
Dependency
Database dependency
---
- PostgreSQL 9.3.6 or 9.4.1
- MongoDB 3.0.2
Import db schema
Import database schema
---
The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`
The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`
To import the sql file in PostgreSQL, type `psql -i hackmd_schema.sql`
The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
Config
Structure
---
```
hackmd/
├── logs/ --- server logs
├── backups/ --- db backups
├── tmp/ --- temporary files
├── lib/ --- server libraries
└── public/ --- client files
├── css/ --- css styles
├── js/ --- js scripts
├── vendor/ --- vendor includes
└── views/ --- view templates
```
Configure
---
There are some config you need to change in below files
```
./run.sh
./config.js
./public/js/common.js
./Procfile --- for heroku start
./run.sh --- for forever start
./processes.json --- for pm2 start
./config.js --- for server settings
./public/js/common.js --- for client settings
./hackmd --- for logrotate
```
The script `run.sh`, it's for someone like me to run the server via npm package `forever`, and can passing environment variable to the server, like heroku does.
**From 0.3.1, we no longer recommend using `forever` to run your server.**
To install `forever`, just type `npm install forever -g`
We using `pm2` to run server.
See [here](https://github.com/Unitech/pm2) for details.
You can use SSL to encrypt your site by passing certificate path in the `config.js` and set `usessl=true`
Run a server
---
To run the server, type `bash run.sh`
Log will be at `~/.forever/hackmd.log`
- forever: `bash run.sh`
- pm2: `pm2 start processes.json`
Stop a server
---
To stop the server, simply type `forever stop hackmd`
- forever: `forever stop hackmd`
- pm2: `pm2 stop hackmd`
Backup db
---
To backup the db, type `bash backup.sh`
Backup files will be at `./backups/`
To backup the db, type `bash backup.sh`
**License under MIT.**

+ 39
- 28
app.js View File

@ -49,13 +49,15 @@ if (config.usessl) {
var app = express();
var server = require('http').createServer(app);
}
//socket io listen
var io = require('socket.io').listen(server);
//logger
app.use(morgan('combined', {
"stream": logger.stream
}));
//socket io
var io = require('socket.io')(server);
// connect to the mongodb
mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring);
@ -80,7 +82,7 @@ var sessionStore = new MongoStore({
touchAfter: config.sessiontouch
},
function (err) {
console.log(err);
logger.info(err);
});
//compression
@ -115,12 +117,12 @@ app.use(passport.session());
//serialize and deserialize
passport.serializeUser(function (user, done) {
//console.log('serializeUser: ' + user._id);
//logger.info('serializeUser: ' + user._id);
done(null, user._id);
});
passport.deserializeUser(function (id, done) {
User.model.findById(id, function (err, user) {
//console.log(user)
//logger.info(user)
if (!err) done(null, user);
else done(err, null);
})
@ -163,7 +165,7 @@ app.get("/temp", function (req, res) {
});
temp.remove(function (err) {
if (err)
console.log('remove temp failed: ' + err);
logger.error('remove temp failed: ' + err);
});
}
});
@ -182,7 +184,7 @@ app.post("/temp", urlencodedParser, function (req, res) {
response.errorForbidden(res);
else {
if (config.debug)
console.log('SERVER received temp from [' + host + ']: ' + req.body.data);
logger.info('SERVER received temp from [' + host + ']: ' + req.body.data);
Temp.newTemp(id, data, function (err, temp) {
if (!err && temp) {
res.header("Access-Control-Allow-Origin", "*");
@ -247,7 +249,7 @@ app.get('/auth/dropbox/callback',
//logout
app.get('/logout', function (req, res) {
if (config.debug && req.session.passport.user)
console.log('user logout: ' + req.session.passport.user);
logger.info('user logout: ' + req.session.passport.user);
req.logout();
res.redirect('/');
});
@ -256,7 +258,7 @@ 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);
logger.error('read history failed: ' + err);
} else {
var history = [];
if (user.history)
@ -274,18 +276,18 @@ app.get('/history', function (req, res) {
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);
logger.info('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);
logger.error('write history failed: ' + err);
} else {
user.history = req.body.history;
user.save(function (err) {
if (err) {
console.log('write user history failed: ' + err);
logger.error('write user history failed: ' + err);
} else {
if (config.debug)
console.log("write user history success: " + user._id);
logger.info("write user history success: " + user._id);
};
});
}
@ -300,7 +302,7 @@ 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);
logger.error('read me failed: ' + err);
} else {
var profile = JSON.parse(user.profile);
res.send({
@ -324,20 +326,25 @@ app.post('/uploadimage', function (req, res) {
response.errorForbidden(res);
} else {
if (config.debug)
console.log('SERVER received uploadimage: ' + JSON.stringify(files.image));
logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image));
imgur.setClientId(config.imgur.clientID);
imgur.uploadFile(files.image.path)
.then(function (json) {
if (config.debug)
console.log('SERVER uploadimage success: ' + JSON.stringify(json));
res.send({
link: json.data.link
try {
imgur.uploadFile(files.image.path)
.then(function (json) {
if (config.debug)
logger.info('SERVER uploadimage success: ' + JSON.stringify(json));
res.send({
link: json.data.link
});
})
.catch(function (err) {
logger.error(err);
res.send('upload image error');
});
})
.catch(function (err) {
console.error(err);
res.send(err.message);
});
} catch (err) {
logger.error(err);
res.send('upload image error');
}
}
});
});
@ -345,6 +352,10 @@ app.post('/uploadimage', function (req, res) {
app.get("/new", response.newNote);
//get features
app.get("/features", response.showFeatures);
//get share note
app.get("/s/:shortid", response.showShareNote);
//share note actions
app.get("/s/:shortid/:action", response.shareNoteActions);
//get note by id
app.get("/:noteId", response.showNote);
//note actions
@ -370,10 +381,10 @@ io.sockets.on('connection', realtime.connection);
//listen
if (config.usessl) {
server.listen(config.sslport, function () {
console.log('HTTPS Server listening at sslport %d', config.sslport);
logger.info('HTTPS Server listening at sslport %d', config.sslport);
});
} else {
server.listen(config.port, function () {
console.log('HTTP Server listening at port %d', config.port);
logger.info('HTTP Server listening at port %d', config.port);
});
}

+ 0
- 0
backups/.keep View File


+ 1
- 1
config.js View File

@ -11,7 +11,7 @@ var urladdport = true; //add port on getserverurl
var config = {
debug: true,
version: '0.2.8',
version: '0.3.1',
domain: domain,
alloworigin: ['add here to allow origin to cross'],
testport: testport,


+ 11
- 0
hackmd View File

@ -0,0 +1,11 @@
/home/hackmd/logs/*.log {
daily
rotate 365
missingok
notifempty
compress
sharedscripts
copytruncate
dateext
dateformat %Y-%m-%d
}

+ 7
- 6
lib/auth.js View File

@ -7,17 +7,18 @@ var GithubStrategy = require('passport-github').Strategy;
var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
//core
var User = require('./user.js')
var config = require('../config.js')
var User = require('./user.js');
var config = require('../config.js');
var logger = require("./logger.js");
function callback(accessToken, refreshToken, profile, done) {
//console.log(profile.displayName || profile.username);
//logger.info(profile.displayName || profile.username);
User.findOrNewUser(profile.id, profile, function (err, user) {
if (err || user == null) {
console.log('auth callback failed: ' + err);
logger.error('auth callback failed: ' + err);
} else {
if(config.debug && user)
console.log('user login: ' + user._id);
if (config.debug && user)
logger.info('user login: ' + user._id);
done(null, user);
}
});


+ 18
- 17
lib/db.js View File

@ -6,6 +6,7 @@ var util = require('util');
//core
var config = require("../config.js");
var logger = require("./logger.js");
//public
var db = {
@ -48,18 +49,18 @@ function newToDB(id, owner, body, callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
var newnotequery = util.format(insertquery, id, owner, body);
//console.log(newnotequery);
//logger.info(newnotequery);
client.query(newnotequery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("new note to db failed: " + err);
return logger.error("new note to db failed: " + err);
} else {
if (config.debug)
console.log("new note to db success");
logger.info("new note to db success");
callback(null, result);
}
});
@ -72,22 +73,22 @@ function readFromDB(id, callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
var readquery = util.format(selectquery, id);
//console.log(readquery);
//logger.info(readquery);
client.query(readquery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("read from db failed: " + err);
return logger.error("read from db failed: " + err);
} else {
//console.log(result.rows);
//logger.info(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db: " + id, null);
} else {
if(config.debug)
console.log("read from db success");
logger.info("read from db success");
callback(null, result);
}
}
@ -101,18 +102,18 @@ function saveToDB(id, title, data, callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
var savequery = util.format(updatequery, title, data, id);
//console.log(savequery);
//logger.info(savequery);
client.query(savequery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("save to db failed: " + err);
return logger.error("save to db failed: " + err);
} else {
if (config.debug)
console.log("save to db success");
logger.info("save to db success");
callback(null, result);
}
});
@ -125,20 +126,20 @@ function countFromDB(callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
client.query(countquery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("count from db failed: " + err);
return logger.error("count from db failed: " + err);
} else {
//console.log(result.rows);
//logger.info(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db", null);
} else {
if(config.debug)
console.log("count from db success");
logger.info("count from db success");
callback(null, result);
}
}


+ 2
- 1
lib/logger.js View File

@ -7,7 +7,8 @@ var logger = new winston.Logger({
level: 'debug',
handleExceptions: true,
json: false,
colorize: true
colorize: true,
timestamp: true
})
],
exitOnError: false


+ 147
- 6
lib/note.js View File

@ -1,22 +1,56 @@
//note
//external modules
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var LZString = require('lz-string');
var marked = require('marked');
var cheerio = require('cheerio');
var shortId = require('shortid');
//others
var db = require("./db.js");
var logger = require("./logger.js");
//permission types
permissionTypes = ["freely", "editable", "locked"];
// create a note model
var model = mongoose.model('note', {
id: String,
shortid: {
type: String,
unique: true,
default: shortId.generate
},
permission: {
type: String,
enum: permissionTypes
},
viewcount: {
type: Number,
default: 0
},
updated: Date,
created: Date
});
//public
var note = {
model: model,
findNote: findNote,
newNote: newNote,
findOrNewNote: findOrNewNote,
checkNoteIdValid: checkNoteIdValid,
checkNoteExist: checkNoteExist,
getNoteTitle: getNoteTitle
getNoteTitle: getNoteTitle,
generateWebTitle: generateWebTitle,
increaseViewCount: increaseViewCount,
updatePermission: updatePermission
};
function checkNoteIdValid(noteId) {
try {
//console.log(noteId);
//logger.info(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;
@ -26,21 +60,21 @@ function checkNoteIdValid(noteId) {
else
return false;
} catch (err) {
console.error(err);
logger.error(err);
return false;
}
}
function checkNoteExist(noteId) {
try {
//console.log(noteId);
//logger.info(noteId);
var id = LZString.decompressFromBase64(noteId);
db.readFromDB(id, function (err, result) {
if (err) return false;
return true;
});
} catch (err) {
console.error(err);
logger.error(err);
return false;
}
}
@ -50,11 +84,118 @@ function getNoteTitle(body) {
var $ = cheerio.load(marked(body));
var h1s = $("h1");
var title = "";
if (h1s.length > 0)
if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
title = h1s.first().text();
else
title = "Untitled";
return title;
}
//generate note web page title
function generateWebTitle(title) {
title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
return title;
}
function findNote(id, callback) {
model.findOne({
$or: [
{
id: id
},
{
shortid: id
}
]
}, function (err, note) {
if (err) {
logger.error('find note failed: ' + err);
callback(err, null);
}
if (!err && note) {
callback(null, note);
} else {
logger.error('find note failed: ' + err);
callback(err, null);
};
});
}
function newNote(id, permission, callback) {
var note = new model({
id: id,
permission: permission,
updated: Date.now(),
created: Date.now()
});
note.save(function (err) {
if (err) {
logger.error('new note failed: ' + err);
callback(err, null);
} else {
logger.info("new note success: " + note.id);
callback(null, note);
};
});
}
function findOrNewNote(id, permission, callback) {
findNote(id, function (err, note) {
if (err || !note) {
newNote(id, permission, function (err, note) {
if (err) {
logger.error('find or new note failed: ' + err);
callback(err, null);
} else {
callback(null, note);
}
});
} else {
if (!note.permission) {
note.permission = permission;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('add note permission failed: ' + err);
callback(err, null);
} else {
logger.info("add note permission success: " + note.id);
callback(null, note);
};
});
} else {
callback(null, note);
}
}
});
}
function increaseViewCount(note, callback) {
note.viewcount++;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('increase note viewcount failed: ' + err);
callback(err, null);
} else {
logger.info("increase note viewcount success: " + note.id);
callback(null, note);
};
});
}
function updatePermission(note, permission, callback) {
note.permission = permission;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('update note permission failed: ' + err);
callback(err, null);
} else {
logger.info("update note permission success: " + note.id);
callback(null, note);
};
});
}
module.exports = note;

+ 234
- 94
lib/realtime.js View File

@ -9,9 +9,12 @@ var shortId = require('shortid');
var randomcolor = require("randomcolor");
var Chance = require('chance'),
chance = new Chance();
var md5 = require("blueimp-md5").md5;
var moment = require('moment');
//core
var config = require("../config.js");
var logger = require("./logger.js");
//others
var db = require("./db.js");
@ -49,7 +52,7 @@ function secure(socket, next) {
next(new Error('AUTH failed: No cookie transmitted.'));
}
if (config.debug)
console.log("AUTH success cookie: " + handshakeData.sessionID);
logger.info("AUTH success cookie: " + handshakeData.sessionID);
next();
} catch (ex) {
@ -65,9 +68,10 @@ var updater = setInterval(function () {
var note = notes[key];
if (note.isDirty) {
if (config.debug)
console.log("updater found dirty note: " + key);
logger.info("updater found dirty note: " + key);
var body = LZString.decompressFromUTF16(note.body);
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
db.saveToDB(key, title, body,
function (err, result) {});
@ -75,36 +79,45 @@ var updater = setInterval(function () {
}
callback();
}, function (err) {
if (err) return console.error('updater error', err);
if (err) return logger.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;
if (err) return logger.info(err);
var distinctaddresses = [];
var regaddresses = [];
var distinctregaddresses = [];
Object.keys(users).forEach(function (key) {
var value = users[key];
if (value.login)
regusers++;
var user = users[key];
var found = false;
for (var i = 0; i < distinctaddresses.length; i++) {
if (value.address == distinctaddresses[i]) {
if (user.address == distinctaddresses[i]) {
found = true;
break;
}
}
if (!found) {
distinctaddresses.push(value.address);
if (value.login)
distinctregusers++;
distinctaddresses.push(user.address);
}
if (user.login) {
regaddresses.push(user.address);
var found = false;
for (var i = 0; i < distinctregaddresses.length; i++) {
if (user.address == distinctregaddresses[i]) {
found = true;
break;
}
}
if (!found) {
distinctregaddresses.push(user.address);
}
}
});
User.getUserCount(function (err, regcount) {
if (err) {
console.log('get status failed: ' + err);
logger.error('get status failed: ' + err);
return;
}
if (callback)
@ -114,8 +127,8 @@ function getStatus(callback) {
distinctOnlineUsers: distinctaddresses.length,
notesCount: data.rows[0].count,
registeredUsers: regcount,
onlineRegisteredUsers: regusers,
distinctOnlineRegisteredUsers: distinctregusers
onlineRegisteredUsers: regaddresses.length,
distinctOnlineRegisteredUsers: distinctregaddresses.length
});
});
});
@ -146,23 +159,40 @@ function emitOnlineUsers(socket) {
if (user)
users.push(buildUserOutData(user));
});
notes[notename].socks.forEach(function (sock) {
var out = {
users: users
};
out = LZString.compressToUTF16(JSON.stringify(out));
sock.emit('online users', out);
});
var out = {
users: users
};
out = LZString.compressToUTF16(JSON.stringify(out));
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock && out)
sock.emit('online users', out);
};
}
function emitUserStatus(socket) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
notes[notename].socks.forEach(function (sock) {
var out = buildUserOutData(users[socket.id]);
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
var out = buildUserOutData(users[socket.id]);
sock.emit('user status', out);
}
};
}
function emitRefresh(socket) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var note = notes[notename];
socket.emit('refresh', {
owner: note.owner,
permission: note.permission,
body: note.body,
otk: note.otk,
hash: note.hash,
updatetime: note.updatetime
});
}
@ -175,9 +205,7 @@ 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
});
emitRefresh(socket);
//clear finished socket in queue
for (var i = 0; i < connectionSocketQueue.length; i++) {
@ -190,11 +218,11 @@ function finishConnection(socket, notename) {
startConnection(connectionSocketQueue[0]);
if (config.debug) {
console.log('SERVER connected a client to [' + notename + ']:');
console.log(JSON.stringify(users[socket.id]));
//console.log(notes);
logger.info('SERVER connected a client to [' + notename + ']:');
logger.info(JSON.stringify(users[socket.id]));
//logger.info(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
logger.info(JSON.stringify(data));
});
}
}
@ -219,17 +247,34 @@ function startConnection(socket) {
connectionSocketQueue.splice(i, 1);
}
isConnectionBusy = false;
return console.error(err);
return logger.error(err);
}
var body = LZString.decompressFromBase64(data.rows[0].content);
body = LZString.compressToUTF16(body);
notes[notename] = {
socks: [],
body: body,
isDirty: false,
users: {}
};
finishConnection(socket, notename);
var owner = data.rows[0].owner;
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
Note.findOrNewNote(notename, permission, function (err, note) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
body = LZString.compressToUTF16(body);
var updatetime = data.rows[0].update_time;
notes[notename] = {
owner: owner,
permission: note.permission,
socks: [],
body: body,
isDirty: false,
users: {},
otk: shortId.generate(),
hash: md5(body),
updatetime: moment(updatetime).valueOf()
};
finishConnection(socket, notename);
});
});
} else {
finishConnection(socket, notename);
@ -241,8 +286,8 @@ function disconnect(socket) {
isDisconnectBusy = true;
if (config.debug) {
console.log("SERVER disconnected a client");
console.log(JSON.stringify(users[socket.id]));
logger.info("SERVER disconnected a client");
logger.info(JSON.stringify(users[socket.id]));
}
var notename = getNotenameFromSocket(socket);
if (!notename) return;
@ -251,24 +296,31 @@ function disconnect(socket) {
}
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);
}
do {
var index = notes[notename].socks.indexOf(socket);
if (index != -1) {
notes[notename].socks.splice(index, 1);
}
} while (index != -1);
if (Object.keys(notes[notename].users).length <= 0) {
var body = LZString.decompressFromUTF16(notes[notename].body);
var title = Note.getNoteTitle(body);
body = LZString.compressToBase64(body);
db.saveToDB(notename, title, body,
function (err, result) {
delete notes[notename];
if (config.debug) {
//console.log(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
});
}
});
if (notes[notename].isDirty) {
var body = LZString.decompressFromUTF16(notes[notename].body);
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
db.saveToDB(notename, title, body,
function (err, result) {
delete notes[notename];
if (config.debug) {
//logger.info(notes);
getStatus(function (data) {
logger.info(JSON.stringify(data));
});
}
});
} else {
delete notes[notename];
}
}
}
emitOnlineUsers(socket);
@ -284,9 +336,9 @@ function disconnect(socket) {
disconnect(disconnectSocketQueue[0]);
if (config.debug) {
//console.log(notes);
//logger.info(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
logger.info(JSON.stringify(data));
});
}
}
@ -309,6 +361,24 @@ function updateUserData(socket, user) {
//retrieve user data from passport
if (socket.request.user && socket.request.user.logged_in) {
var profile = JSON.parse(socket.request.user.profile);
/*
var photo = null;
switch(profile.provider) {
case "facebook":
console.log(profile);
break;
case "twitter":
photo = profile.photos[0];
break;
case "github":
photo = profile.avatar_url;
break;
case "dropbox":
//not image api provided
break;
}
user.photo = photo;
*/
user.name = profile.displayName || profile.username;
user.userid = socket.request.user._id;
user.login = true;
@ -353,7 +423,6 @@ function connection(socket) {
id: socket.id,
address: socket.handshake.address,
'user-agent': socket.handshake.headers['user-agent'],
otk: shortId.generate(),
color: color,
cursor: null,
login: false,
@ -368,24 +437,41 @@ function connection(socket) {
connectionSocketQueue.push(socket);
startConnection(socket);
//when a new client coming or received a client refresh request
socket.on('refresh', function (body_) {
//received client refresh request
socket.on('refresh', function () {
emitRefresh(socket);
});
//received client data updated
socket.on('update', function (body_) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (!notename || !notes[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;
logger.info('SERVER received [' + notename + '] data updated: ' + socket.id);
var note = notes[notename];
if (note.body != body_) {
note.body = body_;
note.hash = md5(body_);
note.updatetime = Date.now();
note.isDirty = true;
}
var out = {
id: socket.id,
hash: note.hash,
updatetime: note.updatetime
};
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.emit('check', out);
};
});
//received user status
socket.on('user status', function (data) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (!notename || !notes[notename]) return;
if (config.debug)
console.log('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
logger.info('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
if (data) {
var user = users[socket.id];
user.idle = data.idle;
@ -394,9 +480,40 @@ function connection(socket) {
emitUserStatus(socket);
});
//received note permission change request
socket.on('permission', function (permission) {
//need login to do more actions
if (socket.request.user && socket.request.user.logged_in) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var note = notes[notename];
//Only owner can change permission
if (note.owner == socket.request.user._id) {
note.permission = permission;
Note.findNote(notename, function (err, _note) {
if (err || !_note) {
return;
}
Note.updatePermission(_note, permission, function (err, _note) {
if (err || !_note) {
return;
}
var out = {
permission: permission
};
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.emit('permission', out);
};
});
});
}
}
});
//reveiced when user logout or changed
socket.on('user changed', function () {
console.log('user changed');
logger.info('user changed');
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
updateUserData(socket, notes[notename].users[socket.id]);
@ -431,11 +548,12 @@ function connection(socket) {
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
notes[notename].socks.forEach(function (sock) {
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
sock.emit('cursor focus', out);
}
});
};
});
//received cursor activity
@ -444,11 +562,12 @@ function connection(socket) {
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
notes[notename].socks.forEach(function (sock) {
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
sock.emit('cursor activity', out);
}
});
};
});
//received cursor blur
@ -459,13 +578,12 @@ function connection(socket) {
var out = {
id: socket.id
};
notes[notename].socks.forEach(function (sock) {
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
if (sock != socket) {
sock.emit('cursor blur', out);
}
sock.emit('cursor blur', out);
}
});
};
});
//when a new client disconnect
@ -477,12 +595,30 @@ function connection(socket) {
//when received client change data request
socket.on('change', function (op) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (!notename || !notes[notename]) return;
var note = notes[notename];
switch (note.permission) {
case "freely":
//not blocking anyone
break;
case "editable":
//only login user can change
if (!socket.request.user || !socket.request.user.logged_in)
return;
break;
case "locked":
//only owner can change
if (note.owner != socket.request.user._id)
return;
break;
}
op = LZString.decompressFromUTF16(op);
if (op)
op = JSON.parse(op);
else
return;
if (config.debug)
console.log('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
logger.info('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
switch (op.origin) {
case '+input':
case '+delete':
@ -499,16 +635,20 @@ function connection(socket) {
case '+joinLines':
case '+duplicateLine':
case '+sortLines':
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.compressToUTF16(JSON.stringify(op)));
}
});
op.id = socket.id;
op.otk = note.otk;
op.nextotk = note.otk = shortId.generate();
var stringop = JSON.stringify(op);
var compressstringop = LZString.compressToUTF16(stringop);
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
if (config.debug)
logger.info('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + stringop);
sock.emit('change', compressstringop);
};
break;
default:
console.log('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
default:
logger.info('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
}
});
}


+ 158
- 19
lib/response.js View File

@ -6,6 +6,8 @@ var path = require('path');
var uuid = require('node-uuid');
var markdownpdf = require("markdown-pdf");
var LZString = require('lz-string');
var S = require('string');
var shortId = require('shortid');
//core
var config = require("../config.js");
@ -31,16 +33,20 @@ var response = {
newNote: newNote,
showFeatures: showFeatures,
showNote: showNote,
noteActions: noteActions
showShareNote: showShareNote,
noteActions: noteActions,
shareNoteActions: shareNoteActions
};
function responseError(res, code, detail, msg) {
res.writeHead(code, {
'Content-Type': 'text/html'
});
var content = ejs.render(fs.readFileSync(config.errorpath, 'utf8'), {
var template = config.errorpath;
var content = ejs.render(fs.readFileSync(template, 'utf8'), {
title: code + ' ' + detail + ' ' + msg,
cache: !config.debug,
filename: config.errorpath,
filename: template,
code: code,
detail: detail,
msg: msg
@ -49,16 +55,44 @@ function responseError(res, code, detail, msg) {
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
function responseHackMD(res, 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;
}
}
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var title = data.rows[0].title;
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
title = Note.generateWebTitle(title);
var template = config.hackmdpath;
var options = {
cache: !config.debug,
filename: template
};
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
var html = compiled({
title: title
});
var buf = html;
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8',
'Cache-Control': 'private',
'Content-Length': buf.length
});
res.end(buf);
});
res.write(content);
res.end();
}
function newNote(req, res, next) {
@ -88,10 +122,10 @@ function showFeatures(req, res, next) {
responseError(res, "500", "Internal Error", "wtf.");
return;
}
responseHackMD(res);
responseHackMD(res, config.featuresnotename);
});
} else {
responseHackMD(res);
responseHackMD(res, config.featuresnotename);
}
});
}
@ -102,22 +136,105 @@ function showNote(req, res, next) {
responseError(res, "404", "Not Found", "oops.");
return;
}
responseHackMD(res);
responseHackMD(res, noteId);
}
function showShareNote(req, res, next) {
var shortid = req.params.shortid;
if (shortId.isValid(shortid)) {
Note.findNote(shortid, function (err, note) {
if (err || !note) {
responseError(res, "404", "Not Found", "oops.");
return;
}
//increase note viewcount
Note.increaseViewCount(note, function (err, note) {
if (err || !note) {
responseError(res, "404", "Not Found", "oops.");
return;
}
db.readFromDB(note.id, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
var updatetime = data.rows[0].update_time;
var text = S(body).escapeHTML().s;
var title = data.rows[0].title;
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
title = Note.generateWebTitle(title);
var template = config.prettypath;
var options = {
cache: !config.debug,
filename: template
};
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
var origin = "//" + req.headers.host;
var html = compiled({
title: title,
viewcount: note.viewcount,
updatetime: updatetime,
url: origin,
body: text
});
var buf = html;
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8',
'Cache-Control': 'private',
'Content-Length': buf.length
});
res.end(buf);
});
});
});
} else {
responseError(res, "404", "Not Found", "oops.");
}
}
function actionShare(req, res, noteId) {
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var owner = data.rows[0].owner;
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
Note.findOrNewNote(noteId, permission, function (err, note) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
res.redirect("/s/" + note.shortid);
});
});
}
//pretty api is deprecated
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 body = LZString.decompressFromBase64(data.rows[0].content);
var text = S(body).escapeHTML().s;
var title = data.rows[0].title;
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
title = Note.generateWebTitle(title);
var template = config.prettypath;
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'));
var origin = "//" + req.headers.host;
var html = compiled({
title: title,
url: origin,
body: body
body: text
});
var buf = html;
res.writeHead(200, {
@ -190,8 +307,9 @@ function noteActions(req, res, next) {
}
var action = req.params.action;
switch (action) {
case "pretty":
actionPretty(req, res, noteId);
case "share":
case "pretty": //pretty deprecated
actionShare(req, res, noteId);
break;
case "download":
actionDownload(req, res, noteId);
@ -208,4 +326,25 @@ function noteActions(req, res, next) {
}
}
function shareNoteActions(req, res, next) {
var action = req.params.action;
switch (action) {
case "edit":
var shortid = req.params.shortid;
if (shortId.isValid(shortid)) {
Note.findNote(shortid, function (err, note) {
if (err || !note) {
responseError(res, "404", "Not Found", "oops.");
return;
}
if (note.id != config.featuresnotename)
res.redirect('/' + LZString.compressToBase64(note.id));
else
res.redirect('/' + note.id);
});
}
break;
}
}
module.exports = response;

+ 7
- 6
lib/temp.js View File

@ -4,6 +4,7 @@ var mongoose = require('mongoose');
//core
var config = require("../config.js");
var logger = require("./logger.js");
// create a temp model
var model = mongoose.model('temp', {
@ -33,13 +34,13 @@ function findTemp(id, callback) {
id: id
}, function (err, temp) {
if (err) {
console.log('find temp failed: ' + err);
logger.error('find temp failed: ' + err);
callback(err, null);
}
if (!err && temp) {
callback(null, temp);
} else {
console.log('find temp failed: ' + err);
logger.error('find temp failed: ' + err);
callback(<