nodebb-plugin-smoothshorts
Version:
Seamless short URLs for topic and post links in NodeBB.
151 lines (135 loc) • 4.53 kB
JavaScript
;
var async = require.main.require('async');
var nconf = require.main.require('nconf');
var winston = require.main.require('winston');
var db = require.main.require('./src/database');
var xxh = require('xxhash');
var hashing = {};
var hashType = {
topics: 'tid',
posts: 'pid'
};
hashing.getHashForId = function(id, type, cb) {
db.getSortedSetRangeByScore(type + ':smoothshorts', 0, -1, id, id, function(err, hash) {
if (err) {
return cb(err);
}
var res = { hash: hash[0] };
res[hashType[type]] = id;
cb(null, res);
});
};
hashing.getHashDelegate = function(type) {
return function(id, cb) {
hashing.getHashForId.call(this, id, type, cb);
};
};
hashing.shortenPost = function(postData, cb) {
shorten(postData, 'posts', cb);
};
hashing.shortenTopic = function(topicData, cb) {
shorten(topicData, 'topics', cb);
};
hashing.purgePost = function(id) {
purge(id, 'posts');
};
hashing.purgeTopic = function(id) {
purge(id, 'topics');
};
hashing.resolveHash = function(req, res, cb) {
var hash = req.params.hash;
async.parallel({
isPost: function isPost(next) {
db.isSortedSetMember('posts:smoothshorts', hash, next);
},
isTopic: function isTopic(next) {
db.isSortedSetMember('topics:smoothshorts', hash, next);
}
}, function(err, result) {
if (err) {
return cb(err);
} else if (result.isPost) {
async.waterfall([function(next) {
db.sortedSetScore('posts:smoothshorts', hash, next);
}, function(pid, next) {
db.getObjectField('post:' + pid, 'tid', function(err, tid) {
if (err) {
return cb(err);
}
return next(null, { pid: pid, tid: tid });
});
}, function(data, next) {
async.parallel({
rank: function rank(next) {
db.sortedSetRank('tid:' + data.tid + ':posts', data.pid, next);
},
slug: function slug(next) {
db.getObjectField('topic:' + data.tid, 'slug', next);
}
}, function(err, result) {
if (err) {
return cb(err);
}
var pIndex = result.rank !== null ? (parseInt(result.rank, 10) + 2).toString() : '1';
var uri = '/topic/' + result.slug + '/' + pIndex;
next(null, uri);
});
}], function(err, uri) {
if (err) {
return cb(err);
}
winston.verbose('[plugins:SmoothShorts] Redirecting ' + hash + ' to ' + uri);
res.redirect(uri);
});
} else if (result.isTopic) {
/* if this hash points to a topic */
async.waterfall([function(next) {
db.sortedSetScore('topics:smoothshorts', hash, next);
}, function(tid, next) {
db.getObjectField('topic:' + tid, 'slug', next);
}], function(err, slug) {
if (err) {
return cb(err);
}
// build URI
var uri = '/topic/' + slug + '/';
winston.verbose('[plugins:SmoothShorts] Redirecting ' + hash + ' to ' + uri);
// redirect user to URI
res.redirect(uri);
});
} else {
winston.warn('[plugins:SmoothShorts] Couldn\'t resolve ' + hash);
cb(null);
}
});
};
function shorten(data, type, cb) {
var hash = hashData(data);
db.sortedSetAdd(type + ':smoothshorts', data[hashType[type]], hash, function(err) {
if (err) {
winston.error('[plugin:smoothshorts] Writing hash to DB failed.' + '(' + hashType[type] + '=' + data[hashType[type]] + ')');
winston.error(err);
return cb ? cb(err) : void 0;
}
winston.verbose(type === 'posts' ? '[plugin:smoothshorts] Hashed post ID ' + data.pid : '[plugin:smoothshorts] Hashed \'' + data.title + '\' (ID: ' + data.tid + ')');
return cb ? cb() : void 0;
});
}
function purge(id, type) {
db.sortedSetsRemoveRangeByScore([type + ':smoothshorts'], id, id, function(err) {
if (err) {
return winston.error('[plugin:smoothshorts] Deleting hash from DB failed.' + '(' + hashType[type] + '=' + id + ')');
}
winston.verbose('[plugin:smoothshorts] Deleted hash for ' + type.substring(0, type.length - 1) + ' ID ' + id);
});
}
function hashData(data) {
var key = nconf.get('secret');
key = parseInt('0x' + key.substring(0, key.indexOf('-')), 16);
var hash = xxh.hash(new Buffer(JSON.stringify(data)), key).toString(16);
// don't take any chances of leaking the secret around;
// not even just parts of it. ;)
key = null;
return hash;
}
module.exports = hashing;