diff --git a/app.js b/app.js index 7507ee4ac1..8defae9ab2 100644 --- a/app.js +++ b/app.js @@ -60,7 +60,7 @@ nconf.file({ file: __dirname + '/config.json' }); - meta = require('./src/meta.js'); + meta = require('./src/meta'); nconf.set('url', nconf.get('base_url') + (nconf.get('use_port') ? ':' + nconf.get('port') : '') + nconf.get('relative_path') + '/'); nconf.set('upload_url', nconf.get('url') + 'uploads/'); @@ -73,58 +73,53 @@ winston.info('Base Configuration OK.'); } - meta.configs.init(function () { - // Initial setup for Redis & Reds - var reds = require('reds'), - RDB = require('./src/redis.js'); - - reds.createClient = function () { - return reds.client || (reds.client = RDB); - }; - - var templates = require('./public/src/templates.js'), - translator = require('./public/src/translator.js'), - webserver = require('./src/webserver.js'), - SocketIO = require('socket.io').listen(global.server, { log: false, transports: ['websocket', 'xhr-polling', 'jsonp-polling', 'flashsocket'], 'browser client minification': true}), - websockets = require('./src/websockets.js'), - posts = require('./src/posts.js'), - plugins = require('./src/plugins'), // Don't remove this - plugins initializes itself - Notifications = require('./src/notifications'), - Upgrade = require('./src/upgrade'); - - Upgrade.check(function(schema_ok) { - if (schema_ok || nconf.get('check-schema') === false) { - websockets.init(SocketIO); - - global.templates = {}; - global.translator = translator; - - translator.loadServer(); - - var customTemplates = meta.config['theme:templates'] ? path.join(__dirname, 'node_modules', meta.config['theme:id'], meta.config['theme:templates']) : false; - - // todo: replace below with read directory code, derp. - templates.init([ - 'header', 'footer', 'logout', 'outgoing', 'admin/header', 'admin/footer', 'admin/index', - 'emails/reset', 'emails/reset_plaintext', 'emails/email_confirm', 'emails/email_confirm_plaintext', - 'emails/header', 'emails/footer', - - 'noscript/header', 'noscript/home', 'noscript/category', 'noscript/topic' - ], customTemplates); - - - plugins.ready(function() { - templates.ready(webserver.init); - }); - - Notifications.init(); - } else { - winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:'); - winston.warn(' node app --upgrade'); - winston.warn('To ignore this error (not recommended):'); - winston.warn(' node app --no-check-schema') - process.exit(); - } + require('./src/database').init(function(err) { + meta.configs.init(function () { + + var templates = require('./public/src/templates'), + translator = require('./public/src/translator'), + webserver = require('./src/webserver'), + SocketIO = require('socket.io').listen(global.server, { log: false, transports: ['websocket', 'xhr-polling', 'jsonp-polling', 'flashsocket'], 'browser client minification': true}), + websockets = require('./src/websockets'), + plugins = require('./src/plugins'), + notifications = require('./src/notifications'), + upgrade = require('./src/upgrade'); + + upgrade.check(function(schema_ok) { + if (schema_ok || nconf.get('check-schema') === false) { + websockets.init(SocketIO); + + plugins.init(); + global.templates = {}; + global.translator = translator; + + translator.loadServer(); + + var customTemplates = meta.config['theme:templates'] ? path.join(__dirname, 'node_modules', meta.config['theme:id'], meta.config['theme:templates']) : false; + + // todo: replace below with read directory code, derp. + templates.init([ + 'header', 'footer', 'logout', 'outgoing', 'admin/header', 'admin/footer', 'admin/index', + 'emails/reset', 'emails/reset_plaintext', 'emails/email_confirm', 'emails/email_confirm_plaintext', + 'emails/header', 'emails/footer', + + 'noscript/header', 'noscript/home', 'noscript/category', 'noscript/topic' + ], customTemplates); + + + plugins.ready(function() { + templates.ready(webserver.init); + }); + + notifications.init(); + } else { + winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:'); + winston.warn(' node app --upgrade'); + winston.warn('To ignore this error (not recommended):'); + winston.warn(' node app --no-check-schema') + process.exit(); + } + }); }); }); } else if (nconf.get('setup') || nconf.get('install') || !fs.existsSync(__dirname + '/config.json')) { @@ -159,10 +154,12 @@ nconf.file({ file: __dirname + '/config.json' }); - meta = require('./src/meta.js'); + require('./src/database').init(function(err) { + meta = require('./src/meta.js'); - meta.configs.init(function () { - require('./src/upgrade').upgrade(); + meta.configs.init(function () { + require('./src/upgrade').upgrade(); + }); }); } else/* if (nconf.get('help') */{ winston.info('Usage: node app [options] [arguments]'); diff --git a/mocks/databasemock.js b/mocks/databasemock.js new file mode 100644 index 0000000000..89e5f850e7 --- /dev/null +++ b/mocks/databasemock.js @@ -0,0 +1,81 @@ +/** + * Database Mock - wrapper for database.js, makes system use separate test db, instead of production + * ATTENTION: testing db is flushed before every use! + */ + +(function(module) { + 'use strict'; + + var utils = require('./../public/src/utils.js'), + path = require('path'), + nconf = require('nconf'), + winston = require('winston'), + errorText; + + + nconf.file({ file: path.join(__dirname, '../config.json') }); + + var dbType = nconf.get('database'), + testDbConfig = nconf.get('test_database'), + productionDbConfig = nconf.get(dbType); + + if(!testDbConfig){ + errorText = 'test_database is not defined'; + winston.info( + "\n===========================================================\n"+ + "Please, add parameters for test database in config.json\n"+ + "For example (redis):\n"+ + '"test_database": {' + '\n' + + ' "host": "127.0.0.1",' + '\n' + + ' "port": "6379",' + '\n' + + ' "password": "",' + '\n' + + ' "database": "1"' + '\n' + + '}\n'+ + " or (mongo):\n" + + '"test_database": {' + '\n' + + ' "host": "127.0.0.1",' + '\n' + + ' "port": "27017",' + '\n' + + ' "password": "",' + '\n' + + ' "database": "1"' + '\n' + + '}\n'+ + "===========================================================" + ); + winston.error(errorText); + throw new Error(errorText); + } + + if( testDbConfig.database === productionDbConfig.database && + testDbConfig.host === productionDbConfig.host && + testDbConfig.port === productionDbConfig.port + ){ + errorText = 'test_database has the same config as production db'; + winston.error(errorText); + throw new Error(errorText); + } + + nconf.set(dbType, testDbConfig); + + db = require('../src/database'); + before(function(done) { + + db.init(function(err) { + //Clean up + db.flushdb(function(err) { + if(err){ + winston.error(err); + throw new Error(err); + } else { + winston.info('test_database flushed'); + done(); + } + + //TODO: data seeding, if needed at all + + + }); + }); + }); + + module.exports = db; + +}(module)); diff --git a/mocks/redismock.js b/mocks/redismock.js deleted file mode 100644 index cfdbb4d9fe..0000000000 --- a/mocks/redismock.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Redis Mock - wrapper for redis.js, makes system use separate test db, instead of production - * ATTENTION: testing db is flushed before every use! - */ - -(function(module) { - 'use strict'; - - var RedisDB, - redis = require('redis'), - utils = require('./../public/src/utils.js'), - path = require('path'), - nconf = require('nconf'), - winston = require('winston'), - errorText; - - - nconf.file({ file: path.join(__dirname, '../config.json') }); - - var testDbConfig = nconf.get('redis_test'), - productionDbConfig = nconf.get('redis'); - if(!testDbConfig){ - errorText = 'redis_test database is not defined'; - winston.info( - "\n===========================================================\n"+ - "Please, add parameters for test database in config.json\n"+ - "For example:\n"+ - '"redis_test": {' + '\n' + - ' "host": "127.0.0.1",' + '\n' + - ' "port": "6379",' + '\n' + - ' "password": "",' + '\n' + - ' "database": "1"' + '\n' + - '}\n'+ - "===========================================================" - ); - winston.error(errorText); - throw new Error(errorText); - } - - if( testDbConfig.database === productionDbConfig.database && - testDbConfig.host === productionDbConfig.host && - testDbConfig.port === productionDbConfig.port - ){ - errorText = 'redis_test database has the same config as production db'; - winston.error(errorText); - throw new Error(errorText); - } - - nconf.set('redis',testDbConfig); - - RedisDB = require('../src/redis.js'); - - - //Clean up - RedisDB.send_command('flushdb', [], function(error){ - if(error){ - winston.error(error); - throw new Error(error); - } else { - winston.info('redis_test db flushed'); - } - }); - - //TODO: data seeding, if needed at all - - - module.exports = RedisDB; - -}(module)); diff --git a/package.json b/package.json index 4710b9daa0..af257f5775 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "dependencies": { "socket.io": "~0.9.16", "redis": "0.8.3", + "mongodb": "1.3.20", "express": "3.2.0", "express-namespace": "~0.1.1", "emailjs": "0.3.4", "cookie": "0.0.6", "connect-redis": "1.4.5", + "connect-mongo": "0.4.0", "passport": "0.1.17", "passport-local": "0.1.6", "passport-twitter": "0.1.5", diff --git a/public/src/forum/admin/settings.js b/public/src/forum/admin/settings.js index 6c75e6457a..989e94a79c 100644 --- a/public/src/forum/admin/settings.js +++ b/public/src/forum/admin/settings.js @@ -32,7 +32,7 @@ define(['uploader'], function(uploader) { break; case 'checkbox': - fields[x].checked = app.config[key] === '1' ? true : false; + fields[x].checked = parseInt(app.config[key], 10) === 1; break; } } diff --git a/public/src/forum/category.js b/public/src/forum/category.js index 77daf9994c..44a11eaea9 100644 --- a/public/src/forum/category.js +++ b/public/src/forum/category.js @@ -140,10 +140,11 @@ define(function () { jQuery('#topics-container, .category-sidebar').removeClass('hidden'); jQuery('#category-no-topics').remove(); + html = $(html); container.append(html); $('#topics-container span.timeago').timeago(); - app.makeNumbersHumanReadable($(html).find('.human-readable-number')); + app.makeNumbersHumanReadable(html.find('.human-readable-number')); } Category.loadMoreTopics = function(cid) { diff --git a/public/src/forum/recent.js b/public/src/forum/recent.js index 4614c2f404..6b162db34f 100644 --- a/public/src/forum/recent.js +++ b/public/src/forum/recent.js @@ -89,9 +89,10 @@ define(function() { $('#category-no-topics').remove(); + html = $(html); container.append(html); $('span.timeago').timeago(); - app.makeNumbersHumanReadable($(html).find('.human-readable-number')); + app.makeNumbersHumanReadable(html.find('.human-readable-number')); } Recent.loadMoreTopics = function() { diff --git a/public/src/forum/unread.js b/public/src/forum/unread.js index 60c4ba4953..6debeeba84 100644 --- a/public/src/forum/unread.js +++ b/public/src/forum/unread.js @@ -79,9 +79,10 @@ define(function() { $('#category-no-topics').remove(); + html = $(html); container.append(html); $('span.timeago').timeago(); - app.makeNumbersHumanReadable($(html).find('.human-readable-number')); + app.makeNumbersHumanReadable(html.find('.human-readable-number')); } function loadMoreTopics() { diff --git a/public/templates/admin/redis.tpl b/public/templates/admin/database.tpl similarity index 62% rename from public/templates/admin/redis.tpl rename to public/templates/admin/database.tpl index aa127ef893..40feb65c12 100644 --- a/public/templates/admin/redis.tpl +++ b/public/templates/admin/database.tpl @@ -1,3 +1,6 @@ + + +

Redis


@@ -21,4 +24,29 @@ Keyspace Hits {keyspace_hits}
Keyspace Misses {keyspace_misses}
+
+

Raw Info

+
+
{raw}
+
+ + +

Mongo

+
+
+ + Collections {collections}
+ Objects {objects}
+ Avg. Object Size {avgObjSize} kb
+
+ Data Size {dataSize} kb
+ Storage Size {storageSize} kb
+ File Size {fileSize} kb
+
+
+

Raw Info

+
+
{raw}
+
+ diff --git a/public/templates/admin/header.tpl b/public/templates/admin/header.tpl index 786b8bcfaa..66ef0fe982 100644 --- a/public/templates/admin/header.tpl +++ b/public/templates/admin/header.tpl @@ -105,7 +105,7 @@
  • Themes
  • Plugins
  • Settings
  • -
  • Redis
  • +
  • Database
  • Logger
  • MOTD
  • diff --git a/public/templates/config.json b/public/templates/config.json index fa435245ae..45e922c3df 100644 --- a/public/templates/config.json +++ b/public/templates/config.json @@ -4,7 +4,7 @@ "^admin/topics.*": "admin/topics", "^admin/categories.*": "admin/categories", "^admin/users.*": "admin/users", - "^admin/redis.*": "admin/redis", + "^admin/database.*": "admin/database", "^admin/index.*": "admin/index", "^admin/themes.*": "admin/themes", "^admin/plugins/?$": "admin/plugins", diff --git a/src/admin/categories.js b/src/admin/categories.js index 494a767e9c..333a22cf70 100644 --- a/src/admin/categories.js +++ b/src/admin/categories.js @@ -1,4 +1,4 @@ -var RDB = require('./../redis'), +var db = require('./../database'), utils = require('./../../public/src/utils'), categories = require('./../categories'); @@ -11,12 +11,12 @@ var RDB = require('./../redis'), var category = modified[cid]; for (var key in category) { - RDB.hset('category:' + cid, key, category[key]); + db.setObjectField('category:' + cid, key, category[key]); if (key == 'name') { // reset slugs if name is updated var slug = cid + '/' + utils.slugify(category[key]); - RDB.hset('category:' + cid, 'slug', slug); + db.setObjectField('category:' + cid, 'slug', slug); } } diff --git a/src/admin/user.js b/src/admin/user.js index d9662749f1..0f37d8590e 100644 --- a/src/admin/user.js +++ b/src/admin/user.js @@ -1,5 +1,4 @@ -var RDB = require('../redis'), - utils = require('../../public/src/utils'), +var utils = require('../../public/src/utils'), user = require('../user'), groups = require('../groups'); diff --git a/src/categories.js b/src/categories.js index 6aff4bfbe7..538bc0d5c3 100644 --- a/src/categories.js +++ b/src/categories.js @@ -1,4 +1,4 @@ -var RDB = require('./redis.js'), +var db = require('./database.js'), posts = require('./posts.js'), utils = require('./../public/src/utils.js'), user = require('./user.js'), @@ -12,13 +12,13 @@ var RDB = require('./redis.js'), "use strict"; Categories.create = function(data, callback) { - RDB.incr('global:next_category_id', function(err, cid) { + db.incrObjectField('global', 'nextCid', function(err, cid) { if (err) { return callback(err, null); } var slug = cid + '/' + utils.slugify(data.name); - RDB.rpush('categories:cid', cid); + db.listAppend('categories:cid', cid); var category = { cid: cid, @@ -33,7 +33,7 @@ var RDB = require('./redis.js'), order: data.order }; - RDB.hmset('category:' + cid, category, function(err, data) { + db.setObject('category:' + cid, category, function(err, data) { callback(err, category); }); }); @@ -134,15 +134,15 @@ var RDB = require('./redis.js'), }; Categories.getTopicIds = function(cid, start, stop, callback) { - RDB.zrevrange('categories:' + cid + ':tid', start, stop, callback); + db.getSortedSetRevRange('categories:' + cid + ':tid', start, stop, callback); }; Categories.getActiveUsers = function(cid, callback) { - RDB.smembers('cid:' + cid + ':active_users', callback); + db.getSetMembers('cid:' + cid + ':active_users', callback); }; Categories.getAllCategories = function(current_user, callback) { - RDB.lrange('categories:cid', 0, -1, function(err, cids) { + db.getListRange('categories:cid', 0, -1, function(err, cids) { if(err) { return callback(err); } @@ -155,7 +155,7 @@ var RDB = require('./redis.js'), }; Categories.getModerators = function(cid, callback) { - RDB.smembers('cid:' + cid + ':moderators', function(err, mods) { + db.getSetMembers('cid:' + cid + ':moderators', function(err, mods) { if (!err) { if (mods && mods.length) { user.getMultipleUserFields(mods, ['username'], function(err, moderators) { @@ -172,7 +172,7 @@ var RDB = require('./redis.js'), }; Categories.isTopicsRead = function(cid, uid, callback) { - RDB.zrange('categories:' + cid + ':tid', 0, -1, function(err, tids) { + db.getSortedSetRange('categories:' + cid + ':tid', 0, -1, function(err, tids) { topics.hasReadTopics(tids, uid, function(hasRead) { @@ -189,31 +189,34 @@ var RDB = require('./redis.js'), }; Categories.markAsRead = function(cid, uid) { - RDB.sadd('cid:' + cid + ':read_by_uid', uid); + db.setAdd('cid:' + cid + ':read_by_uid', uid); }; Categories.hasReadCategories = function(cids, uid, callback) { - var batch = RDB.multi(); + + var sets = []; for (var i = 0, ii = cids.length; i < ii; i++) { - batch.sismember('cid:' + cids[i] + ':read_by_uid', uid); + sets.push('cid:' + cids[i] + ':read_by_uid'); } - batch.exec(function(err, hasRead) { + db.isMemberOfSets(sets, uid, function(err, hasRead) { callback(hasRead); }); }; Categories.hasReadCategory = function(cid, uid, callback) { - RDB.sismember('cid:' + cid + ':read_by_uid', uid, function(err, hasRead) { - RDB.handle(err); + db.isSetMember('cid:' + cid + ':read_by_uid', uid, function(err, hasRead) { + if(err) { + return callback(false); + } callback(hasRead); }); }; Categories.getRecentReplies = function(cid, count, callback) { - RDB.zrevrange('categories:recent_posts:cid:' + cid, 0, (count < 10) ? 10 : count, function(err, pids) { + db.getSortedSetRevRange('categories:recent_posts:cid:' + cid, 0, (count < 10) ? 10 : count, function(err, pids) { if (err) { winston.err(err); @@ -242,8 +245,8 @@ var RDB = require('./redis.js'), return callback(err); } - RDB.zrem('categories:recent_posts:cid:' + oldCid, pid); - RDB.zadd('categories:recent_posts:cid:' + cid, timestamp, pid); + db.sortedSetRemove('categories:recent_posts:cid:' + oldCid, pid); + db.sortedSetAdd('categories:recent_posts:cid:' + cid, timestamp, pid); callback(null); }); } @@ -283,9 +286,9 @@ var RDB = require('./redis.js'), }; Categories.getCategoryData = function(cid, callback) { - RDB.exists('category:' + cid, function(err, exists) { + db.exists('category:' + cid, function(err, exists) { if (exists) { - RDB.hgetall('category:' + cid, callback); + db.getObject('category:' + cid, callback); } else { callback(new Error('No category found!')); } @@ -293,19 +296,19 @@ var RDB = require('./redis.js'), }; Categories.getCategoryField = function(cid, field, callback) { - RDB.hget('category:' + cid, field, callback); + db.getObjectField('category:' + cid, field, callback); }; Categories.getCategoryFields = function(cid, fields, callback) { - RDB.hmgetObject('category:' + cid, fields, callback); + db.getObjectFields('category:' + cid, fields, callback); }; Categories.setCategoryField = function(cid, field, value, callback) { - RDB.hset('category:' + cid, field, value, callback); + db.setObjectField('category:' + cid, field, value, callback); }; Categories.incrementCategoryFieldBy = function(cid, field, value, callback) { - RDB.hincrby('category:' + cid, field, value, callback); + db.incrObjectFieldBy('category:' + cid, field, value, callback); }; Categories.getCategories = function(cids, uid, callback) { @@ -349,7 +352,7 @@ var RDB = require('./redis.js'), Categories.isUserActiveIn = function(cid, uid, callback) { - RDB.lrange('uid:' + uid + ':posts', 0, -1, function(err, pids) { + db.getListRange('uid:' + uid + ':posts', 0, -1, function(err, pids) { if (err) { return callback(err, null); } @@ -387,12 +390,13 @@ var RDB = require('./redis.js'), }; Categories.addActiveUser = function(cid, uid) { - if(parseInt(uid, 10)) - RDB.sadd('cid:' + cid + ':active_users', uid); + if(parseInt(uid, 10)) { + db.setAdd('cid:' + cid + ':active_users', uid); + } }; Categories.removeActiveUser = function(cid, uid) { - RDB.srem('cid:' + cid + ':active_users', uid); + db.setRemove('cid:' + cid + ':active_users', uid); }; }(exports)); \ No newline at end of file diff --git a/src/database.js b/src/database.js new file mode 100644 index 0000000000..71775e8807 --- /dev/null +++ b/src/database.js @@ -0,0 +1,14 @@ + + +var nconf = require('nconf'), + databaseType = nconf.get('database'), + winston = require('winston'); + +if(!databaseType) { + winston.info('Database type not set! Run node app --setup'); + process.exit(); +} + +var db = require('./database/' + databaseType); + +module.exports = db; \ No newline at end of file diff --git a/src/database/mongo.js b/src/database/mongo.js new file mode 100644 index 0000000000..9408aff75a --- /dev/null +++ b/src/database/mongo.js @@ -0,0 +1,737 @@ + + +(function(module) { + 'use strict'; + var mongoClient = require('mongodb').MongoClient, + winston = require('winston'), + async = require('async'), + nconf = require('nconf'), + express = require('express'), + mongoStore = require('connect-mongo')(express), + mongoHost = nconf.get('mongo:host'), + db; + + module.init = function(callback) { + mongoClient.connect('mongodb://'+ mongoHost + ':' + nconf.get('mongo:port') + '/' + nconf.get('mongo:database'), function(err, _db) { + if(err) { + winston.error("NodeBB could not connect to your Mongo database. Mongo returned the following error: " + err.message); + process.exit(); + } + + db = _db; + + module.client = db; + + module.sessionStore = new mongoStore({ + db: db + }); + + + if(nconf.get('mongo:password') && nconf.get('mongo:username')) { + db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) { + if(err) { + winston.error(err.message); + process.exit(); + } + createCollections(); + }); + } else { + createCollections(); + } + + function createCollections() { + db.createCollection('objects', function(err, collection) { + if(err) { + winston.error("Error creating collection " + err.message); + return; + } + if(collection) { + collection.ensureIndex({_key :1, setName:1}, {background:true}, function(err, name){ + if(err) { + winston.error("Error creating index " + err.message); + } + }); + } + }); + + db.createCollection('search', function(err, collection) { + if(err) { + winston.error("Error creating collection " + err.message); + return; + } + if(collection) { + collection.ensureIndex({content:'text'}, {background:true}, function(err, name){ + if(err) { + winston.error("Error creating index " + err.message); + } + }); + } + }); + + callback(null); + } + }); + } + + + // + // Exported functions + // + + module.searchIndex = function(key, content, id) { + + var data = { + id:id, + key:key, + content:content + }; + + db.collection('search').update({id:id, key:key}, {$set:data}, {upsert:true, w: 1}, function(err, result) { + if(err) { + winston.error('Error indexing ' + err.message); + } + }); + } + + module.search = function(key, term, callback) { + + db.command({text:"search" , search: term, filter: {key:key} }, function(err, result) { + if(err) { + return callback(err); + } + + if(!result) { + return callback(null, []); + } + + if(result.results && result.results.length) { + var data = result.results.map(function(item) { + return item.obj.id; + }); + callback(null, data); + } else { + callback(null, []); + } + }); + } + + module.searchRemove = function(key, id) { + db.collection('search').remove({id:id, key:key}, function(err, result) { + if(err) { + winston.error('Error removing search ' + err.message); + } + }); + } + + module.flushdb = function(callback) { + db.dropDatabase(function(err, result) { + if(err){ + winston.error(error); + if(callback) { + return callback(err); + } + } + + if(callback) { + callback(null); + } + }); + } + + + module.getFileName = function(callback) { + throw new Error('not-implemented'); + } + + module.info = function(callback) { + db.stats({scale:1024}, function(err, stats) { + + // TODO : if this it not deleted the templates break, + // it is a nested object inside stats + delete stats.dataFileVersion; + + stats.avgObjSize = (stats.avgObjSize / 1024).toFixed(2); + + stats.raw = JSON.stringify(stats, null, 4); + + stats.mongo = true; + //remove this when andrew adds in undefined checking to templates + stats.redis = false; + callback(err, stats); + + }); + } + + // key + + module.exists = function(key, callback) { + db.collection('objects').findOne({$or:[{_key:key}, {setName:key}]}, function(err, item) { + callback(err, item !== undefined && item !== null); + }); + } + + module.delete = function(key, callback) { + db.collection('objects').remove({_key:key}, function(err, result) { + if(err) { + if(callback) { + return callback(err); + } else { + return winston.error(err.message); + } + } + + if(result === 0) { + db.collection('objects').remove({setName:key}, function(err, result) { + if(callback) { + callback(err, result); + } + }); + } else { + if(callback) { + callback(null, result); + } + } + }); + } + + module.get = function(key, callback) { + module.getObjectField(key, 'value', callback); + } + + module.set = function(key, value, callback) { + var data = {value:value}; + module.setObject(key, data, callback); + } + + module.keys = function(key, callback) { + db.collection('objects').find( { _key: { $regex: key /*, $options: 'i'*/ } }, function(err, result) { + callback(err, result); + }); + } + + //hashes + function removeHiddenFields(item) { + if(item) { + if(item._id) { + delete item._id; + } + if(item._key) { + delete item._key; + } + if(item.setName) { + delete item.setName; + } + } + return item; + } + + module.setObject = function(key, data, callback) { + data['_key'] = key; + db.collection('objects').update({_key:key}, {$set:data}, {upsert:true, w: 1}, function(err, result) { + if(callback) { + callback(err, result); + } + }); + } + + module.setObjectField = function(key, field, value, callback) { + var data = {}; + // if there is a '.' in the field name it inserts subdocument in mongo, replace '.'s with \uff0E + field = field.replace(/\./g, '\uff0E'); + data[field] = value; + db.collection('objects').update({_key:key}, {$set:data}, {upsert:true, w: 1}, function(err, result) { + if(callback) { + callback(err, result); + } + }); + } + + module.getObject = function(key, callback) { + db.collection('objects').findOne({_key:key}, function(err, item) { + removeHiddenFields(item); + + callback(err, item); + }); + } + + module.getObjects = function(keys, callback) { + + db.collection('objects').find({_key:{$in:keys}}, {_id:0}).toArray(function(err, data) { + + if(err) { + return callback(err); + } + + var returnData = [], + resultIndex = 0; + + + function findData(key) { + if(!data) { + return null; + } + + for(var i=0; i=0) { + /* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */ + redisClient = redis.createClient(nconf.get('redis:host')); + } else { + /* Else, connect over tcp/ip */ + redisClient = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host')); + } + + module.client = redisClient; + + module.sessionStore = new connectRedis({ + client: redisClient, + ttl: 60 * 60 * 24 * 30 + }); + + reds.createClient = function () { + return reds.client || (reds.client = redisClient); + }; + + var userSearch = reds.createSearch('nodebbusersearch'), + postSearch = reds.createSearch('nodebbpostsearch'), + topicSearch = reds.createSearch('nodebbtopicsearch'); + + if (nconf.get('redis:password')) { + redisClient.auth(nconf.get('redis:password')); + } + + var db = parseInt(nconf.get('redis:database'), 10); + + if (db){ + redisClient.select(db, function(error) { + if(error) { + winston.error("NodeBB could not connect to your Redis database. Redis returned the following error: " + error.message); + process.exit(); + } + }); + } + + module.init = function(callback) { + callback(null); + } + + + /* + * A possibly more efficient way of doing multiple sismember calls + */ + function sismembers(key, needles, callback) { + var tempkey = key + ':temp:' + utils.generateUUID(); + redisClient.sadd(tempkey, needles, function() { + redisClient.sinter(key, tempkey, function(err, data) { + redisClient.del(tempkey); + callback(err, data); + }); + }); + }; + + // + // Exported functions + // + module.searchIndex = function(key, content, id) { + if(key === 'post') { + postSearch.index(content, id); + } else if(key === 'topic') { + topicSearch.index(content, id); + } else if(key === 'user') { + userSearch.index(content, id); + } + } + + module.search = function(key, term, callback) { + function search(searchObj, callback) { + searchObj + .query(term).type('or') + .end(callback); + } + + if(key === 'post') { + search(postSearch, callback); + } else if(key === 'topic') { + search(topicSearch, callback); + } else if(key === 'user') { + search(userSearch, callback); + } + } + + module.searchRemove = function(key, id) { + if(key === 'post') { + postSearch.remove(id); + } else if(key === 'topic') { + topicSearch.remove(id); + } else if(key === 'user') { + userSearch.remove(id); + } + } + + module.flushdb = function(callback) { + redisClient.send_command('flushdb', [], function(err) { + if(err){ + winston.error(error); + return callback(err); + } + callback(null); + }); + } + + module.getFileName = function(callback) { + var multi = redisClient.multi(); + + multi.config('get', 'dir'); + multi.config('get', 'dbfilename'); + multi.exec(function (err, results) { + if (err) { + return callback(err); + } + + results = results.reduce(function (memo, config) { + memo[config[0]] = config[1]; + return memo; + }, {}); + + var dbFile = path.join(results.dir, results.dbfilename); + callback(null, dbFile); + }); + } + + + module.info = function(callback) { + redisClient.info(function (err, data) { + if(err) { + return callback(err); + } + + data = data.split("\r\n"); + var redisData = {}; + + for (var i in data) { + + if (data[i].indexOf(':') == -1 || !data[i]) + continue; + + try { + data[i] = data[i].replace(/:/, "\":\""); + var json = "{\"" + data[i] + "\"}"; + + var jsonObject = JSON.parse(json); + for (var key in jsonObject) { + redisData[key] = jsonObject[key]; + } + } catch (err) { + winston.warn('can\'t parse redis status variable, ignoring', i, data[i], err); + } + } + redisData.raw = JSON.stringify(redisData, null, 4); + redisData.redis = true; + //remove this when andrew adds in undefined checking to templates + redisData.mongo = false; + + callback(null, redisData); + }); + } + + // key + + module.exists = function(key, callback) { + redisClient.exists(key, function(err, exists) { + callback(err, exists === 1); + }); + } + + module.delete = function(key, callback) { + redisClient.del(key, callback); + } + + module.get = function(key, callback) { + redisClient.get(key, callback); + } + + module.set = function(key, value, callback) { + redisClient.set(key, value, callback); + } + + module.keys = function(key, callback) { + redisClient.keys(key, callback); + } + + //hashes + + module.setObject = function(key, data, callback) { + // TODO: this crashes if callback isnt supplied -baris + redisClient.hmset(key, data, function(err, res) { + if(callback) { + callback(err, res); + } + }); + } + + module.setObjectField = function(key, field, value, callback) { + redisClient.hset(key, field, value, callback); + } + + module.getObject = function(key, callback) { + redisClient.hgetall(key, callback); + } + + module.getObjects = function(keys, callback) { + var multi = redisClient.multi(); + + for(var x=0; x 0) { async.map(gids, function (gid, next) { Groups.get(gid, { @@ -15,7 +15,7 @@ }, function (err, groups) { // Remove deleted and hidden groups from this list callback(err, groups.filter(function (group) { - if (group.deleted === '1' || group.hidden === '1') { + if (parseInt(group.deleted, 10) === 1 || parseInt(group.hidden, 10) === 1) { return false; } else { return true; @@ -31,17 +31,17 @@ Groups.get = function(gid, options, callback) { async.parallel({ base: function (next) { - RDB.hgetall('gid:' + gid, next); + db.getObject('gid:' + gid, next); }, users: function (next) { - RDB.smembers('gid:' + gid + ':members', function (err, uids) { + db.getSetMembers('gid:' + gid + ':members', function (err, uids) { if (options.expand) { if (err) { return next(err); } async.map(uids, function (uid, next) { - User.getUserData(uid, next); + user.getUserData(uid, next); }, function (err, users) { next(err, users); }); @@ -58,7 +58,7 @@ results.base.count = results.users.length; results.base.members = results.users; - results.base.deletable = (results.base.gid !== '1'); + results.base.deletable = parseInt(results.base.gid, 10) !== 1; callback(err, results.base); }); @@ -75,19 +75,19 @@ }; Groups.isDeleted = function(gid, callback) { - RDB.hget('gid:' + gid, 'deleted', function(err, deleted) { - callback(err, deleted === '1'); + db.getObjectField('gid:' + gid, 'deleted', function(err, deleted) { + callback(err, parseInt(deleted, 10) === 1); }); }; Groups.getGidFromName = function(name, callback) { - RDB.hget('group:gid', name, callback); + db.getObjectField('group:gid', name, callback); }; Groups.isMember = function(uid, gid, callback) { Groups.isDeleted(gid, function(err, deleted) { if (!deleted) { - RDB.sismember('gid:' + gid + ':members', uid, callback); + db.isSetMember('gid:' + gid + ':members', uid, callback); } else { callback(err, false); } @@ -107,7 +107,7 @@ }; Groups.isEmpty = function(gid, callback) { - RDB.scard('gid:' + gid + ':members', function(err, numMembers) { + db.setCount('gid:' + gid + ':members', function(err, numMembers) { callback(err, numMembers === 0); }); }; @@ -125,7 +125,7 @@ Groups.exists = function(name, callback) { async.parallel({ exists: function(next) { - RDB.hexists('group:gid', name, next); + db.isObjectField('group:gid', name, next); }, deleted: function(next) { Groups.getGidFromName(name, function(err, gid) { @@ -144,19 +144,23 @@ Groups.exists(name, function (err, exists) { if (!exists) { - RDB.incr('next_gid', function (err, gid) { - RDB.multi() - .hset('group:gid', name, gid) - .hmset('gid:' + gid, { + db.incrObjectField('global', 'nextGid', function (err, gid) { + db.setObjectField('group:gid', name, gid, function(err) { + + var groupData = { gid: gid, name: name, description: description, deleted: '0', hidden: '0' - }) - .exec(function (err) { + }; + + db.setObject('gid:' + gid, groupData, function(err) { + Groups.get(gid, {}, callback); + }); + }); }); } else { callback(new Error('group-exists')); @@ -171,22 +175,24 @@ }; Groups.update = function(gid, values, callback) { - RDB.exists('gid:' + gid, function (err, exists) { + db.exists('gid:' + gid, function (err, exists) { console.log('exists?', gid, exists, values); if (!err && exists) { - RDB.hmset('gid:' + gid, values, callback); + db.setObject('gid:' + gid, values, callback); } else { - if (callback) callback(new Error('gid-not-found')); + if (callback) { + callback(new Error('gid-not-found')); + } } }); }; Groups.destroy = function(gid, callback) { - RDB.hset('gid:' + gid, 'deleted', '1', callback); + db.setObjectField('gid:' + gid, 'deleted', '1', callback); }; Groups.join = function(gid, uid, callback) { - RDB.sadd('gid:' + gid + ':members', uid, callback); + db.setAdd('gid:' + gid + ':members', uid, callback); }; Groups.joinByGroupName = function(groupName, uid, callback) { @@ -210,7 +216,7 @@ }; Groups.leave = function(gid, uid, callback) { - RDB.srem('gid:' + gid + ':members', uid, callback); + db.setRemove('gid:' + gid + ':members', uid, callback); }; Groups.leaveByGroupName = function(groupName, uid, callback) { @@ -225,30 +231,36 @@ Groups.prune = function(callback) { // Actually deletes groups (with the deleted flag) from the redis database - RDB.hvals('group:gid', function (err, gids) { - var multi = RDB.multi(), - groupsDeleted = 0; + db.getObjectValues('group:gid', function (err, gids) { + var groupsDeleted = 0; async.each(gids, function(gid, next) { Groups.get(gid, {}, function(err, groupObj) { - if (!err && groupObj.deleted === '1') { - multi.hdel('group:gid', groupObj.name); - multi.del('gid:' + gid); - groupsDeleted++; + if(err) { + return next(err); } - next(null); + if (parseInt(groupObj.deleted, 10) === 1) { + + db.deleteObjectField('group:gid', groupObj.name, function(err) { + db.delete('gid:' + gid, function(err) { + groupsDeleted++; + next(null); + }); + }); + } else { + next(null); + } }); }, function(err) { - multi.exec(function(err) { - if (!err && process.env.NODE_ENV === 'development') { - winston.info('[groups.prune] Pruned ' + groupsDeleted + ' deleted groups from Redis'); - } - callback(err); - }); + if (!err && process.env.NODE_ENV === 'development') { + winston.info('[groups.prune] Pruned ' + groupsDeleted + ' deleted groups from Redis'); + } + + callback(err); }); }); }; - + }(module.exports)); diff --git a/src/install.js b/src/install.js index 3f12f4b9fb..601fdff077 100644 --- a/src/install.js +++ b/src/install.js @@ -19,8 +19,8 @@ var async = require('async'), name: 'port', description: 'Port number of your NodeBB', 'default': nconf.get('port') || 4567, - pattern: /[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]/, - message: 'Please enter a value betweeen 1 and 65535' + pattern: /[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]/, + message: 'Please enter a value betweeen 1 and 65535' }, { name: 'use_port', description: 'Use a port number to access NodeBB?', @@ -32,6 +32,15 @@ var async = require('async'), description: 'Please enter a NodeBB secret', 'default': nconf.get('secret') || utils.generateUUID() }, { + name: 'bind_address', + description: 'IP or Hostname to bind to', + 'default': nconf.get('bind_address') || '0.0.0.0' + }, { + name: 'database', + description: 'Which database to use', + 'default': nconf.get('database') || 'redis' + }], + redisQuestions : [{ name: 'redis:host', description: 'Host IP or address of your Redis instance', 'default': nconf.get('redis:host') || '127.0.0.1' @@ -46,11 +55,27 @@ var async = require('async'), name: "redis:database", description: "Which database to use (0..n)", 'default': nconf.get('redis:database') || 0 + }], + mongoQuestions : [{ + name: 'mongo:host', + description: 'Host IP or address of your MongoDB instance', + 'default': nconf.get('mongo:host') || '127.0.0.1' }, { - name: 'bind_address', - description: 'IP or Hostname to bind to', - 'default': nconf.get('bind_address') || '0.0.0.0' + name: 'mongo:port', + description: 'Host port of your MongoDB instance', + 'default': nconf.get('mongo:port') || 27017 + }, { + name: 'mongo:user', + description: 'MongoDB username' + }, { + name: 'mongo:password', + description: 'Password of your MongoDB database' + }, { + name: "mongo:database", + description: "Which database to use (0..n)", + 'default': nconf.get('mongo:database') || 0 }], + setup: function (callback) { async.series([ function(next) { @@ -74,7 +99,9 @@ var async = require('async'), if (!setupVal['admin:email']) winston.error(' admin:email'); process.exit(); } - } else next(); + } else { + next(); + } }, function (next) { var success = function(err, config) { @@ -82,36 +109,58 @@ var async = require('async'), return next(new Error('aborted')); } - // Translate redis properties into redis object - config.redis = { - host: config['redis:host'], - port: config['redis:port'], - password: config['redis:password'], - database: config['redis:database'] - }; - delete config['redis:host']; - delete config['redis:port']; - delete config['redis:password']; - delete config['redis:database']; - - // Add hardcoded values - config.bcrypt_rounds = 12; - config.upload_path = '/public/uploads'; - config.use_port = (config.use_port.slice(0, 1) === 'y') ? true : false; - - var urlObject = url.parse(config.base_url), - relative_path = (urlObject.pathname && urlObject.pathname.length > 1) ? urlObject.pathname : '', - host = urlObject.host, - protocol = urlObject.protocol, - server_conf = config, - client_conf = { - relative_path: relative_path - }; - - server_conf.base_url = protocol + '//' + host; - server_conf.relative_path = relative_path; - - install.save(server_conf, client_conf, next); + function dbQuestionsSuccess(err, databaseConfig) { + if (!databaseConfig) { + return next(new Error('aborted')); + } + + // Translate redis properties into redis object + if(config.database === 'redis') { + config.redis = { + host: databaseConfig['redis:host'], + port: databaseConfig['redis:port'], + password: databaseConfig['redis:password'], + database: databaseConfig['redis:database'] + }; + } else if (config.database === 'mongo') { + config.mongo = { + host: databaseConfig['mongo:host'], + port: databaseConfig['mongo:port'], + password: databaseConfig['mongo:password'], + database: databaseConfig['mongo:database'] + }; + } else { + return next(new Error('unknown database : ' + config.database)); + } + + config.bcrypt_rounds = 12; + config.upload_path = '/public/uploads'; + config.use_port = (config.use_port.slice(0, 1) === 'y') ? true : false; + + var urlObject = url.parse(config.base_url), + relative_path = (urlObject.pathname && urlObject.pathname.length > 1) ? urlObject.pathname : '', + host = urlObject.host, + protocol = urlObject.protocol, + server_conf = config, + client_conf = { + relative_path: relative_path + }; + + server_conf.base_url = protocol + '//' + host; + server_conf.relative_path = relative_path; + + install.save(server_conf, client_conf, function(err) { + require('./database').init(next); + }); + } + + if(config.database === 'redis') { + prompt.get(install.redisQuestions, dbQuestionsSuccess); + } else if(config.database === 'mongo') { + prompt.get(install.mongoQuestions, dbQuestionsSuccess); + } else { + return next(new Error('unknown database : ' + config.database)); + } }; // prompt prepends "prompt: " to questions, let's clear that. @@ -119,8 +168,9 @@ var async = require('async'), prompt.message = ''; prompt.delimiter = ''; - if (!install.values) prompt.get(install.questions, success); - else { + if (!install.values) { + prompt.get(install.questions, success); + } else { // Use provided values, fall back to defaults var config = {}, question, x, numQ; @@ -252,9 +302,7 @@ var async = require('async'), }, next); }, function (next) { - // Upgrading schema - var Upgrade = require('./upgrade'); - Upgrade.upgrade(next); + require('./upgrade').upgrade(next); } ], function (err) { if (err) { @@ -343,8 +391,9 @@ var async = require('async'), // Add the password questions questions = questions.concat(passwordQuestions); - if (!install.values) prompt.get(questions, success); - else { + if (!install.values) { + prompt.get(questions, success); + } else { var results = { username: install.values['admin:username'], email: install.values['admin:email'], @@ -375,10 +424,10 @@ var async = require('async'), file: path.join(__dirname, '..', 'config.json') }); - var RDB = require('./redis'); + /*var RDB = require('./redis'); reds.createClient = function () { return reds.client || (reds.client = RDB); - }; + };*/ callback(err); }); diff --git a/src/login.js b/src/login.js index 51f3ed5472..c4eb226586 100644 --- a/src/login.js +++ b/src/login.js @@ -1,9 +1,9 @@ -var user = require('./user.js'), +var user = require('./user'), bcrypt = require('bcrypt'), - RDB = require('./redis.js'), + db = require('./database'), path = require('path'), winston = require('winston'), - utils = require('./../public/src/utils.js'); + utils = require('./../public/src/utils'); (function(Login) { @@ -28,7 +28,7 @@ var user = require('./user.js'), user.getUserFields(uid, ['password', 'banned'], function(err, userData) { if (err) return next(err); - if (userData.banned && userData.banned === '1') { + if (userData.banned && parseInt(userData.banned, 10) === 1) { return next({ status: "error", message: "user-banned" @@ -58,7 +58,11 @@ var user = require('./user.js'), } Login.loginViaTwitter = function(twid, handle, photos, callback) { - user.getUidByTwitterId(twid, function(uid) { + user.getUidByTwitterId(twid, function(err, uid) { + if(err) { + return callback(err); + } + if (uid !== null) { // Existing User callback(null, { @@ -67,32 +71,36 @@ var user = require('./user.js'), } else { // New User user.create(handle, undefined, undefined, function(err, uid) { - if (err !== null) { - callback(err); - } else { - // Save twitter-specific information to the user - user.setUserField(uid, 'twid', twid); - RDB.hset('twid:uid', twid, uid); - - // Save their photo, if present - if (photos && photos.length > 0) { - var photoUrl = photos[0].value; - photoUrl = path.dirname(photoUrl) + '/' + path.basename(photoUrl, path.extname(photoUrl)).slice(0, -6) + 'bigger' + path.extname(photoUrl); - user.setUserField(uid, 'uploadedpicture', photoUrl); - user.setUserField(uid, 'picture', photoUrl); - } + if(err) { + return callback(err); + } - callback(null, { - uid: uid - }); + // Save twitter-specific information to the user + user.setUserField(uid, 'twid', twid); + db.setObjectField('twid:uid', twid, uid); + + // Save their photo, if present + if (photos && photos.length > 0) { + var photoUrl = photos[0].value; + photoUrl = path.dirname(photoUrl) + '/' + path.basename(photoUrl, path.extname(photoUrl)).slice(0, -6) + 'bigger' + path.extname(photoUrl); + user.setUserField(uid, 'uploadedpicture', photoUrl); + user.setUserField(uid, 'picture', photoUrl); } + + callback(null, { + uid: uid + }); }); } }); } Login.loginViaGoogle = function(gplusid, handle, email, callback) { - user.getUidByGoogleId(gplusid, function(uid) { + user.getUidByGoogleId(gplusid, function(err, uid) { + if(err) { + return callback(err); + } + if (uid !== null) { // Existing User callback(null, { @@ -103,27 +111,39 @@ var user = require('./user.js'), var success = function(uid) { // Save google-specific information to the user user.setUserField(uid, 'gplusid', gplusid); - RDB.hset('gplusid:uid', gplusid, uid); + db.setObjectField('gplusid:uid', gplusid, uid); callback(null, { uid: uid }); } - user.getUidByEmail(email, function(uid) { + user.getUidByEmail(email, function(err, uid) { + if(err) { + return callback(err); + } + if (!uid) { user.create(handle, undefined, email, function(err, uid) { - if (err !== null) { - callback(err); - } else success(uid); + if(err) { + return callback(err); + } + + success(uid); }); - } else success(uid); // Existing account -- merge + } else { + success(uid); // Existing account -- merge + } }); } }); } Login.loginViaFacebook = function(fbid, name, email, callback) { - user.getUidByFbid(fbid, function(uid) { + user.getUidByFbid(fbid, function(err, uid) { + if(err) { + return callback(err); + } + if (uid !== null) { // Existing User callback(null, { @@ -134,20 +154,28 @@ var user = require('./user.js'), var success = function(uid) { // Save facebook-specific information to the user user.setUserField(uid, 'fbid', fbid); - RDB.hset('fbid:uid', fbid, uid); + db.setObjectField('fbid:uid', fbid, uid); callback(null, { uid: uid }); } - user.getUidByEmail(email, function(uid) { + user.getUidByEmail(email, function(err, uid) { + if(err) { + return callback(err); + } + if (!uid) { user.create(name, undefined, email, function(err, uid) { - if (err !== null) { - callback(err); - } else success(uid); + if(err) { + return callback(err); + } + + success(uid); }); - } else success(uid); // Existing account -- merge + } else { + success(uid); // Existing account -- merge + } }); } }); diff --git a/src/messaging.js b/src/messaging.js index e6275ec9ed..948f715448 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -1,4 +1,4 @@ -var RDB = require('./redis'), +var db = require('./database'), async = require('async'), user = require('./user'); @@ -14,9 +14,10 @@ var RDB = require('./redis'), Messaging.addMessage = function(fromuid, touid, content, callback) { var uids = sortUids(fromuid, touid); - RDB.incr('global:next_message_id', function(err, mid) { - if (err) + db.incrObjectField('global', 'nextMid', function(err, mid) { + if (err) { return callback(err, null); + } var message = { content: content, @@ -25,8 +26,8 @@ var RDB = require('./redis'), touid: touid }; - RDB.hmset('message:' + mid, message); - RDB.rpush('messages:' + uids[0] + ':' + uids[1], mid); + db.setObject('message:' + mid, message); + db.listAppend('messages:' + uids[0] + ':' + uids[1], mid); Messaging.updateChatTime(fromuid, touid); Messaging.updateChatTime(touid, fromuid); @@ -37,9 +38,10 @@ var RDB = require('./redis'), Messaging.getMessages = function(fromuid, touid, callback) { var uids = sortUids(fromuid, touid); - RDB.lrange('messages:' + uids[0] + ':' + uids[1], 0, -1, function(err, mids) { - if (err) + db.getListRange('messages:' + uids[0] + ':' + uids[1], 0, -1, function(err, mids) { + if (err) { return callback(err, null); + } if (!mids || !mids.length) { return callback(null, []); @@ -51,14 +53,16 @@ var RDB = require('./redis'), var messages = []; function getMessage(mid, next) { - RDB.hgetall('message:' + mid, function(err, message) { - if (err) + db.getObject('message:' + mid, function(err, message) { + if (err) { return next(err); + } - if (message.fromuid === fromuid) + if (message.fromuid === fromuid) { message.content = 'You : ' + message.content; - else + } else { message.content = tousername + ' : ' + message.content; + } messages.push(message); next(null); @@ -66,8 +70,9 @@ var RDB = require('./redis'), } async.eachSeries(mids, getMessage, function(err) { - if (err) + if (err) { return callback(err, null); + } callback(null, messages); }); @@ -76,7 +81,7 @@ var RDB = require('./redis'), } Messaging.updateChatTime = function(uid, toUid, callback) { - RDB.zadd('uid:' + uid + ':chats', Date.now(), toUid, function(err) { + db.sortedSetAdd('uid:' + uid + ':chats', Date.now(), toUid, function(err) { if (callback) { callback(err); } @@ -84,7 +89,7 @@ var RDB = require('./redis'), }; Messaging.getRecentChats = function(uid, callback) { - RDB.zrevrange('uid:' + uid + ':chats', 0, 9, function(err, uids) { + db.getSortedSetRevRange('uid:' + uid + ':chats', 0, 9, function(err, uids) { if (!err) { user.getMultipleUserFields(uids, ['username', 'picture', 'uid'], callback); } else { diff --git a/src/meta.js b/src/meta.js index a5e50fd552..fa50858515 100644 --- a/src/meta.js +++ b/src/meta.js @@ -1,11 +1,13 @@ -var utils = require('./../public/src/utils.js'), - RDB = require('./redis.js'), - plugins = require('./plugins'), - async = require('async'), +var fs = require('fs'), path = require('path'), - fs = require('fs'), + async = require('async'), winston = require('winston'), - nconf = require('nconf'); + nconf = require('nconf'), + + utils = require('./../public/src/utils'), + db = require('./database'), + plugins = require('./plugins'); + (function (Meta) { Meta.config = {}; @@ -15,33 +17,34 @@ var utils = require('./../public/src/utils.js'), delete Meta.config; Meta.configs.list(function (err, config) { - if (!err) { - Meta.config = config; - callback(); - } else { + if(err) { winston.error(err); + return callback(err); } + + Meta.config = config; + callback(); }); }, list: function (callback) { - RDB.hgetall('config', function (err, config) { - if (!err) { - config = config || {}; - config.status = 'ok'; - callback(err, config); - } else { - callback(new Error('could-not-read-config')); + db.getObject('config', function (err, config) { + if(err) { + return callback(new Error('could-not-read-config')); } + + config = config || {}; + config.status = 'ok'; + callback(err, config); }); }, get: function (field, callback) { - RDB.hget('config', field, callback); + db.getObjectField('config', field, callback); }, getFields: function (fields, callback) { - RDB.hmgetObject('config', fields, callback); + db.getObjectFields('config', fields, callback); }, set: function (field, value, callback) { - RDB.hset('config', field, value, function (err, res) { + db.setObjectField('config', field, value, function(err, res) { if (callback) { if(!err && Meta.config) Meta.config[field] = value; @@ -50,7 +53,7 @@ var utils = require('./../public/src/utils.js'), }); }, setOnEmpty: function (field, value, callback) { - this.get(field, function (err, curValue) { + Meta.configs.get(field, function (err, curValue) { if (!curValue) { Meta.configs.set(field, value, callback); } else { @@ -59,7 +62,7 @@ var utils = require('./../public/src/utils.js'), }); }, remove: function (field) { - RDB.hdel('config', field); + db.deleteObjectField('config', field); } }; @@ -125,7 +128,7 @@ var utils = require('./../public/src/utils.js'), themeData['theme:staticDir'] = config.staticDir ? config.staticDir : ''; themeData['theme:templates'] = config.templates ? config.templates : ''; - RDB.hmset('config', themeData, next); + db.setObject('config', themeData, next); } ], function(err) { callback(err); @@ -134,7 +137,7 @@ var utils = require('./../public/src/utils.js'), case 'bootswatch': themeData['theme:src'] = data.src; - RDB.hmset('config', themeData, callback); + db.setObject('config', themeData, callback); break; } } @@ -257,11 +260,16 @@ var utils = require('./../public/src/utils.js'), }), minified; - if (process.env.NODE_ENV === 'development') winston.info('Minifying client-side libraries'); + if (process.env.NODE_ENV === 'development') { + winston.info('Minifying client-side libraries'); + } + minified = uglifyjs.minify(jsPaths); fs.writeFile(Meta.js.minFile, minified.code, function (err) { if (!err) { - if (process.env.NODE_ENV === 'development') winston.info('Minified client-side libraries'); + if (process.env.NODE_ENV === 'development') { + winston.info('Minified client-side libraries'); + } callback(); } else { winston.error('Problem minifying client-side libraries, exiting.'); @@ -273,23 +281,7 @@ var utils = require('./../public/src/utils.js'), Meta.db = { getFile: function (callback) { - var multi = RDB.multi(); - - multi.config('get', 'dir'); - multi.config('get', 'dbfilename'); - multi.exec(function (err, results) { - if (err) { - return callback(err); - } else { - results = results.reduce(function (memo, config) { - memo[config[0]] = config[1]; - return memo; - }, {}); - - var dbFile = path.join(results.dir, results.dbfilename); - callback(null, dbFile); - } - }); + db.getFileName(callback); } }; }(exports)); \ No newline at end of file diff --git a/src/notifications.js b/src/notifications.js index cff6fda563..030478701b 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -1,9 +1,9 @@ -var RDB = require('./redis'), - async = require('async'), - utils = require('../public/src/utils'), +var async = require('async'), winston = require('winston'), cron = require('cron').CronJob, + db = require('./database'), + utils = require('../public/src/utils'), websockets = require('./websockets'); @@ -19,28 +19,21 @@ var RDB = require('./redis'), }; Notifications.get = function(nid, uid, callback) { - RDB.multi() - .hmget('notifications:' + nid, 'text', 'score', 'path', 'datetime', 'uniqueId') - .zrank('uid:' + uid + ':notifications:read', nid) - .exists('notifications:' + nid) - .exec(function(err, results) { - var notification = results[0], - readIdx = results[1]; - - if (!results[2]) { - return callback(null); - } - callback({ - nid: nid, - text: notification[0], - score: notification[1], - path: notification[2], - datetime: notification[3], - uniqueId: notification[4], - read: readIdx !== null ? true : false + db.exists('notifications:' + nid, function(err, exists) { + if(!exists) { + return callback(null); + } + + db.sortedSetRank('uid:' + uid + ':notifications:read', nid, function(err, rank) { + + db.getObjectFields('notifications:' + nid, ['nid', 'text', 'score', 'path', 'datetime', 'uniqueId'], function(err, notification) { + + notification.read = rank !== null ? true:false; + callback(notification); }); }); + }); }; Notifications.create = function(text, path, uniqueId, callback) { @@ -50,9 +43,9 @@ var RDB = require('./redis'), * (un)read list contains the same uniqueId, it will be removed, and * the new one put in its place. */ - RDB.incr('notifications:next_nid', function(err, nid) { - RDB.sadd('notifications', nid); - RDB.hmset('notifications:' + nid, { + db.incrObjectField('global', 'nextNid', function(err, nid) { + db.setAdd('notifications', nid); + db.setObject('notifications:' + nid, { text: text || '', path: path || null, datetime: Date.now(), @@ -66,16 +59,14 @@ var RDB = require('./redis'), }; function destroy(nid) { - var multi = RDB.multi(); - multi.del('notifications:' + nid); - multi.srem('notifications', nid); - - multi.exec(function(err) { - if (err) { - winston.error('Problem deleting expired notifications. Stack follows.'); - winston.error(err.stack); - } + db.delete('notifications:' + nid, function(err, result) { + db.setRemove('notifications', nid, function(err, result) { + if (err) { + winston.error('Problem deleting expired notifications. Stack follows.'); + winston.error(err.stack); + } + }); }); } @@ -92,7 +83,7 @@ var RDB = require('./redis'), if (parseInt(uids[x], 10) > 0) { (function(uid) { remove_by_uniqueId(notif_data.uniqueId, uid, function() { - RDB.zadd('uid:' + uid + ':notifications:unread', notif_data.datetime, nid); + db.sortedSetAdd('uid:' + uid + ':notifications:unread', notif_data.datetime, nid); websockets.in('uid_' + uid).emit('event:new_notification'); @@ -109,12 +100,12 @@ var RDB = require('./redis'), function remove_by_uniqueId(uniqueId, uid, callback) { async.parallel([ function(next) { - RDB.zrange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) { + db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) { if (nids && nids.length > 0) { async.each(nids, function(nid, next) { Notifications.get(nid, uid, function(nid_info) { if (nid_info.uniqueId === uniqueId) { - RDB.zrem('uid:' + uid + ':notifications:unread', nid); + db.sortedSetRemove('uid:' + uid + ':notifications:unread', nid); } next(); @@ -128,12 +119,12 @@ var RDB = require('./redis'), }); }, function(next) { - RDB.zrange('uid:' + uid + ':notifications:read', 0, -1, function(err, nids) { + db.getSortedSetRange('uid:' + uid + ':notifications:read', 0, -1, function(err, nids) { if (nids && nids.length > 0) { async.each(nids, function(nid, next) { Notifications.get(nid, uid, function(nid_info) { if (nid_info && nid_info.uniqueId === uniqueId) { - RDB.zrem('uid:' + uid + ':notifications:read', nid); + db.sortedSetRemove('uid:' + uid + ':notifications:read', nid); } next(); @@ -156,8 +147,8 @@ var RDB = require('./redis'), Notifications.mark_read = function(nid, uid, callback) { if (parseInt(uid, 10) > 0) { Notifications.get(nid, uid, function(notif_data) { - RDB.zrem('uid:' + uid + ':notifications:unread', nid); - RDB.zadd('uid:' + uid + ':notifications:read', notif_data.datetime, nid); + db.sortedSetRemove('uid:' + uid + ':notifications:unread', nid); + db.sortedSetAdd('uid:' + uid + ':notifications:read', notif_data.datetime, nid); if (callback) { callback(); } @@ -184,7 +175,7 @@ var RDB = require('./redis'), }; Notifications.mark_all_read = function(uid, callback) { - RDB.zrange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) { + db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) { if (err) { return callback(err); } @@ -200,6 +191,7 @@ var RDB = require('./redis'), }; Notifications.prune = function(cutoff) { + if (process.env.NODE_ENV === 'development') { winston.info('[notifications.prune] Removing expired notifications from the database.'); } @@ -215,12 +207,20 @@ var RDB = require('./redis'), async.parallel({ "inboxes": function(next) { - RDB.keys('uid:*:notifications:unread', next); + db.getSortedSetRange('users:joindate', 0, -1, function(err, uids) { + if(err) { + return next(err); + } + uids = uids.map(function(uid) { + return 'uid:' + uid + ':notifications:unread'; + }); + next(null, uids); + }); }, - "nids": function(next) { - RDB.smembers('notifications', function(err, nids) { + "expiredNids": function(next) { + db.getSetMembers('notifications', function(err, nids) { async.filter(nids, function(nid, next) { - RDB.hget('notifications:' + nid, 'datetime', function(err, datetime) { + db.getObjectField('notifications:' + nid, 'datetime', function(err, datetime) { if (parseInt(datetime, 10) < cutoffTime) { next(true); } else { @@ -233,43 +233,39 @@ var RDB = require('./redis'), }); } }, function(err, results) { - if (!err) { - var numInboxes = results.inboxes.length, - x; + if(err) { + if (process.env.NODE_ENV === 'development') { + winston.error('[notifications.prune] Ran into trouble pruning expired notifications. Stack trace to follow.'); + winston.error(err.stack); + } + return; + } - async.eachSeries(results.nids, function(nid, next) { - var multi = RDB.multi(); + async.eachSeries(results.expiredNids, function(nid, next) { - for(x=0;x 0) { + async.each(plugins, function(plugin, next) { + var modulePath = path.join(__dirname, '../node_modules/', plugin); + if (fs.existsSync(modulePath)) { + Plugins.loadPlugin(modulePath, next); + } else { + if (global.env === 'development') { + winston.warn('[plugins] Plugin \'' + plugin + '\' not found'); + } + next(); // Ignore this plugin silently + } + }, next); + } else next(); + }, + function(next) { + if (global.env === 'development') winston.info('[plugins] Sorting hooks to fire in priority sequence'); + Object.keys(Plugins.loadedHooks).forEach(function(hook) { + var hooks = Plugins.loadedHooks[hook]; + hooks = hooks.sort(function(a, b) { + return a.priority - b.priority; + }); + }); - if (global.env === 'development') winston.info('[plugins] Plugins OK'); + next(); + } + ], callback); + }; - plugins.initialized = true; - plugins.readyEvent.emit('ready'); - }); - }, - ready: function(callback) { - if (!this.initialized) this.readyEvent.once('ready', callback); - else callback(); - }, - initialized: false, - reload: function(callback) { - var _self = this; - - // Resetting all local plugin data - this.loadedHooks = {}; - this.staticDirs = {}; - this.cssFiles.length = 0; - - // Read the list of activated plugins and require their libraries - async.waterfall([ + Plugins.loadPlugin = function(pluginPath, callback) { + fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) { + if (err) { + return callback(err); + } + + var pluginData = JSON.parse(data), + libraryPath, staticDir; + + async.parallel([ function(next) { - RDB.smembers('plugins:active', next); - }, - function(plugins, next) { - if (plugins && Array.isArray(plugins) && plugins.length > 0) { - async.each(plugins, function(plugin, next) { - var modulePath = path.join(__dirname, '../node_modules/', plugin); - if (fs.existsSync(modulePath)) _self.loadPlugin(modulePath, next); - else { - if (global.env === 'development') winston.warn('[plugins] Plugin \'' + plugin + '\' not found'); - next(); // Ignore this plugin silently + if (pluginData.library) { + libraryPath = path.join(pluginPath, pluginData.library); + + fs.exists(libraryPath, function(exists) { + if (exists) { + if (!Plugins.libraries[pluginData.id]) { + Plugins.libraries[pluginData.id] = require(libraryPath); + } + + // Register hooks for this plugin + if (pluginData.hooks && Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) { + async.each(pluginData.hooks, function(hook, next) { + Plugins.registerHook(pluginData.id, hook, next); + }, next); + } else { + next(null); + } + } else { + winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id); + next(); } - }, next); - } else next(); + }); + } else { + winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id); + next(); + } }, function(next) { - if (global.env === 'development') winston.info('[plugins] Sorting hooks to fire in priority sequence'); - Object.keys(_self.loadedHooks).forEach(function(hook) { - var hooks = _self.loadedHooks[hook]; - hooks = hooks.sort(function(a, b) { - return a.priority - b.priority; + // Static Directories for Plugins + if (pluginData.staticDir) { + staticDir = path.join(pluginPath, pluginData.staticDir); + + fs.exists(staticDir, function(exists) { + if (exists) { + Plugins.staticDirs[pluginData.id] = staticDir; + next(); + } else next(); }); - }); - - next(); - } - ], callback); - }, - loadPlugin: function(pluginPath, callback) { - var _self = this; - - fs.readFile(path.join(pluginPath, 'plugin.json'), function(err, data) { - if (err) return callback(err); - - var pluginData = JSON.parse(data), - libraryPath, staticDir; - - async.parallel([ - function(next) { - if (pluginData.library) { - libraryPath = path.join(pluginPath, pluginData.library); - - fs.exists(libraryPath, function(exists) { - if (exists) { - if (!_self.libraries[pluginData.id]) { - _self.libraries[pluginData.id] = require(libraryPath); - } - - // Register hooks for this plugin - if (pluginData.hooks && Array.isArray(pluginData.hooks) && pluginData.hooks.length > 0) { - async.each(pluginData.hooks, function(hook, next) { - _self.registerHook(pluginData.id, hook, next); - }, next); - } else { - next(null); - } - } else { - winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id); - next(); - } - }); - } else { - winston.warn('[plugins.reload] Library not found for plugin: ' + pluginData.id); - next(); + } else next(); + }, + function(next) { + // CSS Files for plugins + if (pluginData.css && pluginData.css instanceof Array) { + if (global.env === 'development') { + winston.info('[plugins] Found ' + pluginData.css.length + ' CSS file(s) for plugin ' + pluginData.id); } - }, - function(next) { - // Static Directories for Plugins - if (pluginData.staticDir) { - staticDir = path.join(pluginPath, pluginData.staticDir); - - fs.exists(staticDir, function(exists) { - if (exists) { - _self.staticDirs[pluginData.id] = staticDir; - next(); - } else next(); - }); - } else next(); - }, - function(next) { - // CSS Files for plugins - if (pluginData.css && pluginData.css instanceof Array) { - if (global.env === 'development') { - winston.info('[plugins] Found ' + pluginData.css.length + ' CSS file(s) for plugin ' + pluginData.id); - } - _self.cssFiles = _self.cssFiles.concat(pluginData.css.map(function(file) { - return path.join('/plugins', pluginData.id, file); - })); + Plugins.cssFiles = Plugins.cssFiles.concat(pluginData.css.map(function(file) { + return path.join('/plugins', pluginData.id, file); + })); - next(); - } else next(); - } - ], function(err) { - if (!err) { - if (global.env === 'development') winston.info('[plugins] Loaded plugin: ' + pluginData.id); - callback(); - } else callback(new Error('Could not load plugin system')) - }); - }); - }, - registerHook: function(id, data, callback) { - /* - `data` is an object consisting of (* is required): - `data.hook`*, the name of the NodeBB hook - `data.method`*, the method called in that plugin - `data.callbacked`, whether or not the hook expects a callback (true), or a return (false). Only used for filters. (Default: false) - `data.priority`, the relative priority of the method when it is eventually called (default: 10) - */ - var _self = this; - - if (data.hook && data.method) { - data.id = id; - if (!data.priority) data.priority = 10; - data.method = data.method.split('.').reduce(function(memo, prop) { - if (memo[prop]) { - return memo[prop]; + next(); } else { - // Couldn't find method by path, assuming property with periods in it (evil!) - _self.libraries[data.id][data.method]; + next(); } - }, _self.libraries[data.id]); - - _self.loadedHooks[data.hook] = _self.loadedHooks[data.hook] || []; - _self.loadedHooks[data.hook].push(data); - - if (global.env === 'development') winston.info('[plugins] Hook registered: ' + data.hook + ' will call ' + id); - callback(); - } else return; - }, - fireHook: function(hook, args, callback) { - var _self = this - hookList = this.loadedHooks[hook]; - - if (hookList && Array.isArray(hookList)) { - //if (global.env === 'development') winston.info('[plugins] Firing hook: \'' + hook + '\''); - var hookType = hook.split(':')[0]; - switch (hookType) { - case 'filter': - async.reduce(hookList, args, function(value, hookObj, next) { - if (hookObj.method) { - if (hookObj.callbacked) { // If a callback is present (asynchronous method) - hookObj.method.call(_self.libraries[hookObj.id], value, next); - } else { // Synchronous method - value = hookObj.method.call(_self.libraries[hookObj.id], value); - next(null, value); - } - } else { - if (global.env === 'development') winston.info('[plugins] Expected method \'' + hookObj.method + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); + } + ], function(err) { + if (!err) { + if (global.env === 'development') { + winston.info('[plugins] Loaded plugin: ' + pluginData.id); + } + callback(); + } else { + callback(new Error('Could not load plugin system')); + } + }); + }); + }; + + Plugins.registerHook = function(id, data, callback) { + /* + `data` is an object consisting of (* is required): + `data.hook`*, the name of the NodeBB hook + `data.method`*, the method called in that plugin + `data.callbacked`, whether or not the hook expects a callback (true), or a return (false). Only used for filters. (Default: false) + `data.priority`, the relative priority of the method when it is eventually called (default: 10) + */ + + if (data.hook && data.method) { + data.id = id; + if (!data.priority) data.priority = 10; + data.method = data.method.split('.').reduce(function(memo, prop) { + if (memo[prop]) { + return memo[prop]; + } else { + // Couldn't find method by path, assuming property with periods in it (evil!) + Plugins.libraries[data.id][data.method]; + } + }, Plugins.libraries[data.id]); + + Plugins.loadedHooks[data.hook] = Plugins.loadedHooks[data.hook] || []; + Plugins.loadedHooks[data.hook].push(data); + + if (global.env === 'development') { + winston.info('[plugins] Hook registered: ' + data.hook + ' will call ' + id); + } + callback(); + } else return; + }; + + Plugins.fireHook = function(hook, args, callback) { + hookList = Plugins.loadedHooks[hook]; + + if (hookList && Array.isArray(hookList)) { + //if (global.env === 'development') winston.info('[plugins] Firing hook: \'' + hook + '\''); + var hookType = hook.split(':')[0]; + switch (hookType) { + case 'filter': + async.reduce(hookList, args, function(value, hookObj, next) { + if (hookObj.method) { + if (hookObj.callbacked) { // If a callback is present (asynchronous method) + hookObj.method.call(Plugins.libraries[hookObj.id], value, next); + } else { // Synchronous method + value = hookObj.method.call(Plugins.libraries[hookObj.id], value); next(null, value); } - }, function(err, value) { - if (err) { - if (global.env === 'development') { - winston.info('[plugins] Problem executing hook: ' + hook); - } + } else { + if (global.env === 'development') { + winston.info('[plugins] Expected method \'' + hookObj.method + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); } + next(null, value); + } + }, function(err, value) { + if (err) { + if (global.env === 'development') { + winston.info('[plugins] Problem executing hook: ' + hook); + } + } - callback.apply(plugins, arguments); - }); - break; - case 'action': - async.each(hookList, function(hookObj) { - if (hookObj.method) { - hookObj.method.call(_self.libraries[hookObj.id], args); - } else { - if (global.env === 'development') { - winston.info('[plugins] Expected method \'' + hookObj.method + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); - } + callback.apply(Plugins, arguments); + }); + break; + case 'action': + async.each(hookList, function(hookObj) { + if (hookObj.method) { + hookObj.method.call(Plugins.libraries[hookObj.id], args); + } else { + if (global.env === 'development') { + winston.info('[plugins] Expected method \'' + hookObj.method + '\' in plugin \'' + hookObj.id + '\' not found, skipping.'); } - }); - break; - default: - // Do nothing... - break; - } - } else { - // Otherwise, this hook contains no methods - var returnVal = args; - if (callback) { - callback(null, returnVal); - } + } + }); + break; + default: + // Do nothing... + break; + } + } else { + // Otherwise, this hook contains no methods + var returnVal = args; + if (callback) { + callback(null, returnVal); } - }, - isActive: function(id, callback) { - RDB.sismember('plugins:active', id, callback); - }, - toggleActive: function(id, callback) { - this.isActive(id, function(err, active) { + } + }; + + Plugins.isActive = function(id, callback) { + db.isSetMember('plugins:active', id, callback); + }; + + Plugins.toggleActive = function(id, callback) { + Plugins.isActive(id, function(err, active) { + if (err) { + if (global.env === 'development') winston.info('[plugins] Could not toggle active state on plugin \'' + id + '\''); + return; + } + + db[(active ? 'setRemove' : 'setAdd')]('plugins:active', id, function(err, success) { if (err) { if (global.env === 'development') winston.info('[plugins] Could not toggle active state on plugin \'' + id + '\''); return; } - RDB[(active ? 'srem' : 'sadd')]('plugins:active', id, function(err, success) { - if (err) { - if (global.env === 'development') winston.info('[plugins] Could not toggle active state on plugin \'' + id + '\''); - return; - } - - // Reload meta data - plugins.reload(function() { - // (De)activation Hooks - plugins.fireHook('action:plugin.' + (active ? 'de' : '') + 'activate', id); + // Reload meta data + Plugins.reload(function() { + // (De)activation Hooks + Plugins.fireHook('action:plugin.' + (active ? 'de' : '') + 'activate', id); - if (callback) { - callback({ - id: id, - active: !active - }); - } - }); + if (callback) { + callback({ + id: id, + active: !active + }); + } }); }); - }, - showInstalled: function(callback) { - var _self = this; - npmPluginPath = path.join(__dirname, '../node_modules'); - - async.waterfall([ - function(next) { - fs.readdir(npmPluginPath, function(err, dirs) { - dirs = dirs.map(function(file) { - return path.join(npmPluginPath, file); - }).filter(function(file) { - var stats = fs.statSync(file); - if (stats.isDirectory() && file.substr(npmPluginPath.length + 1, 14) === 'nodebb-plugin-') return true; - else return false; - }); + }); + } - next(err, dirs); + Plugins.showInstalled = function(callback) { + npmPluginPath = path.join(__dirname, '../node_modules'); + + async.waterfall([ + function(next) { + fs.readdir(npmPluginPath, function(err, dirs) { + dirs = dirs.map(function(file) { + return path.join(npmPluginPath, file); + }).filter(function(file) { + var stats = fs.statSync(file); + if (stats.isDirectory() && file.substr(npmPluginPath.length + 1, 14) === 'nodebb-plugin-') return true; + else return false; }); - }, - function(files, next) { - var plugins = []; - - async.each(files, function(file, next) { - var configPath; - - async.waterfall([ - function(next) { - fs.readFile(path.join(file, 'plugin.json'), next); - }, - function(configJSON, next) { - try { - var config = JSON.parse(configJSON); - } catch (err) { - winston.warn("Plugin: " + file + " is corrupted or invalid. Please check plugin.json for errors.") - return next(err, null); - } - - _self.isActive(config.id, function(err, active) { - if (err) next(new Error('no-active-state')); - delete config.library; - delete config.hooks; - config.active = active; - config.activeText = ' ' + (active ? 'Dea' : 'A') + 'ctivate'; - next(null, config); - }); + next(err, dirs); + }); + }, + function(files, next) { + var plugins = []; + + async.each(files, function(file, next) { + var configPath; + + async.waterfall([ + function(next) { + fs.readFile(path.join(file, 'plugin.json'), next); + }, + function(configJSON, next) { + try { + var config = JSON.parse(configJSON); + } catch (err) { + winston.warn("Plugin: " + file + " is corrupted or invalid. Please check plugin.json for errors.") + return next(err, null); } - ], function(err, config) { - if (err) return next(); // Silently fail - plugins.push(config); - next(); - }); - }, function(err) { - next(null, plugins); - }); - } - ], function(err, plugins) { - callback(err, plugins); - }); - } - } + Plugins.isActive(config.id, function(err, active) { + if (err) { + next(new Error('no-active-state')); + } -plugins.init(); + delete config.library; + delete config.hooks; + config.active = active; + config.activeText = ' ' + (active ? 'Dea' : 'A') + 'ctivate'; + next(null, config); + }); + } + ], function(err, config) { + if (err) return next(); // Silently fail -module.exports = plugins; \ No newline at end of file + plugins.push(config); + next(); + }); + }, function(err) { + next(null, plugins); + }); + } + ], function(err, plugins) { + callback(err, plugins); + }); + } +}(exports)); diff --git a/src/postTools.js b/src/postTools.js index c70e88f167..e31fa70552 100644 --- a/src/postTools.js +++ b/src/postTools.js @@ -1,4 +1,4 @@ -var RDB = require('./redis'), +var db = require('./database'), posts = require('./posts'), topics = require('./topics'), threadTools = require('./threadTools'), @@ -10,16 +10,14 @@ var RDB = require('./redis'), utils = require('../public/src/utils'), plugins = require('./plugins'), - reds = require('reds'), - postSearch = reds.createSearch('nodebbpostsearch'), - topicSearch = reds.createSearch('nodebbtopicsearch'), + winston = require('winston'), meta = require('./meta'), Feed = require('./feed'); (function(PostTools) { PostTools.isMain = function(pid, tid, callback) { - RDB.lrange('tid:' + tid + ':posts', 0, 0, function(err, pids) { + db.getListRange('tid:' + tid + ':posts', 0, 0, function(err, pids) { if(err) { return callback(err); } @@ -83,8 +81,8 @@ var RDB = require('./redis'), } ]); - postSearch.remove(pid, function() { - postSearch.index(content, pid); + db.searchRemove('post', pid, function() { + db.searchIndex('post', content, pid); }); async.parallel([ @@ -93,8 +91,8 @@ var RDB = require('./redis'), PostTools.isMain(pid, tid, function(err, isMainPost) { if (isMainPost) { topics.setTopicField(tid, 'title', title); - topicSearch.remove(tid, function() { - topicSearch.index(title, tid); + db.searchRemove('topic', tid, function() { + db.searchIndex('topic', title, tid); }); } @@ -131,14 +129,14 @@ var RDB = require('./redis'), PostTools.delete = function(uid, pid, callback) { var success = function() { posts.setPostField(pid, 'deleted', 1); - RDB.decr('totalpostcount'); - postSearch.remove(pid); + db.decrObjectField('global', 'postCount'); + db.searchRemove('post', pid); posts.getPostFields(pid, ['tid', 'uid'], function(err, postData) { - RDB.hincrby('topic:' + postData.tid, 'postcount', -1); + db.incrObjectFieldBy('topic:' + postData.tid, 'postcount', -1); user.decrementUserFieldBy(postData.uid, 'postcount', 1, function(err, postcount) { - RDB.zadd('users:postcount', postcount, postData.uid); + db.sortedSetAdd('users:postcount', postcount, postData.uid); }); // Delete the thread if it is the last undeleted post @@ -164,7 +162,7 @@ var RDB = require('./redis'), }; posts.getPostField(pid, 'deleted', function(err, deleted) { - if(deleted === '1') { + if(parseInt(deleted, 10) === 1) { return callback(new Error('Post already deleted!')); } @@ -180,10 +178,10 @@ var RDB = require('./redis'), PostTools.restore = function(uid, pid, callback) { var success = function() { posts.setPostField(pid, 'deleted', 0); - RDB.incr('totalpostcount'); + db.incrObjectField('global', 'postCount'); posts.getPostFields(pid, ['tid', 'uid', 'content'], function(err, postData) { - RDB.hincrby('topic:' + postData.tid, 'postcount', 1); + db.incrObjectFieldBy('topic:' + postData.tid, 'postcount', 1); user.incrementUserFieldBy(postData.uid, 'postcount', 1); @@ -195,7 +193,7 @@ var RDB = require('./redis'), // Restore topic if it is the only post topics.getTopicField(postData.tid, 'postcount', function(err, count) { - if (count === '1') { + if (parseInt(count, 10) === 1) { threadTools.restore(postData.tid, uid); } }); @@ -203,14 +201,14 @@ var RDB = require('./redis'), Feed.updateTopic(postData.tid); Feed.updateRecent(); - postSearch.index(postData.content, pid); + db.searchIndex('post', postData.content, pid); callback(); }); }; posts.getPostField(pid, 'deleted', function(err, deleted) { - if(deleted === '0') { + if(parseInt(deleted, 10) === 0) { return callback(new Error('Post already restored')); } diff --git a/src/posts.js b/src/posts.js index ac4cf01400..c496afdd99 100644 --- a/src/posts.js +++ b/src/posts.js @@ -1,4 +1,4 @@ -var RDB = require('./redis'), +var db = require('./database'), utils = require('./../public/src/utils'), user = require('./user'), topics = require('./topics'), @@ -12,8 +12,6 @@ var RDB = require('./redis'), meta = require('./meta'), async = require('async'), - reds = require('reds'), - postSearch = reds.createSearch('nodebbpostsearch'), nconf = require('nconf'), validator = require('validator'), winston = require('winston'); @@ -23,18 +21,17 @@ var RDB = require('./redis'), Posts.create = function(uid, tid, content, callback) { if (uid === null) { - callback(new Error('invalid-user'), null); - return; + return callback(new Error('invalid-user'), null); } topics.isLocked(tid, function(err, locked) { if(err) { return callback(err, null); } else if(locked) { - callback(new Error('topic-locked'), null); + return callback(new Error('topic-locked'), null); } - RDB.incr('global:next_post_id', function(err, pid) { + db.incrObjectField('global', 'nextPid', function(err, pid) { if(err) { return callback(err, null); } @@ -57,7 +54,7 @@ var RDB = require('./redis'), 'deleted': 0 }; - RDB.hmset('post:' + pid, postData); + db.setObject('post:' + pid, postData); postData.favourited = false; postData.display_moderator_tools = true; @@ -67,26 +64,24 @@ var RDB = require('./redis'), topics.increasePostCount(tid); topics.updateTimestamp(tid, timestamp); - RDB.incr('totalpostcount'); + db.incrObjectField('global', 'postCount'); topics.getTopicFields(tid, ['cid', 'pinned'], function(err, topicData) { - RDB.handle(err); - var cid = topicData.cid; feed.updateTopic(tid); feed.updateRecent(); - RDB.zadd('categories:recent_posts:cid:' + cid, timestamp, pid); + db.sortedSetAdd('categories:recent_posts:cid:' + cid, timestamp, pid); - if(topicData.pinned === '0') { - RDB.zadd('categories:' + cid + ':tid', timestamp, tid); + if(parseInt(topicData.pinned, 10) === 0) { + db.sortedSetAdd('categories:' + cid + ':tid', timestamp, tid); } - RDB.scard('cid:' + cid + ':active_users', function(err, amount) { + db.setCount('cid:' + cid + ':active_users', function(err, amount) { if (amount > 15) { - RDB.spop('cid:' + cid + ':active_users'); + db.setRemoveRandom('cid:' + cid + ':active_users'); } categories.addActiveUser(cid, uid); @@ -110,7 +105,7 @@ var RDB = require('./redis'), plugins.fireHook('action:post.save', postData); - postSearch.index(content, pid); + db.searchIndex('post', content, pid); callback(null, postData); }); @@ -158,7 +153,7 @@ var RDB = require('./redis'), return next(err); } - RDB.del('cid:' + cid + ':read_by_uid'); + db.delete('cid:' + cid + ':read_by_uid'); next(); }); }, @@ -185,8 +180,10 @@ var RDB = require('./redis'), } Posts.getPostsByTid = function(tid, start, end, callback) { - RDB.lrange('tid:' + tid + ':posts', start, end, function(err, pids) { - RDB.handle(err); + db.getListRange('tid:' + tid + ':posts', start, end, function(err, pids) { + if(err) { + return callback(err); + } if (pids.length) { plugins.fireHook('filter:post.getTopic', pids, function(err, posts) { @@ -216,7 +213,7 @@ var RDB = require('./redis'), post.userslug = userData.userslug || ''; post.user_rep = userData.reputation || 0; post.user_postcount = userData.postcount || 0; - post.user_banned = userData.banned === '1'; + post.user_banned = parseInt(userData.banned, 10) === 1; post.picture = userData.picture || require('gravatar').url('', {}, https = nconf.get('https')); post.signature = signature; @@ -253,7 +250,7 @@ var RDB = require('./redis'), async.waterfall([ function(next) { Posts.getPostFields(pid, ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted'], function(err, postData) { - if (postData.deleted === '1') { + if (parseInt(postData.deleted, 10) === 1) { return callback(null); } else { postData.relativeTime = new Date(parseInt(postData.timestamp || 0, 10)).toISOString(); @@ -270,7 +267,7 @@ var RDB = require('./redis'), topics.getTopicFields(postData.tid, ['title', 'cid', 'slug', 'deleted'], function(err, topicData) { if (err) { return callback(err); - } else if (topicData.deleted === '1') { + } else if (parseInt(topicData.deleted, 10) === 1) { return callback(null); } categories.getCategoryFields(topicData.cid, ['name', 'icon', 'slug'], function(err, categoryData) { @@ -313,7 +310,7 @@ var RDB = require('./redis'), }; Posts.getPostData = function(pid, callback) { - RDB.hgetall('post:' + pid, function(err, data) { + db.getObject('post:' + pid, function(err, data) { if(err) { return callback(err, null); } @@ -328,7 +325,7 @@ var RDB = require('./redis'), } Posts.getPostFields = function(pid, fields, callback) { - RDB.hmgetObject('post:' + pid, fields, function(err, data) { + db.getObjectFields('post:' + pid, fields, function(err, data) { if(err) { return callback(err, null); } @@ -358,7 +355,7 @@ var RDB = require('./redis'), } Posts.setPostField = function(pid, field, value, callback) { - RDB.hset('post:' + pid, field, value, callback); + db.setObjectField('post:' + pid, field, value, callback); plugins.fireHook('action:post.setField', { 'pid': pid, 'field': field, @@ -367,28 +364,27 @@ var RDB = require('./redis'), } Posts.getPostsByPids = function(pids, callback) { - var posts = [], - multi = RDB.multi(); + var keys = []; for(var x=0, numPids=pids.length; x=0) { - /* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */ - RedisDB = redis.createClient(nconf.get('redis:host')); - } else { - /* Else, connect over tcp/ip */ - RedisDB = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host')); - } - - if (nconf.get('redis:password')) { - RedisDB.auth(nconf.get('redis:password')); - } - - var db = parseInt(nconf.get('redis:database'), 10); - if (db){ - RedisDB.select(db, function(error){ - if(error !== null){ - winston.error("NodeBB could not connect to your Redis database. Redis returned the following error: " + error.message); - process.exit(); - } - }); - } - - RedisDB.handle = function(error) { - if (error !== null) { - winston.err(error); - if (global.env !== 'production') { - throw new Error(error); - } - } - }; - - - /* - * A possibly more efficient way of doing multiple sismember calls - */ - RedisDB.sismembers = function(key, needles, callback) { - var tempkey = key + ':temp:' + utils.generateUUID(); - RedisDB.sadd(tempkey, needles, function() { - RedisDB.sinter(key, tempkey, function(err, data) { - RedisDB.del(tempkey); - callback(err, data); - }); - }); - }; - - /* - * gets fields of a hash as an object instead of an array - */ - RedisDB.hmgetObject = function(key, fields, callback) { - RedisDB.hmget(key, fields, function(err, data) { - - if(err) { - return callback(err, null); - } - - var returnData = {}; - - for (var i = 0, ii = fields.length; i < ii; ++i) { - returnData[fields[i]] = data[i]; - } - - callback(null, returnData); - }); - }; - - module.exports = RedisDB; - -}(module)); \ No newline at end of file diff --git a/src/routes/admin.js b/src/routes/admin.js index 736fb9c50d..2831787434 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -3,7 +3,7 @@ var nconf = require('nconf'), path = require('path'), winston = require('winston'), - RDB = require('./../redis'), + db = require('./../database'), user = require('./../user'), groups = require('../groups'), topics = require('./../topics'), @@ -55,7 +55,7 @@ var nconf = require('nconf'), (function () { var routes = [ 'categories/active', 'categories/disabled', 'users', 'topics', 'settings', 'themes', - 'twitter', 'facebook', 'gplus', 'redis', 'motd', 'groups', 'plugins', 'logger', + 'twitter', 'facebook', 'gplus', 'database', 'motd', 'groups', 'plugins', 'logger', 'users/latest', 'users/sort-posts', 'users/sort-reputation', 'users/search' ]; @@ -241,7 +241,7 @@ var nconf = require('nconf'), app.get('/categories/active', function (req, res) { categories.getAllCategories(0, function (err, data) { data.categories = data.categories.filter(function (category) { - return (!category.disabled || category.disabled === "0"); + return (!category.disabled || parseInt(category.disabled, 10) === 0); }); res.json(data); }); @@ -250,7 +250,7 @@ var nconf = require('nconf'), app.get('/categories/disabled', function (req, res) { categories.getAllCategories(0, function (err, data) { data.categories = data.categories.filter(function (category) { - return category.disabled === "1"; + return parseInt(category.disabled, 10) === 1; }); res.json(data); }); @@ -265,31 +265,10 @@ var nconf = require('nconf'), }); }); - app.namespace('/redis', function () { + app.namespace('/database', function () { app.get('/', function (req, res) { - RDB.info(function (err, data) { - data = data.split("\r\n"); - var finalData = {}; - - for (var i in data) { - - if (data[i].indexOf(':') == -1 || !data[i]) - continue; - - try { - data[i] = data[i].replace(/:/, "\":\""); - var json = "{\"" + data[i] + "\"}"; - - var jsonObject = JSON.parse(json); - for (var key in jsonObject) { - finalData[key] = jsonObject[key]; - } - } catch (err) { - winston.warn('can\'t parse redis status variable, ignoring', i, data[i], err); - } - } - - res.json(finalData); + db.info(function (err, data) { + res.json(data); }); }); diff --git a/src/routes/api.js b/src/routes/api.js index 9ee9fb14c3..0bebd9f975 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -42,7 +42,7 @@ var path = require('path'), var uid = (req.user) ? req.user.uid : 0; categories.getAllCategories(uid, function (err, data) { data.categories = data.categories.filter(function (category) { - return (!category.disabled || category.disabled === "0"); + return (!category.disabled || parseInt(category.disabled, 10) === 0); }); function iterator(category, callback) { @@ -54,7 +54,7 @@ var path = require('path'), } async.each(data.categories, iterator, function (err) { - data.motd_class = (meta.config.show_motd === '1' || meta.config.show_motd === undefined) ? '' : ' none'; + data.motd_class = (parseInt(meta.config.show_motd, 10) === 1 || meta.config.show_motd === undefined) ? '' : ' none'; data.motd_class += (meta.config.motd && meta.config.motd.length > 0 ? '' : ' default'); data.motd = require('marked')(meta.config.motd || "\n\n# NodeBB v" + pkg.version + "\nWelcome to NodeBB, the discussion platform of the future."); @@ -117,7 +117,7 @@ var path = require('path'), var uid = (req.user) ? req.user.uid : 0; topics.getTopicWithPosts(req.params.id, uid, 0, 10, false, function (err, data) { if (!err) { - if (data.deleted === '1' && data.expose_tools === 0) { + if (parseInt(data.deleted, 10) === 1 && parseInt(data.expose_tools, 10) === 0) { return res.json(404, {}); } res.json(data); @@ -132,10 +132,11 @@ var path = require('path'), categoryTools.privileges(req.params.id, uid, function(err, privileges) { if (!err && privileges.read) { categories.getCategoryById(req.params.id, uid, function (err, data) { - if (!err && data && data.disabled === "0") + if (!err && data && parseInt(data.disabled, 10) === 0) { res.json(data); - else + } else { next(); + } }, req.params.id, uid); } else { res.send(403); @@ -225,18 +226,8 @@ var path = require('path'), app.get('/search/:term', function (req, res, next) { - var reds = require('reds'); - var postSearch = reds.createSearch('nodebbpostsearch'); - var topicSearch = reds.createSearch('nodebbtopicsearch'); - - function search(searchObj, callback) { - searchObj - .query(query = req.params.term).type('or') - .end(callback); - } - function searchPosts(callback) { - search(postSearch, function (err, pids) { + db.search('post', req.params.term, function(err, pids) { if (err) { return callback(err, null); } @@ -247,11 +238,11 @@ var path = require('path'), } callback(null, posts); }); - }) + }); } function searchTopics(callback) { - search(topicSearch, function (err, tids) { + db.search('topic', req.params.term, function(err, tids) { if (err) { return callback(err, null); } diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 88fcc1fd23..743ad9f9b0 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -14,8 +14,11 @@ passport.use(new passportLocal(function(user, password, next) { login_module.loginViaLocal(user, password, function(err, login) { - if (!err) next(null, login.user); - else next(null, false, err); + if (!err) { + next(null, login.user); + } else { + next(null, false, err); + } }); })); diff --git a/src/routes/debug.js b/src/routes/debug.js index 104e48bd08..d089a48b2c 100644 --- a/src/routes/debug.js +++ b/src/routes/debug.js @@ -78,11 +78,6 @@ var DebugRoute = function(app) { }); }); }); - - app.get('/test', function(req, res) { - topics.pushUnreadCount(); - res.send(); - }); }); }; diff --git a/src/routes/user.js b/src/routes/user.js index b0de83ebcb..3eb0acd6b8 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -9,7 +9,7 @@ var fs = require('fs'), postTools = require('../postTools'), utils = require('./../../public/src/utils'), meta = require('./../meta'), - RDB = require('./../redis'), + db = require('./../database'), websockets = require('./../websockets'); (function (User) { @@ -318,10 +318,11 @@ var fs = require('fs'), return next(err); if (userData) { - if (userData.showemail && userData.showemail === "1") + if (userData.showemail && parseInt(userData.showemail, 10) === 1) { userData.showemail = "checked"; - else + } else { userData.showemail = ""; + } res.json(userData); } else { res.json(404, { @@ -382,13 +383,15 @@ var fs = require('fs'), posts.getPostsByUid(userData.theirid, 0, 9, function (posts) { userData.posts = posts.filter(function (p) { - return p && p.deleted !== "1"; + return p && parseInt(p.deleted, 10) !== 1; }); userData.isFollowing = isFollowing; - if (!userData.profileviews) + if (!userData.profileviews) { userData.profileviews = 1; - if (callerUID !== userData.uid) + } + if (callerUID !== userData.uid) { user.incrementUserFieldBy(userData.uid, 'profileviews', 1); + } postTools.parse(userData.signature, function (err, signature) { userData.signature = signature; @@ -454,7 +457,7 @@ var fs = require('fs'), if(websockets.isUserOnline(user.uid)) { onlineUsers.push(user); } else { - RDB.zrem('users:online', user.uid); + db.sortedSetRemove('users:online', user.uid); } callback(null); } @@ -501,21 +504,21 @@ var fs = require('fs'), } function canSeeEmail() { - return callerUID == uid || (data.email && (data.showemail && data.showemail === "1")); + return callerUID == uid || (data.email && (data.showemail && parseInt(data.showemail, 10) === 1)); } if (!canSeeEmail()) { data.email = ""; } - if (callerUID == uid && (!data.showemail || data.showemail === "0")) { + if (callerUID == uid && (!data.showemail || parseInt(data.showemail, 10) === 0)) { data.emailClass = ""; } else { data.emailClass = "hide"; } data.websiteName = data.website.replace('http://', '').replace('https://', ''); - data.banned = data.banned === '1'; + data.banned = parseInt(data.banned, 10) === 1; data.uid = uid; data.yourid = callerUID; data.theirid = uid; diff --git a/src/sitemap.js b/src/sitemap.js index b359b90d73..5568944297 100644 --- a/src/sitemap.js +++ b/src/sitemap.js @@ -45,7 +45,7 @@ var path = require('path'), var topicUrls = []; topics.getAllTopics(null, null, function(err, topics) { topics.forEach(function(topic) { - if (topic.deleted !== '1') { + if (parseInt(topic.deleted, 10) !== 1) { topicUrls.push({ url: path.join('topic', topic.slug), changefreq: 'daily', diff --git a/src/threadTools.js b/src/threadTools.js index a7df6eeaf8..ed32f795be 100644 --- a/src/threadTools.js +++ b/src/threadTools.js @@ -1,4 +1,4 @@ -var RDB = require('./redis'), +var db = require('./database'), topics = require('./topics'), categories = require('./categories'), CategoryTools = require('./categoryTools'), @@ -8,18 +8,20 @@ var RDB = require('./redis'), posts = require('./posts'), meta = require('./meta'), websockets = require('./websockets'); - - reds = require('reds'), - topicSearch = reds.createSearch('nodebbtopicsearch'), winston = require('winston'), nconf = require('nconf'), (function(ThreadTools) { ThreadTools.exists = function(tid, callback) { - RDB.sismember('topics:tid', tid, function(err, ismember) { - if (err) RDB.handle(err); - callback( !! ismember || false); + + db.isSetMember('topics:tid', tid, function(err, ismember) { + + if (err) { + callback(false); + } + + callback(ismember); }); } @@ -85,11 +87,11 @@ var RDB = require('./redis'), ThreadTools.delete = function(tid, callback) { topics.delete(tid); - RDB.decr('totaltopiccount'); + db.decrObjectField('global', 'topicCount'); ThreadTools.lock(tid); - topicSearch.remove(tid); + db.searchRemove('topic', tid); websockets.in('topic_' + tid).emit('event:topic_deleted', { tid: tid, @@ -103,7 +105,7 @@ var RDB = require('./redis'), ThreadTools.restore = function(tid, socket, callback) { topics.restore(tid); - RDB.incr('totaltopiccount'); + db.incrObjectField('global', 'topicCount'); ThreadTools.unlock(tid); websockets.in('topic_' + tid).emit('event:topic_restored', { @@ -112,7 +114,7 @@ var RDB = require('./redis'), }); topics.getTopicField(tid, 'title', function(err, title) { - topicSearch.index(title, tid); + db.searchIndex('topic', title, tid); }); if(callback) { @@ -123,7 +125,7 @@ var RDB = require('./redis'), ThreadTools.pin = function(tid, socket) { topics.setTopicField(tid, 'pinned', 1); topics.getTopicField(tid, 'cid', function(err, cid) { - RDB.zadd('categories:' + cid + ':tid', Math.pow(2, 53), tid); + db.sortedSetAdd('categories:' + cid + ':tid', Math.pow(2, 53), tid); }); if (socket) { @@ -144,7 +146,7 @@ var RDB = require('./redis'), ThreadTools.unpin = function(tid, socket) { topics.setTopicField(tid, 'pinned', 0); topics.getTopicFields(tid, ['cid', 'lastposttime'], function(err, topicData) { - RDB.zadd('categories:' + topicData.cid + ':tid', topicData.lastposttime, tid); + db.sortedSetAdd('categories:' + topicData.cid + ':tid', topicData.lastposttime, tid); }); if (socket) { websockets.in('topic_' + tid).emit('event:topic_unpinned', { @@ -165,14 +167,16 @@ var RDB = require('./redis'), topics.getTopicFields(tid, ['cid', 'lastposttime'], function(err, topicData) { var oldCid = topicData.cid; - var multi = RDB.multi(); - multi.zrem('categories:' + oldCid + ':tid', tid); - multi.zadd('categories:' + cid + ':tid', topicData.lastposttime, tid); + db.sortedSetRemove('categories:' + oldCid + ':tid', tid, function(err, result) { + db.sortedSetAdd('categories:' + cid + ':tid', topicData.lastposttime, tid, function(err, result) { - multi.exec(function(err, result) { - - if (!err && result[0] === 1 && result[1] === 1) { + if(err) { + socket.emit('api:topic.move', { + status: 'error' + }); + return; + } topics.setTopicField(tid, 'cid', cid); @@ -198,17 +202,13 @@ var RDB = require('./redis'), websockets.in('topic_' + tid).emit('event:topic_moved', { tid: tid }); - } else { - socket.emit('api:topic.move', { - status: 'error' - }); - } + }); }); }); } ThreadTools.isFollowing = function(tid, current_user, callback) { - RDB.sismember('tid:' + tid + ':followers', current_user, function(err, following) { + db.isSetMember('tid:' + tid + ':followers', current_user, function(err, following) { callback(following); }); } @@ -216,7 +216,7 @@ var RDB = require('./redis'), ThreadTools.toggleFollow = function(tid, current_user, callback) { ThreadTools.isFollowing(tid, current_user, function(following) { if (!following) { - RDB.sadd('tid:' + tid + ':followers', current_user, function(err, success) { + db.setAdd('tid:' + tid + ':followers', current_user, function(err, success) { if (callback) { if (!err) { callback({ @@ -229,7 +229,7 @@ var RDB = require('./redis'), } }); } else { - RDB.srem('tid:' + tid + ':followers', current_user, function(err, success) { + db.setRemove('tid:' + tid + ':followers', current_user, function(err, success) { if (callback) { if (!err) { callback({ @@ -246,7 +246,7 @@ var RDB = require('./redis'), } ThreadTools.getFollowers = function(tid, callback) { - RDB.smembers('tid:' + tid + ':followers', function(err, followers) { + db.getSetMembers('tid:' + tid + ':followers', function(err, followers) { callback(err, followers.map(function(follower) { return parseInt(follower, 10); })); @@ -274,24 +274,33 @@ var RDB = require('./redis'), }); } ], function(err, results) { - if (!err) notifications.push(results[0], results[1]); - // Otherwise, do nothing + if (!err) { + notifications.push(results[0], results[1]); + } }); } ThreadTools.getLatestUndeletedPid = function(tid, callback) { - RDB.lrange('tid:' + tid + ':posts', 0, -1, function(err, pids) { - if (pids.length === 0) return callback(new Error('no-undeleted-pids-found')); + db.getListRange('tid:' + tid + ':posts', 0, -1, function(err, pids) { + if (pids.length === 0) { + return callback(new Error('no-undeleted-pids-found')); + } pids.reverse(); async.detectSeries(pids, function(pid, next) { posts.getPostField(pid, 'deleted', function(err, deleted) { - if (deleted === '0') next(true); - else next(false); + if (parseInt(deleted, 10) === 0) { + next(true); + } else { + next(false); + } }); }, function(pid) { - if (pid) callback(null, pid); - else callback(new Error('no-undeleted-pids-found')); + if (pid) { + callback(null, pid); + } else { + callback(new Error('no-undeleted-pids-found')); + } }); }); } diff --git a/src/topics.js b/src/topics.js index 3b64676df9..f5a496bc77 100644 --- a/src/topics.js +++ b/src/topics.js @@ -2,10 +2,8 @@ var async = require('async'), gravatar = require('gravatar'), nconf = require('nconf'), validator = require('validator'), - reds = require('reds'), - topicSearch = reds.createSearch('nodebbtopicsearch'), - RDB = require('./redis'), + db = require('./database'), posts = require('./posts'), utils = require('./../public/src/utils'), user = require('./user'), @@ -60,16 +58,16 @@ var async = require('async'), return callback(new Error('too-many-posts'), null); } - RDB.incr('next_topic_id', function(err, tid) { + db.incrObjectField('global', 'nextTid', function(err, tid) { if(err) { return callback(err); } - RDB.sadd('topics:tid', tid); + db.setAdd('topics:tid', tid); var slug = tid + '/' + utils.slugify(title); var timestamp = Date.now(); - RDB.hmset('topic:' + tid, { + db.setObject('topic:' + tid, { 'tid': tid, 'uid': uid, 'cid': cid, @@ -84,19 +82,19 @@ var async = require('async'), 'pinned': 0 }); - topicSearch.index(title, tid); + db.searchIndex('topic', title, tid); user.addTopicIdToUser(uid, tid); // let everyone know that there is an unread topic in this category - RDB.del('cid:' + cid + ':read_by_uid', function(err, data) { + db.delete('cid:' + cid + ':read_by_uid', function(err, data) { Topics.markAsRead(tid, uid); }); // in future it may be possible to add topics to several categories, so leaving the door open here. - RDB.zadd('categories:' + cid + ':tid', timestamp, tid); - RDB.hincrby('category:' + cid, 'topic_count', 1); - RDB.incr('totaltopiccount'); + db.sortedSetAdd('categories:' + cid + ':tid', timestamp, tid); + db.incrObjectField('category:' + cid, 'topic_count'); + db.incrObjectField('global', 'topicCount'); feed.updateCategory(cid); @@ -127,7 +125,7 @@ var async = require('async'), }; Topics.getTopicData = function(tid, callback) { - RDB.hgetall('topic:' + tid, function(err, data) { + db.getObject('topic:' + tid, function(err, data) { if(err) { return callback(err, null); } @@ -173,7 +171,7 @@ var async = require('async'), } postData = postData.filter(function(post) { - return parseInt(current_user, 10) !== 0 || post.deleted === "0"; + return parseInt(current_user, 10) !== 0 || parseInt(post.deleted, 10) === 0; }); function getFavouritesData(next) { @@ -209,7 +207,7 @@ var async = require('async'), privileges = results[2]; for (var i = 0; i < postData.length; ++i) { - postData[i].favourited = fav_data[postData[i].pid] === 1; + postData[i].favourited = fav_data[postData[i].pid]; postData[i].display_moderator_tools = ((current_user != 0) && (postData[i].uid == current_user || privileges.editable)); } @@ -239,8 +237,7 @@ var async = require('async'), since = terms[term]; var args = ['topics:recent', '+inf', timestamp - since, 'LIMIT', start, end - start + 1]; - - RDB.zrevrangebyscore(args, function(err, tids) { + db.getSortedSetRevRangeByScore(args, function(err, tids) { if (err) { return callback(err); } @@ -275,7 +272,7 @@ var async = require('async'), return unreadTids.length < 21 && !done; }, function(callback) { - RDB.zrevrange('topics:recent', start, stop, function(err, tids) { + db.getSortedSetRevRange('topics:recent', start, stop, function(err, tids) { if (err) return callback(err); @@ -316,7 +313,7 @@ var async = require('async'), } async.whilst(continueCondition, function(callback) { - RDB.zrevrange('topics:recent', start, stop, function(err, tids) { + db.getSortedSetRevRange('topics:recent', start, stop, function(err, tids) { if (err) { return callback(err); } @@ -489,18 +486,18 @@ var async = require('async'), getTopicInfo(topicData, function(topicInfo) { - topicData['pin-icon'] = topicData.pinned === '1' ? 'fa-thumb-tack' : 'none'; - topicData['lock-icon'] = topicData.locked === '1' ? 'fa-lock' : 'none'; - topicData['deleted-class'] = topicData.deleted === '1' ? 'deleted' : ''; + topicData['pin-icon'] = parseInt(topicData.pinned, 10) === 1 ? 'fa-thumb-tack' : 'none'; + topicData['lock-icon'] = parseInt(topicData.locked, 10) === 1 ? 'fa-lock' : 'none'; + topicData['deleted-class'] = parseInt(topicData.deleted, 10) === 1 ? 'deleted' : ''; - topicData.unreplied = topicData.postcount === '1'; + topicData.unreplied = parseInt(topicData.postcount, 10) === 1; topicData.username = topicInfo.username || 'anonymous'; topicData.userslug = topicInfo.userslug || ''; topicData.picture = topicInfo.picture || gravatar.url('', {}, https = nconf.get('https')); topicData.categoryIcon = topicInfo.categoryData.icon; topicData.categoryName = topicInfo.categoryData.name; topicData.categorySlug = topicInfo.categoryData.slug; - topicData.badgeclass = (topicInfo.hasread && current_user != 0) ? '' : 'badge-important'; + topicData.badgeclass = (topicInfo.hasread && parseInt(current_user, 10) !== 0) ? '' : 'badge-important'; topicData.teaser_text = topicInfo.teaserInfo.text || '', topicData.teaser_username = topicInfo.teaserInfo.username || ''; topicData.teaser_userslug = topicInfo.teaserInfo.userslug || ''; @@ -577,7 +574,7 @@ var async = require('async'), 'slug': topicData.slug, 'postcount': topicData.postcount, 'viewcount': topicData.viewcount, - 'unreplied': topicData.postcount > 1, + 'unreplied': parseInt(topicData.postcount, 10) > 1, 'topic_id': tid, 'expose_tools': privileges.editable ? 1 : 0, 'posts': topicPosts @@ -594,7 +591,7 @@ var async = require('async'), } function getReadStatus(next) { - if (uid && parseInt(uid) > 0) { + if (uid && parseInt(uid, 10) > 0) { Topics.hasReadTopic(tid, uid, function(read) { next(null, read); }); @@ -619,8 +616,8 @@ var async = require('async'), hasRead = results[1], teaser = results[2]; - topicData['pin-icon'] = topicData.pinned === '1' ? 'fa-thumb-tack' : 'none'; - topicData['lock-icon'] = topicData.locked === '1' ? 'fa-lock' : 'none'; + topicData['pin-icon'] = parseInt(topicData.pinned, 10) === 1 ? 'fa-thumb-tack' : 'none'; + topicData['lock-icon'] = parseInt(topicData.locked, 10) === 1 ? 'fa-lock' : 'none'; topicData.badgeclass = hasRead ? '' : 'badge-important'; topicData.teaser_text = teaser.text || ''; @@ -635,7 +632,7 @@ var async = require('async'), } Topics.getAllTopics = function(limit, after, callback) { - RDB.smembers('topics:tid', function(err, tids) { + db.getSetMembers('topics:tid', function(err, tids) { if(err) { return callback(err, null); } @@ -681,7 +678,7 @@ var async = require('async'), } Topics.markAllRead = function(uid, callback) { - RDB.smembers('topics:tid', function(err, tids) { + db.getSetMembers('topics:tid', function(err, tids) { if (err) { return callback(err, null); } @@ -705,12 +702,12 @@ var async = require('async'), } Topics.markUnRead = function(tid, callback) { - RDB.del('tid:' + tid + ':read_by_uid', callback); + db.delete('tid:' + tid + ':read_by_uid', callback); } Topics.markAsRead = function(tid, uid) { - RDB.sadd('tid:' + tid + ':read_by_uid', uid); + db.setAdd('tid:' + tid + ':read_by_uid', uid); Topics.getTopicField(tid, 'cid', function(err, cid) { @@ -729,19 +726,19 @@ var async = require('async'), } Topics.hasReadTopics = function(tids, uid, callback) { - var batch = RDB.multi(); + var sets = []; for (var i = 0, ii = tids.length; i < ii; i++) { - batch.sismember('tid:' + tids[i] + ':read_by_uid', uid); + sets.push('tid:' + tids[i] + ':read_by_uid'); } - batch.exec(function(err, hasRead) { + db.isMemberOfSets(sets, uid, function(err, hasRead) { callback(hasRead); }); } Topics.hasReadTopic = function(tid, uid, callback) { - RDB.sismember('tid:' + tid + ':read_by_uid', uid, function(err, hasRead) { + db.isSetMember('tid:' + tid + ':read_by_uid', uid, function(err, hasRead) { if (err === null) { callback(hasRead); @@ -757,7 +754,9 @@ var async = require('async'), if (Array.isArray(tids)) { async.eachSeries(tids, function(tid, next) { Topics.getTeaser(tid, function(err, teaser_info) { - if (err) teaser_info = {}; + if (err) { + teaser_info = {}; + } teasers.push(teaser_info); next(); }); @@ -821,23 +820,23 @@ var async = require('async'), } Topics.getTopicField = function(tid, field, callback) { - RDB.hget('topic:' + tid, field, callback); + db.getObjectField('topic:' + tid, field, callback); } Topics.getTopicFields = function(tid, fields, callback) { - RDB.hmgetObject('topic:' + tid, fields, callback); + db.getObjectFields('topic:' + tid, fields, callback); } Topics.setTopicField = function(tid, field, value, callback) { - RDB.hset('topic:' + tid, field, value, callback); + db.setObjectField('topic:' + tid, field, value, callback); } Topics.increasePostCount = function(tid, callback) { - RDB.hincrby('topic:' + tid, 'postcount', 1, callback); + db.incrObjectField('topic:' + tid, 'postcount', callback); } Topics.increaseViewCount = function(tid, callback) { - RDB.hincrby('topic:' + tid, 'viewcount', 1, callback); + db.incrObjectField('topic:' + tid, 'viewcount', callback); } Topics.isLocked = function(tid, callback) { @@ -845,21 +844,21 @@ var async = require('async'), if(err) { return callback(err, null); } - callback(null, locked === "1"); + callback(null, parseInt(locked, 10) === 1); }); } Topics.updateTimestamp = function(tid, timestamp) { - RDB.zadd('topics:recent', timestamp, tid); + db.sortedSetAdd('topics:recent', timestamp, tid); Topics.setTopicField(tid, 'lastposttime', timestamp); } Topics.addPostToTopic = function(tid, pid) { - RDB.rpush('tid:' + tid + ':posts', pid); + db.listAppend('tid:' + tid + ':posts', pid); } Topics.getPids = function(tid, callback) { - RDB.lrange('tid:' + tid + ':posts', 0, -1, callback); + db.getListRange('tid:' + tid + ':posts', 0, -1, callback); } Topics.getUids = function(tid, callback) { @@ -886,23 +885,23 @@ var async = require('async'), Topics.delete = function(tid) { Topics.setTopicField(tid, 'deleted', 1); - RDB.zrem('topics:recent', tid); + db.sortedSetRemove('topics:recent', tid); Topics.getTopicField(tid, 'cid', function(err, cid) { feed.updateCategory(cid); - RDB.hincrby('category:' + cid, 'topic_count', -1); + db.incrObjectFieldBy('category:' + cid, 'topic_count', -1); }); } Topics.restore = function(tid) { Topics.setTopicField(tid, 'deleted', 0); Topics.getTopicField(tid, 'lastposttime', function(err, lastposttime) { - RDB.zadd('topics:recent', lastposttime, tid); + db.sortedSetAdd('topics:recent', lastposttime, tid); }); Topics.getTopicField(tid, 'cid', function(err, cid) { feed.updateCategory(cid); - RDB.hincrby('category:' + cid, 'topic_count', 1); + db.incrObjectFieldBy('category:' + cid, 'topic_count', 1); }); } @@ -923,7 +922,7 @@ var async = require('async'), } Topics.reIndexAll = function(callback) { - RDB.smembers('topics:tid', function(err, tids) { + db.getSetMembers('topics:tid', function(err, tids) { if (err) { callback(err, null); } else { diff --git a/src/upgrade.js b/src/upgrade.js index cec799914d..75265c4643 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -1,19 +1,20 @@ "use strict"; -var RDB = require('./redis.js'), +var db = require('./database'), async = require('async'), winston = require('winston'), notifications = require('./notifications'), categories = require('./categories'), + nconf = require('nconf'), Upgrade = {}, schemaDate, thisSchemaDate; Upgrade.check = function(callback) { // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema - var latestSchema = new Date(2013, 10, 26).getTime(); + var latestSchema = new Date(2013, 11, 2).getTime(); - RDB.get('schemaDate', function(err, value) { + db.get('schemaDate', function(err, value) { if (parseInt(value, 10) >= latestSchema) { callback(true); } else { @@ -23,6 +24,22 @@ Upgrade.check = function(callback) { }; Upgrade.upgrade = function(callback) { + var databaseType = nconf.get('database'); + + if(databaseType === 'redis') { + Upgrade.upgradeRedis(callback); + } else if(databaseType === 'mongo') { + Upgrade.upgradeMongo(callback); + } else { + winston.error('Unknown database type. Aborting upgrade'); + callback(new Error('unknown-database')); + } +}; + +Upgrade.upgradeRedis = function(callback) { + + var RDB = db.client; + winston.info('Beginning Redis database schema update'); async.series([ @@ -179,7 +196,7 @@ Upgrade.upgrade = function(callback) { }, function(next) { thisSchemaDate = new Date(2013, 10, 26).getTime(); - if (schemaDate < thisSchemaDate || 1) { + if (schemaDate < thisSchemaDate) { categories.getAllCategories(0, function(err, categories) { function updateIcon(category, next) { @@ -209,7 +226,57 @@ Upgrade.upgrade = function(callback) { winston.info('[2013/11/26] Update to Category icons skipped.'); next(); } - } + }, + function(next) { + + function updateKeyToHash(key, next) { + RDB.get(key, function(err, value) { + RDB.hset('global', newKeys[key], value, next); + }); + } + + thisSchemaDate = new Date(2013, 11, 2).getTime(); + if (schemaDate < thisSchemaDate) { + + var keys = [ + 'global:next_user_id', + 'next_topic_id', + 'next_gid', + 'notifications:next_nid', + 'global:next_category_id', + 'global:next_message_id', + 'global:next_post_id', + 'usercount', + 'totaltopiccount', + 'totalpostcount' + ]; + + var newKeys = { + 'global:next_user_id':'nextUid', + 'next_topic_id':'nextTid', + 'next_gid':'nextGid', + 'notifications:next_nid':'nextNid', + 'global:next_category_id':'nextCid', + 'global:next_message_id':'nextMid', + 'global:next_post_id':'nextPid', + 'usercount':'userCount', + 'totaltopiccount':'topicCount', + 'totalpostcount':'postCount' + }; + + async.each(keys, updateKeyToHash, function(err) { + if(err) { + return next(err); + } + winston.info('[2013/12/2] Updated global keys to hash.'); + next(); + }); + + } else { + winston.info('[2013/12/2] Update to global keys skipped'); + next(); + } + }, // Add new schema updates here // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 12!!! ], function(err) { @@ -234,4 +301,41 @@ Upgrade.upgrade = function(callback) { }); }; +Upgrade.upgradeMongo = function(callback) { + var MDB = db.client; + + winston.info('Beginning Mongo database schema update'); + + async.series([ + function(next) { + db.get('schemaDate', function(err, value) { + schemaDate = value; + thisSchemaDate = new Date(2013, 11, 6).getTime(); + next(); + }); + } + // Add new schema updates here + + ], function(err) { + if (!err) { + db.set('schemaDate', thisSchemaDate, function(err) { + if (!err) { + winston.info('[upgrade] Mongo schema update complete!'); + if (callback) { + callback(err); + } else { + process.exit(); + } + } else { + winston.error('[upgrade] Could not update NodeBB schema date!'); + process.exit(); + } + }); + } else { + winston.error('[upgrade] Errors were encountered while updating the NodeBB schema: ' + err.message); + process.exit(); + } + }); +} + module.exports = Upgrade; \ No newline at end of file diff --git a/src/user.js b/src/user.js index 7dd14e2426..b45bf30275 100644 --- a/src/user.js +++ b/src/user.js @@ -4,16 +4,15 @@ var bcrypt = require('bcrypt'), nconf = require('nconf'), winston = require('winston'), gravatar = require('gravatar'), - userSearch = require('reds').createSearch('nodebbusersearch'), check = require('validator').check, sanitize = require('validator').sanitize, utils = require('./../public/src/utils'), plugins = require('./plugins'), - RDB = require('./redis'), + db = require('./database'), meta = require('./meta'), emailjsServer = emailjs.server.connect(meta.config['email:smtp:host'] || '127.0.0.1'), - Groups = require('./groups'), + groups = require('./groups'), notifications = require('./notifications'), topics = require('./topics'); @@ -65,16 +64,18 @@ var bcrypt = require('bcrypt'), } ], function(err, results) { if (err) { - return callback(err, null); + return callback(err); } - RDB.incr('global:next_user_id', function(err, uid) { - RDB.handle(err); + db.incrObjectField('global', 'nextUid', function(err, uid) { + if(err) { + return callback(err); + } var gravatar = User.createGravatarURLFromEmail(email); var timestamp = Date.now(); - RDB.hmset('user:' + uid, { + db.setObject('user:' + uid, { 'uid': uid, 'username': username, 'userslug': userslug, @@ -96,22 +97,22 @@ var bcrypt = require('bcrypt'), 'showemail': 0 }); - RDB.hset('username:uid', username, uid); - RDB.hset('userslug:uid', userslug, uid); + db.setObjectField('username:uid', username, uid); + db.setObjectField('userslug:uid', userslug, uid); if (email !== undefined) { - RDB.hset('email:uid', email, uid); + db.setObjectField('email:uid', email, uid); User.sendConfirmationEmail(email); } plugins.fireHook('action:user.create', {uid: uid, username: username, email: email, picture: gravatar, timestamp: timestamp}); - RDB.incr('usercount'); + db.incrObjectField('global', 'userCount'); - RDB.zadd('users:joindate', timestamp, uid); - RDB.zadd('users:postcount', 0, uid); - RDB.zadd('users:reputation', 0, uid); + db.sortedSetAdd('users:joindate', timestamp, uid); + db.sortedSetAdd('users:postcount', 0, uid); + db.sortedSetAdd('users:reputation', 0, uid); - userSearch.index(username, uid); + db.searchIndex('user', username, uid); if (password !== undefined) { User.hashPassword(password, function(err, hash) { @@ -134,11 +135,11 @@ var bcrypt = require('bcrypt'), }; User.getUserField = function(uid, field, callback) { - RDB.hget('user:' + uid, field, callback); + db.getObjectField('user:' + uid, field, callback); }; User.getUserFields = function(uid, fields, callback) { - RDB.hmgetObject('user:' + uid, fields, callback); + db.getObjectFields('user:' + uid, fields, callback); }; User.getMultipleUserFields = function(uids, fields, callback) { @@ -168,7 +169,10 @@ var bcrypt = require('bcrypt'), }; User.getUserData = function(uid, callback) { - RDB.hgetall('user:' + uid, function(err, data) { + db.getObject('user:' + uid, function(err, data) { + if(err) { + return callback(err); + } if (data && data.password) { delete data.password; @@ -177,12 +181,6 @@ var bcrypt = require('bcrypt'), }); }; - User.filterBannedUsers = function(users) { - return users.filter(function(user) { - return (!user.banned || user.banned === '0'); - }); - }; - User.updateProfile = function(uid, data, callback) { var fields = ['email', 'fullname', 'website', 'location', 'birthday', 'signature']; @@ -253,8 +251,8 @@ var bcrypt = require('bcrypt'), return next(err); } - RDB.hdel('email:uid', userData.email); - RDB.hset('email:uid', data.email, uid); + db.deleteObjectField('email:uid', userData.email); + db.setObjectField('email:uid', data.email, uid); User.setUserField(uid, field, data[field]); if (userData.picture !== userData.uploadedpicture) { returnData.picture = gravatarpicture; @@ -282,7 +280,7 @@ var bcrypt = require('bcrypt'), }; User.isEmailAvailable = function(email, callback) { - RDB.hexists('email:uid', email, function(err, exists) { + db.isObjectField('email:uid', email, function(err, exists) { callback(err, !exists); }); }; @@ -316,25 +314,25 @@ var bcrypt = require('bcrypt'), }; User.setUserField = function(uid, field, value, callback) { - RDB.hset('user:' + uid, field, value, callback); + db.setObjectField('user:' + uid, field, value, callback); }; User.setUserFields = function(uid, data, callback) { - RDB.hmset('user:' + uid, data, callback); + db.setObject('user:' + uid, data, callback); }; User.incrementUserFieldBy = function(uid, field, value, callback) { - RDB.hincrby('user:' + uid, field, value, callback); + db.incrObjectFieldBy('user:' + uid, field, value, callback); }; User.decrementUserFieldBy = function(uid, field, value, callback) { - RDB.hincrby('user:' + uid, field, -value, callback); + db.incrObjectFieldBy('user:' + uid, field, -value, callback); }; User.getUsers = function(set, start, stop, callback) { var data = []; - RDB.zrevrange(set, start, stop, function(err, uids) { + db.getSortedSetRevRange(set, start, stop, function(err, uids) { if (err) { return callback(err, null); } @@ -355,7 +353,6 @@ var bcrypt = require('bcrypt'), callback(err, data); }); }); - }; User.createGravatarURLFromEmail = function(email) { @@ -392,8 +389,8 @@ var bcrypt = require('bcrypt'), } function reIndexUser(uid, username) { - userSearch.remove(uid, function() { - userSearch.index(username, uid); + db.searchRemove('user', uid, function() { + db.searchIndex('user', username, uid); }); } @@ -409,7 +406,7 @@ var bcrypt = require('bcrypt'), callback([]); return; } - userSearch.query(username).type('or').end(function(err, uids) { + db.search('user', username, function(err, uids) { if (err) { console.log(err); return; @@ -428,7 +425,7 @@ var bcrypt = require('bcrypt'), User.addPostIdToUser(uid, pid); User.incrementUserFieldBy(uid, 'postcount', 1, function(err, newpostcount) { - RDB.zadd('users:postcount', newpostcount, uid); + db.sortedSetAdd('users:postcount', newpostcount, uid); }); User.setUserField(uid, 'lastposttime', timestamp); @@ -437,15 +434,15 @@ var bcrypt = require('bcrypt'), }; User.addPostIdToUser = function(uid, pid) { - RDB.lpush('uid:' + uid + ':posts', pid); + db.listPrepend('uid:' + uid + ':posts', pid); }; User.addTopicIdToUser = function(uid, tid) { - RDB.lpush('uid:' + uid + ':topics', tid); + db.listPrepend('uid:' + uid + ':topics', tid); }; - User.getPostIds = function(uid, start, end, callback) { - RDB.lrange('uid:' + uid + ':posts', start, end, function(err, pids) { + User.getPostIds = function(uid, start, stop, callback) { + db.getListRange('uid:' + uid + ':posts', start, stop, function(err, pids) { if (!err) { if (pids && pids.length) { callback(pids); @@ -459,51 +456,12 @@ var bcrypt = require('bcrypt'), }); }; - User.sendConfirmationEmail = function(email) { - if (meta.config['email:smtp:host'] && meta.config['email:smtp:port'] && meta.config['email:from']) { - var confirm_code = utils.generateUUID(), - confirm_link = nconf.get('url') + 'confirm/' + confirm_code, - confirm_email = global.templates['emails/header'] + global.templates['emails/email_confirm'].parse({ - 'CONFIRM_LINK': confirm_link - }) + global.templates['emails/footer'], - confirm_email_plaintext = global.templates['emails/email_confirm_plaintext'].parse({ - 'CONFIRM_LINK': confirm_link - }); - - // Email confirmation code - var expiry_time = 60 * 60 * 2, // Expire after 2 hours - email_key = 'email:' + email + ':confirm', - confirm_key = 'confirm:' + confirm_code + ':email'; - RDB.set(email_key, confirm_code); - RDB.expire(email_key, expiry_time); - RDB.set(confirm_key, email); - RDB.expire(confirm_key, expiry_time); - - // Send intro email w/ confirm code - var message = emailjs.message.create({ - text: confirm_email_plaintext, - from: meta.config['email:from'] || 'localhost@example.org', - to: email, - subject: '[NodeBB] Registration Email Verification', - attachment: [{ - data: confirm_email, - alternative: true - }] - }); - - emailjsServer.send(message, function(err, success) { - if (err) { - console.log(err); - } - }); - } - }; User.follow = function(uid, followid, callback) { - RDB.sadd('following:' + uid, followid, function(err, data) { + db.setAdd('following:' + uid, followid, function(err, data) { if (!err) { - RDB.sadd('followers:' + followid, uid, function(err, data) { + db.setAdd('followers:' + followid, uid, function(err, data) { if (!err) { callback(true); } else { @@ -519,9 +477,9 @@ var bcrypt = require('bcrypt'), }; User.unfollow = function(uid, unfollowid, callback) { - RDB.srem('following:' + uid, unfollowid, function(err, data) { + db.setRemove('following:' + uid, unfollowid, function(err, data) { if (!err) { - RDB.srem('followers:' + unfollowid, uid, function(err, data) { + db.setRemove('followers:' + unfollowid, uid, function(err, data) { callback(data); }); } else { @@ -531,7 +489,7 @@ var bcrypt = require('bcrypt'), }; User.getFollowing = function(uid, callback) { - RDB.smembers('following:' + uid, function(err, userIds) { + db.getSetMembers('following:' + uid, function(err, userIds) { if (!err) { User.getDataForUsers(userIds, callback); } else { @@ -541,7 +499,7 @@ var bcrypt = require('bcrypt'), }; User.getFollowers = function(uid, callback) { - RDB.smembers('followers:' + uid, function(err, userIds) { + db.getSetMembers('followers:' + uid, function(err, userIds) { if (!err) { User.getDataForUsers(userIds, callback); } else { @@ -551,12 +509,12 @@ var bcrypt = require('bcrypt'), }; User.getFollowingCount = function(uid, callback) { - RDB.smembers('following:' + uid, function(err, userIds) { + db.getSetMembers('following:' + uid, function(err, userIds) { if (err) { console.log(err); } else { userIds = userIds.filter(function(value) { - return value !== '0'; + return parseInt(value, 10) !== 0; }); callback(userIds.length); } @@ -564,12 +522,12 @@ var bcrypt = require('bcrypt'), }; User.getFollowerCount = function(uid, callback) { - RDB.smembers('followers:' + uid, function(err, userIds) { + db.getSetMembers('followers:' + uid, function(err, userIds) { if(err) { console.log(err); } else { userIds = userIds.filter(function(value) { - return value !== '0'; + return parseInt(value, 10) !== 0; }); callback(userIds.length); } @@ -585,7 +543,7 @@ var bcrypt = require('bcrypt'), } function iterator(uid, callback) { - if(uid === "0") { + if(parseInt(uid, 10) === 0) { return callback(null); } @@ -603,7 +561,7 @@ var bcrypt = require('bcrypt'), User.sendPostNotificationToFollowers = function(uid, tid, pid) { User.getUserField(uid, 'username', function(err, username) { - RDB.smembers('followers:' + uid, function(err, followers) { + db.getSetMembers('followers:' + uid, function(err, followers) { topics.getTopicField(tid, 'slug', function(err, slug) { var message = '' + username + ' made a new post'; @@ -616,9 +574,9 @@ var bcrypt = require('bcrypt'), }; User.isFollowing = function(uid, theirid, callback) { - RDB.sismember('following:' + uid, theirid, function(err, data) { + db.isSetMember('following:' + uid, theirid, function(err, isMember) { if (!err) { - callback(data === 1); + callback(isMember); } else { console.log(err); } @@ -632,8 +590,10 @@ var bcrypt = require('bcrypt'), }; User.count = function(socket) { - RDB.get('usercount', function(err, count) { - RDB.handle(err); + db.getObjectField('global', 'userCount', function(err, count) { + if(err) { + return; + } socket.emit('user.count', { count: count ? count : 0 @@ -642,11 +602,11 @@ var bcrypt = require('bcrypt'), }; User.getUidByUsername = function(username, callback) { - RDB.hget('username:uid', username, callback); + db.getObjectField('username:uid', username, callback); }; User.getUidByUserslug = function(userslug, callback) { - RDB.hget('userslug:uid', userslug, callback); + db.getObjectField('userslug:uid', userslug, callback); }; User.getUsernamesByUids = function(uids, callback) { @@ -688,52 +648,54 @@ var bcrypt = require('bcrypt'), }; User.getUidByEmail = function(email, callback) { - RDB.hget('email:uid', email, function(err, data) { + db.getObjectField('email:uid', email, function(err, data) { if (err) { - RDB.handle(err); + return callback(err); } - callback(data); + callback(null, data); }); }; User.getUidByTwitterId = function(twid, callback) { - RDB.hget('twid:uid', twid, function(err, uid) { + db.getObjectField('twid:uid', twid, function(err, uid) { if (err) { - RDB.handle(err); + return callback(err); } - callback(uid); + callback(null, uid); }); }; User.getUidByGoogleId = function(gplusid, callback) { - RDB.hget('gplusid:uid', gplusid, function(err, uid) { + db.getObjectField('gplusid:uid', gplusid, function(err, uid) { if (err) { - RDB.handle(err); + return callback(err); } - callback(uid); + callback(null, uid); }); }; User.getUidByFbid = function(fbid, callback) { - RDB.hget('fbid:uid', fbid, function(err, uid) { + db.getObjectField('fbid:uid', fbid, function(err, uid) { if (err) { - RDB.handle(err); + return callback(err); } - callback(uid); + callback(null, uid); }); }; User.isModerator = function(uid, cid, callback) { - RDB.sismember('cid:' + cid + ':moderators', uid, function(err, exists) { - RDB.handle(err); - callback(err, !! exists); + db.isSetMember('cid:' + cid + ':moderators', uid, function(err, exists) { + if(err) { + return calback(err); + } + callback(err, exists); }); }; User.isAdministrator = function(uid, callback) { - Groups.getGidFromName('Administrators', function(err, gid) { - Groups.isMember(uid, gid, function(err, isAdmin) { - callback(err, !! isAdmin); + groups.getGidFromName('Administrators', function(err, gid) { + groups.isMember(uid, gid, function(err, isAdmin) { + callback(err, isAdmin); }); }); }; @@ -745,15 +707,15 @@ var bcrypt = require('bcrypt'), callback = null; } - RDB.hget('reset:uid', code, function(err, uid) { + db.getObjectField('reset:uid', code, function(err, uid) { if (err) { - RDB.handle(err); + return callback(false); } if (uid !== null) { - RDB.hget('reset:expiry', code, function(err, expiry) { + db.getObjectField('reset:expiry', code, function(err, expiry) { if (err) { - RDB.handle(err); + return callback(false); } if (expiry >= +Date.now() / 1000 | 0) { @@ -766,8 +728,8 @@ var bcrypt = require('bcrypt'), } } else { // Expired, delete from db - RDB.hdel('reset:uid', code); - RDB.hdel('reset:expiry', code); + db.deleteObjectField('reset:uid', code); + db.deleteObjectField('reset:expiry', code); if (!callback) { socket.emit('user:reset.valid', { valid: false @@ -789,12 +751,12 @@ var bcrypt = require('bcrypt'), }); }, send: function(socket, email) { - User.getUidByEmail(email, function(uid) { + User.getUidByEmail(email, function(err, uid) { if (uid !== null) { // Generate a new reset code var reset_code = utils.generateUUID(); - RDB.hset('reset:uid', reset_code, uid); - RDB.hset('reset:expiry', reset_code, (60 * 60) + new Date() / 1000 | 0); // Active for one hour + db.setObjectField('reset:uid', reset_code, uid); + db.setobjectField('reset:expiry', reset_code, (60 * 60) + new Date() / 1000 | 0); // Active for one hour var reset_link = nconf.get('url') + 'reset/' + reset_code, reset_email = global.templates['emails/reset'].parse({ @@ -842,17 +804,17 @@ var bcrypt = require('bcrypt'), commit: function(socket, code, password) { this.validate(socket, code, function(validated) { if (validated) { - RDB.hget('reset:uid', code, function(err, uid) { + db.getObjectField('reset:uid', code, function(err, uid) { if (err) { - RDB.handle(err); + return; } User.hashPassword(password, function(err, hash) { User.setUserField(uid, 'password', hash); }); - RDB.hdel('reset:uid', code); - RDB.hdel('reset:expiry', code); + db.deleteObjectField('reset:uid', code); + db.deleteObjectField('reset:expiry', code); socket.emit('user:reset.commit', { status: 'ok' @@ -863,9 +825,48 @@ var bcrypt = require('bcrypt'), } }; + User.sendConfirmationEmail = function(email) { + if (meta.config['email:smtp:host'] && meta.config['email:smtp:port'] && meta.config['email:from']) { + var confirm_code = utils.generateUUID(), + confirm_link = nconf.get('url') + 'confirm/' + confirm_code, + confirm_email = global.templates['emails/header'] + global.templates['emails/email_confirm'].parse({ + 'CONFIRM_LINK': confirm_link + }) + global.templates['emails/footer'], + confirm_email_plaintext = global.templates['emails/email_confirm_plaintext'].parse({ + 'CONFIRM_LINK': confirm_link + }); + + // Email confirmation code + var expiry_time = Date.now() / 1000 + 60 * 60 * 2; + + db.setObjectField('email:confirm', email, confirm_code); + + db.setObjectField('confirm:email', confirm_code, email); + db.setObjectField('confirm:email', confirm_code + ':expire', expiry_time); + + // Send intro email w/ confirm code + var message = emailjs.message.create({ + text: confirm_email_plaintext, + from: meta.config['email:from'] || 'localhost@example.org', + to: email, + subject: '[NodeBB] Registration Email Verification', + attachment: [{ + data: confirm_email, + alternative: true + }] + }); + + emailjsServer.send(message, function(err, success) { + if (err) { + console.log(err); + } + }); + } + }; + User.email = { exists: function(socket, email, callback) { - User.getUidByEmail(email, function(exists) { + User.getUidByEmail(email, function(err, exists) { exists = !! exists; if (typeof callback !== 'function') { socket.emit('user.email.exists', { @@ -877,14 +878,30 @@ var bcrypt = require('bcrypt'), }); }, confirm: function(code, callback) { - RDB.get('confirm:' + code + ':email', function(err, email) { + db.getObjectFields('confirm:email', [code, code + ':expire'], function(err, data) { if (err) { - RDB.handle(err); + return callback({ + status:'error' + }); + } + + var email = data.email; + var expiry = data[code + ':expire']; + if (parseInt(expiry, 10) >= Date.now() / 1000) { + + db.deleteObjectField('confirm:email', code); + db.deleteObjectField('confirm:email', code + ':expire'); + + return callback({ + status: 'expired' + }); } if (email !== null) { - RDB.set('email:' + email + ':confirm', true); - RDB.del('confirm:' + code + ':email'); + db.setObjectField('email:confirm', email, true); + + db.deleteObjectField('confirm:email', code); + db.deleteObjectField('confirm:email', code + ':expire'); callback({ status: 'ok' }); @@ -903,7 +920,7 @@ var bcrypt = require('bcrypt'), async.parallel({ unread: function(next) { - RDB.zrevrange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) { // @todo handle err var unread = []; @@ -919,7 +936,7 @@ var bcrypt = require('bcrypt'), if (notif_data) { unread.push(notif_data); } else { - RDB.zrem('uid:' + uid + ':notifications:unread', nid); + db.sortedSetRemove('uid:' + uid + ':notifications:unread', nid); } next(); @@ -933,7 +950,7 @@ var bcrypt = require('bcrypt'), }); }, read: function(next) { - RDB.zrevrange('uid:' + uid + ':notifications:read', 0, 10, function(err, nids) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, 10, function(err, nids) { // @todo handle err var read = []; @@ -949,7 +966,7 @@ var bcrypt = require('bcrypt'), if (notif_data) { read.push(notif_data); } else { - RDB.zrem('uid:' + uid + ':notifications:read', nid); + db.sortedSetRemove('uid:' + uid + ':notifications:read', nid); } next(); @@ -981,13 +998,13 @@ var bcrypt = require('bcrypt'), before = new Date(parseInt(before, 10)); } - RDB.multi() - .zrevrangebyscore('uid:' + uid + ':notifications:read', before ? before.getTime(): now.getTime(), -Infinity, 'LIMIT', 0, limit) - .zrevrangebyscore('uid:' + uid + ':notifications:unread', before ? before.getTime(): now.getTime(), -Infinity, 'LIMIT', 0, limit) - .exec(function(err, results) { - // Merge the read and unread notifications - var nids = results[0].concat(results[1]); + var args1 = ['uid:' + uid + ':notifications:read', before ? before.getTime(): now.getTime(), -Infinity, 'LIMIT', 0, limit]; + var args2 = ['uid:' + uid + ':notifications:unread', before ? before.getTime(): now.getTime(), -Infinity, 'LIMIT', 0, limit]; + + db.getSortedSetRevRangeByScore(args1, function(err, results1) { + db.getSortedSetRevRangeByScore(args2, function(err, results2) { + var nids = results1.concat(results2); async.map(nids, function(nid, next) { notifications.get(nid, uid, function(notif_data) { next(null, notif_data); @@ -1007,14 +1024,20 @@ var bcrypt = require('bcrypt'), callback(err, notifs); }); }); + }); + }, getUnreadCount: function(uid, callback) { - RDB.zcount('uid:' + uid + ':notifications:unread', -Infinity, Infinity, callback); + db.sortedSetCount('uid:' + uid + ':notifications:unread', -Infinity, Infinity, callback); }, getUnreadByUniqueId: function(uid, uniqueId, callback) { - RDB.zrange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) { + db.getSortedSetRange('uid:' + uid + ':notifications:unread', 0, -1, function(err, nids) { + async.filter(nids, function(nid, next) { notifications.get(nid, uid, function(notifObj) { + if(!notifObj) { + next(false); + } if (notifObj.uniqueId === uniqueId) { next(true); } else { diff --git a/src/webserver.js b/src/webserver.js index 389255a0a7..ad8614911d 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -5,7 +5,6 @@ var path = require('path'), express_namespace = require('express-namespace'), WebServer = express(), server = require('http').createServer(WebServer), - RedisStore = require('connect-redis')(express), nconf = require('nconf'), winston = require('winston'), validator = require('validator'), @@ -14,7 +13,7 @@ var path = require('path'), pkg = require('../package.json'), utils = require('../public/src/utils'), - RDB = require('./redis'), + db = require('./database'), user = require('./user'), categories = require('./categories'), posts = require('./posts'), @@ -141,17 +140,16 @@ var path = require('path'), })); app.use(express.bodyParser()); // Puts POST vars in request.body app.use(express.cookieParser()); // If you want to parse cookies (res.cookies) + app.use(express.session({ - store: new RedisStore({ - client: RDB, - ttl: 60 * 60 * 24 * 30 - }), + store: db.sessionStore, secret: nconf.get('secret'), key: 'express.sid', cookie: { maxAge: 60 * 60 * 24 * 30 * 1000 // 30 days } })); + app.use(express.csrf()); // Local vars, other assorted setup @@ -173,33 +171,33 @@ var path = require('path'), function(next) { async.parallel([ function(next) { - // Theme configuration - RDB.hmget('config', 'theme:type', 'theme:id', 'theme:staticDir', 'theme:templates', function(err, themeData) { - var themeId = (themeData[1] || 'nodebb-theme-vanilla'); + + db.getObjectFields('config', ['theme:type', 'theme:id', 'theme:staticDir', 'theme:templates'], function(err, themeData) { + var themeId = (themeData['theme:id'] || 'nodebb-theme-vanilla'); // Detect if a theme has been selected, and handle appropriately - if (!themeData[0] || themeData[0] === 'local') { + if (!themeData['theme:type'] || themeData['theme:type'] === 'local') { // Local theme if (process.env.NODE_ENV === 'development') { winston.info('[themes] Using theme ' + themeId); } // Theme's static directory - if (themeData[2]) { - app.use('/css/assets', express.static(path.join(__dirname, '../node_modules', themeData[1], themeData[2]), { + if (themeData['theme:staticDir']) { + app.use('/css/assets', express.static(path.join(__dirname, '../node_modules', themeData['theme:id'], themeData['theme:staticDir']), { maxAge: app.enabled('cache') ? 5184000000 : 0 })); if (process.env.NODE_ENV === 'development') { - winston.info('Static directory routed for theme: ' + themeData[1]); + winston.info('Static directory routed for theme: ' + themeData['theme:id']); } } - if (themeData[3]) { - app.use('/templates', express.static(path.join(__dirname, '../node_modules', themeData[1], themeData[3]), { + if (themeData['theme:templates']) { + app.use('/templates', express.static(path.join(__dirname, '../node_modules', themeData['theme:id'], themeData['theme:templates']), { maxAge: app.enabled('cache') ? 5184000000 : 0 })); if (process.env.NODE_ENV === 'development') { - winston.info('Custom templates directory routed for theme: ' + themeData[1]); + winston.info('Custom templates directory routed for theme: ' + themeData['theme:id']); } } @@ -411,7 +409,7 @@ var path = require('path'), "categories": function (next) { categories.getAllCategories(0, function (err, returnData) { returnData.categories = returnData.categories.filter(function (category) { - if (category.disabled !== '1') { + if (parseInt(category.disabled, 10) !== 1) { return true; } else { return false; @@ -467,7 +465,7 @@ var path = require('path'), function (next) { topics.getTopicWithPosts(tid, ((req.user) ? req.user.uid : 0), 0, -1, true, function (err, topicData) { if (topicData) { - if (topicData.deleted === '1' && topicData.expose_tools === 0) { + if (parseInt(topicData.deleted, 10) === 1 && parseInt(topicData.expose_tools, 10) === 0) { return next(new Error('Topic deleted'), null); } } @@ -589,7 +587,7 @@ var path = require('path'), categories.getCategoryById(cid, 0, function (err, categoryData) { if (categoryData) { - if (categoryData.disabled === '1') { + if (parseInt(categoryData.disabled, 10) === 1) { return next(new Error('Category disabled'), null); } } diff --git a/src/websockets.js b/src/websockets.js index 6e18ee6edd..8a76ca3402 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -9,15 +9,10 @@ var cookie = require('cookie'), gravatar = require('gravatar'), winston = require('winston'), - RedisStoreLib = require('connect-redis')(express), - RDB = require('./redis'), - RedisStore = new RedisStoreLib({ - client: RDB, - ttl: 60 * 60 * 24 * 14 - }), + db = require('./database'), user = require('./user'), - Groups = require('./groups'), + groups = require('./groups'), posts = require('./posts'), favourites = require('./favourites'), utils = require('../public/src/utils'), @@ -70,9 +65,12 @@ websockets.init = function(io) { // Validate the session, if present socketCookieParser(hs, {}, function(err) { sessionID = socket.handshake.signedCookies["express.sid"]; - RedisStore.get(sessionID, function(err, sessionData) { - if (!err && sessionData && sessionData.passport && sessionData.passport.user) uid = users[sessionID] = sessionData.passport.user; - else uid = users[sessionID] = 0; + db.sessionStore.get(sessionID, function(err, sessionData) { + if (!err && sessionData && sessionData.passport && sessionData.passport.user) { + uid = users[sessionID] = sessionData.passport.user; + } else { + uid = users[sessionID] = 0; + } userSockets[uid] = userSockets[uid] || []; userSockets[uid].push(socket); @@ -89,7 +87,7 @@ websockets.init = function(io) { if (uid) { - RDB.zadd('users:online', Date.now(), uid, function(err, data) { + db.sortedSetAdd('users:online', Date.now(), uid, function(err, data) { socket.join('uid_' + uid); user.getUserField(uid, 'username', function(err, username) { @@ -117,7 +115,7 @@ websockets.init = function(io) { delete users[sessionID]; delete userSockets[uid]; if (uid) { - RDB.zrem('users:online', uid, function(err, data) { + db.sortedSetRemove('users:online', uid, function(err, data) { }); } } @@ -348,7 +346,7 @@ websockets.init = function(io) { }); socket.on('api:topics.post', function(data) { - if (uid < 1 && meta.config.allowGuestPosting === '0') { + if (uid < 1 && parseInt(meta.config.allowGuestPosting, 10) === 0) { socket.emit('event:alert', { title: 'Post Unsuccessful', message: 'You don't seem to be logged in, so you cannot reply.', @@ -419,7 +417,7 @@ websockets.init = function(io) { }); socket.on('api:posts.reply', function(data) { - if (uid < 1 && meta.config.allowGuestPosting === '0') { + if (uid < 1 && parseInt(meta.config.allowGuestPosting, 10) === 0) { socket.emit('event:alert', { title: 'Reply Unsuccessful', message: 'You don't seem to be logged in, so you cannot reply.', @@ -781,7 +779,7 @@ websockets.init = function(io) { }); socket.on('api:composer.push', function(data) { - if (uid > 0 || meta.config.allowGuestPosting === '1') { + if (parseInt(uid, 10) > 0 || parseInt(meta.config.allowGuestPosting, 10) === 1) { if (parseInt(data.tid) > 0) { topics.getTopicData(data.tid, function(err, topicData) { if (data.body) @@ -1029,16 +1027,16 @@ websockets.init = function(io) { }; if (set) { - Groups.joinByGroupName('cid:' + cid + ':privileges:' + privilege, uid, cb); + groups.joinByGroupName('cid:' + cid + ':privileges:' + privilege, uid, cb); } else { - Groups.leaveByGroupName('cid:' + cid + ':privileges:' + privilege, uid, cb); + groups.leaveByGroupName('cid:' + cid + ':privileges:' + privilege, uid, cb); } }); socket.on('api:admin.categories.getPrivilegeSettings', function(cid, callback) { async.parallel({ "+r": function(next) { - Groups.getByGroupName('cid:' + cid + ':privileges:+r', { expand: true }, function(err, groupObj) { + groups.getByGroupName('cid:' + cid + ':privileges:+r', { expand: true }, function(err, groupObj) { if (!err) { next.apply(this, arguments); } else { @@ -1049,7 +1047,7 @@ websockets.init = function(io) { }); }, "+w": function(next) { - Groups.getByGroupName('cid:' + cid + ':privileges:+w', { expand: true }, function(err, groupObj) { + groups.getByGroupName('cid:' + cid + ':privileges:+w', { expand: true }, function(err, groupObj) { if (!err) { next.apply(this, arguments); } else { @@ -1090,19 +1088,19 @@ websockets.init = function(io) { */ socket.on('api:groups.create', function(data, callback) { - Groups.create(data.name, data.description, function(err, groupObj) { + groups.create(data.name, data.description, function(err, groupObj) { callback(err ? err.message : null, groupObj || undefined); }); }); socket.on('api:groups.delete', function(gid, callback) { - Groups.destroy(gid, function(err) { + groups.destroy(gid, function(err) { callback(err ? err.message : null, err ? null : 'OK'); }); }); socket.on('api:groups.get', function(gid, callback) { - Groups.get(gid, { + groups.get(gid, { expand: true }, function(err, groupObj) { callback(err ? err.message : null, groupObj || undefined); @@ -1110,15 +1108,15 @@ websockets.init = function(io) { }); socket.on('api:groups.join', function(data, callback) { - Groups.join(data.gid, data.uid, callback); + groups.join(data.gid, data.uid, callback); }); socket.on('api:groups.leave', function(data, callback) { - Groups.leave(data.gid, data.uid, callback); + groups.leave(data.gid, data.uid, callback); }); socket.on('api:groups.update', function(data, callback) { - Groups.update(data.gid, data.values, function(err) { + groups.update(data.gid, data.values, function(err) { callback(err ? err.message : null); }); }); @@ -1128,14 +1126,14 @@ websockets.init = function(io) { function emitTopicPostStats() { - RDB.mget(['totaltopiccount', 'totalpostcount'], function(err, data) { + db.getObjectFields('global', ['topicCount', 'postCount'], function(err, data) { if (err) { return winston.err(err); } var stats = { - topics: data[0] ? data[0] : 0, - posts: data[1] ? data[1] : 0 + topics: data.topicCount ? data.topicCount : 0, + posts: data.postCount ? data.postCount : 0 }; io.sockets.emit('post.stats', stats); @@ -1143,7 +1141,7 @@ websockets.init = function(io) { } websockets.emitUserCount = function() { - RDB.get('usercount', function(err, count) { + db.getObjectField('global', 'userCount', function(err, count) { io.sockets.emit('user.count', { count: count }); @@ -1154,8 +1152,10 @@ websockets.init = function(io) { return io.sockets.in(room); }; +} + websockets.getConnectedClients = function() { return userSockets; } -} -})(module.exports); \ No newline at end of file + +})(module.exports); diff --git a/tests/categories.js b/tests/categories.js index 11456b54a4..070a71e884 100644 --- a/tests/categories.js +++ b/tests/categories.js @@ -7,14 +7,7 @@ process.on('uncaughtException', function (err) { }); var assert = require('assert'), - RDB = require('../mocks/redismock'); - -// Reds is not technically used in this test suite, but its invocation is required to stop the included -// libraries from trying to connect to the default Redis host/port -var reds = require('reds'); -reds.createClient = function () { - return reds.client || (reds.client = RDB); -}; + db = require('../mocks/databasemock'); var Categories = require('../src/categories'); @@ -23,6 +16,7 @@ describe('Categories', function() { describe('.create', function() { it('should create a new category', function(done) { + Categories.create({ name: 'Test Category', description: 'Test category created by testing script', @@ -62,9 +56,7 @@ describe('Categories', function() { }); after(function() { - RDB.multi() - .del('category:'+categoryObj.cid) - .rpop('categories:cid') - .exec(); + db.delete('category:' + categoryObj.cid); + db.listRemoveLast('categories:cid'); }); }); \ No newline at end of file diff --git a/tests/database.js b/tests/database.js new file mode 100644 index 0000000000..071b291957 --- /dev/null +++ b/tests/database.js @@ -0,0 +1,330 @@ +var assert = require('assert'), + db = require('../mocks/databasemock'), + async = require('async'); + + +describe('Test database', function() { + it('should work', function(){ + assert.doesNotThrow(function(){ + var db = require('../mocks/databasemock'); + }); + }); + + it('should not throw err', function(done) { + var objectKey = 'testObj'; + + function setObject(callback) { + db.setObject(objectKey, {name:'baris', 'lastname':'usakli', age:3}, function(err, result) { + callback(err, {'setObject':result}); + }); + } + + function getObject(callback) { + db.getObject(objectKey, function(err, data) { + callback(err, {'getObject':data}); + }); + } + + function getObjects(callback) { + db.getObjects(['testing1', objectKey, 'doesntexist', 'user:1'], function(err, data) { + callback(err, {'getObjects':data}); + }); + } + + function setObjectField(callback) { + db.setObjectField(objectKey, 'reputation', 5, function(err, result) { + callback(err, {'setObjectField': result}); + }); + } + + function getObjectField(callback) { + db.getObjectField(objectKey, 'age', function(err, age) { + callback(err, {'getObjectField' : age}); + }); + } + + function getObjectFields(callback) { + db.getObjectFields(objectKey, ['name', 'lastname'], function(err, data) { + callback(err, {'getObjectFields':data}); + }); + } + + function getObjectValues(callback) { + db.getObjectValues(objectKey, function(err, data) { + callback(err, {'getObjectValues':data}); + }); + } + + function isObjectField(callback) { + db.isObjectField(objectKey, 'age', function(err, data) { + callback(err, {'isObjectField':data}); + }); + } + + function deleteObjectField(callback) { + db.deleteObjectField(objectKey, 'reputation', function(err, data) { + callback(err, {'deleteObjectField':data}); + }); + } + + function incrObjectFieldBy(callback) { + db.incrObjectFieldBy(objectKey, 'age', 3, function(err, data) { + callback(err, {'incrObjectFieldBy':data}); + }); + } + + var objectTasks = [ + setObject, + getObject, + deleteObjectField, + getObject, + setObjectField, + getObject, + deleteObjectField, + getObject, + getObjectField, + getObjectFields, + getObjectValues, + isObjectField, + incrObjectFieldBy, + getObject, + getObjects + ]; + + async.series(objectTasks, function(err, results) { + assert.equal(err, null, 'error in object methods'); + assert.ok(results); + + done(); + }); + }); + + it('should not throw err', function(done) { + + function sortedSetAdd(callback) { + db.sortedSetAdd('sortedSet3', 12, 5, function(err, data) { + callback(err, {'sortedSetAdd': data}); + }); + } + + function sortedSetRemove(callback) { + db.sortedSetRemove('sortedSet3', 12, function(err, data) { + callback(err, {'sortedSetRemove': data}); + }); + } + + function getSortedSetRange(callback) { + db.getSortedSetRevRange('sortedSet3', 0, -1, function(err, data) { + callback(err, {'getSortedSetRange': data}); + }); + } + + function getSortedSetRevRangeByScore(callback) { + var args = ['sortedSet2', '+inf', 100, 'LIMIT', 0, 10]; + db.getSortedSetRevRangeByScore(args, function(err, data) { + callback(err, {'getSortedSetRevRangeByScore': data}); + }); + } + + function sortedSetCount(callback) { + db.sortedSetCount('sortedSet3', -Infinity, Infinity, function(err, data) { + callback(err, {'sortedSetCount': data}); + }); + } + + function sortedSetScore(callback) { + db.sortedSetScore('users:joindate', 1, function(err, data) { + callback(err, {'sortedSetScore': data}); + }); + } + + function sortedSetsScore(callback) { + db.sortedSetsScore(['users:joindate', 'users:derp', 'users:postcount'], 1, function(err, data) { + callback(err, {'sortedSetsScore': data}); + }); + } + + var sortedSetTasks = [ + sortedSetAdd, + getSortedSetRange, + sortedSetAdd, + getSortedSetRange, + sortedSetRemove, + getSortedSetRange, + sortedSetCount, + sortedSetScore, + sortedSetsScore, + getSortedSetRevRangeByScore + ]; + + async.series(sortedSetTasks, function(err, results) { + assert.equal(err, null, 'error in sorted set methods'); + assert.ok(results); + + done(); + }); + + }); + + it('should not throw err', function(done) { + + function listAppend(callback) { + db.listAppend('myList5', 5, function(err, data) { + callback(err, {'listAppend': data}); + }); + } + + function listPrepend(callback) { + db.listPrepend('myList5', 4, function(err, data) { + callback(err, {'listPrepend': data}); + }); + } + + + function listRemoveLast(callback) { + db.listRemoveLast('myList5', function(err, data) { + callback(err, {'listRemoveLast': data}); + }); + } + + + function getListRange(callback) { + db.getListRange('myList5', 0, -1, function(err, data) { + callback(err, {'getListRange': data}); + }); + } + + var listTasks = [ + listAppend, + listPrepend, + getListRange, + listRemoveLast, + getListRange + ]; + + async.series(listTasks, function(err, results) { + assert.equal(err, null, 'error in list methods'); + assert.ok(results); + + done(); + }); + + }); + + it('should not throw err', function(done) { + + function get(callback) { + db.get('testingStr', function(err, data) { + callback(err, {'get': data}); + }); + } + + function set(callback) { + db.set('testingStr', 'opppa gangastayla', function(err, data) { + callback(err, {'set': data}); + }); + } + + function deleteKey(callback) { + db.delete('testingStr', function(err, data) { + callback(err, {'delete': data}); + }); + } + + function exists(callback) { + db.exists('testingStr', function(err, data) { + callback(err, {'exists': data}); + }); + } + + var keyTasks = [ + get, + set, + get, + exists, + deleteKey, + deleteKey, + get, + exists + ]; + + async.series(keyTasks, function(err, results) { + assert.equal(err, null, 'error in key methods'); + assert.ok(results); + + done(); + }); + + }); + + it('should not throw err', function(done) { + + + function setAdd(callback) { + db.setAdd('myTestSet', 15, function(err, data) { + callback(err, {'setAdd': data}); + }); + } + + function setRemove(callback) { + db.setRemove('myTestSet', 15, function(err, data) { + callback(err, {'setRemove': data}); + }); + } + + function getSetMembers(callback) { + db.getSetMembers('myTestSet', function(err, data) { + callback(err, {'getSetMembers': data}); + }); + } + + function isSetMember(callback) { + db.isSetMember('myTestSet', 15, function(err, data) { + callback(err, {'isSetMember': data}); + }); + } + + function isMemberOfSets(callback) { + db.isMemberOfSets(['doesntexist', 'myTestSet', 'nonexistingSet'], 15, function(err, data) { + callback(err, {'isMemberOfSets': data}); + }); + } + + function setRemoveRandom(callback) { + db.setRemoveRandom('myTestSet', function(err, data) { + callback(err, {'setRemoveRandom': data}); + }); + } + + function setCount(callback) { + db.setCount('myTestSet', function(err, data) { + callback(err, {'setCount': data}); + }); + } + + + var setTasks = [ + getSetMembers, + setAdd, + getSetMembers, + setRemove, + getSetMembers, + isSetMember, + setAdd, + getSetMembers, + isSetMember, + setRemoveRandom, + getSetMembers, + setCount, + isMemberOfSets + ]; + + + require('async').series(setTasks, function(err, results) { + assert.equal(err, null, 'error in set methods'); + assert.ok(results); + + done(); + }); + }); +}); diff --git a/tests/redis.js b/tests/redis.js deleted file mode 100644 index 0af4f1a120..0000000000 --- a/tests/redis.js +++ /dev/null @@ -1,10 +0,0 @@ -var assert = require('assert'); - - -describe('Test database', function() { - it('should work', function(){ - assert.doesNotThrow(function(){ - var RDB = require('../mocks/redismock'); - }); - }); -}); diff --git a/tests/topics.js b/tests/topics.js index 063451cc3e..9d27db8cf7 100644 --- a/tests/topics.js +++ b/tests/topics.js @@ -5,14 +5,8 @@ process.on('uncaughtException', function (err) { }); var assert = require('assert'), - RDB = require('../mocks/redismock'); + db = require('../mocks/databasemock'); -// Reds is not technically used in this test suite, but its invocation is required to stop the included -// libraries from trying to connect to the default Redis host/port -var reds = require('reds'); -reds.createClient = function () { - return reds.client || (reds.client = RDB); -}; var Topics = require('../src/topics'); @@ -43,7 +37,7 @@ describe('Topic\'s', function() { topic.userId = null; Topics.post(topic.userId, topic.title, topic.content, topic.categoryId, function(err, result) { - assert.equal(err.message, 'not-logged-in'); + assert.equal(err.message, 'invalid-user'); done(); }); }); @@ -75,11 +69,6 @@ describe('Topic\'s', function() { }); after(function() { - RDB.send_command('flushdb', [], function(error){ - if(error){ - winston.error(error); - throw new Error(error); - } - }); + db.flushdb(); }); }); \ No newline at end of file diff --git a/tests/user.js b/tests/user.js index cb89ac1fc9..7adfca61ee 100644 --- a/tests/user.js +++ b/tests/user.js @@ -7,7 +7,7 @@ process.on('uncaughtException', function (err) { }); var assert = require('assert'), - RDB = require('../mocks/redismock'); + db = require('../mocks/databasemock'); var User = require('../src/user'); @@ -43,12 +43,6 @@ describe('User', function() { }); after(function() { - //Clean up - RDB.send_command('flushdb', [], function(error){ - if(error){ - winston.error(error); - throw new Error(error); - } - }); + db.flushdb(); }); }); \ No newline at end of file