summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/connections.js2
-rw-r--r--app/controllers/files.js3
-rw-r--r--app/controllers/messages.js3
-rw-r--r--app/controllers/rooms.js88
-rw-r--r--app/controllers/usermessages.js88
-rw-r--r--app/controllers/users.js19
-rw-r--r--app/core/account.js2
-rw-r--r--app/core/avatar-cache.js50
-rw-r--r--app/core/files.js37
-rw-r--r--app/core/index.js17
-rw-r--r--app/core/messages.js31
-rw-r--r--app/core/presence.js16
-rw-r--r--app/core/presence/connection-collection.js43
-rw-r--r--app/core/presence/room-collection.js19
-rw-r--r--app/core/presence/room.js62
-rw-r--r--app/core/presence/user-collection.js23
-rw-r--r--app/core/rooms.js66
-rw-r--r--app/core/usermessages.js128
-rw-r--r--app/core/users.js47
-rw-r--r--app/models/room.js80
-rw-r--r--app/models/usermessage.js54
-rw-r--r--app/xmpp/events/user-avatar-ready.js48
-rw-r--r--app/xmpp/events/user-connect.js71
-rw-r--r--app/xmpp/events/user-disconnect.js52
-rw-r--r--app/xmpp/events/user-join.js4
-rw-r--r--app/xmpp/events/usermessage-created.js44
-rw-r--r--app/xmpp/helper.js13
-rw-r--r--app/xmpp/index.js33
-rw-r--r--app/xmpp/msg-processors/room-join.js101
-rw-r--r--app/xmpp/msg-processors/root-join.js49
-rw-r--r--app/xmpp/msg-processors/roster-get.js53
-rw-r--r--app/xmpp/msg-processors/user-message.js53
-rw-r--r--app/xmpp/msg-processors/vcard-get.js62
-rw-r--r--defaults.yml8
-rw-r--r--media/js/client.js150
-rw-r--r--media/js/views/browser.js53
-rw-r--r--media/js/views/client.js4
-rw-r--r--media/js/views/modals.js86
-rw-r--r--media/js/views/room.js57
-rw-r--r--media/js/views/transcript.js2
-rw-r--r--media/js/views/window.js5
-rw-r--r--media/less/style/chat/browser.less2
-rw-r--r--media/less/style/chat/client.less23
-rw-r--r--media/less/style/chat/rooms.less4
-rw-r--r--templates/chat.html2
-rw-r--r--templates/includes/js/browser-item.html7
-rw-r--r--templates/includes/js/room.html34
-rw-r--r--templates/includes/modals/add-room.html19
-rw-r--r--templates/includes/modals/giphy.html26
-rw-r--r--templates/includes/modals/password.html23
50 files changed, 1737 insertions, 229 deletions
diff --git a/app/controllers/connections.js b/app/controllers/connections.js
index 1fcdb1d..17108de 100644
--- a/app/controllers/connections.js
+++ b/app/controllers/connections.js
@@ -44,7 +44,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 d87ce6e..20fdc23 100644
--- a/app/controllers/files.js
+++ b/app/controllers/files.js
@@ -109,6 +109,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 9f0b182..aff70d8 100644
--- a/app/controllers/messages.js
+++ b/app/controllers/messages.js
@@ -62,6 +62,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 749d4bb..36a04b2 100644
--- a/app/controllers/rooms.js
+++ b/app/controllers/rooms.js
@@ -4,7 +4,8 @@
'use strict';
-var _ = require('lodash');
+var _ = require('lodash'),
+ settings = require('./../config').rooms;
module.exports = function() {
var app = this.app,
@@ -18,7 +19,12 @@ module.exports = function() {
User.findById(data.userId, function (err, user) {
user = user.toJSON();
user.room = data.roomId;
- app.io.emit('users:join', user);
+
+ if (data.roomHasPassword) {
+ app.io.to(data.roomId).emit('users:join', user);
+ } else {
+ app.io.emit('users:join', user);
+ }
});
});
@@ -26,7 +32,12 @@ module.exports = function() {
User.findById(data.userId, function (err, user) {
user = user.toJSON();
user.room = data.roomId;
- app.io.emit('users:leave', user);
+
+ if (data.roomHasPassword) {
+ app.io.to(data.roomId).emit('users:leave', user);
+ } else {
+ app.io.emit('users:leave', user);
+ }
});
});
@@ -80,6 +91,9 @@ module.exports = function() {
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')
};
@@ -90,26 +104,6 @@ module.exports = function() {
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);
});
},
@@ -131,11 +125,16 @@ module.exports = function() {
},
create: function(req, res) {
var options = {
- owner: req.user._id,
- name: req.param('name'),
- slug: req.param('slug'),
- description: req.param('description')
- };
+ 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) {
@@ -152,7 +151,9 @@ module.exports = function() {
var options = {
name: req.param('name'),
slug: req.param('slug'),
- description: req.param('description')
+ description: req.param('description'),
+ password: req.param('password'),
+ user: req.user
};
core.rooms.update(roomId, options, function(err, room) {
@@ -185,21 +186,40 @@ module.exports = function() {
});
},
join: function(req, res) {
- var roomId = req.data;
- core.rooms.get(roomId, function(err, room) {
+ 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(400);
+ 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._id, room.slug);
+ core.presence.join(req.socket.conn, room);
req.socket.join(room._id);
res.json(room.toJSON());
});
@@ -227,7 +247,7 @@ module.exports = function() {
}
var users = core.presence.rooms
- .getOrAdd(room._id, room.slug)
+ .getOrAdd(room)
.getUsers()
.map(function(user) {
// TODO: Do we need to do this?
diff --git a/app/controllers/usermessages.js b/app/controllers/usermessages.js
new file mode 100644
index 0000000..fee5311
--- /dev/null
+++ b/app/controllers/usermessages.js
@@ -0,0 +1,88 @@
+//
+// UserMessages Controller
+//
+
+'use strict';
+
+var _ = require('lodash'),
+ settings = require('./../config');
+
+module.exports = function() {
+
+ var app = this.app,
+ core = this.core,
+ middlewares = this.middlewares,
+ models = this.models,
+ Room = models.room;
+
+
+ if (!settings.private.enable) {
+ return;
+ }
+
+ core.on('user-messages:new', function(message, user, owner) {
+ _.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, res) {
+ req.io.route('user-messages:list');
+ })
+ .post(function(req, res) {
+ 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 903e3d6..a66c28c 100644
--- a/app/controllers/users.js
+++ b/app/controllers/users.js
@@ -36,24 +36,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..dac1de5
--- /dev/null
+++ b/app/core/avatar-cache.js
@@ -0,0 +1,50 @@
+'use strict';
+
+var crypto = require('crypto'),
+ fs = require('fs'),
+ http = require('http'),
+ util = require('util'),
+ _ = require('lodash');
+
+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';
+
+ var request = 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);
+ var image = buffer.toString('base64');
+
+ 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 f9ef61d..a271785 100644
--- a/app/core/files.js
+++ b/app/core/files.js
@@ -32,8 +32,8 @@ FileManager.prototype.create = function(options, cb) {
}
var File = mongoose.model('File'),
- Room = mongoose.model('Room'),
- User = mongoose.model('User');
+ Room = mongoose.model('Room'),
+ User = mongoose.model('User');
if (settings.restrictTypes &&
settings.allowedTypes &&
@@ -93,6 +93,8 @@ FileManager.prototype.create = function(options, cb) {
};
FileManager.prototype.list = function(options, cb) {
+ var Room = mongoose.model('Room');
+
if (!enabled) {
return cb(null, []);
}
@@ -144,14 +146,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 8c2d893..481a612 100644
--- a/app/core/messages.js
+++ b/app/core/messages.js
@@ -46,6 +46,8 @@ MessageManager.prototype.create = function(options, cb) {
};
MessageManager.prototype.list = function(options, cb) {
+ var Room = mongoose.model('Room');
+
options = options || {};
if (!options.room) {
@@ -105,14 +107,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 08711e5..f6316fd 100644
--- a/app/core/presence.js
+++ b/app/core/presence.js
@@ -2,15 +2,17 @@
var _ = require('lodash'),
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));
@@ -30,7 +32,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);
@@ -40,13 +43,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 a0ced68..867c5af 100644
--- a/app/core/presence/connection-collection.js
+++ b/app/core/presence/connection-collection.js
@@ -10,6 +10,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);
@@ -28,28 +29,40 @@ ConnectionCollection.prototype.contains = function(connection) {
return !!this.connections[connection.id];
};
-ConnectionCollection.prototype.getUsers = function() {
- var users = _.map(this.connections, function(value, key) {
- return value.user;
- });
+ConnectionCollection.prototype.getUsers = function(filter) {
+ var connections = this.connections;
+
+ if (filter) {
+ connections = this.query(filter);
+ }
- return _.uniq(users, 'id');
+ var users = _.chain(connections)
+ .filter(function(value, key) {
+ return !!value.user;
+ })
+ .map(function(value, key) {
+ return value.user;
+ })
+ .uniq('id')
+ .value();
+
+ return users;
};
-ConnectionCollection.prototype.getUserIds = function() {
- var userIds = _.map(this.connections, function(value, key) {
- return value.user.id;
- });
+ConnectionCollection.prototype.getUserIds = function(filter) {
+ var users = this.getUsers(filter);
- return _.uniq(userIds);
+ return _.map(users, function(user) {
+ return user.id;
+ });
};
-ConnectionCollection.prototype.getUsernames = function() {
- var usernames = _.map(this.connections, function(value, key) {
- return value.user.username;
- });
+ConnectionCollection.prototype.getUsernames = function(filter) {
+ var users = this.getUsers(filter);
- return _.uniq(usernames);
+ return _.map(users, function(user) {
+ return user.username;
+ });
};
ConnectionCollection.prototype.query = function(options) {
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 07f1cee..82ce8ca 100644
--- a/app/core/presence/room.js
+++ b/app/core/presence/room.js
@@ -5,10 +5,23 @@ var EventEmitter = require('events').EventEmitter,
_ = require('lodash'),
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;
@@ -25,15 +38,15 @@ function Room(roomId, roomSlug) {
util.inherits(Room, EventEmitter);
-Room.prototype.getUsers = function() {
+Room.prototype.getUsers = function(filter) {
return this.connections.getUsers();
};
-Room.prototype.getUserIds = function() {
+Room.prototype.getUserIds = function(filter) {
return this.connections.getUserIds();
};
-Room.prototype.getUsernames = function() {
+Room.prototype.getUsernames = function(filter) {
return this.connections.getUsernames();
};
@@ -43,22 +56,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) {
@@ -86,6 +118,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
});
@@ -104,6 +137,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 e5187ce..b6ac709 100644
--- a/app/core/presence/user-collection.js
+++ b/app/core/presence/user-collection.js
@@ -1,10 +1,14 @@
'use strict';
var EventEmitter = require('events').EventEmitter,
+ crypto = require('crypto'),
+ http = require('http'),
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);
@@ -12,16 +16,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..a358b18 100644
--- a/app/core/rooms.js
+++ b/app/core/rooms.js
@@ -1,12 +1,33 @@
'use strict';
var mongoose = require('mongoose'),
+ _ = require('lodash'),
+ bcrypt = require('bcryptjs'),
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 +57,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 +135,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..fe6ac11
--- /dev/null
+++ b/app/core/usermessages.js
@@ -0,0 +1,128 @@
+'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'),
+ User = mongoose.model('User');
+
+ 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 86ad322..bb597ce 100644
--- a/app/models/room.js
+++ b/app/models/room.js
@@ -7,6 +7,7 @@
var mongoose = require('mongoose'),
ObjectId = mongoose.Schema.Types.ObjectId,
uniqueValidator = require('mongoose-unique-validator'),
+ bcrypt = require('bcryptjs'),
settings = require('./../config');
var RoomSchema = new mongoose.Schema({
@@ -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..8d3c10a
--- /dev/null
+++ b/app/models/usermessage.js
@@ -0,0 +1,54 @@
+//
+// Message
+//
+
+'use strict';
+
+var mongoose = require('mongoose'),
+ ObjectId = mongoose.Schema.Types.ObjectId,
+ settings = require('./../config');
+
+var MessageSchema = new mongoose.Schema({
+ users: [{
+ type: ObjectId,
+ ref: 'User'
+ }],
+ owner: {
+ type: ObjectId,
+ ref: 'User',
+ required: true
+ },
+ text: {
+ type: String,
+ required: true,
+ trim: 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..362cc65
--- /dev/null
+++ b/app/xmpp/events/user-avatar-ready.js
@@ -0,0 +1,48 @@
+'use strict';
+
+var _ = require('lodash'),
+ Stanza = require('node-xmpp-core').Stanza,
+ settings = require('./../../config'),
+ helper = require('./../helper'),
+ 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(x) {
+ if (x.user.id === user.id) {
+ return;
+ }
+
+ // Reannounce presence
+ var presence = new Stanza.Presence({
+ from: helper.getUserJid(user.username)
+ });
+
+ helper.populateVcard(presence, user, this.core);
+
+ this.send(x, presence);
+ }, this);
+ }
+
+});
diff --git a/app/xmpp/events/user-connect.js b/app/xmpp/events/user-connect.js
new file mode 100644
index 0000000..822e34f
--- /dev/null
+++ b/app/xmpp/events/user-connect.js
@@ -0,0 +1,71 @@
+'use strict';
+
+var _ = require('lodash'),
+ Stanza = require('node-xmpp-core').Stanza,
+ settings = require('./../../config'),
+ helper = require('./../helper'),
+ 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: helper.getUserJid(x.user.username)
+ });
+
+ roster.c('query', {
+ xmlns: 'jabber:iq:roster'
+ }).c('item', {
+ jid: helper.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: helper.getUserJid(connection.user.username)
+ });
+
+ helper.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..83407d1
--- /dev/null
+++ b/app/xmpp/events/user-disconnect.js
@@ -0,0 +1,52 @@
+'use strict';
+
+var _ = require('lodash'),
+ Stanza = require('node-xmpp-core').Stanza,
+ settings = require('./../../config'),
+ helper = require('./../helper'),
+ 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: helper.getUserJid(x.user.username),
+ from: helper.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 b52a405..d0a41bb 100644
--- a/app/xmpp/events/user-join.js
+++ b/app/xmpp/events/user-join.js
@@ -27,6 +27,10 @@ module.exports = EventListener.extend({
role: 'participant'
});
+ if (data.user) {
+ helper.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..921d820
--- /dev/null
+++ b/app/xmpp/events/usermessage-created.js
@@ -0,0 +1,44 @@
+'use strict';
+
+var Stanza = require('node-xmpp-core').Stanza,
+ settings = require('./../../config'),
+ helper = require('./../helper'),
+ EventListener = require('./../event-listener');
+
+var mentionPattern = /\B@(\w+)(?!@)\b/g;
+
+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: helper.getUserJid(user.username),
+ from: helper.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/helper.js b/app/xmpp/helper.js
index e84d785..449e01f 100644
--- a/app/xmpp/helper.js
+++ b/app/xmpp/helper.js
@@ -17,7 +17,18 @@ function getRoomJid(roomId, username) {
return jid;
}
+function populateVcard(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 = {
getUserJid: getUserJid,
- getRoomJid: getRoomJid
+ getRoomJid: getRoomJid,
+ populateVcard: populateVcard
};
diff --git a/app/xmpp/index.js b/app/xmpp/index.js
index dce0053..8017f5a 100644
--- a/app/xmpp/index.js
+++ b/app/xmpp/index.js
@@ -47,7 +47,7 @@ function xmppStart(core) {
var conn = new XmppConnection(user, client);
core.presence.connect(conn);
-
+
cb(null, opts);
});
});
@@ -61,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 1282b7e..2e26789 100644
--- a/app/xmpp/msg-processors/room-join.js
+++ b/app/xmpp/msg-processors/room-join.js
@@ -43,35 +43,52 @@ module.exports = MessageProcessor.extend({
// TODO: Do we need to track nickname for each individual room?
this.connection.nickname = 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);
},
@@ -95,6 +112,63 @@ 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) {
+ var connection = this.client.conn;
+ var username = connection.user.username;
+
+ //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: helper.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;
@@ -124,6 +198,9 @@ module.exports = MessageProcessor.extend({
role: 'participant'
});
+ // TODO: Add avatar for each room user
+ // helper.populateVcard(presence, user, this.core);
+
return presence;
}, this);
@@ -134,26 +211,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 +274,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..4f7a492
--- /dev/null
+++ b/app/xmpp/msg-processors/root-join.js
@@ -0,0 +1,49 @@
+'use strict';
+
+var _ = require('lodash'),
+ moment = require('moment'),
+ Stanza = require('node-xmpp-core').Stanza,
+ MessageProcessor = require('./../msg-processor'),
+ settings = require('./../../config'),
+ helper = require('./../helper');
+
+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: helper.getUserJid(user.username),
+ type: undefined
+ });
+
+ helper.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 ac5e8b5..0e3ad95 100644
--- a/app/xmpp/msg-processors/roster-get.js
+++ b/app/xmpp/msg-processors/roster-get.js
@@ -1,7 +1,9 @@
'use strict';
-var MessageProcessor = require('./../msg-processor'),
- settings = require('./../../config');
+var _ = require('lodash'),
+ MessageProcessor = require('./../msg-processor'),
+ settings = require('./../../config'),
+ helper = require('./../helper');
module.exports = MessageProcessor.extend({
@@ -9,15 +11,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) {
+ var users = 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();
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: helper.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..e53606c
--- /dev/null
+++ b/app/xmpp/msg-processors/user-message.js
@@ -0,0 +1,53 @@
+'use strict';
+
+var _ = require('lodash'),
+ MessageProcessor = require('./../msg-processor'),
+ settings = require('./../../config');
+
+var mentionPattern = /^([a-z0-9_]+\:)\B/;
+
+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 a1bedf4..3885f33 100644
--- a/app/xmpp/msg-processors/vcard-get.js
+++ b/app/xmpp/msg-processors/vcard-get.js
@@ -12,42 +12,56 @@ module.exports = MessageProcessor.extend({
},
then: function(cb) {
- var jid = helper.getUserJid(this.client.conn.user.username);
+ var jid = helper.getUserJid(this.connection.user.username);
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 (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(helper.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 (user) {
- sendVcard(user);
- }
- });
- } else {
- sendVcard(this.client.user);
+ vcard.c('JABBERID').t(helper.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/defaults.yml b/defaults.yml
index c707a62..8c8592b 100644
--- a/defaults.yml
+++ b/defaults.yml
@@ -57,4 +57,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 c638d03..12386c4 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]);
- }
-
- // Remove joining lock
- _.defer(function() {
- that.joining = _.without(that.joining, id);
- });
+ RoomStore.add(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));
}
//
@@ -430,7 +488,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) {
@@ -470,6 +528,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
@@ -482,6 +541,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 48071e9..736f81b 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 bf79ca2..e6f3c97 100644
--- a/media/js/views/window.js
+++ b/media/js/views/window.js
@@ -110,6 +110,7 @@
keys: {
'up+shift+alt down+shift+alt': 'nextRoom',
's+shift+alt': 'toggleRoomSidebar',
+ 'g+shift+alt': 'openGiphyModal',
'space+shift+alt': 'recallRoom'
},
initialize: function(options) {
@@ -132,6 +133,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 2c53f18..79ff3e2 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 9022653..e7ec6fe 100644
--- a/media/less/style/chat/client.less
+++ b/media/less/style/chat/client.less
@@ -193,3 +193,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 41438de..e6e5997 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 26eabf8..64c01f4 100644
--- a/templates/chat.html
+++ b/templates/chat.html
@@ -102,6 +102,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">
@@ -113,5 +114,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 8afe4ef..6144d75 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>
@@ -84,6 +92,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">&times;</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">&times;</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 -->