diff options
50 files changed, 1916 insertions, 435 deletions
diff --git a/app/controllers/connections.js b/app/controllers/connections.js index 810c677..fb0b562 100644 --- a/app/controllers/connections.js +++ b/app/controllers/connections.js @@ -40,7 +40,7 @@ module.exports = function() { query.user = req.param('user');
}
- var connections = core.presence.connections.query(query);
+ var connections = core.presence.system.connections.query(query);
res.json(connections);
}
});
diff --git a/app/controllers/files.js b/app/controllers/files.js index 008ac05..6e88a9d 100644 --- a/app/controllers/files.js +++ b/app/controllers/files.js @@ -105,6 +105,9 @@ module.exports = function() { },
list: function(req, res) {
var options = {
+ userId: req.user._id,
+ password: req.param('password'),
+
room: req.param('room'),
reverse: req.param('reverse'),
skip: req.param('skip'),
diff --git a/app/controllers/messages.js b/app/controllers/messages.js index fa1f306..4268c6e 100644 --- a/app/controllers/messages.js +++ b/app/controllers/messages.js @@ -60,6 +60,9 @@ module.exports = function() { },
list: function(req, res) {
var options = {
+ userId: req.user._id,
+ password: req.param('password'),
+
room: req.param('room'),
since_id: req.param('since_id'),
from: req.param('from'),
diff --git a/app/controllers/rooms.js b/app/controllers/rooms.js index ac72255..5b80eae 100644 --- a/app/controllers/rooms.js +++ b/app/controllers/rooms.js @@ -1,245 +1,262 @@ -//
-// Rooms Controller
-//
-
-'use strict';
-
-var _ = require('lodash');
-
-module.exports = function() {
- var app = this.app,
- core = this.core,
- middlewares = this.middlewares,
- models = this.models,
- User = models.user;
-
- core.on('presence:user_join', function(data) {
- User.findById(data.userId, function (err, user) {
- if (!err && user) {
- user = user.toJSON();
- user.room = data.roomId;
- app.io.emit('users:join', user);
- }
- });
- });
-
- core.on('presence:user_leave', function(data) {
- User.findById(data.userId, function (err, user) {
- if (!err && user) {
- user = user.toJSON();
- user.room = data.roomId;
- app.io.emit('users:leave', user);
- }
- });
- });
-
- core.on('rooms:new', function(room) {
- app.io.emit('rooms:new', room);
- });
-
- core.on('rooms:update', function(room) {
- app.io.emit('rooms:update', room);
- });
-
- core.on('rooms:archive', function(room) {
- app.io.emit('rooms:archive', room);
- });
-
-
- //
- // Routes
- //
- app.route('/rooms')
- .all(middlewares.requireLogin)
- .get(function(req) {
- req.io.route('rooms:list');
- })
- .post(function(req) {
- req.io.route('rooms:create');
- });
-
- app.route('/rooms/:room')
- .all(middlewares.requireLogin, middlewares.roomRoute)
- .get(function(req) {
- req.io.route('rooms:get');
- })
- .put(function(req) {
- req.io.route('rooms:update');
- })
- .delete(function(req) {
- req.io.route('rooms:archive');
- });
-
- app.route('/rooms/:room/users')
- .all(middlewares.requireLogin, middlewares.roomRoute)
- .get(function(req) {
- req.io.route('rooms:users');
- });
-
-
- //
- // Sockets
- //
- app.io.route('rooms', {
- list: function(req, res) {
- var options = {
- skip: req.param('skip'),
- take: req.param('take')
- };
-
- core.rooms.list(options, function(err, rooms) {
- if (err) {
- console.error(err);
- return res.status(400).json(err);
- }
-
- if (req.param('users')) {
- rooms = _.map(rooms, function(room) {
- room = room.toJSON();
- room.users = core.presence.getUsersForRoom(room.id.toString());
- room.userCount = room.users.length;
- return room;
- });
- }
- else if (req.param('userCounts')) {
- rooms = _.map(rooms, function(room) {
- room = room.toJSON();
- room.userCount =
- core.presence.getUserCountForRoom(room.id);
- return room;
- });
- }
-
- rooms = _.sortByAll(rooms, ['userCount', 'lastActive'])
- .reverse();
-
- res.json(rooms);
- });
- },
- get: function(req, res) {
- var roomId = req.param('room') || req.param('id');
-
- core.rooms.get(roomId, function(err, room) {
- if (err) {
- console.error(err);
- return res.status(400).json(err);
- }
-
- if (!room) {
- return res.sendStatus(404);
- }
-
- res.json(room);
- });
- },
- create: function(req, res) {
- var options = {
- owner: req.user._id,
- name: req.param('name'),
- slug: req.param('slug'),
- description: req.param('description')
- };
-
- core.rooms.create(options, function(err, room) {
- if (err) {
- console.error(err);
- return res.status(400).json(err);
- }
-
- res.status(201).json(room);
- });
- },
- update: function(req, res) {
- var roomId = req.param('room') || req.param('id');
-
- var options = {
- name: req.param('name'),
- slug: req.param('slug'),
- description: req.param('description')
- };
-
- core.rooms.update(roomId, options, function(err, room) {
- if (err) {
- console.error(err);
- return res.status(400).json(err);
- }
-
- if (!room) {
- return res.sendStatus(404);
- }
-
- res.json(room);
- });
- },
- archive: function(req, res) {
- var roomId = req.param('room') || req.param('id');
-
- core.rooms.archive(roomId, function(err, room) {
- if (err) {
- console.log(err);
- return res.sendStatus(400);
- }
-
- if (!room) {
- return res.sendStatus(404);
- }
-
- res.sendStatus(204);
- });
- },
- join: function(req, res) {
- var roomId = req.data;
- core.rooms.get(roomId, function(err, room) {
- if (err) {
- console.error(err);
- return res.sendStatus(400);
- }
-
- if (!room) {
- return res.sendStatus(400);
- }
-
- var user = req.user.toJSON();
- user.room = room._id;
-
- core.presence.join(req.socket.conn, room._id, room.slug);
- req.socket.join(room._id);
- res.json(room.toJSON());
- });
- },
- leave: function(req, res) {
- var roomId = req.data;
- var user = req.user.toJSON();
- user.room = roomId;
-
- core.presence.leave(req.socket.conn, roomId);
- req.socket.leave(roomId);
- res.json();
- },
- users: function(req, res) {
- var roomId = req.param('room');
-
- core.rooms.get(roomId, function(err, room) {
- if (err) {
- console.error(err);
- return res.sendStatus(400);
- }
-
- if (!room) {
- return res.sendStatus(404);
- }
-
- var users = core.presence.rooms
- .getOrAdd(room._id, room.slug)
- .getUsers()
- .map(function(user) {
- // TODO: Do we need to do this?
- user.room = room.id;
- return user;
- });
-
- res.json(users);
- });
- }
- });
-};
+// +// Rooms Controller +// + +'use strict'; + +var settings = require('./../config').rooms; + +module.exports = function() { + var app = this.app, + core = this.core, + middlewares = this.middlewares, + models = this.models, + User = models.user; + + core.on('presence:user_join', function(data) { + User.findById(data.userId, function (err, user) { + if (!err && user) { + user = user.toJSON(); + user.room = data.roomId; + if (data.roomHasPassword) { + app.io.to(data.roomId).emit('users:join', user); + } else { + app.io.emit('users:join', user); + } + } + }); + }); + + core.on('presence:user_leave', function(data) { + User.findById(data.userId, function (err, user) { + if (!err && user) { + user = user.toJSON(); + user.room = data.roomId; + if (data.roomHasPassword) { + app.io.to(data.roomId).emit('users:leave', user); + } else { + app.io.emit('users:leave', user); + } + } + }); + }); + + core.on('rooms:new', function(room) { + app.io.emit('rooms:new', room); + }); + + core.on('rooms:update', function(room) { + app.io.emit('rooms:update', room); + }); + + core.on('rooms:archive', function(room) { + app.io.emit('rooms:archive', room); + }); + + + // + // Routes + // + app.route('/rooms') + .all(middlewares.requireLogin) + .get(function(req) { + req.io.route('rooms:list'); + }) + .post(function(req) { + req.io.route('rooms:create'); + }); + + app.route('/rooms/:room') + .all(middlewares.requireLogin, middlewares.roomRoute) + .get(function(req) { + req.io.route('rooms:get'); + }) + .put(function(req) { + req.io.route('rooms:update'); + }) + .delete(function(req) { + req.io.route('rooms:archive'); + }); + + app.route('/rooms/:room/users') + .all(middlewares.requireLogin, middlewares.roomRoute) + .get(function(req) { + req.io.route('rooms:users'); + }); + + + // + // Sockets + // + app.io.route('rooms', { + list: function(req, res) { + var options = { + userId: req.user._id, + users: req.param('users'), + + skip: req.param('skip'), + take: req.param('take') + }; + + core.rooms.list(options, function(err, rooms) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + res.json(rooms); + }); + }, + get: function(req, res) { + var roomId = req.param('room') || req.param('id'); + + core.rooms.get(roomId, function(err, room) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + if (!room) { + return res.sendStatus(404); + } + + res.json(room); + }); + }, + create: function(req, res) { + var options = { + owner: req.user._id, + name: req.param('name'), + slug: req.param('slug'), + description: req.param('description'), + password: req.param('password') + }; + + if(!settings.passwordProtected) { + delete options.password; + } + + core.rooms.create(options, function(err, room) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + res.status(201).json(room); + }); + }, + update: function(req, res) { + var roomId = req.param('room') || req.param('id'); + + var options = { + name: req.param('name'), + slug: req.param('slug'), + description: req.param('description'), + password: req.param('password'), + user: req.user + }; + + core.rooms.update(roomId, options, function(err, room) { + if (err) { + console.error(err); + return res.status(400).json(err); + } + + if (!room) { + return res.sendStatus(404); + } + + res.json(room); + }); + }, + archive: function(req, res) { + var roomId = req.param('room') || req.param('id'); + + core.rooms.archive(roomId, function(err, room) { + if (err) { + console.log(err); + return res.sendStatus(400); + } + + if (!room) { + return res.sendStatus(404); + } + + res.sendStatus(204); + }); + }, + join: function(req, res) { + var options = { + userId: req.user._id, + saveMembership: true + }; + + if (typeof req.data === 'string') { + options.id = req.data; + } else { + options.id = req.param('roomId'); + options.password = req.param('password'); + } + + core.rooms.canJoin(options, function(err, room, canJoin) { + if (err) { + console.error(err); + return res.sendStatus(400); + } + + if (!room) { + return res.sendStatus(404); + } + + if(!canJoin) { + return res.status(403).json({ + status: 'error', + message: 'password required', + errors: 'password required' + }); + } + + var user = req.user.toJSON(); + user.room = room._id; + + core.presence.join(req.socket.conn, room); + req.socket.join(room._id); + res.json(room.toJSON()); + }); + }, + leave: function(req, res) { + var roomId = req.data; + var user = req.user.toJSON(); + user.room = roomId; + + core.presence.leave(req.socket.conn, roomId); + req.socket.leave(roomId); + res.json(); + }, + users: function(req, res) { + var roomId = req.param('room'); + + core.rooms.get(roomId, function(err, room) { + if (err) { + console.error(err); + return res.sendStatus(400); + } + + if (!room) { + return res.sendStatus(404); + } + + var users = core.presence.rooms + .getOrAdd(room) + .getUsers() + .map(function(user) { + // TODO: Do we need to do this? + user.room = room.id; + return user; + }); + + res.json(users); + }); + } + }); +}; diff --git a/app/controllers/usermessages.js b/app/controllers/usermessages.js new file mode 100644 index 0000000..0fe03ac --- /dev/null +++ b/app/controllers/usermessages.js @@ -0,0 +1,86 @@ +//
+// UserMessages Controller
+//
+
+'use strict';
+
+var _ = require('lodash'),
+ settings = require('./../config');
+
+module.exports = function() {
+
+ var app = this.app,
+ core = this.core,
+ middlewares = this.middlewares;
+
+
+ if (!settings.private.enable) {
+ return;
+ }
+
+ core.on('user-messages:new', function(message) {
+ _.each(message.users, function(userId) {
+ var connections = core.presence.system.connections.query({
+ type: 'socket.io', userId: userId.toString()
+ });
+
+ _.each(connections, function(connection) {
+ connection.socket.emit('user-messages:new', message);
+ });
+ });
+ });
+
+ //
+ // Routes
+ //
+
+ app.route('/users/:user/messages')
+ .all(middlewares.requireLogin)
+ .get(function(req) {
+ req.io.route('user-messages:list');
+ })
+ .post(function(req) {
+ req.io.route('user-messages:create');
+ });
+
+ //
+ // Sockets
+ //
+ app.io.route('user-messages', {
+ create: function(req, res) {
+ var options = {
+ owner: req.user._id,
+ user: req.param('user'),
+ text: req.param('text')
+ };
+
+ core.usermessages.create(options, function(err, message) {
+ if (err) {
+ return res.sendStatus(400);
+ }
+ res.status(201).json(message);
+ });
+ },
+ list: function(req, res) {
+ var options = {
+ currentUser: req.user._id,
+ user: req.param('user'),
+ since_id: req.param('since_id'),
+ from: req.param('from'),
+ to: req.param('to'),
+ reverse: req.param('reverse'),
+ skip: req.param('skip'),
+ take: req.param('take'),
+ expand: req.param('expand')
+ };
+
+ core.usermessages.list(options, function(err, messages) {
+ if (err) {
+ return res.sendStatus(400);
+ }
+ res.json(messages);
+ });
+ }
+ });
+
+};
diff --git a/app/controllers/users.js b/app/controllers/users.js index e44c050..3d40fca 100644 --- a/app/controllers/users.js +++ b/app/controllers/users.js @@ -6,9 +6,8 @@ module.exports = function() {
- var helpers = require('./../core/helpers');
-
var app = this.app,
+ core = this.core,
middlewares = this.middlewares,
models = this.models,
User = models.user;
@@ -34,24 +33,7 @@ module.exports = function() { take: req.param('take')
};
- options = helpers.sanitizeQuery(options, {
- defaults: {
- take: 500
- },
- maxTake: 5000
- });
-
- var find = User.find();
-
- if (options.skip) {
- find.skip(options.skip);
- }
-
- if (options.take) {
- find.limit(options.take);
- }
-
- find.exec(function(err, users) {
+ core.users.list(options, function(err, users) {
if (err) {
console.log(err);
return res.status(400).json(err);
diff --git a/app/core/account.js b/app/core/account.js index a5a8dea..539eb4e 100644 --- a/app/core/account.js +++ b/app/core/account.js @@ -41,7 +41,7 @@ AccountManager.prototype.update = function(id, options, cb) { } if (options.username && options.username !== user.username) { - var xmppConns = this.core.presence.connections.query({ + var xmppConns = this.core.presence.system.connections.query({ userId: user._id, type: 'xmpp' }); diff --git a/app/core/avatar-cache.js b/app/core/avatar-cache.js new file mode 100644 index 0000000..96a33fe --- /dev/null +++ b/app/core/avatar-cache.js @@ -0,0 +1,46 @@ +'use strict'; + +var crypto = require('crypto'), + http = require('http'); + +function AvatarCache(options) { + this.core = options.core; + this.avatars = {}; + + this.get = this.get.bind(this); + this.add = this.add.bind(this); +} + +AvatarCache.prototype.get = function(userId) { + return this.avatars[userId]; +}; + +AvatarCache.prototype.add = function(user) { + var userId = (user.id || user._id).toString(); + var url = 'http://www.gravatar.com/avatar/' + user.avatar + '?s=64'; + + http.get(url, function(response) { + if (response.statusCode !== 200) { + return; + } + + var buffers = []; + + response.on('data', function(buffer) { + buffers.push(buffer); + }); + + response.on('end', function() { + var buffer = Buffer.concat(buffers); + + this.avatars[userId] = { + base64: buffer.toString('base64'), + sha1: crypto.createHash('sha1').update(buffer).digest('hex') + }; + + this.core.emit('avatar-cache:update', user); + }.bind(this)); + }.bind(this)); +}; + +module.exports = AvatarCache; diff --git a/app/core/files.js b/app/core/files.js index a51d859..7b5efa8 100644 --- a/app/core/files.js +++ b/app/core/files.js @@ -98,6 +98,8 @@ FileManager.prototype.create = function(options, cb) { }; FileManager.prototype.list = function(options, cb) { + var Room = mongoose.model('Room'); + if (!enabled) { return cb(null, []); } @@ -148,14 +150,37 @@ FileManager.prototype.list = function(options, cb) { find.sort({ 'uploaded': 1 }); } - find - .limit(options.take) - .exec(function(err, files) { + Room.findById(options.room, function(err, room) { if (err) { console.error(err); return cb(err); } - cb(null, files); + + var opts = { + userId: options.userId, + password: options.password + }; + + room.canJoin(opts, function(err, canJoin) { + if (err) { + console.error(err); + return cb(err); + } + + if (!canJoin) { + return cb(null, []); + } + + find + .limit(options.take) + .exec(function(err, files) { + if (err) { + console.error(err); + return cb(err); + } + cb(null, files); + }); + }); }); }; diff --git a/app/core/index.js b/app/core/index.js index 77b04c1..83fd0ad 100644 --- a/app/core/index.js +++ b/app/core/index.js @@ -4,10 +4,13 @@ var EventEmitter = require('events').EventEmitter, util = require('util'), _ = require('lodash'), AccountManager = require('./account'), + AvatarCache = require('./avatar-cache'), FileManager = require('./files'), MessageManager = require('./messages'), PresenceManager = require('./presence'), - RoomManager = require('./rooms'); + RoomManager = require('./rooms'), + UserManager = require('./users'), + UserMessageManager = require('./usermessages'); function Core() { EventEmitter.call(this); @@ -32,6 +35,18 @@ function Core() { core: this }); + this.users = new UserManager({ + core: this + }); + + this.usermessages = new UserMessageManager({ + core: this + }); + + this.avatars = new AvatarCache({ + core: this + }); + this.onAccountUpdated = this.onAccountUpdated.bind(this); this.on('account:update', this.onAccountUpdated); diff --git a/app/core/messages.js b/app/core/messages.js index 7165161..bc665f4 100644 --- a/app/core/messages.js +++ b/app/core/messages.js @@ -50,6 +50,8 @@ MessageManager.prototype.create = function(options, cb) { }; MessageManager.prototype.list = function(options, cb) { + var Room = mongoose.model('Room'); + options = options || {}; if (!options.room) { @@ -108,14 +110,37 @@ MessageManager.prototype.list = function(options, cb) { find.sort({ 'posted': 1 }); } - find.limit(options.take) - .exec(function(err, messages) { + Room.findById(options.room, function(err, room) { + if (err) { + console.error(err); + return cb(err); + } + + var opts = { + userId: options.userId, + password: options.password + }; + + room.canJoin(opts, function(err, canJoin) { if (err) { console.error(err); return cb(err); } - cb(null, messages); + + if (!canJoin) { + return cb(null, []); + } + + find.limit(options.take) + .exec(function(err, messages) { + if (err) { + console.error(err); + return cb(err); + } + cb(null, messages); + }); }); + }); }; module.exports = MessageManager; diff --git a/app/core/presence.js b/app/core/presence.js index 17b3533..dbf7440 100644 --- a/app/core/presence.js +++ b/app/core/presence.js @@ -1,15 +1,17 @@ 'use strict'; var Connection = require('./presence/connection'), + Room = require('./presence/room'), ConnectionCollection = require('./presence/connection-collection'), RoomCollection = require('./presence/room-collection'), UserCollection = require('./presence/user-collection'); function PresenceManager(options) { this.core = options.core; + this.system = new Room({ system: true }); this.connections = new ConnectionCollection(); this.rooms = new RoomCollection(); - this.users = new UserCollection(); + this.users = new UserCollection({ core: this.core }); this.rooms.on('user_join', this.onJoin.bind(this)); this.rooms.on('user_leave', this.onLeave.bind(this)); @@ -29,7 +31,8 @@ PresenceManager.prototype.getUsersForRoom = function(roomId) { }; PresenceManager.prototype.connect = function(connection) { - this.connections.add(connection); + this.system.addConnection(connection); + this.core.emit('connect', connection); connection.user = this.users.getOrAdd(connection.user); @@ -39,13 +42,14 @@ PresenceManager.prototype.connect = function(connection) { }; PresenceManager.prototype.disconnect = function(connection) { - this.connections.remove(connection); + this.system.removeConnection(connection); + this.core.emit('disconnect', connection); this.rooms.removeConnection(connection); }; -PresenceManager.prototype.join = function(connection, roomId, roomSlug) { - var room = this.rooms.getOrAdd(roomId, roomSlug); - room.addConnection(connection); +PresenceManager.prototype.join = function(connection, room) { + var pRoom = this.rooms.getOrAdd(room); + pRoom.addConnection(connection); }; PresenceManager.prototype.leave = function(connection, roomId) { diff --git a/app/core/presence/connection-collection.js b/app/core/presence/connection-collection.js index 3adb2c4..4e7846d 100644 --- a/app/core/presence/connection-collection.js +++ b/app/core/presence/connection-collection.js @@ -8,6 +8,7 @@ function ConnectionCollection() { this.get = this.get.bind(this); this.getUsers = this.getUsers.bind(this); this.getUserIds = this.getUserIds.bind(this); + this.getUsernames = this.getUsernames.bind(this); this.add = this.add.bind(this); this.remove = this.remove.bind(this); @@ -26,27 +27,38 @@ ConnectionCollection.prototype.contains = function(connection) { return !!this.connections[connection.id]; }; -ConnectionCollection.prototype.getUsers = function() { - return _.chain(this.connections) - .filter(function(value) { - // User shouldn't be undefined - but sometimes it happens :/ - return value.user; - }) - .map(function(value) { - return value.user; - }) - .uniq('id') - .value(); +ConnectionCollection.prototype.getUsers = function(filter) { + var connections = this.connections; + + if (filter) { + connections = this.query(filter); + } + + var users = _.chain(connections) + .filter(function(value) { + return !!value.user; + }) + .map(function(value) { + return value.user; + }) + .uniq('id') + .value(); + + return users; }; -ConnectionCollection.prototype.getUserIds = function() { - return _.map(this.getUsers(), function(user) { +ConnectionCollection.prototype.getUserIds = function(filter) { + var users = this.getUsers(filter); + + return _.map(users, function(user) { return user.id; }); }; -ConnectionCollection.prototype.getUsernames = function() { - return _.map(this.getUsers(), function(user) { +ConnectionCollection.prototype.getUsernames = function(filter) { + var users = this.getUsers(filter); + + return _.map(users, function(user) { return user.username; }); }; diff --git a/app/core/presence/room-collection.js b/app/core/presence/room-collection.js index 2c660e7..7c7d934 100644 --- a/app/core/presence/room-collection.js +++ b/app/core/presence/room-collection.js @@ -29,16 +29,17 @@ RoomCollection.prototype.slug = function(slug) { }); }; -RoomCollection.prototype.getOrAdd = function(roomId, roomSlug) { - roomId = roomId.toString(); - roomSlug = roomSlug && roomSlug.toString() || roomId.toString(); - var room = this.rooms[roomId]; - if (!room) { - room = this.rooms[roomId] = new Room(roomId, roomSlug); - room.on('user_join', this.onJoin); - room.on('user_leave', this.onLeave); +RoomCollection.prototype.getOrAdd = function(room) { + var roomId = room._id.toString(); + var pRoom = this.rooms[roomId]; + if (!pRoom) { + pRoom = this.rooms[roomId] = new Room({ + room: room + }); + pRoom.on('user_join', this.onJoin); + pRoom.on('user_leave', this.onLeave); } - return room; + return pRoom; }; RoomCollection.prototype.onJoin = function(data) { diff --git a/app/core/presence/room.js b/app/core/presence/room.js index 985f78e..e95ee22 100644 --- a/app/core/presence/room.js +++ b/app/core/presence/room.js @@ -4,10 +4,23 @@ var EventEmitter = require('events').EventEmitter, util = require('util'), ConnectionCollection = require('./connection-collection'); -function Room(roomId, roomSlug) { +function Room(options) { EventEmitter.call(this); - this.roomId = roomId; - this.roomSlug = roomSlug; + + if (options.system) { + // This is the system room + // Used for tracking what users are online + this.system = true; + this.roomId = undefined; + this.roomSlug = undefined; + this.hasPassword = false; + } else { + this.system = false; + this.roomId = options.room._id.toString(); + this.roomSlug = options.room.slug; + this.hasPassword = options.room.hasPassword; + } + this.connections = new ConnectionCollection(); this.userCount = 0; @@ -42,22 +55,41 @@ Room.prototype.containsUser = function(userId) { Room.prototype.emitUserJoin = function(data) { this.userCount++; - this.emit('user_join', { - roomId: this.roomId, - roomSlug: this.roomSlug, + + var d = { userId: data.userId, username: data.username - }); + }; + + if (this.system) { + d.system = true; + } else { + d.roomId = this.roomId; + d.roomSlug = this.roomSlug; + d.roomHasPassword = this.hasPassword; + } + + this.emit('user_join', d); }; Room.prototype.emitUserLeave = function(data) { this.userCount--; - this.emit('user_leave', { - roomId: this.roomId, - roomSlug: this.roomSlug, + + var d = { + user: data.user, userId: data.userId, username: data.username - }); + }; + + if (this.system) { + d.system = true; + } else { + d.roomId = this.roomId; + d.roomSlug = this.roomSlug; + d.roomHasPassword = this.hasPassword; + } + + this.emit('user_leave', d); }; Room.prototype.usernameChanged = function(data) { @@ -85,6 +117,7 @@ Room.prototype.addConnection = function(connection) { !this.containsUser(connection.user.id)) { // User joining room this.emitUserJoin({ + user: connection.user, userId: connection.user.id, username: connection.user.username }); @@ -103,6 +136,7 @@ Room.prototype.removeConnection = function(connection) { !this.containsUser(connection.user.id)) { // Leaving room altogether this.emitUserLeave({ + user: connection.user, userId: connection.user.id, username: connection.user.username }); diff --git a/app/core/presence/user-collection.js b/app/core/presence/user-collection.js index 0ecbf8d..f65f5e6 100644 --- a/app/core/presence/user-collection.js +++ b/app/core/presence/user-collection.js @@ -1,8 +1,12 @@ 'use strict'; -var _ = require('lodash'); +var EventEmitter = require('events').EventEmitter, + util = require('util'), + _ = require('lodash'); -function UserCollection() { +function UserCollection(options) { + EventEmitter.call(this); + this.core = options.core; this.users = {}; this.get = this.get.bind(this); @@ -10,16 +14,25 @@ function UserCollection() { this.remove = this.remove.bind(this); } +util.inherits(UserCollection, EventEmitter); + UserCollection.prototype.get = function(userId) { return this.users[userId]; }; +UserCollection.prototype.getByUsername = function(username) { + return _.find(this.users, function(user) { + return user.username === username; + }); +}; + UserCollection.prototype.getOrAdd = function(user) { - user = typeof user.toJSON === 'function' ? user.toJSON() : user; - var userId = user.id.toString(); + var user2 = typeof user.toJSON === 'function' ? user.toJSON() : user; + var userId = user2.id.toString(); if (!this.users[userId]) { - _.assign(user, { id: userId }); - this.users[userId] = user; + _.assign(user2, { id: userId }); + this.users[userId] = user2; + this.core.avatars.add(user); } return this.users[userId]; }; diff --git a/app/core/rooms.js b/app/core/rooms.js index 88ad540..0463827 100644 --- a/app/core/rooms.js +++ b/app/core/rooms.js @@ -1,12 +1,32 @@ 'use strict'; var mongoose = require('mongoose'), + _ = require('lodash'), helpers = require('./helpers'); function RoomManager(options) { this.core = options.core; } +RoomManager.prototype.canJoin = function(options, cb) { + var method = options.id ? 'get' : 'slug', + roomId = options.id ? options.id : options.slug; + + this[method](roomId, function(err, room) { + if (err) { + return cb(err); + } + + if (!room) { + return cb(); + } + + room.canJoin(options, function(err, canJoin) { + cb(err, room, canJoin); + }); + }); +}; + RoomManager.prototype.create = function(options, cb) { var Room = mongoose.model('Room'); Room.create(options, function(err, room) { @@ -36,10 +56,19 @@ RoomManager.prototype.update = function(roomId, options, cb) { return cb('Room does not exist.'); } + if(room.hasPassword && !room.owner.equals(options.user.id)) { + return cb('Only owner can change password-protected room.'); + } + room.name = options.name; // DO NOT UPDATE SLUG // room.slug = options.slug; room.description = options.description; + + if (room.hasPassword && options.password) { + room.password = options.password; + } + room.save(function(err, room) { if (err) { console.error(err); @@ -105,9 +134,43 @@ RoomManager.prototype.list = function(options, cb) { if (options.sort) { var sort = options.sort.replace(',', ' '); find.sort(sort); + } else { + find.sort('-lastActive'); } - find.exec(cb); + find.exec(function(err, rooms) { + if (err) { + return cb(err); + } + + if (options.users) { + rooms = _.map(rooms, function(room) { + var users = []; + + // Better approach would be this, + // but need to fix join/leave events: + // var auth = room.isAuthorized(options.userId); + + if (!room.password) { + users = this.core.presence + .getUsersForRoom(room.id.toString()); + } + + room = room.toJSON(); + room.users = users || []; + room.userCount = room.users.length; + return room; + }, this); + + if (!options.sort) { + rooms = _.sortByAll(rooms, ['userCount', 'lastActive']) + .reverse(); + } + } + + cb(null, rooms); + + }.bind(this)); }; RoomManager.prototype.get = function(identifier, cb) { diff --git a/app/core/usermessages.js b/app/core/usermessages.js new file mode 100644 index 0000000..68c0e54 --- /dev/null +++ b/app/core/usermessages.js @@ -0,0 +1,127 @@ +'use strict'; + +var _ = require('lodash'), + mongoose = require('mongoose'), + helpers = require('./helpers'); + +function UserMessageManager(options) { + this.core = options.core; +} + +// options.currentUser, options.user + +UserMessageManager.prototype.onMessageCreated = function(message, user, cb) { + var User = mongoose.model('User'); + + User.findOne(message.owner, function(err, owner) { + if (err) { + console.error(err); + return cb(err); + } + if (cb) { + cb(null, message, user, owner); + } + + this.core.emit('user-messages:new', message, user, owner); + }.bind(this)); +}; + +UserMessageManager.prototype.create = function(options, cb) { + var UserMessage = mongoose.model('UserMessage'), + User = mongoose.model('User'); + + User.findById(options.user, function(err, user) { + if (err) { + console.error(err); + return cb(err); + } + if (!user) { + return cb('User does not exist.'); + } + + var data = { + users: [options.owner, options.user], + owner: options.owner, + text: options.text + }; + + var message = new UserMessage(data); + + // Test if this message is OTR + if (data.text.match(/^\?OTR/)) { + message._id = 'OTR'; + this.onMessageCreated(message, user, cb); + } else { + message.save(function(err) { + if (err) { + console.error(err); + return cb(err); + } + this.onMessageCreated(message, user, cb); + }.bind(this)); + } + }.bind(this)); +}; + +UserMessageManager.prototype.list = function(options, cb) { + options = options || {}; + + if (!options.room) { + return cb(null, []); + } + + options = helpers.sanitizeQuery(options, { + defaults: { + reverse: true, + take: 500 + }, + maxTake: 5000 + }); + + var UserMessage = mongoose.model('Message'); + + var find = UserMessage.find({ + users: { $all: [options.currentUser, options.user] } + }); + + if (options.since_id) { + find.where('_id').gt(options.since_id); + } + + if (options.from) { + find.where('posted').gt(options.from); + } + + if (options.to) { + find.where('posted').lte(options.to); + } + + if (options.expand) { + var includes = options.expand.split(','); + + if (_.includes(includes, 'owner')) { + find.populate('owner', 'id username displayName email avatar'); + } + } + + if (options.skip) { + find.skip(options.skip); + } + + if (options.reverse) { + find.sort({ 'posted': -1 }); + } else { + find.sort({ 'posted': 1 }); + } + + find.limit(options.take) + .exec(function(err, messages) { + if (err) { + console.error(err); + return cb(err); + } + cb(null, messages); + }); +}; + +module.exports = UserMessageManager; diff --git a/app/core/users.js b/app/core/users.js new file mode 100644 index 0000000..19aba48 --- /dev/null +++ b/app/core/users.js @@ -0,0 +1,47 @@ +'use strict'; + +var mongoose = require('mongoose'), + helpers = require('./helpers'); + +function UserManager(options) { + this.core = options.core; +} + +UserManager.prototype.list = function(options, cb) { + options = options || {}; + + options = helpers.sanitizeQuery(options, { + defaults: { + take: 500 + }, + maxTake: 5000 + }); + + var User = mongoose.model('User'); + + var find = User.find(); + + if (options.skip) { + find.skip(options.skip); + } + + if (options.take) { + find.limit(options.take); + } + + find.exec(cb); +}; + +UserManager.prototype.get = function(identifier, cb) { + var User = mongoose.model('User'); + User.findById(identifier, cb); +}; + +UserManager.prototype.username = function(username, cb) { + var User = mongoose.model('User'); + User.findOne({ + username: username + }, cb); +}; + +module.exports = UserManager; diff --git a/app/models/room.js b/app/models/room.js index d36157f..2186e6d 100644 --- a/app/models/room.js +++ b/app/models/room.js @@ -5,7 +5,8 @@ 'use strict'; var mongoose = require('mongoose'), - uniqueValidator = require('mongoose-unique-validator'); + uniqueValidator = require('mongoose-unique-validator'), + bcrypt = require('bcryptjs'); var ObjectId = mongoose.Schema.Types.ObjectId; @@ -36,6 +37,10 @@ var RoomSchema = new mongoose.Schema({ ref: 'User', required: true }, + participants: [{ // We can have an array per role + type: ObjectId, + ref: 'User' + }], messages: [{ type: ObjectId, ref: 'Message' @@ -47,6 +52,10 @@ var RoomSchema = new mongoose.Schema({ lastActive: { type: Date, default: Date.now + }, + password: { + type: String, + required: false//only for password-protected room } }); @@ -54,10 +63,78 @@ RoomSchema.virtual('handle').get(function() { return this.slug || this.name.replace(/\W/i, ''); }); +RoomSchema.virtual('hasPassword').get(function() { + return !!this.password; +}); + +RoomSchema.pre('save', function(next) { + var room = this; + if (!room.password || !room.isModified('password')) { + return next(); + } + + bcrypt.hash(room.password, 10, function(err, hash) { + if (err) { + return next(err); + } + room.password = hash; + next(); + }); +}); + RoomSchema.plugin(uniqueValidator, { message: 'Expected {PATH} to be unique' }); +RoomSchema.method('isAuthorized', function(userId) { + if(!this.password) { + return true; + } + + if (this.owner.equals(userId)) { + return true; + } + + return this.participants.some(function(participant) { + return participant.equals(userId); + }); +}); + +RoomSchema.method('canJoin', function(options, cb) { + var userId = options.userId, + password = options.password, + saveMembership = options.saveMembership; + + if (this.isAuthorized(userId)) { + return cb(null, true); + } + + bcrypt.compare(password || '', this.password, function(err, isMatch) { + if(err) { + return cb(err); + } + + if (!isMatch) { + return cb(null, false); + } + + if (!saveMembership) { + return cb(null, true); + } + + this.participants.push(userId); + + this.save(function(err) { + if(err) { + return cb(err); + } + + cb(null, true); + }); + + }.bind(this)); +}); + RoomSchema.method('toJSON', function() { var room = this.toObject(); return { @@ -67,7 +144,8 @@ RoomSchema.method('toJSON', function() { description: room.description, lastActive: room.lastActive, created: room.created, - owner: room.owner + owner: room.owner, + hasPassword: this.hasPassword }; }); diff --git a/app/models/usermessage.js b/app/models/usermessage.js new file mode 100644 index 0000000..91cfb47 --- /dev/null +++ b/app/models/usermessage.js @@ -0,0 +1,54 @@ +// +// Message +// + +'use strict'; + +var mongoose = require('mongoose'), + settings = require('./../config'); + +var ObjectId = mongoose.Schema.Types.ObjectId; + +var MessageSchema = new mongoose.Schema({ + users: [{ + type: ObjectId, + ref: 'User' + }], + owner: { + type: ObjectId, + ref: 'User', + required: true + }, + text: { + type: String, + required: true + }, + posted: { + type: Date, + default: Date.now + } +}); + +if (settings.private.expire !== false) { + var defaultExpire = 6 * 60; // 6 hours + + MessageSchema.index({ posted: 1 }, { + expireAfterSeconds: (settings.private.expire || defaultExpire) * 60 + }); +} + +MessageSchema.index({ users: 1, posted: -1, _id: 1 }); + +// EXPOSE ONLY CERTAIN FIELDS +// This helps ensure that the client gets +// data that can be digested properly +MessageSchema.method('toJSON', function() { + return { + id: this._id, + owner: this.owner, + text: this.text, + posted: this.posted + }; +}); + +module.exports = mongoose.model('UserMessage', MessageSchema); diff --git a/app/xmpp/events/user-avatar-ready.js b/app/xmpp/events/user-avatar-ready.js new file mode 100644 index 0000000..6a27b0d --- /dev/null +++ b/app/xmpp/events/user-avatar-ready.js @@ -0,0 +1,47 @@ +'use strict'; + +var _ = require('lodash'), + Stanza = require('node-xmpp-core').Stanza, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'avatar-cache:update', + + then: function(user) { + if (!settings.private.enable) { + return; + } + + var user_connections = this.core.presence.system.connections.query({ + type: 'xmpp', + userid: user.id + }); + + if (!user_connections.length) { + // Don't publish presence for this user + return; + } + + var connections = this.core.presence.system.connections.query({ + type: 'xmpp' + }); + + _.each(connections, function(connection) { + if (connection.user.id === user.id) { + return; + } + + // Reannounce presence + var presence = new Stanza.Presence({ + from: connection.getUserJid(user.username) + }); + + connection.populateVcard(presence, user, this.core); + + this.send(connection, presence); + }, this); + } + +}); diff --git a/app/xmpp/events/user-connect.js b/app/xmpp/events/user-connect.js new file mode 100644 index 0000000..fe1a0f5 --- /dev/null +++ b/app/xmpp/events/user-connect.js @@ -0,0 +1,70 @@ +'use strict'; + +var _ = require('lodash'), + Stanza = require('node-xmpp-core').Stanza, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'connect', + + then: function(connection) { + if (!settings.private.enable) { + return; + } + + if (connection.type !== 'xmpp') { + return; + } + + var existing = this.core.presence.system.connections.query({ + userId: connection.user.id, + type: 'xmpp' + }); + + if (existing.length > 1) { + // Was already connected via XMPP + return; + } + + var connections = this.core.presence.system.connections.query({ + type: 'xmpp' + }); + + _.each(connections, function(x) { + if (x.user.id === connection.user.id) { + return; + } + + // Update rosters + var roster = new Stanza.Iq({ + id: connection.user.id, + type: 'set', + to: x.jid() + }); + + roster.c('query', { + xmlns: 'jabber:iq:roster' + }).c('item', { + jid: x.getUserJid(connection.user.username), + name: connection.user.displayName, + subscription: 'both' + }).c('group').t('Let\'s Chat'); + + this.send(x, roster); + + + // Announce presence + var presence = new Stanza.Presence({ + from: x.getUserJid(connection.user.username) + }); + + x.populateVcard(presence, connection.user, this.core); + + this.send(x, presence); + + }, this); + } + +}); diff --git a/app/xmpp/events/user-disconnect.js b/app/xmpp/events/user-disconnect.js new file mode 100644 index 0000000..7ac3848 --- /dev/null +++ b/app/xmpp/events/user-disconnect.js @@ -0,0 +1,51 @@ +'use strict'; + +var _ = require('lodash'), + Stanza = require('node-xmpp-core').Stanza, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'disconnect', + + then: function(connection) { + if (!settings.private.enable) { + return; + } + + if (connection.type !== 'xmpp') { + return; + } + + var existing = this.core.presence.system.connections.query({ + userId: connection.user.id, + type: 'xmpp' + }); + + if (existing.length > 0) { + // Still has other XMPP connections + return; + } + + var connections = this.core.presence.system.connections.query({ + type: 'xmpp' + }); + + _.each(connections, function(x) { + if (x.user.id === connection.user.id) { + return; + } + + var presence = new Stanza.Presence({ + to: x.jid(), + from: x.getUserJid(connection.user.username), + type: 'unavailable' + }); + + this.send(x, presence); + + }, this); + } + +}); diff --git a/app/xmpp/events/user-join.js b/app/xmpp/events/user-join.js index ed4d12a..5f65c13 100644 --- a/app/xmpp/events/user-join.js +++ b/app/xmpp/events/user-join.js @@ -26,6 +26,10 @@ module.exports = EventListener.extend({ role: 'participant' }); + if (data.user) { + connection.populateVcard(presence, data.user, this.core); + } + this.send(connection, presence); }, this); } diff --git a/app/xmpp/events/usermessage-created.js b/app/xmpp/events/usermessage-created.js new file mode 100644 index 0000000..ef70f9a --- /dev/null +++ b/app/xmpp/events/usermessage-created.js @@ -0,0 +1,41 @@ +'use strict'; + +var Stanza = require('node-xmpp-core').Stanza, + settings = require('./../../config'), + EventListener = require('./../event-listener'); + +module.exports = EventListener.extend({ + + on: 'user-messages:new', + + then: function(msg, user, owner) { + if (!settings.private.enable) { + return; + } + + var connections = this.core.presence.system.connections.query({ + userId: user._id.toString(), + type: 'xmpp' + }); + + connections.forEach(function(connection) { + + var stanza = new Stanza.Message({ + id: msg._id, + type: 'chat', + to: connection.getUserJid(user.username), + from: connection.getUserJid(owner.username) + }); + + stanza.c('active', { + xmlns: 'http://jabber.org/protocol/chatstates' + }); + + stanza.c('body').t(msg.text); + + this.send(connection, stanza); + + }, this); + } + +}); diff --git a/app/xmpp/index.js b/app/xmpp/index.js index 66f43e4..fb168c4 100644 --- a/app/xmpp/index.js +++ b/app/xmpp/index.js @@ -4,6 +4,7 @@ var xmpp = require('node-xmpp-server'), settings = require('./../config'), auth = require('./../auth/index'), all = require('require-tree'), + Stanza = require('node-xmpp-core').Stanza, XmppConnection = require('./xmpp-connection'); var allArray = function(path) { @@ -60,11 +61,40 @@ function xmppStart(core) { return processor.run(); }); - if (!handled && settings.xmpp.debug.unhandled) { + if (handled) { + return; + } + + if (settings.xmpp.debug.unhandled) { // Print unhandled request console.log(' '); console.log(stanza.root().toString().red); } + + if (stanza.name !== 'iq') { + return; + } + + var msg = new Stanza.Iq({ + type: 'error', + id: stanza.attrs.id, + to: stanza.attrs.from, + from: stanza.attrs.to + }); + + msg.c('not-implemented', { + code: 501, + type: 'CANCEL' + }).c('feature-not-implemented', { + xmlns: 'urn:ietf:params:xml:n:xmpp-stanzas' + }); + + + if (settings.xmpp.debug.unhandled) { + console.log(msg.root().toString().green); + } + + client.send(msg); }); // On Disconnect event. When a client disconnects diff --git a/app/xmpp/msg-processors/room-join.js b/app/xmpp/msg-processors/room-join.js index 541175c..8b27b3d 100644 --- a/app/xmpp/msg-processors/room-join.js +++ b/app/xmpp/msg-processors/room-join.js @@ -40,35 +40,52 @@ module.exports = MessageProcessor.extend({ this.connection.nickname(roomSlug, nickname); - this.core.rooms.slug(roomSlug, function(err, room) { + var options = { + userId: this.connection.user.id, + slug: roomSlug, + password: this.getPassword(), + saveMembership: true + }; + + this.core.rooms.canJoin(options, function(err, room, canJoin) { if (err) { return cb(err); } - if (room) { + if (room && canJoin) { return this.handleJoin(room, cb); } + if (room && !canJoin) { + return this.sendErrorPassword(room, cb); + } + if (!settings.xmpp.roomCreation) { return this.cantCreateRoom(roomSlug, cb); } - this.createRoom(roomSlug, function(err, room) { + return this.createRoom(roomSlug, function(err, room) { if (err) { return cb(err); } this.handleJoin(room, cb); }.bind(this)); + }.bind(this)); }, createRoom: function(roomSlug, cb) { + var password = this.getPassword(); var options = { owner: this.connection.user.id, name: roomSlug, slug: roomSlug, - description: '' + description: '', + password: password }; + if(!settings.rooms.passwordProtected) { + delete options.password; + } this.core.rooms.create(options, cb); }, @@ -92,6 +109,60 @@ module.exports = MessageProcessor.extend({ cb(null, presence); }, + _getXNode: function() { + if(!this.xNode) { + this.xNode = _.find(this.request.children, function(child) { + return child.name === 'x'; + }); + } + return this.xNode; + }, + + getHistoryNode: function() { + var xNode = this._getXNode(); + if (xNode) { + return _.find(xNode.children, function(child) { + return child.name === 'history'; + }); + } + }, + + getPassword: function() { + var xNode = this._getXNode(); + if (xNode) { + var passwordNode = _.find(xNode.children, function(child) { + return child.name === 'password'; + }); + if(passwordNode && passwordNode.children) { + return passwordNode.children[0]; + } + } + + return ''; + }, + + sendErrorPassword: function(room, cb) { + //from http://xmpp.org/extensions/xep-0045.html#enter-pw + var presence = this.Presence({ + type: 'error' + }); + + presence + .c('x', { + xmlns: 'http://jabber.org/protocol/muc' + }); + presence + .c('error', { + type: 'auth', + by: this.connection.getRoomJid(room.slug) + }) + .c('not-authorized', { + xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas' + }); + + return cb(null, presence); + }, + handleJoin: function(room, cb) { var username = this.connection.user.username; @@ -121,6 +192,9 @@ module.exports = MessageProcessor.extend({ role: 'participant' }); + // TODO: Add avatar for each room user + // helper.populateVcard(presence, user, this.core); + return presence; }, this); @@ -131,26 +205,18 @@ module.exports = MessageProcessor.extend({ subject.c('subject').t(room.name + ' | ' + room.description); - var xNode = _.find(this.request.children, function(child) { - return child.name === 'x'; - }); - - var historyNode; - if (xNode) { - historyNode = _.find(xNode.children, function(child) { - return child.name === 'history'; - }); - } + var historyNode = this.getHistoryNode(); if (!historyNode || historyNode.attrs.maxchars === 0 || historyNode.attrs.maxchars === '0') { // Send no history - this.core.presence.join(this.connection, room._id, room.slug); + this.core.presence.join(this.connection, room); return cb(null, presences, subject); } var query = { + userId: this.connection.user.id, room: room._id, expand: 'owner' }; @@ -205,7 +271,7 @@ module.exports = MessageProcessor.extend({ }, this); - this.core.presence.join(this.connection, room._id, room.slug); + this.core.presence.join(this.connection, room); cb(null, presences, msgs, subject); }.bind(this)); diff --git a/app/xmpp/msg-processors/root-join.js b/app/xmpp/msg-processors/root-join.js new file mode 100644 index 0000000..8ecb5c8 --- /dev/null +++ b/app/xmpp/msg-processors/root-join.js @@ -0,0 +1,46 @@ +'use strict'; + +var _ = require('lodash'), + MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return !this.request.to && + !this.request.type && + this.request.name === 'presence'; + }, + + then: function(cb) { + if (!settings.private.enable) { + return cb(); + } + + var msgs = []; + + var users = this.core.presence.system.connections.getUsers({ + type: 'xmpp' // Only XMPP supports private messaging - for now + }); + + _.each(users, function(user) { + if (user.id === this.connection.user.id) { + return; + } + + + var presence = this.Presence({ + from: this.connection.getUserJid(user.username), + type: undefined + }); + + this.connection.populateVcard(presence, user, this.core); + + msgs.push(presence); + + }, this); + + cb(null, msgs); + } + +}); diff --git a/app/xmpp/msg-processors/roster-get.js b/app/xmpp/msg-processors/roster-get.js index 03b393f..fa3262c 100644 --- a/app/xmpp/msg-processors/roster-get.js +++ b/app/xmpp/msg-processors/roster-get.js @@ -1,6 +1,8 @@ 'use strict'; -var MessageProcessor = require('./../msg-processor'); +var _ = require('lodash'), + MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); module.exports = MessageProcessor.extend({ @@ -8,15 +10,58 @@ module.exports = MessageProcessor.extend({ return this.request.type === 'get' && this.ns['jabber:iq:roster']; }, - // Roster is always empty - everyone is friendless - // This should only be implemented if we support 1-to-1 private convos then: function(cb) { + if (!settings.private.enable) { + return this.sendRoster([], cb); + } + + if (settings.private.roster === 'all') { + return this.sendAllUsers(cb); + } + + this.sendOnlineUsers(cb); + }, + + sendOnlineUsers: function(cb) { + var users = this.core.presence.system.connections.getUsers({ + type: 'xmpp' // Only XMPP supports private messaging - for now + }); + + this.sendRoster(users, cb); + }, + + sendAllUsers: function(cb) { + this.core.users.list({}, function(err, users) { + if (err) { + return cb(err); + } + + this.sendRoster(users, cb); + }.bind(this)); + }, + + sendRoster: function(users, cb) { var stanza = this.Iq(); - stanza.c('query', { + var v = stanza.c('query', { xmlns: 'jabber:iq:roster' }); + _.each(users, function(user) { + if (user._id && user._id.equals(this.connection.user.id)) { + return; + } + if (user.id && user.id === this.connection.user.id) { + return; + } + + v.c('item', { + jid: this.connection.getUserJid(user.username), + name: user.displayName, + subscription: 'both' + }).c('group').t('Let\'s Chat'); + }, this); + cb(null, stanza); } diff --git a/app/xmpp/msg-processors/user-message.js b/app/xmpp/msg-processors/user-message.js new file mode 100644 index 0000000..3150a1b --- /dev/null +++ b/app/xmpp/msg-processors/user-message.js @@ -0,0 +1,51 @@ +'use strict'; + +var _ = require('lodash'), + MessageProcessor = require('./../msg-processor'), + settings = require('./../../config'); + +module.exports = MessageProcessor.extend({ + + if: function() { + return this.request.name === 'message' && + this.request.type === 'chat' && + !this.toARoom && + this.request.attrs.to; + }, + + then: function(cb) { + if (!settings.private.enable) { + return cb(); + } + + var username = this.request.attrs.to.split('@')[0]; + + var body = _.find(this.request.children, function (child) { + return child.name === 'body'; + }); + + if (!body) { + return cb(); + } + + this.core.users.username(username, function(err, user) { + if (err) { + return cb(err); + } + + if (!user) { + return cb(); + } + + this.core.usermessages.create({ + owner: this.connection.user.id, + user: user._id, + text: body.text() + }, function(err) { + cb(err); + }); + + }.bind(this)); + } + +}); diff --git a/app/xmpp/msg-processors/vcard-get.js b/app/xmpp/msg-processors/vcard-get.js index b0990fd..fdd686a 100644 --- a/app/xmpp/msg-processors/vcard-get.js +++ b/app/xmpp/msg-processors/vcard-get.js @@ -13,39 +13,53 @@ module.exports = MessageProcessor.extend({ var jid = this.connection.jid(); var other = this.to && this.to !== jid; - var sendVcard = function (user) { - var stanza = this.Iq(); + if (!other) { + return this.sendVcard(this.connection.user, cb); + } + + var username = this.to.split('@')[0]; + var user = this.core.presence.users.getByUsername(username); - var v = stanza.c('vCard', { - xmlns: 'vcard-temp' - }); + if (user) { + return this.sendVcard(user, cb); + } - v.c('FN').t(user.firstName + ' ' + user.lastName); + var User = mongoose.model('User'); + User.findByIdentifier(username, function(err, user) { + if (!err && user) { + this.sendVcard(user, cb); + } + }); + }, + sendVcard: function(user, cb) { + var stanza = this.Iq(); - var name = v.c('N'); - name.c('GIVEN').t(user.firstName); - name.c('FAMILY').t(user.lastName); + var vcard = stanza.c('vCard', { + xmlns: 'vcard-temp' + }); - v.c('NICKNAME').t(user.username); + vcard.c('FN').t(user.firstName + ' ' + user.lastName); - v.c('JABBERID').t(this.connection.getUserJid(user.username)); - cb(null, stanza); + var name = vcard.c('N'); + name.c('GIVEN').t(user.firstName); + name.c('FAMILY').t(user.lastName); - }.bind(this); + vcard.c('NICKNAME').t(user.username); - if (other) { - var User = mongoose.model('User'); - var username = this.to.split('@')[0]; - User.findByIdentifier(username, function(err, user) { - if (!err && user) { - sendVcard(user); - } - }); - } else { - sendVcard(this.client.user); + vcard.c('JABBERID').t(this.connection.getUserJid(user.username)); + + var userId = (user.id || user._id).toString(); + + var avatar = this.core.avatars.get(userId); + if (avatar) { + var photo = vcard.c('PHOTO'); + photo.c('TYPE').t('image/jpeg'); + photo.c('BINVAL').t(avatar.base64); } + + cb(null, stanza); } }); diff --git a/app/xmpp/xmpp-connection.js b/app/xmpp/xmpp-connection.js index 0d0f37a..19bcd80 100644 --- a/app/xmpp/xmpp-connection.js +++ b/app/xmpp/xmpp-connection.js @@ -69,4 +69,14 @@ XmppConnection.prototype.getRoomJid = function(roomId, username) { return jid;
};
+XmppConnection.prototype.populateVcard = function(presence, user, core) {
+ var vcard = presence.c('x', { xmlns: 'vcard-temp:x:update' });
+ var photo = vcard.c('photo');
+
+ var avatar = core.avatars.get(user.id);
+ if (avatar) {
+ photo.t(avatar.sha1);
+ }
+};
+
module.exports = XmppConnection;
diff --git a/defaults.yml b/defaults.yml index 4af1d09..69604a0 100644 --- a/defaults.yml +++ b/defaults.yml @@ -58,4 +58,12 @@ auth: passwordRegex: ^.{8,64}$ salt: secretsauce # Required when upgrading from version < 0.3 +private: + enable: false + roster: online # online / all + expire: 360 # false or number of minutes + noRobots: true # Serve robots.txt with disallow + +rooms: + passwordProtected: false diff --git a/media/js/client.js b/media/js/client.js index bb756f0..35f32c2 100644 --- a/media/js/client.js +++ b/media/js/client.js @@ -3,6 +3,29 @@ // (function(window, $, _) { + + var RoomStore = { + add: function(id) { + var rooms = store.get('openrooms') || []; + if (!_.contains(rooms, id)) { + rooms.push(id); + store.set('openrooms', rooms); + } + }, + remove: function(id) { + var rooms = store.get('openrooms') || []; + if (_.contains(rooms, id)) { + store.set('openrooms', _.without(rooms, id)); + } + }, + get: function() { + var rooms = store.get('openrooms') || []; + rooms = _.uniq(rooms); + store.set('openrooms', rooms); + return rooms; + } + }; + // // Base // @@ -39,7 +62,8 @@ var room = { name: data.name, slug: data.slug, - description: data.description + description: data.description, + password: data.password }; var callback = data.callback; this.socket.emit('rooms:create', room, function(room) { @@ -88,8 +112,10 @@ replace: true }); return; + } else if(room) { + this.joinRoom(room, true); } else { - this.joinRoom(id, true); + this.joinRoom({id: id}, true); } }; Client.prototype.updateRoom = function(room) { @@ -123,38 +149,81 @@ this.leaveRoom(room.id); this.rooms.remove(room.id); }; - Client.prototype.rejoinRoom = function(id) { - this.joinRoom(id, undefined, true); + Client.prototype.rejoinRoom = function(room) { + this.joinRoom(room, undefined, true); }; - Client.prototype.joinRoom = function(id, switchRoom, rejoin) { - var that = this; + Client.prototype.lockJoin = function(id) { + if (_.contains(this.joining, id)) { + return false; + } - // We need an id and unlocked joining - if (!id || _.contains(this.joining, id)) { - // Nothing to do + this.joining = this.joining || []; + this.joining.push(id); + return true; + }; + Client.prototype.unlockJoin = function(id) { + var that = this; + _.defer(function() { + that.joining = _.without(that.joining, id); + }); + }; + Client.prototype.joinRoom = function(room, switchRoom, rejoin) { + if (!room || !room.id) { return; } + var that = this; + var id = room.id; + var password = room.password; + if (!rejoin) { // Must not have already joined - var room = that.rooms.get(id); - if (room && room.get('joined')) { + var room1 = that.rooms.get(id); + if (room1 && room1.get('joined')) { return; } } - // - // Setup joining lock - // - this.joining = this.joining || []; - this.joining.push(id); - this.socket.emit('rooms:join', id, function(resRoom) { + if (!this.lockJoin(id)) { + return; + } + + var passwordCB = function(password) { + room.password = password; + that.joinRoom(room, switchRoom, rejoin); + }; + + this.socket.emit('rooms:join', {roomId: id, password: password}, function(resRoom) { // Room was likely archived if this returns if (!resRoom) { return; } + + if (resRoom && resRoom.errors && + resRoom.errors === 'password required') { + + that.passwordModal.show({ + callback: passwordCB + }); + + that.unlockJoin(id); + return; + } + + if (resRoom && resRoom.errors) { + that.unlockJoin(id); + return; + } + var room = that.addRoom(resRoom); room.set('joined', true); + + if (room.get('hasPassword')) { + that.getRoomUsers(room.id, _.bind(function(users) { + this.setUsers(room.id, users); + }, that)); + } + // Get room history that.getMessages({ room: room.id, @@ -167,6 +236,7 @@ that.addMessages(messages, !rejoin && !room.lastMessage.get('id')); !rejoin && room.lastMessage.set(messages[messages.length - 1]); }); + if (that.options.filesEnabled) { that.getFiles({ room: room.id, @@ -183,21 +253,9 @@ // // Add room id to localstorage so we can reopen it on refresh // - var openRooms = store.get('openrooms'); - if (openRooms instanceof Array) { - // Check for duplicates - if (!_.contains(openRooms, id)) { - openRooms.push(id); - } - store.set('openrooms', openRooms); - } else { - store.set('openrooms', [id]); - } + RoomStore.add(id); - // Remove joining lock - _.defer(function() { - that.joining = _.without(that.joining, id); - }); + that.unlockJoin(id); }); }; Client.prototype.leaveRoom = function(id) { @@ -205,6 +263,9 @@ if (room) { room.set('joined', false); room.lastMessage.clear(); + if (room.get('hasPassword')) { + room.users.set([]); + } } this.socket.emit('rooms:leave', id); if (id === this.rooms.current.get('id')) { @@ -212,7 +273,7 @@ this.switchRoom(room && room.get('joined') ? room.id : ''); } // Remove room id from localstorage - store.set('openrooms', _.without(store.get('openrooms'), id)); + RoomStore.remove(id); }; Client.prototype.getRoomUsers = function(id, callback) { this.socket.emit('rooms:users', { @@ -399,19 +460,16 @@ return room.id; }); - var openRooms = store.get('openrooms'); - if (openRooms instanceof Array) { - // Flush the stored array - store.set('openrooms', []); - - openRooms = _.uniq(openRooms); - // Let's open some rooms! + var openRooms = RoomStore.get(); + // Let's open some rooms! + _.defer(function() { + //slow down because router can start a join with no password _.each(openRooms, function(id) { - if (roomIds.indexOf(id) !== -1) { - that.joinRoom(id); + if (_.contains(roomIds, id)) { + that.joinRoom({ id: id }); } }); - } + }.bind(this)); } var path = '/' + _.compact( @@ -435,7 +493,7 @@ }); this.socket.on('reconnect', function() { _.each(that.rooms.where({ joined: true }), function(room) { - that.rejoinRoom(room.id); + that.rejoinRoom(room); }); }); this.socket.on('messages:new', function(message) { @@ -475,6 +533,7 @@ this.events.on('rooms:switch', this.switchRoom, this); this.events.on('rooms:archive', this.archiveRoom, this); this.events.on('profile:update', this.updateProfile, this); + this.events.on('rooms:join', this.joinRoom, this); }; // // Start @@ -487,6 +546,9 @@ this.view = new window.LCB.ClientView({ client: this }); + this.passwordModal = new window.LCB.RoomPasswordModalView({ + el: $('#lcb-password') + }); return this; }; // diff --git a/media/js/views/browser.js b/media/js/views/browser.js index 858f059..6235a59 100644 --- a/media/js/views/browser.js +++ b/media/js/views/browser.js @@ -44,11 +44,16 @@ $input = $target.is(':checkbox') && $target || $target.siblings('[type="checkbox"]'), id = $input.data('id'), room = this.rooms.get(id); + if (!room) { return; } - (!$input.is(':checked') && this.client.joinRoom(room.id)) || - (this.rooms.get(room.id).get('joined') && this.client.leaveRoom(room.id)); + + if (room.get('joined')) { + this.client.leaveRoom(room.id); + } else { + this.client.joinRoom(room); + } }, add: function(room) { var room = room.toJSON ? room.toJSON() : room, @@ -100,28 +105,60 @@ }); }, create: function(e) { + var that = this; e.preventDefault(); - var $modal = this.$('#lcb-add-room'), - $form = this.$(e.target), + var $form = this.$(e.target), + $modal = this.$('#lcb-add-room'), + $name = this.$('.lcb-room-name'), + $slug = this.$('.lcb-room-slug'), + $description = this.$('.lcb-room-description'), + $password = this.$('.lcb-room-password'), + $confirmPassword = this.$('.lcb-room-confirm-password'), data = { - name: this.$('.lcb-room-name').val().trim(), - slug: this.$('.lcb-room-slug').val().trim(), - description: this.$('.lcb-room-description').val(), + name: $name.val().trim(), + slug: $slug.val().trim(), + description: $description.val(), + password: $password.val(), callback: function success() { $modal.modal('hide'); $form.trigger('reset'); } }; + + $name.parent().removeClass('has-error'); + $slug.parent().removeClass('has-error'); + $confirmPassword.parent().removeClass('has-error'); + // we require name is non-empty if (!data.name) { $name.parent().addClass('has-error'); return; } + // we require slug is non-empty if (!data.slug) { $slug.parent().addClass('has-error'); return; } + + // remind the user, that users may share the password with others + if (data.password) { + if (data.password !== $confirmPassword.val()) { + $confirmPassword.parent().addClass('has-error'); + return; + } + + swal({ + title: 'Password-protected room', + text: 'You\'re creating a room with a shared password.\n' + + 'Anyone who obtains the password may enter the room.', + showCancelButton: true + }, function(){ + that.client.events.trigger('rooms:create', data); + }); + return; + } + this.client.events.trigger('rooms:create', data); }, addUser: function(user, room) { @@ -135,4 +172,4 @@ }); -}(window, $, _);
\ No newline at end of file +}(window, $, _); diff --git a/media/js/views/client.js b/media/js/views/client.js index 6231f08..b14b305 100644 --- a/media/js/views/client.js +++ b/media/js/views/client.js @@ -61,6 +61,7 @@ rooms: this.client.rooms }); } + // // Modals // @@ -78,6 +79,9 @@ this.notificationsModal = new window.LCB.NotificationsModalView({ el: this.$el.find('#lcb-notifications') }); + this.giphyModal = new window.LCB.GiphyModalView({ + el: this.$el.find('#lcb-giphy') + }); // // Misc // diff --git a/media/js/views/modals.js b/media/js/views/modals.js index e1d2fa7..a6d1c41 100644 --- a/media/js/views/modals.js +++ b/media/js/views/modals.js @@ -86,6 +86,29 @@ } }); + window.LCB.RoomPasswordModalView = Backbone.View.extend({ + events: { + 'click .btn-primary': 'enterRoom' + }, + initialize: function(options) { + this.render(); + this.$password = this.$('input.lcb-room-password-required'); + }, + render: function() { + // this.$el.on('shown.bs.modal hidden.bs.modal', + // _.bind(this.refresh, this)); + }, + show: function(options) { + this.callback = options.callback; + this.$password.val(''); + this.$el.modal('show'); + }, + enterRoom: function() { + this.$el.modal('hide'); + this.callback(this.$password.val()); + } + }); + window.LCB.AuthTokensModalView = Backbone.View.extend({ events: { 'click .generate-token': 'generateToken', @@ -176,4 +199,67 @@ } }); + window.LCB.GiphyModalView = Backbone.View.extend({ + events: { + 'keypress .search-giphy': 'stopReturn', + 'keyup .search-giphy': 'loadGifs' + }, + initialize: function(options) { + this.render(); + }, + render: function() { + this.$el.on('shown.bs.modal hidden.bs.modal', + _.bind(this.refresh, this)); + }, + refresh: function() { + this.$el.find('.giphy-results ul').empty(); + this.$('.search-giphy').val('').focus(); + }, + stopReturn: function(e) { + if(e.keyCode === 13) { + return false; + } + }, + loadGifs: _.throttle(function() { + console.log(1) + var that = this; + var search = this.$el.find('.search-giphy').val(); + + $.get('https://api.giphy.com/v1/gifs/search?limit=24&rating=pg-13&api_key=dc6zaTOxFJmzC&q=' + search) + .done(function(result) { + var images = result.data.filter(function(entry) { + return entry.images.fixed_width.url; + }).map(function(entry) { + return entry.images.fixed_width.url; + }); + + that.appendGifs(images); + }); + }, 400, {leading: false}), + appendGifs: function(images) { + var eles = images.map(function(url) { + var that = this; + var $img = $('<img src="' + url + + '" alt="gif" data-dismiss="modal"/></li>'); + + $img.click(function() { + var src = $(this).attr('src'); + $('.lcb-entry-input:visible').val(src); + $('.lcb-entry-button:visible').click(); + that.$el.modal('hide'); + }); + + return $("<li>").append($img); + }, this); + + var $div = this.$el.find('.giphy-results ul'); + + $div.empty(); + + eles.forEach(function($ele) { + $div.append($ele); + }); + } + }); + }(window, $, _); diff --git a/media/js/views/room.js b/media/js/views/room.js index 741b26a..45d7ae7 100644 --- a/media/js/views/room.js +++ b/media/js/views/room.js @@ -25,6 +25,13 @@ }, initialize: function(options) { this.client = options.client; + + var iAmOwner = this.model.get('owner') === this.client.user.id; + var iCanEdit = iAmOwner || !this.model.get('hasPassword'); + + this.model.set('iAmOwner', iAmOwner); + this.model.set('iCanEdit', iCanEdit); + this.template = options.template; this.messageTemplate = Handlebars.compile($('#template-message').html()); @@ -76,11 +83,17 @@ } }, getAtwhoUserFilter: function(collection) { + var currentUser = this.client.user; + return function filter(query, data, searchKey) { var q = query.toLowerCase(); var results = collection.filter(function(user) { var attr = user.attributes; + if (user.id === currentUser.id) { + return false; + } + if (!attr.safeName) { attr.safeName = attr.displayName.replace(/\W/g, ''); } @@ -213,7 +226,19 @@ if (e) { e.preventDefault(); } - this.$('.lcb-room-edit').modal(); + + var $modal = this.$('.lcb-room-edit'), + $name = $modal.find('input[name="name"]'), + $description = $modal.find('textarea[name="description"]'), + $password = $modal.find('input[name="password"]'), + $confirmPassword = $modal.find('input[name="confirmPassword"]'); + + $name.val(this.model.get('name')); + $description.val(this.model.get('description')); + $password.val(''); + $confirmPassword.val(''); + + $modal.modal(); }, hideEditRoom: function(e) { if (e) { @@ -225,14 +250,34 @@ if (e) { e.preventDefault(); } - var name = this.$('.edit-room input[name="name"]').val(); - var description = this.$('.edit-room textarea[name="description"]').val(); + + var $modal = this.$('.lcb-room-edit'), + $name = $modal.find('input[name="name"]'), + $description = $modal.find('textarea[name="description"]'), + $password = $modal.find('input[name="password"]'), + $confirmPassword = $modal.find('input[name="confirmPassword"]'); + + $name.parent().removeClass('has-error'); + $confirmPassword.parent().removeClass('has-error'); + + if (!$name.val()) { + $name.parent().addClass('has-error'); + return; + } + + if ($password.val() && $password.val() !== $confirmPassword.val()) { + $confirmPassword.parent().addClass('has-error'); + return; + } + this.client.events.trigger('rooms:update', { id: this.model.id, - name: name, - description: description + name: $name.val(), + description: $description.val(), + password: $password.val() }); - this.$('.lcb-room-edit').modal('hide'); + + $modal.modal('hide'); }, archiveRoom: function(e) { var that = this; diff --git a/media/js/views/transcript.js b/media/js/views/transcript.js index 0cab630..195343f 100644 --- a/media/js/views/transcript.js +++ b/media/js/views/transcript.js @@ -72,7 +72,7 @@ search: _.throttle(function() { this.query = this.$query.val() this.loadTranscript(); - }, 400), + }, 400, {leading: false}), loadTranscript: function() { var that = this; this.clearMessages(); diff --git a/media/js/views/window.js b/media/js/views/window.js index 3a455b5..39bb094 100644 --- a/media/js/views/window.js +++ b/media/js/views/window.js @@ -129,6 +129,7 @@ keys: { 'up+shift+alt down+shift+alt': 'nextRoom', 's+shift+alt': 'toggleRoomSidebar', + 'g+shift+alt': 'openGiphyModal', 'space+shift+alt': 'recallRoom' }, initialize: function(options) { @@ -151,6 +152,10 @@ e.preventDefault(); var view = this.client.view.panes.views[this.rooms.current.get('id')]; view && view.toggleSidebar && view.toggleSidebar(); + }, + openGiphyModal: function(e) { + e.preventDefault(); + $('.lcb-giphy').modal('show'); } }); diff --git a/media/less/style/chat/browser.less b/media/less/style/chat/browser.less index 1db3ff1..3024a0f 100644 --- a/media/less/style/chat/browser.less +++ b/media/less/style/chat/browser.less @@ -85,7 +85,7 @@ font-size: 20px; } -.lcb-rooms-list-item-slug { +.lcb-rooms-list-item-slug, .lcb-rooms-list-item-password { font-size: 16px; font-weight: 300; color: #aaa; diff --git a/media/less/style/chat/client.less b/media/less/style/chat/client.less index 8da1b4d..1c4805f 100644 --- a/media/less/style/chat/client.less +++ b/media/less/style/chat/client.less @@ -195,3 +195,26 @@ .lcb-avatar { border-radius: 100%; } + +.lcb-giphy .giphy-results { + max-height: 350px; + overflow: auto; + + ul { + margin: 0; + padding: 0; + width: 100%; + text-align: center; + + li { + display: inline-block; + *display: inline; + *zoom: 1; + margin:5px; + + img { + cursor: pointer; + } + } + } +} diff --git a/media/less/style/chat/rooms.less b/media/less/style/chat/rooms.less index fff09ef..cac237e 100644 --- a/media/less/style/chat/rooms.less +++ b/media/less/style/chat/rooms.less @@ -77,6 +77,10 @@ box-shadow: none; } } + .password { + font-size: 14px; + cursor: default; + } .btn:hover { color: #666; } diff --git a/templates/chat.html b/templates/chat.html index b128cb6..ad66d3f 100644 --- a/templates/chat.html +++ b/templates/chat.html @@ -104,6 +104,7 @@ </header> <ul class="lcb-rooms-list"></ul> <% include 'includes/modals/add-room.html' %> + <% include 'includes/modals/password.html' %> </div> </section> <div class="lcb-loading lcb-client-loading"> @@ -115,5 +116,6 @@ <% include 'includes/modals/xmpp.html' %> <% include 'includes/modals/tokens.html' %> <% include 'includes/modals/upload.html' %> + <% include 'includes/modals/giphy.html' %> </div><!-- lcb-client end --> <% endblock %> diff --git a/templates/includes/js/browser-item.html b/templates/includes/js/browser-item.html index 456a4a9..6c6ba2b 100644 --- a/templates/includes/js/browser-item.html +++ b/templates/includes/js/browser-item.html @@ -1,7 +1,12 @@ <script type="text/x-handlebars-template" id="template-room-browser-item"> <li class="lcb-rooms-list-item" data-id="{{id}}"> <div class="lcb-rooms-list-item-top"> - <a class="lcb-rooms-list-item-name" href="#!/room/{{id}}">{{name}}</a> + {{#if hasPassword}} + <span class="lcb-rooms-list-item-password fa fa-lock"></span> + <a class="lcb-rooms-list-item-name" href="#!/room/{{id}}">{{name}}</a> + {{else}} + <a class="lcb-rooms-list-item-name" href="#!/room/{{id}}">{{name}}</a> + {{/if}} <span class="lcb-rooms-list-item-slug">#{{slug}}</span> </div> <div class="lcb-rooms-list-item-description">{{description}}</div> diff --git a/templates/includes/js/room.html b/templates/includes/js/room.html index 0dae496..620b44f 100644 --- a/templates/includes/js/room.html +++ b/templates/includes/js/room.html @@ -6,10 +6,15 @@ <h2 class="lcb-room-heading"> <span class="name">{{name}}</span> <span class="slug">#{{slug}}</span> - <a class="btn hidden-xs show-edit-room" - title="Edit Room"> - <i class="fa fa-edit"></i> - </a> + {{#if hasPassword}} + <span class="fa fa-lock btn password" title="This room require password to enter."></span> + {{/if}} + {{#if iCanEdit}} + <a class="btn hidden-xs show-edit-room" + title="Edit Room"> + <i class="fa fa-edit"></i> + </a> + {{/if}} <a class="btn hidden-xs" title="Chat History" href="./transcript?room={{ id }}" target="_blank"> <i class="fa fa-history"></i> @@ -19,6 +24,9 @@ <i class="fa fa-upload"></i> </a> <% endif %> + <a class="btn hidden-xs lcb-giphy" href="#lcb-giphy" title="Giphy" data-toggle="modal"> + <i class="fa fa-gift"></i> + </a> </h2> <div class="lcb-room-description"> <p>{{description}}</p> @@ -85,6 +93,24 @@ "description">{{description}}</textarea> </div> </div> + {{#if hasPassword}} + <div class="form-group"> + <label class= + "control-label col-sm-4">Password</label> + <div class="col-sm-7"> + <input class="form-control" name= + "password" type="password" value="{{password}}"> + </div> + </div> + <div class="form-group"> + <label class= + "control-label col-sm-4">Confirm Password</label> + <div class="col-sm-7"> + <input class="form-control" name= + "confirmPassword" type="password" value="{{password}}"> + </div> + </div> + {{/if}} <p class="response" style="display: none;"></p> </div> <div class="modal-footer"> diff --git a/templates/includes/modals/add-room.html b/templates/includes/modals/add-room.html index 5c26fe0..7e7de7a 100644 --- a/templates/includes/modals/add-room.html +++ b/templates/includes/modals/add-room.html @@ -14,7 +14,7 @@ </div> <div class="form-group"> - <label for="lcb-room-slug">Slug</label> + <label>Slug</label> <div class="input-group"> <span class="input-group-addon">#</span> <input type="text" placeholder="Slug" @@ -22,10 +22,25 @@ </div> </div> <div class="form-group"> - <label for="lcb-room-name">Description</label> + <label>Description</label> <input type="text" placeholder="Description" class="lcb-room-description form-control"> </div> + <% if settings.rooms.passwordProtected %> + <div class="row"> + <div class="form-group col-sm-9"> + <label>Password</label> + <input type="password" + placeholder="Empty for public room" + class="lcb-room-password form-control"> + </div> + <div class="form-group col-sm-9"> + <label>Confirm Password</label> + <input type="password" + class="lcb-room-confirm-password form-control"> + </div> + </div> + <% endif %> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> diff --git a/templates/includes/modals/giphy.html b/templates/includes/modals/giphy.html new file mode 100644 index 0000000..8590756 --- /dev/null +++ b/templates/includes/modals/giphy.html @@ -0,0 +1,26 @@ +<div id="lcb-giphy" class="lcb-giphy modal fade"> + <div class="modal-dialog"> + <div class="modal-content"> + <form class="validate form-horizontal" action="./messages" method="POST"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title"><i class="icon-edit"></i> Giphy</h4> + </div> + <div class="modal-body"> + <div class="form-group"> + <label class="control-label col-sm-6">Search</label> + <div class="col-sm-11"> + <input class="form-control search-giphy" name="search" /> + <p class="help-block"> + Powered by <a href="http://giphy.com/" target="_blank">Giphy</a> + </p> + </div> + </div> + <div class="giphy-results"> + <ul></ul> + </div> + </div> + </form> + </div> + </div> +</div> diff --git a/templates/includes/modals/password.html b/templates/includes/modals/password.html new file mode 100644 index 0000000..8791602 --- /dev/null +++ b/templates/includes/modals/password.html @@ -0,0 +1,23 @@ +<div id="lcb-password" class="modal fade" data-backdrop=false> + <div class="modal-dialog"> + <div class="modal-content"> + <form role="form" class="lcb-password"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">Password required</h4> + </div> + <div class="modal-body"> + <div class="form-group"> + <label for="lcb-room-password">Password</label> + <input type="password" + class="lcb-room-password-required form-control"> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> + <button type="submit" class="btn btn-primary">Enter</button> + </div> + </form> + </div><!-- /.modal-content --> + </div><!-- /.modal-dialog --> +</div><!-- /.modal --> |