Skip to content

Commit

Permalink
Store replies to posts, show under the post body
Browse files Browse the repository at this point in the history
First step towards #35.

Also deletes the boolean/array portions when doing export, since these
aren't supported by manual CSV.

Todo:

- Clean up reply links on post deletion
- Allow target post author to delete reply links
- Add a "reply" link under the post to encourage replying
- Pagination?  Top-level "my replies" tab?
- Enable/disable reply links per-post

It'd be nice to be able to block reply links from a specific user
entirely.  (Ie, maybe they're not bannable, but they are annoying, more
like a mute feature.)  The user would still be able to post the reply
link, but it wouldn't show up anywhere.
  • Loading branch information
isaacs committed Jan 7, 2015
1 parent 56cbf6c commit a12dac5
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 21 deletions.
153 changes: 144 additions & 9 deletions lib/posts.js
Expand Up @@ -7,6 +7,7 @@ var conf = require('./conf');

var crypto = require('crypto');
var utils = require('./utils');
var url = require('url');

var MAX_POSTS = 10;

Expand All @@ -16,6 +17,43 @@ var getTime = function () {
return Math.floor(Date.now() / 1000);
};

var internalLinkRe = function () {
// The better to inline you with...
var key = '(post![0-9]+-[0-9a-f]+|user![0-9a-f-]{36}![0-9]+-[0-9a-f]+)';
var pattern = 'https?://[^/\\s]+/post/' + key;
return new RegExp(pattern);
};

// find replies that look like links to other posts in the system.
// to be "internal" must have this hostname
var getInternalLinks = function (reply, host) {
if (!host) return false;

reply = reply.trim();
if (!reply) return false;

var urls = reply.trim().split(/\s+/);

var internalLinks = urls.map(function (r) {
var parsed = r.match(internalLinkRe());
if (!parsed) return false;

var u = url.parse(parsed[0]);
if (u.host === host) {
// internal link. Just save the postid.
return parsed[1].replace(/^(post!|user![^!]+!)(.*)$/, '$2');
} else {
console.error('not an internal link host=%j u=%j', host, u);
}

return false;
}).filter(function (r) {
return r;
});

return internalLinks;
}

exports.add = function (request, reply) {
var time = getTime();
var uid = request.session.get('uid');
Expand All @@ -34,10 +72,13 @@ exports.add = function (request, reply) {
return reply(Boom.wrap(err, 400));
}

var host = request.headers.host;

var postItem = {
uid: uid,
name: name,
created: time,
replyto: getInternalLinks(request.payload.reply, host),
reply: utils.autoLink(request.payload.reply) || '',
content: utils.autoLink(request.payload.content, {
htmlEscapeNonEntities: true,
Expand All @@ -47,18 +88,31 @@ exports.add = function (request, reply) {

var postid = time + '-' + crypto.randomBytes(1).toString('hex');

var done = function (err) {
if (err) {
return reply(Boom.wrap(err, 400));
}
reply.redirect('/posts');
};

var savePost = function () {
db.put('user!' + request.session.get('uid') + '!' + postid, postItem, function (err) {
if (err) {
return reply(Boom.wrap(err, 400));
return done(err);
}

db.put('post!' + postid, postItem, function (err) {
if (err) {
return reply(Boom.wrap(err, 400));
return done(err);
}

reply.redirect('/posts');
saveReply(postItem, function (err) {
if (err) {
return done(err);
}

reply.redirect('/posts');
});
});
});
};
Expand All @@ -84,6 +138,44 @@ exports.add = function (request, reply) {
getId();
};

var saveReply = function (postItem, next) {
if (!postItem.replyto) {
return process.nextTick(next);
}

var count = postItem.replyto.length;
if (!count) {
return process.nextTick(next);
}

var postid = postItem.postid;

postItem.replyto.forEach(function (target) {
// content and replies get big. We just need a few basics.
// TODO(isaacs): Look up target post to make sure it's valid.
// TODO(isaacs): put target UID on this object for moderation privs.
var replyItem = {
uid: postItem.uid,
name: postItem.name,
created: postItem.created
};

db.put('replyto!' + target + '!' + postid, replyItem, function (err) {
then(err);
});
});

var error;
var then = function (err) {
if (err) {
error = err;
}
if (--count <= 0) {
return next(error);
}
};
}

var setDate = function (created) {
return moment(created * 1000).format('MMM Do, YYYY - HH:mm a');
};
Expand Down Expand Up @@ -295,6 +387,7 @@ exports.getRecentForUser = function (uid, request, next) {
};

exports.del = function (request, reply) {
// TODO(isaacs): delete replies to and from this post.
if (request.session && (request.session.get('uid') === request.payload.uid) || request.session.get('op')) {
var keyArr = request.params.key.split('!');
var time = keyArr[keyArr.length - 1];
Expand Down Expand Up @@ -328,12 +421,54 @@ exports.get = function (request, reply) {

post.created = setDate(post.created);

reply.view('post', {
analytics: conf.get('analytics'),
id: request.params.key,
session: request.session.get('uid') || false,
op: request.session.get('op'),
post: post
getReplyPosts(post, function (err, post) {
if (err) {
return reply(Boom.wrap(err, 404));
}

reply.view('post', {
analytics: conf.get('analytics'),
id: request.params.key,
session: request.session.get('uid') || false,
op: request.session.get('op'),
post: post
});
});
});
};

// TODO(isaacs): Pagination. Maybe only show the first 100 here,
// but have them all in a top-level "replies" tab?
var getReplyPosts = function (post, next) {
var streamOpt = {
gte: 'replyto!' + post.postid,
lte: 'replyto!' + post.postid + '\xff',
limit: 100
};

var replies = [];
var rs = db.createReadStream(streamOpt);

rs.on('data', function (reply) {
var val = reply.value;
var key = reply.key;

var reply = key.match(/^replyto![^!]+!([^!]+)$/);
if (!reply) return;
reply = reply[1];
if (!reply) return;

var html = '\n<a href="/post/post!' + reply + '" rel="nofollow">' +
setDate(val.created) + '</a> - ' +
'<a href="/user/' + val.uid + '">' + val.name + '</a>';

replies.push(html);
});

rs.on('end', function () {
post.replies = replies.join('');
next(null, post);
});

rs.on('error', next);
};
3 changes: 3 additions & 0 deletions lib/profile.js
Expand Up @@ -304,6 +304,9 @@ exports.exportPosts = function (request, reply) {
if (!post.value.postid) {
post.value.postid = String(post.value.created);
}
// Manual CSV encoding doesn't handle arrays or booleans
delete post.value.replyto;
delete post.value.showreplies;
return post.value;
});

Expand Down
1 change: 1 addition & 0 deletions public/styl/global.styl
Expand Up @@ -434,6 +434,7 @@ article .posted {
.content article p.reply {
font-style: italic;
margin-bottom: 0;
margin-top: 15px;
}

.delete-account {
Expand Down

0 comments on commit a12dac5

Please sign in to comment.