mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Initial Open Source commit
This commit is contained in:
commit
8cb71e8fad
52 changed files with 41511 additions and 0 deletions
11
services/chat/.gitignore
vendored
Normal file
11
services/chat/.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
**.swp
|
||||||
|
|
||||||
|
app.js
|
||||||
|
app/js/
|
||||||
|
test/unit/js/
|
||||||
|
public/build/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
/public/js/chat.js
|
||||||
|
plato/
|
124
services/chat/Gruntfile.coffee
Normal file
124
services/chat/Gruntfile.coffee
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
module.exports = (grunt) ->
|
||||||
|
|
||||||
|
# Project configuration.
|
||||||
|
grunt.initConfig
|
||||||
|
coffee:
|
||||||
|
client:
|
||||||
|
expand: true,
|
||||||
|
flatten: false,
|
||||||
|
cwd: 'public/coffee',
|
||||||
|
src: ['**/*.coffee'],
|
||||||
|
dest: 'public/build/',
|
||||||
|
ext: '.js'
|
||||||
|
|
||||||
|
server:
|
||||||
|
expand: true,
|
||||||
|
flatten: false,
|
||||||
|
cwd: 'app/coffee',
|
||||||
|
src: ['**/*.coffee'],
|
||||||
|
dest: 'app/js/',
|
||||||
|
ext: '.js'
|
||||||
|
|
||||||
|
app_server:
|
||||||
|
expand: true,
|
||||||
|
flatten: false,
|
||||||
|
src: ['app.coffee'],
|
||||||
|
dest: './',
|
||||||
|
ext: '.js'
|
||||||
|
|
||||||
|
server_tests:
|
||||||
|
expand: true,
|
||||||
|
flatten: false,
|
||||||
|
cwd: 'test/unit/coffee',
|
||||||
|
src: ['**/*.coffee'],
|
||||||
|
dest: 'test/unit/js/',
|
||||||
|
ext: '.js'
|
||||||
|
|
||||||
|
watch:
|
||||||
|
server_coffee:
|
||||||
|
files: ['app/**/*.coffee', 'test/unit/**/*.coffee']
|
||||||
|
tasks: ['compile:server', 'compile:server_tests', 'mochaTest']
|
||||||
|
|
||||||
|
client_coffee:
|
||||||
|
files: ['public/**/*.coffee']
|
||||||
|
tasks: ['compile']
|
||||||
|
|
||||||
|
less:
|
||||||
|
files: ['public/less/*.less']
|
||||||
|
tasks: ['compile']
|
||||||
|
|
||||||
|
jade:
|
||||||
|
files: ['public/jade/*.jade']
|
||||||
|
tasks: ['compile']
|
||||||
|
|
||||||
|
|
||||||
|
less:
|
||||||
|
production:
|
||||||
|
files:
|
||||||
|
"public/build/css/chat.css": "public/less/chat.less"
|
||||||
|
|
||||||
|
jade:
|
||||||
|
compile:
|
||||||
|
files:
|
||||||
|
"public/build/html/templates.html": ["public/jade/templates.jade"]
|
||||||
|
|
||||||
|
requirejs:
|
||||||
|
compile:
|
||||||
|
options:
|
||||||
|
mainConfigFile: 'public/app.build.js',
|
||||||
|
|
||||||
|
uglify:
|
||||||
|
my_target:
|
||||||
|
files:
|
||||||
|
'public/build/chat.js': ['public/build/chat.js']
|
||||||
|
|
||||||
|
copy:
|
||||||
|
main:
|
||||||
|
expand: true
|
||||||
|
cwd: 'public/js'
|
||||||
|
src: '**'
|
||||||
|
dest: 'public/build/'
|
||||||
|
|
||||||
|
clean: ["public/build", "app/js", "test/unit/js"]
|
||||||
|
|
||||||
|
nodemon:
|
||||||
|
dev:
|
||||||
|
options:
|
||||||
|
file: 'app.js'
|
||||||
|
|
||||||
|
concurrent:
|
||||||
|
dev:
|
||||||
|
tasks: ['nodemon', 'watch']
|
||||||
|
options:
|
||||||
|
logConcurrentOutput: true
|
||||||
|
|
||||||
|
mochaTest:
|
||||||
|
unit:
|
||||||
|
options:
|
||||||
|
reporter: process.env.MOCHA_RUNNER || "spec"
|
||||||
|
grep: grunt.option("grep")
|
||||||
|
src: ['test/**/*.js']
|
||||||
|
|
||||||
|
plato:
|
||||||
|
your_task:
|
||||||
|
files: 'plato': ['app/js/**/*.js'],
|
||||||
|
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-coffee'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-copy'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-less'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-jade'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-requirejs'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-uglify'
|
||||||
|
grunt.loadNpmTasks 'grunt-nodemon'
|
||||||
|
grunt.loadNpmTasks 'grunt-contrib-clean'
|
||||||
|
grunt.loadNpmTasks 'grunt-concurrent'
|
||||||
|
grunt.loadNpmTasks 'grunt-mocha-test'
|
||||||
|
grunt.loadNpmTasks 'grunt-plato'
|
||||||
|
|
||||||
|
grunt.registerTask 'compile', ['clean', 'copy', 'coffee', 'less', 'jade', 'requirejs']
|
||||||
|
grunt.registerTask 'install', ['compile']
|
||||||
|
grunt.registerTask 'compileAndCompress', ['compile', 'uglify']
|
||||||
|
grunt.registerTask 'default', ['compile', 'concurrent']
|
||||||
|
grunt.registerTask 'test:unit', ['compile', 'mochaTest:unit']
|
||||||
|
|
5
services/chat/app.coffee
Normal file
5
services/chat/app.coffee
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
logger = require 'logger-sharelatex'
|
||||||
|
|
||||||
|
Server = require "./app/js/server"
|
||||||
|
Server.server.listen(3010, "localhost")
|
||||||
|
logger.log "chat sharelatex listening on port 3010"
|
|
@ -0,0 +1,23 @@
|
||||||
|
async = require "async"
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
WebApiManager = require("../WebApi/WebApiManager")
|
||||||
|
UserFormatter = require("../Users/UserFormatter")
|
||||||
|
|
||||||
|
module.exports = AuthenticationController =
|
||||||
|
authClient: (client, data, callback = (error) ->) ->
|
||||||
|
logger.log auth_token: data.auth_token, "authenticating user"
|
||||||
|
WebApiManager.getUserDetailsFromAuthToken data.auth_token, (error, user) =>
|
||||||
|
if error?
|
||||||
|
logger.error data: data, client_id: client.id, err: error, "error authenticating user"
|
||||||
|
return callback("something went wrong")
|
||||||
|
logger.log user: user, auth_token: data.auth_token, "authenticated user"
|
||||||
|
user = UserFormatter.formatUserForClientSide user
|
||||||
|
jobs = []
|
||||||
|
for key, value of user
|
||||||
|
do (key, value) ->
|
||||||
|
jobs.push (callback) -> client.set key, value, callback
|
||||||
|
jobs.push (callback) -> client.set "auth_token", data.auth_token, callback
|
||||||
|
async.series jobs, (error, results) =>
|
||||||
|
callback(error, user)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
WebApiManager = require "../WebApi/WebApiManager"
|
||||||
|
SocketManager = require "../Sockets/SocketManager"
|
||||||
|
|
||||||
|
module.exports = AuthorizationManager =
|
||||||
|
canClientJoinProjectRoom: (client, project_id, callback = (error, authorized) ->) ->
|
||||||
|
client.get "auth_token", (error, auth_token) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
WebApiManager.getProjectCollaborators project_id, auth_token, (error, collaborators) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
client.get "id", (error, user_id) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
authorized = false
|
||||||
|
for collaborator in collaborators
|
||||||
|
if collaborator.id == user_id
|
||||||
|
authorized = true
|
||||||
|
break
|
||||||
|
callback null, authorized
|
||||||
|
|
||||||
|
canClientSendMessageToRoom: (client, room_id, callback = (error, authorized) ->) ->
|
||||||
|
SocketManager.isClientInRoom(client, room_id, callback)
|
||||||
|
|
||||||
|
canClientReadMessagesInRoom: (client, room_id, callback = (error, authorized) ->) ->
|
||||||
|
SocketManager.isClientInRoom(client, room_id, callback)
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
metrics = require "../../metrics"
|
||||||
|
MessageManager = require "./MessageManager"
|
||||||
|
MessageFormatter = require "./MessageFormatter"
|
||||||
|
SocketManager = require "../Sockets/SocketManager"
|
||||||
|
AuthorizationManager = require "../Authorization/AuthorizationManager"
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = MessageController =
|
||||||
|
DEFAULT_MESSAGE_LIMIT: 50
|
||||||
|
|
||||||
|
sendMessage: (client, data, callback = (error) ->) ->
|
||||||
|
content = data?.message?.content
|
||||||
|
room_id = data?.room?.id
|
||||||
|
return callback("malformed message") if not (content? and room_id?)
|
||||||
|
|
||||||
|
client.get "id", (error, user_id) ->
|
||||||
|
logger.log user_id: user_id, room_id: room_id, "sending message"
|
||||||
|
AuthorizationManager.canClientSendMessageToRoom client, room_id, (error, authorized) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went wrong checking if canClientSendMessageToRoom"
|
||||||
|
return callback("something went wrong")
|
||||||
|
if authorized
|
||||||
|
SocketManager.getClientAttributes client, ["id"], (error, values) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went wrong getClientAttributes"
|
||||||
|
return callback("something went wrong")
|
||||||
|
newMessageOpts =
|
||||||
|
content: content
|
||||||
|
room_id: room_id
|
||||||
|
user_id: values[0]
|
||||||
|
timestamp: Date.now()
|
||||||
|
MessageManager.createMessage newMessageOpts, (error, message) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went wrong createMessage"
|
||||||
|
return callback("something went wrong")
|
||||||
|
MessageManager.populateMessagesWithUsers [message], (error, messages) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went wrong populateMessagesWithUsers"
|
||||||
|
return callback("something went wrong")
|
||||||
|
message = MessageFormatter.formatMessageForClientSide(messages[0])
|
||||||
|
message.room =
|
||||||
|
id: room_id
|
||||||
|
SocketManager.emitToRoom data.room.id, "messageReceived", message:message
|
||||||
|
metrics.inc "editor.instant-message"
|
||||||
|
logger.log user_id: user_id, room_id: room_id, "sent message"
|
||||||
|
callback()
|
||||||
|
else
|
||||||
|
logger.log user_id: user_id, room_id: room_id, "unauthorized attempt to send message"
|
||||||
|
callback("unknown room")
|
||||||
|
|
||||||
|
getMessages: (client, data, callback = (error, messages) ->) ->
|
||||||
|
room_id = data?.room?.id
|
||||||
|
return callback("malformed message") if not room_id?
|
||||||
|
|
||||||
|
client.get "id", (error, user_id) ->
|
||||||
|
logger.log user_id: user_id, room_id: room_id, "getting messages"
|
||||||
|
AuthorizationManager.canClientReadMessagesInRoom client, room_id, (error, authorized) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went canClientReadMessagesInRoom"
|
||||||
|
return callback("something went wrong")
|
||||||
|
if authorized
|
||||||
|
query = room_id: room_id
|
||||||
|
if data.before?
|
||||||
|
query.timestamp = $lt: data.before
|
||||||
|
options =
|
||||||
|
order_by: "timestamp"
|
||||||
|
sort_order: -1
|
||||||
|
limit: data.limit || MessageController.DEFAULT_MESSAGE_LIMIT
|
||||||
|
MessageManager.getMessages query, options, (error, messages) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went getMessages"
|
||||||
|
return callback("something went wrong")
|
||||||
|
MessageManager.populateMessagesWithUsers messages, (error, messages) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went populateMessagesWithUsers"
|
||||||
|
return callback("something went wrong")
|
||||||
|
messages = MessageFormatter.formatMessagesForClientSide messages
|
||||||
|
logger.log user_id: user_id, room_id: room_id, "got messages"
|
||||||
|
callback null, messages
|
||||||
|
else
|
||||||
|
logger.log user_id: user_id, room_id: room_id, "unauthorized attempt to get messages"
|
||||||
|
callback("unknown room")
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
UserFormatter = require "../Users/UserFormatter"
|
||||||
|
|
||||||
|
module.exports = MessageFormatter =
|
||||||
|
formatMessageForClientSide: (message) ->
|
||||||
|
if message._id?
|
||||||
|
message.id = message._id.toString()
|
||||||
|
delete message._id
|
||||||
|
formattedMessage =
|
||||||
|
id: message.id
|
||||||
|
content: message.content
|
||||||
|
timestamp: message.timestamp
|
||||||
|
user: UserFormatter.formatUserForClientSide(message.user)
|
||||||
|
return formattedMessage
|
||||||
|
|
||||||
|
formatMessagesForClientSide: (messages) ->
|
||||||
|
(@formatMessageForClientSide(message) for message in messages)
|
|
@ -0,0 +1,62 @@
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
metrics = require "../../metrics"
|
||||||
|
MessageManager = require "./MessageManager"
|
||||||
|
MessageFormatter = require "./MessageFormatter"
|
||||||
|
RoomManager = require "../Rooms/RoomManager"
|
||||||
|
|
||||||
|
module.exports = MessageHttpController =
|
||||||
|
DEFAULT_MESSAGE_LIMIT: 50
|
||||||
|
|
||||||
|
sendMessage: (req, res, next) ->
|
||||||
|
{user_id, content} = req?.body
|
||||||
|
{project_id} = req.params
|
||||||
|
|
||||||
|
logger.log user_id: user_id, content: content, "new message recived"
|
||||||
|
RoomManager.findOrCreateRoom project_id: project_id, (error, room) ->
|
||||||
|
return next(error) if error?
|
||||||
|
newMessageOpts =
|
||||||
|
content: content
|
||||||
|
room_id: room._id
|
||||||
|
user_id: user_id
|
||||||
|
timestamp: Date.now()
|
||||||
|
MessageManager.createMessage newMessageOpts, (error, message) ->
|
||||||
|
if err?
|
||||||
|
logger.err err:error, user_id:user_id, "something went wrong with create message"
|
||||||
|
return next(err)
|
||||||
|
MessageManager.populateMessagesWithUsers [message], (error, messages) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, user_id:user_id, "something went wrong populateMessagesWithUsers"
|
||||||
|
return next("something went wrong")
|
||||||
|
message = MessageFormatter.formatMessageForClientSide(messages[0])
|
||||||
|
message.room =
|
||||||
|
id: project_id
|
||||||
|
res.send(201, message)
|
||||||
|
|
||||||
|
getMessages: (req, res, next) ->
|
||||||
|
{project_id} = req.params
|
||||||
|
query = {}
|
||||||
|
if req.query?.before?
|
||||||
|
query.timestamp = $lt: parseInt(req.query.before, 10)
|
||||||
|
if req.query?.limit?
|
||||||
|
limit = parseInt(req.query.limit, 10)
|
||||||
|
else
|
||||||
|
limit = MessageHttpController.DEFAULT_MESSAGE_LIMIT
|
||||||
|
options =
|
||||||
|
order_by: "timestamp"
|
||||||
|
sort_order: -1
|
||||||
|
limit: limit
|
||||||
|
logger.log options:options, "get message request recived"
|
||||||
|
RoomManager.findOrCreateRoom project_id: project_id, (error, room) ->
|
||||||
|
return next(error) if error?
|
||||||
|
query.room_id = room._id
|
||||||
|
MessageManager.getMessages query, options, (error, messages) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, "something went getMessages"
|
||||||
|
return next("something went wrong")
|
||||||
|
MessageManager.populateMessagesWithUsers messages, (error, messages) ->
|
||||||
|
if error?
|
||||||
|
logger.err err:error, "something went populateMessagesWithUsers"
|
||||||
|
return next("something went wrong")
|
||||||
|
messages = MessageFormatter.formatMessagesForClientSide messages
|
||||||
|
logger.log project_id: project_id, "got messages"
|
||||||
|
res.send 200, messages
|
|
@ -0,0 +1,52 @@
|
||||||
|
mongojs = require "../../mongojs"
|
||||||
|
db = mongojs.db
|
||||||
|
ObjectId = mongojs.ObjectId
|
||||||
|
WebApiManager = require "../WebApi/WebApiManager"
|
||||||
|
async = require "async"
|
||||||
|
|
||||||
|
module.exports = MessageManager =
|
||||||
|
createMessage: (message, callback = (error, message) ->) ->
|
||||||
|
message = @_ensureIdsAreObjectIds(message)
|
||||||
|
db.messages.save message, callback
|
||||||
|
|
||||||
|
getMessages: (query, options, callback = (error, messages) ->) ->
|
||||||
|
query = @_ensureIdsAreObjectIds(query)
|
||||||
|
cursor = db.messages.find(query)
|
||||||
|
if options.order_by?
|
||||||
|
options.sort_order ||= 1
|
||||||
|
sortQuery = {}
|
||||||
|
sortQuery[options.order_by] = options.sort_order
|
||||||
|
cursor = cursor.sort(sortQuery)
|
||||||
|
if options.limit?
|
||||||
|
cursor = cursor.limit(options.limit)
|
||||||
|
cursor.toArray callback
|
||||||
|
|
||||||
|
populateMessagesWithUsers: (messages, callback = (error, messages) ->) ->
|
||||||
|
jobs = new Array()
|
||||||
|
|
||||||
|
userCache = {}
|
||||||
|
getUserDetails = (user_id, callback = (error, user) ->) ->
|
||||||
|
return callback(null, userCache[user_id]) if userCache[user_id]?
|
||||||
|
WebApiManager.getUserDetails user_id, (error, user) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
userCache[user_id] = user
|
||||||
|
callback null, user
|
||||||
|
|
||||||
|
for message in messages
|
||||||
|
do (message) ->
|
||||||
|
jobs.push (callback) ->
|
||||||
|
getUserDetails message.user_id.toString(), (error, user) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
delete message.user_id
|
||||||
|
message.user = user
|
||||||
|
callback(null, message)
|
||||||
|
|
||||||
|
async.series jobs, callback
|
||||||
|
|
||||||
|
_ensureIdsAreObjectIds: (query) ->
|
||||||
|
if query.user_id? and query.user_id not instanceof ObjectId
|
||||||
|
query.user_id = ObjectId(query.user_id)
|
||||||
|
if query.room_id? and query.room_id not instanceof ObjectId
|
||||||
|
query.room_id = ObjectId(query.room_id)
|
||||||
|
return query
|
||||||
|
|
101
services/chat/app/coffee/Features/Rooms/RoomController.coffee
Normal file
101
services/chat/app/coffee/Features/Rooms/RoomController.coffee
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
async = require "async"
|
||||||
|
logger = require "logger-sharelatex"
|
||||||
|
AuthorizationManager = require "../Authorization/AuthorizationManager"
|
||||||
|
RoomManager = require "../Rooms/RoomManager"
|
||||||
|
SocketManager = require "../Sockets/SocketManager"
|
||||||
|
|
||||||
|
module.exports = RoomController =
|
||||||
|
joinRoom: (client, data, callback = (error) ->) ->
|
||||||
|
if !data.room?.project_id?
|
||||||
|
return callback("unknown room")
|
||||||
|
project_id = data.room.project_id
|
||||||
|
|
||||||
|
client.get "id", (error, id) ->
|
||||||
|
logger.log user_id: id, project_id: project_id, "joining room"
|
||||||
|
AuthorizationManager.canClientJoinProjectRoom client, project_id, (error, authorized) ->
|
||||||
|
return callback("something went wrong") if error?
|
||||||
|
if authorized
|
||||||
|
RoomManager.findOrCreateRoom project_id: project_id, (error, room) ->
|
||||||
|
return callback("something went wrong") if error?
|
||||||
|
room_id = room._id.toString()
|
||||||
|
RoomController._addClientToRoom client, room_id, (error) ->
|
||||||
|
return callback("something went wrong") if error?
|
||||||
|
RoomController._getClientsInRoom room_id, (error, clients) ->
|
||||||
|
return callback("something went wrong") if error?
|
||||||
|
logger.log user_id: id, project_id: project_id, room_id: room_id, "joined room"
|
||||||
|
roomDetails =
|
||||||
|
room:
|
||||||
|
id: room_id
|
||||||
|
connectedUsers: clients
|
||||||
|
callback null, roomDetails
|
||||||
|
else
|
||||||
|
logger.log user_id: id, project_id: project_id, "unauthorized attempt to join room"
|
||||||
|
callback("unknown room")
|
||||||
|
|
||||||
|
leaveAllRooms: (client, callback = (error) ->) ->
|
||||||
|
client.get "id", (error, id) ->
|
||||||
|
logger.log user_id: id, "leaving all rooms"
|
||||||
|
SocketManager.getRoomIdsClientHasJoined client, (error, room_ids) ->
|
||||||
|
return callback("something went wrong") if error?
|
||||||
|
jobs = []
|
||||||
|
for room_id in room_ids
|
||||||
|
do (room_id) ->
|
||||||
|
jobs.push (callback) ->
|
||||||
|
RoomController.leaveRoom client, room_id, callback
|
||||||
|
async.series jobs, (error)-> callback(error)
|
||||||
|
|
||||||
|
leaveRoom: (client, room_id, callback = (error) ->) ->
|
||||||
|
client.get "id", (error, id) ->
|
||||||
|
logger.log user_id: id, room_id: room_id, "leaving room"
|
||||||
|
RoomController._getClientAttributes client, (error, attributes) ->
|
||||||
|
return callback("something went wrong") if error?
|
||||||
|
SocketManager.removeClientFromRoom client, room_id, (error) ->
|
||||||
|
return callback("something went wrong") if error?
|
||||||
|
leftRoomUpdate =
|
||||||
|
room:
|
||||||
|
id: room_id
|
||||||
|
user: attributes
|
||||||
|
SocketManager.emitToRoom room_id, "userLeft", leftRoomUpdate
|
||||||
|
logger.log user_id: id, room_id: room_id, "left room"
|
||||||
|
callback()
|
||||||
|
|
||||||
|
_addClientToRoom: (client, room_id, callback = (error) ->) ->
|
||||||
|
RoomController._getClientAttributes client, (error, attributes) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
update =
|
||||||
|
room:
|
||||||
|
id: room_id
|
||||||
|
user: attributes
|
||||||
|
SocketManager.emitToRoom room_id, "userJoined", update
|
||||||
|
SocketManager.addClientToRoom client, room_id, callback
|
||||||
|
|
||||||
|
_getClientsInRoom: (room_id, callback = (error, clients) ->) ->
|
||||||
|
SocketManager.getClientsInRoom room_id, (error, clients) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
formattedClients = []
|
||||||
|
jobs = []
|
||||||
|
|
||||||
|
for client in clients
|
||||||
|
do (client) ->
|
||||||
|
jobs.push (callback) ->
|
||||||
|
RoomController._getClientAttributes client, (error, attributes) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
formattedClients.push attributes
|
||||||
|
callback()
|
||||||
|
|
||||||
|
async.series jobs, (error) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
callback null, formattedClients
|
||||||
|
|
||||||
|
_getClientAttributes: (client, callback = (error, attributes) ->) ->
|
||||||
|
SocketManager.getClientAttributes client, ["id", "first_name", "last_name", "email", "gravatar_url"], (error, attributes) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
[id, first_name, last_name, email, gravatar_url] = attributes
|
||||||
|
clientAttributes =
|
||||||
|
id : id
|
||||||
|
first_name : first_name
|
||||||
|
last_name : last_name
|
||||||
|
email : email
|
||||||
|
gravatar_url : gravatar_url
|
||||||
|
callback null, clientAttributes
|
||||||
|
|
18
services/chat/app/coffee/Features/Rooms/RoomManager.coffee
Normal file
18
services/chat/app/coffee/Features/Rooms/RoomManager.coffee
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
mongojs = require("../../mongojs")
|
||||||
|
db = mongojs.db
|
||||||
|
ObjectId = mongojs.ObjectId
|
||||||
|
|
||||||
|
module.exports = RoomManager =
|
||||||
|
findOrCreateRoom: (query, callback = (error, room) ->) ->
|
||||||
|
if query.project_id? and query.project_id not instanceof ObjectId
|
||||||
|
query.project_id = ObjectId(query.project_id)
|
||||||
|
|
||||||
|
db.rooms.findOne query, (error, room) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
if room?
|
||||||
|
callback null, room
|
||||||
|
else
|
||||||
|
db.rooms.save query, (error, room) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
callback null, room
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
settings = require 'settings-sharelatex'
|
||||||
|
rclientPub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host)
|
||||||
|
rclientPub.auth(settings.redis.web.password)
|
||||||
|
rclientSub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host)
|
||||||
|
rclientSub.auth(settings.redis.web.password)
|
||||||
|
|
||||||
|
module.exports = RealTimeEventManager =
|
||||||
|
|
||||||
|
rclientPub:rclientPub
|
||||||
|
rclientSub:rclientSub
|
||||||
|
|
||||||
|
emitToRoom: (room_id, message, payload...) ->
|
||||||
|
RealTimeEventManager.rclientPub.publish "chat-events", JSON.stringify
|
||||||
|
room_id: room_id
|
||||||
|
message: message
|
||||||
|
payload: payload
|
||||||
|
|
||||||
|
listenForChatEvents: () ->
|
||||||
|
@rclientSub.subscribe "chat-events"
|
||||||
|
@rclientSub.on "message", @_processEditorEvent.bind(@)
|
||||||
|
|
||||||
|
_processEditorEvent: (channel, message) ->
|
||||||
|
io = require('../../server').io
|
||||||
|
message = JSON.parse(message)
|
||||||
|
io.sockets.in(message.room_id).emit(message.message, message.payload...)
|
|
@ -0,0 +1,40 @@
|
||||||
|
async = require "async"
|
||||||
|
RealTimeEventManager = require("./RealTimeEventManager")
|
||||||
|
|
||||||
|
module.exports = SocketManager =
|
||||||
|
addClientToRoom: (client, room_id, callback = (error) ->) ->
|
||||||
|
client.join(room_id)
|
||||||
|
callback()
|
||||||
|
|
||||||
|
removeClientFromRoom: (client, room_id, callback = (error) ->) ->
|
||||||
|
client.leave(room_id)
|
||||||
|
callback()
|
||||||
|
|
||||||
|
getClientAttributes: (client, attributes, callback = (error, values) ->) ->
|
||||||
|
jobs = []
|
||||||
|
for attribute in attributes
|
||||||
|
do (attribute) ->
|
||||||
|
jobs.push (cb) -> client.get attribute, cb
|
||||||
|
async.series jobs, callback
|
||||||
|
|
||||||
|
emitToRoom: RealTimeEventManager.emitToRoom
|
||||||
|
|
||||||
|
isClientInRoom: (targetClient, room_id, callback = (error, inRoom) ->) ->
|
||||||
|
io = require("../../server").io
|
||||||
|
for client in io.sockets.clients(room_id)
|
||||||
|
if client.id == targetClient.id
|
||||||
|
return callback null, true
|
||||||
|
callback null, false
|
||||||
|
|
||||||
|
getClientsInRoom: (room_id, callback = (error, clients) ->) ->
|
||||||
|
io = require("../../server").io
|
||||||
|
callback null, io.sockets.clients(room_id)
|
||||||
|
|
||||||
|
getRoomIdsClientHasJoined: (client, callback = (error, room_ids) ->) ->
|
||||||
|
io = require("../../server").io
|
||||||
|
room_ids = []
|
||||||
|
for room_id, value of io.sockets.manager.roomClients[client.id]
|
||||||
|
if room_id[0] == "/"
|
||||||
|
room_ids.push room_id.slice(1)
|
||||||
|
callback null, room_ids
|
||||||
|
|
18
services/chat/app/coffee/Features/Users/UserFormatter.coffee
Normal file
18
services/chat/app/coffee/Features/Users/UserFormatter.coffee
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
crypto = require "crypto"
|
||||||
|
|
||||||
|
module.exports = UserFormatter =
|
||||||
|
formatUserForClientSide: (user) ->
|
||||||
|
if user._id?
|
||||||
|
user.id = user._id.toString()
|
||||||
|
delete user._id
|
||||||
|
return {
|
||||||
|
id: user.id
|
||||||
|
first_name: user.first_name
|
||||||
|
last_name: user.last_name
|
||||||
|
email: user.email
|
||||||
|
gravatar_url: @_getGravatarUrlForEmail(user.email)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getGravatarUrlForEmail: (email) ->
|
||||||
|
hash = crypto.createHash("md5").update(email.toLowerCase()).digest("hex")
|
||||||
|
return "//www.gravatar.com/avatar/#{hash}"
|
|
@ -0,0 +1,32 @@
|
||||||
|
request = require('request').defaults(jar: false)
|
||||||
|
Settings = require("settings-sharelatex")
|
||||||
|
|
||||||
|
module.exports = WebApiManager =
|
||||||
|
apiRequest: (url, method, options = {}, callback = (error, result) ->) ->
|
||||||
|
if typeof options == "function"
|
||||||
|
callback = options
|
||||||
|
options = {}
|
||||||
|
url = "#{Settings.apis.web.url}#{url}"
|
||||||
|
options.url = url
|
||||||
|
options.method = method
|
||||||
|
request options, (error, response, body) ->
|
||||||
|
return callback(error) if error?
|
||||||
|
try
|
||||||
|
result = JSON.parse(body)
|
||||||
|
catch e
|
||||||
|
return callback(e)
|
||||||
|
return callback null, result
|
||||||
|
|
||||||
|
getUserDetailsFromAuthToken: (auth_token, callback = (error, details) ->) ->
|
||||||
|
@apiRequest "/user/personal_info?auth_token=#{auth_token}", "get", callback
|
||||||
|
|
||||||
|
getUserDetails: (user_id, callback = (error, details) ->) ->
|
||||||
|
@apiRequest "/user/#{user_id}/personal_info", "get", {
|
||||||
|
auth:
|
||||||
|
user: Settings.apis.web.user
|
||||||
|
pass: Settings.apis.web.pass
|
||||||
|
sendImmediately: true
|
||||||
|
}, callback
|
||||||
|
|
||||||
|
getProjectCollaborators: (project_id, auth_token, callback = (error, collaborators) ->) ->
|
||||||
|
@apiRequest "/project/#{project_id}/collaborators?auth_token=#{auth_token}", "get", callback
|
22
services/chat/app/coffee/metrics.coffee
Normal file
22
services/chat/app/coffee/metrics.coffee
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
StatsD = require('lynx')
|
||||||
|
statsd = new StatsD('localhost', 8125, {on_error:->})
|
||||||
|
|
||||||
|
buildKey = (key)-> "chat.#{process.env.NODE_ENV}.#{key}"
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
set : (key, value, sampleRate = 1)->
|
||||||
|
statsd.set buildKey(key), value, sampleRate
|
||||||
|
|
||||||
|
inc : (key, sampleRate = 1)->
|
||||||
|
statsd.increment buildKey(key), sampleRate
|
||||||
|
|
||||||
|
Timer : class
|
||||||
|
constructor :(key, sampleRate = 1)->
|
||||||
|
this.start = new Date()
|
||||||
|
this.key = buildKey(key)
|
||||||
|
done:->
|
||||||
|
timeSpan = new Date - this.start
|
||||||
|
statsd.timing(this.key, timeSpan, this.sampleRate)
|
||||||
|
|
||||||
|
gauge : (key, value, sampleRate = 1)->
|
||||||
|
statsd.gauge key, value, sampleRate
|
6
services/chat/app/coffee/mongojs.coffee
Normal file
6
services/chat/app/coffee/mongojs.coffee
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Settings = require("settings-sharelatex")
|
||||||
|
mongojs = require "mongojs"
|
||||||
|
db = mongojs.connect(Settings.mongo.url, ["rooms", "messages"])
|
||||||
|
module.exports =
|
||||||
|
db: db
|
||||||
|
ObjectId: mongojs.ObjectId
|
31
services/chat/app/coffee/router.coffee
Normal file
31
services/chat/app/coffee/router.coffee
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
AuthenticationController = require("./Features/Authentication/AuthenticationController")
|
||||||
|
MessageController = require("./Features/Messages/MessageController")
|
||||||
|
RoomController = require("./Features/Rooms/RoomController")
|
||||||
|
MessageHttpController = require('./Features/Messages/MessageHttpController')
|
||||||
|
|
||||||
|
module.exports = Router =
|
||||||
|
route: (app, io) ->
|
||||||
|
|
||||||
|
app.get "/room/:project_id/messages", MessageHttpController.getMessages
|
||||||
|
app.post "/room/:project_id/messages", MessageHttpController.sendMessage
|
||||||
|
|
||||||
|
app.get "/status", (req, res, next) ->
|
||||||
|
res.send("chat is alive")
|
||||||
|
|
||||||
|
io.sockets.on "connection", (client) ->
|
||||||
|
client.on "disconnect", () ->
|
||||||
|
RoomController.leaveAllRooms(client)
|
||||||
|
|
||||||
|
client.on "auth", (data, callback = (error) ->) ->
|
||||||
|
AuthenticationController.authClient(client, data, callback)
|
||||||
|
|
||||||
|
client.on "joinRoom", (data, callback = (error) ->) ->
|
||||||
|
RoomController.joinRoom(client, data, callback)
|
||||||
|
|
||||||
|
client.on "sendMessage", (data, callback = (error) ->) ->
|
||||||
|
MessageController.sendMessage(client, data, callback)
|
||||||
|
|
||||||
|
client.on "getMessages", (data, callback = (error) ->) ->
|
||||||
|
MessageController.getMessages(client, data, callback)
|
||||||
|
|
||||||
|
|
52
services/chat/app/coffee/server.coffee
Normal file
52
services/chat/app/coffee/server.coffee
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
logger = require 'logger-sharelatex'
|
||||||
|
logger.initialize("chat-sharelatex")
|
||||||
|
metrics = require("metrics-sharelatex")
|
||||||
|
metrics.initialize("chat")
|
||||||
|
Path = require("path")
|
||||||
|
express = require("express")
|
||||||
|
app = express()
|
||||||
|
server = require("http").createServer(app)
|
||||||
|
io = require("socket.io").listen(server)
|
||||||
|
io.set("resource", "/chat/socket.io")
|
||||||
|
io.set("log level", 1)
|
||||||
|
Router = require "./router"
|
||||||
|
|
||||||
|
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../node_modules/mongojs/node_modules/mongodb"), logger)
|
||||||
|
|
||||||
|
app.configure ()->
|
||||||
|
app.use express.bodyParser()
|
||||||
|
app.use metrics.http.monitor(logger)
|
||||||
|
Router.route(app, io)
|
||||||
|
|
||||||
|
app.configure 'development', ->
|
||||||
|
console.log "Development Enviroment"
|
||||||
|
app.use express.errorHandler({ dumpExceptions: true, showStack: true })
|
||||||
|
|
||||||
|
app.configure 'production', ->
|
||||||
|
console.log "Production Enviroment"
|
||||||
|
app.use express.logger()
|
||||||
|
app.use express.errorHandler()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
mountPoint = "/chat"
|
||||||
|
app.use (req, res, next) ->
|
||||||
|
|
||||||
|
if req.url.slice(0, mountPoint.length) == mountPoint
|
||||||
|
req.url = req.url.slice(mountPoint.length)
|
||||||
|
next()
|
||||||
|
else
|
||||||
|
res.send(404)
|
||||||
|
|
||||||
|
app.use(express.static(__dirname + "/../../public/build"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
server: server
|
||||||
|
io: io
|
||||||
|
app: app
|
||||||
|
}
|
||||||
|
|
||||||
|
require("./Features/Sockets/RealTimeEventManager").listenForChatEvents()
|
||||||
|
|
14
services/chat/config/settings.defaults.coffee
Normal file
14
services/chat/config/settings.defaults.coffee
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module.exports =
|
||||||
|
apis:
|
||||||
|
web:
|
||||||
|
url: "http://localhost:3000"
|
||||||
|
user: "sharelatex"
|
||||||
|
pass: "password"
|
||||||
|
mongo:
|
||||||
|
url : 'mongodb://127.0.0.1/sharelatex'
|
||||||
|
|
||||||
|
redis:
|
||||||
|
web:
|
||||||
|
host: "localhost"
|
||||||
|
port: "6379"
|
||||||
|
password: ""
|
39
services/chat/package.json
Normal file
39
services/chat/package.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "chat-sharelatex",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "0.2.9",
|
||||||
|
"express": "3.3.1",
|
||||||
|
"lynx": "0.0.11",
|
||||||
|
"request": "2.21.0",
|
||||||
|
"socket.io": "0.9.14",
|
||||||
|
"settings": "git+ssh://git@bitbucket.org:sharelatex/settings-sharelatex.git#master",
|
||||||
|
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#master",
|
||||||
|
"logger": "git+ssh://git@bitbucket.org:sharelatex/logger-sharelatex.git#bunyan",
|
||||||
|
"grunt-requirejs": "~0.4.0",
|
||||||
|
"grunt-mocha-test": "~0.8.0",
|
||||||
|
"mongojs": "0.9.11",
|
||||||
|
"redis": "~0.10.1",
|
||||||
|
"coffee-script": "~1.7.1",
|
||||||
|
"timekeeper": "0.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "",
|
||||||
|
"sandboxed-module": "",
|
||||||
|
"sinon": "",
|
||||||
|
"timekeeper": "",
|
||||||
|
"grunt": "~0.4.1",
|
||||||
|
"grunt-contrib-requirejs": "~0.4.1",
|
||||||
|
"grunt-contrib-coffee": "~0.7.0",
|
||||||
|
"grunt-contrib-watch": "~0.5.3",
|
||||||
|
"grunt-contrib-copy": "~0.4.1",
|
||||||
|
"grunt-contrib-less": "~0.8.2",
|
||||||
|
"grunt-contrib-jade": "~0.8.0",
|
||||||
|
"grunt-contrib-uglify": "~0.2.7",
|
||||||
|
"grunt-nodemon": "~0.1.2",
|
||||||
|
"grunt-contrib-clean": "~0.5.0",
|
||||||
|
"grunt-concurrent": "~0.4.2",
|
||||||
|
"grunt-plato": "~0.2.1",
|
||||||
|
"grunt-notify": "~0.2.16"
|
||||||
|
}
|
||||||
|
}
|
25
services/chat/public/app.build.js
Normal file
25
services/chat/public/app.build.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
requirejs.config({
|
||||||
|
baseUrl: "./build",
|
||||||
|
out: "./build/chat.js",
|
||||||
|
inlineText:true,
|
||||||
|
preserveLicenseComments:false,
|
||||||
|
shim: {
|
||||||
|
"libs/underscore": {
|
||||||
|
init: function() {
|
||||||
|
return _.noConflict();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libs/backbone": {
|
||||||
|
deps: ["libs/underscore"],
|
||||||
|
init: function() {
|
||||||
|
return Backbone.noConflict();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
"moment": "libs/moment",
|
||||||
|
},
|
||||||
|
name:"chat",
|
||||||
|
optimize: 'none',
|
||||||
|
skipDirOptimize: true
|
||||||
|
})
|
109
services/chat/public/coffee/chat.coffee
Normal file
109
services/chat/public/coffee/chat.coffee
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
define [
|
||||||
|
"utils/staticLoader"
|
||||||
|
"libs/underscore"
|
||||||
|
"libs/backbone"
|
||||||
|
"libs/jquery.storage"
|
||||||
|
"models/room"
|
||||||
|
"models/user"
|
||||||
|
"views/chatWindowView"
|
||||||
|
|
||||||
|
], (staticLoader, _, Backbone, jqueryStorage, Room, User, ChatWindowView) ->
|
||||||
|
|
||||||
|
staticLoader.appendAssets()
|
||||||
|
_.templateSettings = escape : /\{\{(.+?)\}\}/g
|
||||||
|
|
||||||
|
class GlobalNotificationManager
|
||||||
|
constructor: (@chat) ->
|
||||||
|
@focussed = true
|
||||||
|
$(window).on "focus", () =>
|
||||||
|
@clearNewMessageNotification()
|
||||||
|
@focussed = true
|
||||||
|
$(window).on "blur", () => @focussed = false
|
||||||
|
|
||||||
|
@chat.on "joinedRoom", (room) =>
|
||||||
|
notifyIfAppropriate = (message) =>
|
||||||
|
if message.get("user") != @chat.user and !message.get("preloaded")
|
||||||
|
@notifyAboutNewMessage()
|
||||||
|
|
||||||
|
room.get("messages").on "add", notifyIfAppropriate
|
||||||
|
room.on "disconnect", () ->
|
||||||
|
room.get("messages").off "add", notifyIfAppropriate
|
||||||
|
|
||||||
|
notifyAboutNewMessage: () ->
|
||||||
|
if !@focussed and !@newMessageNotificationTimeout?
|
||||||
|
@originalTitle ||= window.document.title
|
||||||
|
do changeTitle = () =>
|
||||||
|
if window.document.title == @originalTitle
|
||||||
|
window.document.title = "New Message"
|
||||||
|
else
|
||||||
|
window.document.title = @originalTitle
|
||||||
|
@newMessageNotificationTimeout = setTimeout changeTitle, 800
|
||||||
|
|
||||||
|
clearNewMessageNotification: () ->
|
||||||
|
clearTimeout @newMessageNotificationTimeout
|
||||||
|
delete @newMessageNotificationTimeout
|
||||||
|
if @originalTitle?
|
||||||
|
window.document.title = @originalTitle
|
||||||
|
|
||||||
|
class Chat
|
||||||
|
constructor: (options) ->
|
||||||
|
_.extend(@, Backbone.Events)
|
||||||
|
window.chat = @
|
||||||
|
@rooms = {}
|
||||||
|
project_id = window.location.pathname.split( '/' )[2]
|
||||||
|
@socket = socket = io.connect options.url, {
|
||||||
|
resource: "chat/socket.io",
|
||||||
|
"force new connection": true
|
||||||
|
query:"project_id=#{project_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@socket.on "connect", () =>
|
||||||
|
@connected = true
|
||||||
|
@getAuthToken (error, auth_token) =>
|
||||||
|
return @handleError(error) if error?
|
||||||
|
@socket.emit "auth", {auth_token: auth_token}, (error, user_info) =>
|
||||||
|
return @handleError(error) if error?
|
||||||
|
@user = User.findOrCreate(user_info)
|
||||||
|
@joinProjectRoom(options.room.project_id)
|
||||||
|
@trigger "authed"
|
||||||
|
|
||||||
|
@socket.on "disconnect", () =>
|
||||||
|
@connected = false
|
||||||
|
@trigger "disconnected"
|
||||||
|
|
||||||
|
@socket.on "messageReceived", (data) =>
|
||||||
|
@getRoom(data.message.room.id)?.onMessageReceived(data)
|
||||||
|
|
||||||
|
@socket.on "userJoined", (data) =>
|
||||||
|
@getRoom(data.room.id).addConnectedUser(data.user)
|
||||||
|
|
||||||
|
@socket.on "userLeft", (data) =>
|
||||||
|
@getRoom(data.room.id)?.removeConnectedUser(data.user)
|
||||||
|
|
||||||
|
@globalNotificationManager = new GlobalNotificationManager(@)
|
||||||
|
|
||||||
|
getRoom: (room_id) ->
|
||||||
|
@rooms[room_id]
|
||||||
|
|
||||||
|
joinProjectRoom: (project_id) ->
|
||||||
|
if !@room?
|
||||||
|
@room = new Room(
|
||||||
|
project_id: project_id
|
||||||
|
chat: @
|
||||||
|
)
|
||||||
|
@window = new ChatWindowView({
|
||||||
|
room: @room
|
||||||
|
chat: @
|
||||||
|
})
|
||||||
|
@room.on "joined", => @trigger("joinedRoom", @room)
|
||||||
|
|
||||||
|
getAuthToken: (callback = (error, auth_token) ->) ->
|
||||||
|
$.ajax "/user/auth_token", {
|
||||||
|
success: (data, status, xhr) ->
|
||||||
|
callback null, data
|
||||||
|
error: (xhr, status, error) ->
|
||||||
|
callback error
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError: (error) ->
|
||||||
|
console.error error
|
|
@ -0,0 +1,9 @@
|
||||||
|
define [
|
||||||
|
"libs/backbone"
|
||||||
|
"models/user"
|
||||||
|
], (Backbone, User) ->
|
||||||
|
ConnectedUsers = Backbone.Collection.extend
|
||||||
|
model: User
|
||||||
|
|
||||||
|
initialize: (models, options) ->
|
||||||
|
{@chat, @room} = options
|
45
services/chat/public/coffee/collections/messages.coffee
Normal file
45
services/chat/public/coffee/collections/messages.coffee
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
define [
|
||||||
|
"libs/backbone"
|
||||||
|
"models/message"
|
||||||
|
"models/user"
|
||||||
|
|
||||||
|
], (Backbone, Message, User) ->
|
||||||
|
|
||||||
|
Messages = Backbone.Collection.extend
|
||||||
|
model: Message
|
||||||
|
|
||||||
|
initialize: (models, options) ->
|
||||||
|
{@chat, @room} = options
|
||||||
|
|
||||||
|
fetchMoreMessages: (options = { preloading: false }, callback = (error) ->) ->
|
||||||
|
limit = Messages.DEFAULT_MESSAGE_LIMIT
|
||||||
|
|
||||||
|
@room.fetchMessages @_buildMessagesQuery(limit), (error, messages) =>
|
||||||
|
if error?
|
||||||
|
callback(error)
|
||||||
|
return @chat.handleError(error)
|
||||||
|
if messages.length < limit
|
||||||
|
@trigger "noMoreMessages"
|
||||||
|
@_parseAndAddMessages(messages, options)
|
||||||
|
callback()
|
||||||
|
|
||||||
|
_parseAndAddMessages: (messages, options) ->
|
||||||
|
for message in messages
|
||||||
|
user = User.findOrCreate message.user
|
||||||
|
@add new Message(
|
||||||
|
content : message.content
|
||||||
|
timestamp : message.timestamp
|
||||||
|
user : user
|
||||||
|
preloaded : !!options.preloading
|
||||||
|
), at: 0
|
||||||
|
|
||||||
|
_buildMessagesQuery: (limit) ->
|
||||||
|
query =
|
||||||
|
limit: limit
|
||||||
|
firstMessage = @at(0)
|
||||||
|
if firstMessage?
|
||||||
|
query.before = firstMessage.get("timestamp")
|
||||||
|
return query
|
||||||
|
Messages.DEFAULT_MESSAGE_LIMIT = 50
|
||||||
|
|
||||||
|
return Messages
|
5
services/chat/public/coffee/models/message.coffee
Normal file
5
services/chat/public/coffee/models/message.coffee
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
define [
|
||||||
|
"libs/backbone"
|
||||||
|
], (Backbone) ->
|
||||||
|
|
||||||
|
Message = Backbone.Model.extend {}
|
85
services/chat/public/coffee/models/room.coffee
Normal file
85
services/chat/public/coffee/models/room.coffee
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
define [
|
||||||
|
"libs/underscore"
|
||||||
|
"libs/backbone"
|
||||||
|
"collections/messages"
|
||||||
|
"collections/connectedUsers"
|
||||||
|
"models/user"
|
||||||
|
"models/message"
|
||||||
|
], (_, Backbone, Messages, ConnectedUsers, User, Message) ->
|
||||||
|
|
||||||
|
|
||||||
|
Room = Backbone.Model.extend
|
||||||
|
initialize: () ->
|
||||||
|
@chat = @get("chat")
|
||||||
|
@set "messages", new Messages([], chat: @chat, room: @)
|
||||||
|
@set "connectedUsers", new ConnectedUsers([], chat: @chat, room: @)
|
||||||
|
|
||||||
|
@get("connectedUsers").on "change", () ->
|
||||||
|
@get("connectedUsers").on "add", () ->
|
||||||
|
@get("connectedUsers").on "remove", () ->
|
||||||
|
|
||||||
|
@connected = false
|
||||||
|
|
||||||
|
@chat.on "authed", () => @join()
|
||||||
|
@chat.on "disconnected", () => @_onDisconnect()
|
||||||
|
|
||||||
|
join: () ->
|
||||||
|
@chat.socket.emit "joinRoom", room: project_id: @get("project_id"), (error, data) =>
|
||||||
|
return @chat.handleError(error) if error?
|
||||||
|
room = data.room
|
||||||
|
@set("id", room.id)
|
||||||
|
@chat.rooms[room.id] = @
|
||||||
|
@addConnectedUsers(room.connectedUsers)
|
||||||
|
@_onJoin()
|
||||||
|
|
||||||
|
_onJoin: () ->
|
||||||
|
@trigger "joined"
|
||||||
|
@connected = true
|
||||||
|
|
||||||
|
if @get("messages").models.length == 0
|
||||||
|
@get("messages").fetchMoreMessages preloading: true, () =>
|
||||||
|
@trigger("afterMessagesPreloaded")
|
||||||
|
|
||||||
|
_onDisconnect: () ->
|
||||||
|
@trigger "disconnected"
|
||||||
|
@connected = false
|
||||||
|
|
||||||
|
addConnectedUsers: (users) ->
|
||||||
|
for user in users
|
||||||
|
@addConnectedUser(user)
|
||||||
|
|
||||||
|
addConnectedUser: (user) ->
|
||||||
|
if user not instanceof User
|
||||||
|
user = User.findOrCreate(user)
|
||||||
|
@get("connectedUsers").add user
|
||||||
|
|
||||||
|
removeConnectedUser: (user) ->
|
||||||
|
if user not instanceof User
|
||||||
|
user = User.findOrCreate(user)
|
||||||
|
@get("connectedUsers").remove user
|
||||||
|
|
||||||
|
sendMessage: (content, callback = (error) ->) ->
|
||||||
|
if !@connected
|
||||||
|
return callback(new Error("Not connected"))
|
||||||
|
@chat.socket.emit "sendMessage", {
|
||||||
|
message:
|
||||||
|
content: content
|
||||||
|
room:
|
||||||
|
id: @get("id")
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMessages: (query, callback = (error, messages) ->) ->
|
||||||
|
if !@connected
|
||||||
|
return callback(new Error("Not connected"))
|
||||||
|
query.room = id: @get("id")
|
||||||
|
@chat.socket.emit "getMessages", query, callback
|
||||||
|
|
||||||
|
onMessageReceived: (data) ->
|
||||||
|
message = data.message
|
||||||
|
user = User.findOrCreate message.user
|
||||||
|
message = new Message(
|
||||||
|
content : data.message.content
|
||||||
|
timestamp : data.message.timestamp
|
||||||
|
user : user
|
||||||
|
)
|
||||||
|
@get("messages").add message
|
13
services/chat/public/coffee/models/user.coffee
Normal file
13
services/chat/public/coffee/models/user.coffee
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
define [
|
||||||
|
"libs/backbone"
|
||||||
|
], (Backbone, room) ->
|
||||||
|
|
||||||
|
User = Backbone.Model.extend {},
|
||||||
|
findOrCreate: (attributes) ->
|
||||||
|
User.cache ||= {}
|
||||||
|
if User.cache[attributes.id]?
|
||||||
|
return User.cache[attributes.id]
|
||||||
|
else
|
||||||
|
user = new User(attributes)
|
||||||
|
User.cache[attributes.id] = user
|
||||||
|
return user
|
10
services/chat/public/coffee/utils/staticLoader.coffee
Normal file
10
services/chat/public/coffee/utils/staticLoader.coffee
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
define [
|
||||||
|
"text!html/templates.html"
|
||||||
|
"text!css/chat.css"
|
||||||
|
], (templates, css)->
|
||||||
|
|
||||||
|
appendAssets : ->
|
||||||
|
$(document.body).append($(templates))
|
||||||
|
style = $("<style/>")
|
||||||
|
style.html(css)
|
||||||
|
$(document.body).append(style)
|
219
services/chat/public/coffee/views/chatWindowView.coffee
Normal file
219
services/chat/public/coffee/views/chatWindowView.coffee
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
define [
|
||||||
|
"libs/underscore"
|
||||||
|
"libs/backbone"
|
||||||
|
"views/userMessageBlockView"
|
||||||
|
"views/timeMessageBlockView"
|
||||||
|
], (_, Backbone, UserMessageBlockView, TimeMessageBlockView) ->
|
||||||
|
FIVE_MINS = 5 * 60 * 1000
|
||||||
|
ONE_HOUR = 60 * 60 * 1000
|
||||||
|
TWELVE_HOURS = ONE_HOUR * 12
|
||||||
|
ONE_DAY = ONE_HOUR * 24
|
||||||
|
|
||||||
|
ChatWindowView = Backbone.View.extend
|
||||||
|
events:
|
||||||
|
"keydown textarea" : "_onTextAreaKeyDown"
|
||||||
|
"click .js-load-older-messages" : "_loadOlderMessages"
|
||||||
|
"click .js-minimize-toggle" : "_toggleMinimizeState"
|
||||||
|
"click h3" : "_toggleMinimizeState"
|
||||||
|
"click .js-new-message-alert" : "_toggleMinimizeState"
|
||||||
|
"click" : "_removeNotification"
|
||||||
|
|
||||||
|
initialize: () ->
|
||||||
|
@template = $("#chatWindowTemplate").html()
|
||||||
|
@chat = @options.chat
|
||||||
|
@room = @options.room
|
||||||
|
@listenTo @room.get("messages"), "add", (model, collection) -> @_onNewMessage(model, collection)
|
||||||
|
@listenTo @room.get("messages"), "noMoreMessages", () -> @$(".js-loading").hide()
|
||||||
|
@listenTo @room.get("connectedUsers"), "add", (user, collection) -> @_renderConnectedUsers()
|
||||||
|
@listenTo @room.get("connectedUsers"), "remove", (user, collection) -> @_renderConnectedUsers()
|
||||||
|
@listenTo @room.get("connectedUsers"), "change", (user, collection) -> @_renderConnectedUsers()
|
||||||
|
@listenTo @room, "joined", -> @_onJoin()
|
||||||
|
@listenTo @room, "disconnected", -> @_onDisconnect()
|
||||||
|
@listenTo @room, "afterMessagesPreloaded", -> @_scrollToBottomOfMessages()
|
||||||
|
|
||||||
|
render: () ->
|
||||||
|
@setElement($(@template))
|
||||||
|
$(document.body).append(@$el)
|
||||||
|
@_renderConnectedUsers()
|
||||||
|
@_initializeMinimizedState()
|
||||||
|
|
||||||
|
_onJoin: () ->
|
||||||
|
if !@rendered?
|
||||||
|
@render()
|
||||||
|
@rendered = true
|
||||||
|
@$el.removeClass("disconnected")
|
||||||
|
@$("textarea").removeAttr("disabled")
|
||||||
|
|
||||||
|
_onDisconnect: () ->
|
||||||
|
@$el.addClass("disconnected")
|
||||||
|
@$("textarea").attr("disabled", "disabled")
|
||||||
|
|
||||||
|
_onNewMessage: (message, collection) ->
|
||||||
|
@_renderMessage(message, collection)
|
||||||
|
@_notifyAboutNewMessage(message)
|
||||||
|
|
||||||
|
_renderMessage: (message, collection) ->
|
||||||
|
|
||||||
|
@messageBlocks ||= []
|
||||||
|
scrollEl = @$(".sent-message-area")
|
||||||
|
|
||||||
|
isOldestMessage = (message, collection)->
|
||||||
|
collection.indexOf(message) == 0
|
||||||
|
|
||||||
|
ismessageFromNewUser = (messageView, message)->
|
||||||
|
!messageView? or messageView.user != message.get("user")
|
||||||
|
|
||||||
|
isTimeForNewBlockBackwards = (message, previousUserMessageBlockView)->
|
||||||
|
if !message? or !previousUserMessageBlockView?
|
||||||
|
return true
|
||||||
|
|
||||||
|
timeSinceMessageWasSent = new Date().getTime() - message.get("timestamp")
|
||||||
|
|
||||||
|
if timeSinceMessageWasSent < ONE_HOUR
|
||||||
|
timeBlockSize = FIVE_MINS
|
||||||
|
else if timeSinceMessageWasSent > ONE_HOUR and timeSinceMessageWasSent < (ONE_DAY + TWELVE_HOURS)
|
||||||
|
timeBlockSize = ONE_HOUR
|
||||||
|
else
|
||||||
|
timeBlockSize = ONE_DAY
|
||||||
|
|
||||||
|
timeSinceLastPrinted = previousUserMessageBlockView.getTime() - message.get("timestamp")
|
||||||
|
|
||||||
|
if timeSinceLastPrinted > timeBlockSize
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
isTimeForNewBlock = (message, previousUserMessageBlockView)->
|
||||||
|
(message.get("timestamp") - previousUserMessageBlockView.getTime()) > FIVE_MINS
|
||||||
|
|
||||||
|
|
||||||
|
if isOldestMessage(message, collection)
|
||||||
|
oldScrollTopFromBottom = scrollEl[0].scrollHeight - scrollEl.scrollTop()
|
||||||
|
|
||||||
|
userMessageBlockView = @messageBlocks[0]
|
||||||
|
if ismessageFromNewUser(userMessageBlockView, message) or isTimeForNewBlockBackwards(message, userMessageBlockView)
|
||||||
|
userMessageBlockView = new UserMessageBlockView(user: message.get("user"))
|
||||||
|
@$(".sent-messages").prepend(userMessageBlockView.$el)
|
||||||
|
@messageBlocks.unshift userMessageBlockView
|
||||||
|
|
||||||
|
userMessageBlockView.prependMessage(message)
|
||||||
|
|
||||||
|
scrollEl.scrollTop(scrollEl[0].scrollHeight - oldScrollTopFromBottom)
|
||||||
|
else
|
||||||
|
oldScrollBottom = @_getScrollBottom()
|
||||||
|
userMessageBlockView = @messageBlocks[@messageBlocks.length - 1]
|
||||||
|
|
||||||
|
if ismessageFromNewUser(userMessageBlockView, message) or isTimeForNewBlock(message, userMessageBlockView)
|
||||||
|
userMessageBlockView = new UserMessageBlockView(user: message.get("user"))
|
||||||
|
@$(".sent-messages").append(userMessageBlockView.$el)
|
||||||
|
@messageBlocks.push userMessageBlockView
|
||||||
|
|
||||||
|
userMessageBlockView.appendMessage(message)
|
||||||
|
|
||||||
|
if oldScrollBottom <= 0
|
||||||
|
@_scrollToBottomOfMessages()
|
||||||
|
|
||||||
|
|
||||||
|
_renderConnectedUsers: () ->
|
||||||
|
users = @room.get("connectedUsers")
|
||||||
|
names = users
|
||||||
|
.reject((user) => user == @chat.user)
|
||||||
|
.map((user) -> "#{user.get("first_name")} #{user.get("last_name")}")
|
||||||
|
if names.length == 0
|
||||||
|
text = "No one else is online :("
|
||||||
|
else if names.length == 1
|
||||||
|
text = "#{names[0]} is also online"
|
||||||
|
else
|
||||||
|
text = "#{names.slice(0, -1).join(", ")} and #{names[names.length - 1]} are also online"
|
||||||
|
@$(".js-connected-users").text(text)
|
||||||
|
@_resizeSentMessageArea()
|
||||||
|
|
||||||
|
_resizeSentMessageArea: () ->
|
||||||
|
marginTop = @$(".js-header").outerHeight() + @$(".js-connected-users").outerHeight()
|
||||||
|
@$(".js-sent-message-area").css({
|
||||||
|
top: marginTop + "px"
|
||||||
|
})
|
||||||
|
|
||||||
|
_getScrollBottom: () ->
|
||||||
|
scrollEl = @$(".sent-message-area")
|
||||||
|
return scrollEl[0].scrollHeight - scrollEl.scrollTop() - scrollEl.innerHeight()
|
||||||
|
|
||||||
|
_scrollToBottomOfMessages: () ->
|
||||||
|
scrollEl = @$(".sent-message-area")
|
||||||
|
doScroll = ->
|
||||||
|
return scrollEl.scrollTop(scrollEl[0].scrollHeight - scrollEl.innerHeight())
|
||||||
|
MathJax.Hub.Queue(["Typeset", doScroll])
|
||||||
|
|
||||||
|
_notifyAboutNewMessage: (message) ->
|
||||||
|
isMessageNewToUser = message.get("user") != @chat.user and !message.get("preloaded")
|
||||||
|
isTextAreaFocused = @$("textarea").is(":focus")
|
||||||
|
if !isTextAreaFocused and isMessageNewToUser
|
||||||
|
@unseenMessages ||= 0
|
||||||
|
@unseenMessages += 1
|
||||||
|
@$el.addClass("new-messages")
|
||||||
|
@$(".new-message-alert").text(@unseenMessages)
|
||||||
|
|
||||||
|
_removeNotification: () ->
|
||||||
|
@unseenMessages = 0
|
||||||
|
@$el.removeClass("new-messages")
|
||||||
|
@$(".new-message-alert").text("")
|
||||||
|
|
||||||
|
_onTextAreaKeyDown: (e) ->
|
||||||
|
if e.keyCode == 13 # Enter
|
||||||
|
e.preventDefault()
|
||||||
|
message = @$("textarea").val()
|
||||||
|
@$("textarea").val("")
|
||||||
|
@_sendMessage(message)
|
||||||
|
|
||||||
|
_loadOlderMessages: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@room.get("messages").fetchMoreMessages()
|
||||||
|
|
||||||
|
_sendMessage: (content) ->
|
||||||
|
@room.sendMessage(content)
|
||||||
|
|
||||||
|
isMinimized: () ->
|
||||||
|
minimized = $.localStorage "chat.rooms.project-chat.minimized"
|
||||||
|
if !minimized?
|
||||||
|
minimized = false
|
||||||
|
return minimized
|
||||||
|
|
||||||
|
_setMinimizedState: (state) ->
|
||||||
|
$.localStorage "chat.rooms.project-chat.minimized", state
|
||||||
|
|
||||||
|
_initializeMinimizedState: () ->
|
||||||
|
minimized = @isMinimized()
|
||||||
|
if minimized
|
||||||
|
@_minimize(false)
|
||||||
|
|
||||||
|
_toggleMinimizeState: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
minimized = @isMinimized()
|
||||||
|
if !minimized
|
||||||
|
@_setMinimizedState(true)
|
||||||
|
@_minimize()
|
||||||
|
else
|
||||||
|
@_setMinimizedState(false)
|
||||||
|
@_unminimize()
|
||||||
|
|
||||||
|
_minimize: (animate = true) ->
|
||||||
|
@$(".new-message-area").hide()
|
||||||
|
@$(".js-connected-users").hide()
|
||||||
|
@$el.addClass("minimized")
|
||||||
|
if animate
|
||||||
|
@$el.animate height: 20, width: 80
|
||||||
|
else
|
||||||
|
@$el.css height: 20, width: 80
|
||||||
|
|
||||||
|
_unminimize: () ->
|
||||||
|
@$(".new-message-area").show()
|
||||||
|
@$(".js-connected-users").show()
|
||||||
|
@$el.removeClass("minimized")
|
||||||
|
@$el.animate height: 260, width: 220, () =>
|
||||||
|
@_resizeSentMessageArea()
|
||||||
|
@_scrollToBottomOfMessages()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
define [
|
||||||
|
"libs/underscore"
|
||||||
|
"libs/backbone"
|
||||||
|
"moment"
|
||||||
|
], (_, Backbone, moment) ->
|
||||||
|
ONE_WEEK = 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
TimeMessageBlockView = Backbone.View.extend
|
||||||
|
|
||||||
|
className : "timeSinceMessage"
|
||||||
|
|
||||||
|
initialize: () ->
|
||||||
|
@autoRefresh()
|
||||||
|
|
||||||
|
setTimeOnce: (timestamp)->
|
||||||
|
if !@timestamp?
|
||||||
|
@timestamp = timestamp
|
||||||
|
@render()
|
||||||
|
return @
|
||||||
|
|
||||||
|
setTime: (@timestamp)->
|
||||||
|
@render()
|
||||||
|
return @
|
||||||
|
|
||||||
|
autoRefresh: ->
|
||||||
|
if @timestamp?
|
||||||
|
@render()
|
||||||
|
self = @
|
||||||
|
doIt = =>
|
||||||
|
self.autoRefresh()
|
||||||
|
setTimeout doIt, 60 * 1000
|
||||||
|
|
||||||
|
render: () ->
|
||||||
|
milisecondsSince = new Date().getTime() - @timestamp
|
||||||
|
if milisecondsSince > ONE_WEEK
|
||||||
|
time = moment(@timestamp).format("D/MMM/YY, h:mm:ss a")
|
||||||
|
else
|
||||||
|
time = moment(@timestamp).fromNow()
|
||||||
|
this.$el.html(time)
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
define [
|
||||||
|
"libs/underscore"
|
||||||
|
"libs/backbone"
|
||||||
|
"views/timeMessageBlockView"
|
||||||
|
"moment"
|
||||||
|
"https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"
|
||||||
|
], (_, Backbone, TimeMessageBlockView, moment) ->
|
||||||
|
|
||||||
|
mathjaxConfig =
|
||||||
|
"HTML-CSS": { availableFonts: ["TeX"] },
|
||||||
|
TeX:
|
||||||
|
equationNumbers: { autoNumber: "AMS" },
|
||||||
|
useLabelIDs: false
|
||||||
|
tex2jax:
|
||||||
|
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
|
||||||
|
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
|
||||||
|
processEscapes: true
|
||||||
|
|
||||||
|
|
||||||
|
MathJax.Hub.Config(mathjaxConfig);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
UserMessageBlockView = Backbone.View.extend
|
||||||
|
|
||||||
|
initialize: () ->
|
||||||
|
@template = _.template($("#messageBlockTemplate").html())
|
||||||
|
@user = @options.user
|
||||||
|
@timeMessageBlock = new TimeMessageBlockView()
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: () ->
|
||||||
|
@setElement $(@template(
|
||||||
|
first_name: @user.get("first_name")
|
||||||
|
last_name: @user.get("last_name")
|
||||||
|
gravatar_url: @user.get("gravatar_url")
|
||||||
|
))
|
||||||
|
@$(".timeArea").html(@timeMessageBlock.$el)
|
||||||
|
|
||||||
|
getTime: ->
|
||||||
|
return @timeMessageBlock.timestamp
|
||||||
|
|
||||||
|
appendMessage: (message) ->
|
||||||
|
el = @buildHtml(message)
|
||||||
|
@$(".messages").append(el)
|
||||||
|
@_renderMathJax(el)
|
||||||
|
@timeMessageBlock.setTimeOnce message.get("timestamp")
|
||||||
|
|
||||||
|
prependMessage: (message) ->
|
||||||
|
el = @buildHtml(message)
|
||||||
|
@$(".messages").prepend(el)
|
||||||
|
@_renderMathJax(el)
|
||||||
|
@timeMessageBlock.setTime message.get("timestamp")
|
||||||
|
|
||||||
|
buildHtml : (message)->
|
||||||
|
time = moment(message.get("timestamp")).format("dddd, MMMM Do YYYY, h:mm:ss a")
|
||||||
|
el = $("<div class='message' title='#{time}'>")
|
||||||
|
el.text(message.get("content"))
|
||||||
|
return el
|
||||||
|
|
||||||
|
|
||||||
|
_renderMathJax: (element)->
|
||||||
|
if element?
|
||||||
|
MathJax.Hub.Queue(["Typeset", MathJax.Hub, element.get(0)])
|
24
services/chat/public/jade/templates.jade
Normal file
24
services/chat/public/jade/templates.jade
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
script(type="text/templates")#chatWindowTemplate
|
||||||
|
.chat-window
|
||||||
|
.header.js-header
|
||||||
|
h3 Chat
|
||||||
|
.new-message-alert.js-new-message-alert
|
||||||
|
.window-controls
|
||||||
|
.js-minimize-toggle.minimize-toggle
|
||||||
|
.connected-users.js-connected-users
|
||||||
|
.sent-message-area.js-sent-message-area
|
||||||
|
.loading.js-loading
|
||||||
|
a(href="#").load-older-messages.js-load-older-messages Load older messages
|
||||||
|
.sent-messages
|
||||||
|
.new-message-area
|
||||||
|
textarea(placeholder="Your message")
|
||||||
|
|
||||||
|
script(type="text/templates")#messageBlockTemplate
|
||||||
|
.chat-block
|
||||||
|
.message-block
|
||||||
|
.timeArea
|
||||||
|
.avatar
|
||||||
|
img(src="{{ gravatar_url }}?d=mm&s=40", alt="{{first_name}} {{last_name}}")
|
||||||
|
span.name {{first_name}} {{last_name}}
|
||||||
|
.messages
|
||||||
|
|
1571
services/chat/public/js/libs/backbone.js
Normal file
1571
services/chat/public/js/libs/backbone.js
Normal file
File diff suppressed because it is too large
Load diff
9478
services/chat/public/js/libs/jquery.js
vendored
Normal file
9478
services/chat/public/js/libs/jquery.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
85
services/chat/public/js/libs/jquery.storage.js
Normal file
85
services/chat/public/js/libs/jquery.storage.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*!
|
||||||
|
* jquery.storage.js 0.0.3 - https://github.com/yckart/jquery.storage.js
|
||||||
|
* The client-side storage for every browser, on any device.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2012 Yannick Albert (http://yckart.com)
|
||||||
|
* Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php).
|
||||||
|
* 2013/02/10
|
||||||
|
**/
|
||||||
|
define([], function() {
|
||||||
|
;(function($, window, document) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
$.map(['localStorage', 'sessionStorage'], function( method ) {
|
||||||
|
var defaults = {
|
||||||
|
cookiePrefix : 'fallback:' + method + ':',
|
||||||
|
cookieOptions : {
|
||||||
|
path : '/',
|
||||||
|
domain : document.domain,
|
||||||
|
expires : ('localStorage' === method) ? { expires: 365 } : undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
$.support[method] = method in window && window[method] !== null;
|
||||||
|
} catch (e) {
|
||||||
|
$.support[method] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$[method] = function(key, value) {
|
||||||
|
var options = $.extend({}, defaults, $[method].options);
|
||||||
|
|
||||||
|
this.getItem = function( key ) {
|
||||||
|
var returns = function(key){
|
||||||
|
return JSON.parse($.support[method] ? window[method].getItem(key) : $.cookie(options.cookiePrefix + key));
|
||||||
|
};
|
||||||
|
if(typeof key === 'string') return returns(key);
|
||||||
|
|
||||||
|
var arr = [],
|
||||||
|
i = key.length;
|
||||||
|
while(i--) arr[i] = returns(key[i]);
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setItem = function( key, value ) {
|
||||||
|
value = JSON.stringify(value);
|
||||||
|
return $.support[method] ? window[method].setItem(key, value) : $.cookie(options.cookiePrefix + key, value, options.cookieOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.removeItem = function( key ) {
|
||||||
|
return $.support[method] ? window[method].removeItem(key) : $.cookie(options.cookiePrefix + key, null, $.extend(options.cookieOptions, {
|
||||||
|
expires: -1
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clear = function() {
|
||||||
|
if($.support[method]) {
|
||||||
|
return window[method].clear();
|
||||||
|
} else {
|
||||||
|
var reg = new RegExp('^' + options.cookiePrefix, ''),
|
||||||
|
opts = $.extend(options.cookieOptions, {
|
||||||
|
expires: -1
|
||||||
|
});
|
||||||
|
|
||||||
|
if(document.cookie && document.cookie !== ''){
|
||||||
|
$.map(document.cookie.split(';'), function( cookie ){
|
||||||
|
if(reg.test(cookie = $.trim(cookie))) {
|
||||||
|
$.cookie( cookie.substr(0,cookie.indexOf('=')), null, opts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof key !== "undefined") {
|
||||||
|
return typeof value !== "undefined" ? ( value === null ? this.removeItem(key) : this.setItem(key, value) ) : this.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
$[method].options = defaults;
|
||||||
|
});
|
||||||
|
}(jQuery, window, document));
|
||||||
|
|
||||||
|
});
|
6
services/chat/public/js/libs/moment.js
Normal file
6
services/chat/public/js/libs/moment.js
Normal file
File diff suppressed because one or more lines are too long
1227
services/chat/public/js/libs/underscore.js
Normal file
1227
services/chat/public/js/libs/underscore.js
Normal file
File diff suppressed because it is too large
Load diff
26058
services/chat/public/js/r.js
Normal file
26058
services/chat/public/js/r.js
Normal file
File diff suppressed because one or more lines are too long
373
services/chat/public/js/text.js
Normal file
373
services/chat/public/js/text.js
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
/**
|
||||||
|
* @license RequireJS text 2.0.7 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
|
||||||
|
* Available via the MIT or new BSD license.
|
||||||
|
* see: http://github.com/requirejs/text for details
|
||||||
|
*/
|
||||||
|
/*jslint regexp: true */
|
||||||
|
/*global require, XMLHttpRequest, ActiveXObject,
|
||||||
|
define, window, process, Packages,
|
||||||
|
java, location, Components, FileUtils */
|
||||||
|
|
||||||
|
define(['module'], function (module) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var text, fs, Cc, Ci,
|
||||||
|
progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
|
||||||
|
xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
|
||||||
|
bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
|
||||||
|
hasLocation = typeof location !== 'undefined' && location.href,
|
||||||
|
defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
|
||||||
|
defaultHostName = hasLocation && location.hostname,
|
||||||
|
defaultPort = hasLocation && (location.port || undefined),
|
||||||
|
buildMap = {},
|
||||||
|
masterConfig = (module.config && module.config()) || {};
|
||||||
|
|
||||||
|
text = {
|
||||||
|
version: '2.0.7',
|
||||||
|
|
||||||
|
strip: function (content) {
|
||||||
|
//Strips <?xml ...?> declarations so that external SVG and XML
|
||||||
|
//documents can be added to a document without worry. Also, if the string
|
||||||
|
//is an HTML document, only the part inside the body tag is returned.
|
||||||
|
if (content) {
|
||||||
|
content = content.replace(xmlRegExp, "");
|
||||||
|
var matches = content.match(bodyRegExp);
|
||||||
|
if (matches) {
|
||||||
|
content = matches[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = "";
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
|
||||||
|
jsEscape: function (content) {
|
||||||
|
return content.replace(/(['\\])/g, '\\$1')
|
||||||
|
.replace(/[\f]/g, "\\f")
|
||||||
|
.replace(/[\b]/g, "\\b")
|
||||||
|
.replace(/[\n]/g, "\\n")
|
||||||
|
.replace(/[\t]/g, "\\t")
|
||||||
|
.replace(/[\r]/g, "\\r")
|
||||||
|
.replace(/[\u2028]/g, "\\u2028")
|
||||||
|
.replace(/[\u2029]/g, "\\u2029");
|
||||||
|
},
|
||||||
|
|
||||||
|
createXhr: masterConfig.createXhr || function () {
|
||||||
|
//Would love to dump the ActiveX crap in here. Need IE 6 to die first.
|
||||||
|
var xhr, i, progId;
|
||||||
|
if (typeof XMLHttpRequest !== "undefined") {
|
||||||
|
return new XMLHttpRequest();
|
||||||
|
} else if (typeof ActiveXObject !== "undefined") {
|
||||||
|
for (i = 0; i < 3; i += 1) {
|
||||||
|
progId = progIds[i];
|
||||||
|
try {
|
||||||
|
xhr = new ActiveXObject(progId);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (xhr) {
|
||||||
|
progIds = [progId]; // so faster next time
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return xhr;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a resource name into its component parts. Resource names
|
||||||
|
* look like: module/name.ext!strip, where the !strip part is
|
||||||
|
* optional.
|
||||||
|
* @param {String} name the resource name
|
||||||
|
* @returns {Object} with properties "moduleName", "ext" and "strip"
|
||||||
|
* where strip is a boolean.
|
||||||
|
*/
|
||||||
|
parseName: function (name) {
|
||||||
|
var modName, ext, temp,
|
||||||
|
strip = false,
|
||||||
|
index = name.indexOf("."),
|
||||||
|
isRelative = name.indexOf('./') === 0 ||
|
||||||
|
name.indexOf('../') === 0;
|
||||||
|
|
||||||
|
if (index !== -1 && (!isRelative || index > 1)) {
|
||||||
|
modName = name.substring(0, index);
|
||||||
|
ext = name.substring(index + 1, name.length);
|
||||||
|
} else {
|
||||||
|
modName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
temp = ext || modName;
|
||||||
|
index = temp.indexOf("!");
|
||||||
|
if (index !== -1) {
|
||||||
|
//Pull off the strip arg.
|
||||||
|
strip = temp.substring(index + 1) === "strip";
|
||||||
|
temp = temp.substring(0, index);
|
||||||
|
if (ext) {
|
||||||
|
ext = temp;
|
||||||
|
} else {
|
||||||
|
modName = temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
moduleName: modName,
|
||||||
|
ext: ext,
|
||||||
|
strip: strip
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is an URL on another domain. Only works for browser use, returns
|
||||||
|
* false in non-browser environments. Only used to know if an
|
||||||
|
* optimized .js version of a text resource should be loaded
|
||||||
|
* instead.
|
||||||
|
* @param {String} url
|
||||||
|
* @returns Boolean
|
||||||
|
*/
|
||||||
|
useXhr: function (url, protocol, hostname, port) {
|
||||||
|
var uProtocol, uHostName, uPort,
|
||||||
|
match = text.xdRegExp.exec(url);
|
||||||
|
if (!match) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
uProtocol = match[2];
|
||||||
|
uHostName = match[3];
|
||||||
|
|
||||||
|
uHostName = uHostName.split(':');
|
||||||
|
uPort = uHostName[1];
|
||||||
|
uHostName = uHostName[0];
|
||||||
|
|
||||||
|
return (!uProtocol || uProtocol === protocol) &&
|
||||||
|
(!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
|
||||||
|
((!uPort && !uHostName) || uPort === port);
|
||||||
|
},
|
||||||
|
|
||||||
|
finishLoad: function (name, strip, content, onLoad) {
|
||||||
|
content = strip ? text.strip(content) : content;
|
||||||
|
if (masterConfig.isBuild) {
|
||||||
|
buildMap[name] = content;
|
||||||
|
}
|
||||||
|
onLoad(content);
|
||||||
|
},
|
||||||
|
|
||||||
|
load: function (name, req, onLoad, config) {
|
||||||
|
//Name has format: some.module.filext!strip
|
||||||
|
//The strip part is optional.
|
||||||
|
//if strip is present, then that means only get the string contents
|
||||||
|
//inside a body tag in an HTML string. For XML/SVG content it means
|
||||||
|
//removing the <?xml ...?> declarations so the content can be inserted
|
||||||
|
//into the current doc without problems.
|
||||||
|
|
||||||
|
// Do not bother with the work if a build and text will
|
||||||
|
// not be inlined.
|
||||||
|
if (config.isBuild && !config.inlineText) {
|
||||||
|
onLoad();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
masterConfig.isBuild = config.isBuild;
|
||||||
|
|
||||||
|
var parsed = text.parseName(name),
|
||||||
|
nonStripName = parsed.moduleName +
|
||||||
|
(parsed.ext ? '.' + parsed.ext : ''),
|
||||||
|
url = req.toUrl(nonStripName),
|
||||||
|
useXhr = (masterConfig.useXhr) ||
|
||||||
|
text.useXhr;
|
||||||
|
|
||||||
|
//Load the text. Use XHR if possible and in a browser.
|
||||||
|
if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
|
||||||
|
text.get(url, function (content) {
|
||||||
|
text.finishLoad(name, parsed.strip, content, onLoad);
|
||||||
|
}, function (err) {
|
||||||
|
if (onLoad.error) {
|
||||||
|
onLoad.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//Need to fetch the resource across domains. Assume
|
||||||
|
//the resource has been optimized into a JS module. Fetch
|
||||||
|
//by the module name + extension, but do not include the
|
||||||
|
//!strip part to avoid file system issues.
|
||||||
|
req([nonStripName], function (content) {
|
||||||
|
text.finishLoad(parsed.moduleName + '.' + parsed.ext,
|
||||||
|
parsed.strip, content, onLoad);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
write: function (pluginName, moduleName, write, config) {
|
||||||
|
if (buildMap.hasOwnProperty(moduleName)) {
|
||||||
|
var content = text.jsEscape(buildMap[moduleName]);
|
||||||
|
write.asModule(pluginName + "!" + moduleName,
|
||||||
|
"define(function () { return '" +
|
||||||
|
content +
|
||||||
|
"';});\n");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
writeFile: function (pluginName, moduleName, req, write, config) {
|
||||||
|
var parsed = text.parseName(moduleName),
|
||||||
|
extPart = parsed.ext ? '.' + parsed.ext : '',
|
||||||
|
nonStripName = parsed.moduleName + extPart,
|
||||||
|
//Use a '.js' file name so that it indicates it is a
|
||||||
|
//script that can be loaded across domains.
|
||||||
|
fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
|
||||||
|
|
||||||
|
//Leverage own load() method to load plugin value, but only
|
||||||
|
//write out values that do not have the strip argument,
|
||||||
|
//to avoid any potential issues with ! in file names.
|
||||||
|
text.load(nonStripName, req, function (value) {
|
||||||
|
//Use own write() method to construct full module value.
|
||||||
|
//But need to create shell that translates writeFile's
|
||||||
|
//write() to the right interface.
|
||||||
|
var textWrite = function (contents) {
|
||||||
|
return write(fileName, contents);
|
||||||
|
};
|
||||||
|
textWrite.asModule = function (moduleName, contents) {
|
||||||
|
return write.asModule(moduleName, fileName, contents);
|
||||||
|
};
|
||||||
|
|
||||||
|
text.write(pluginName, nonStripName, textWrite, config);
|
||||||
|
}, config);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (masterConfig.env === 'node' || (!masterConfig.env &&
|
||||||
|
typeof process !== "undefined" &&
|
||||||
|
process.versions &&
|
||||||
|
!!process.versions.node)) {
|
||||||
|
//Using special require.nodeRequire, something added by r.js.
|
||||||
|
fs = require.nodeRequire('fs');
|
||||||
|
|
||||||
|
text.get = function (url, callback, errback) {
|
||||||
|
try {
|
||||||
|
var file = fs.readFileSync(url, 'utf8');
|
||||||
|
//Remove BOM (Byte Mark Order) from utf8 files if it is there.
|
||||||
|
if (file.indexOf('\uFEFF') === 0) {
|
||||||
|
file = file.substring(1);
|
||||||
|
}
|
||||||
|
callback(file);
|
||||||
|
} catch (e) {
|
||||||
|
errback(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
|
||||||
|
text.createXhr())) {
|
||||||
|
text.get = function (url, callback, errback, headers) {
|
||||||
|
var xhr = text.createXhr(), header;
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
|
||||||
|
//Allow plugins direct access to xhr headers
|
||||||
|
if (headers) {
|
||||||
|
for (header in headers) {
|
||||||
|
if (headers.hasOwnProperty(header)) {
|
||||||
|
xhr.setRequestHeader(header.toLowerCase(), headers[header]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Allow overrides specified in config
|
||||||
|
if (masterConfig.onXhr) {
|
||||||
|
masterConfig.onXhr(xhr, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.onreadystatechange = function (evt) {
|
||||||
|
var status, err;
|
||||||
|
//Do not explicitly handle errors, those should be
|
||||||
|
//visible via console output in the browser.
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
status = xhr.status;
|
||||||
|
if (status > 399 && status < 600) {
|
||||||
|
//An http 4xx or 5xx error. Signal an error.
|
||||||
|
err = new Error(url + ' HTTP status: ' + status);
|
||||||
|
err.xhr = xhr;
|
||||||
|
errback(err);
|
||||||
|
} else {
|
||||||
|
callback(xhr.responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (masterConfig.onXhrComplete) {
|
||||||
|
masterConfig.onXhrComplete(xhr, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(null);
|
||||||
|
};
|
||||||
|
} else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
|
||||||
|
typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
|
||||||
|
//Why Java, why is this so awkward?
|
||||||
|
text.get = function (url, callback) {
|
||||||
|
var stringBuffer, line,
|
||||||
|
encoding = "utf-8",
|
||||||
|
file = new java.io.File(url),
|
||||||
|
lineSeparator = java.lang.System.getProperty("line.separator"),
|
||||||
|
input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
|
||||||
|
content = '';
|
||||||
|
try {
|
||||||
|
stringBuffer = new java.lang.StringBuffer();
|
||||||
|
line = input.readLine();
|
||||||
|
|
||||||
|
// Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
|
||||||
|
// http://www.unicode.org/faq/utf_bom.html
|
||||||
|
|
||||||
|
// Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
|
||||||
|
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
|
||||||
|
if (line && line.length() && line.charAt(0) === 0xfeff) {
|
||||||
|
// Eat the BOM, since we've already found the encoding on this file,
|
||||||
|
// and we plan to concatenating this buffer with others; the BOM should
|
||||||
|
// only appear at the top of a file.
|
||||||
|
line = line.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line !== null) {
|
||||||
|
stringBuffer.append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((line = input.readLine()) !== null) {
|
||||||
|
stringBuffer.append(lineSeparator);
|
||||||
|
stringBuffer.append(line);
|
||||||
|
}
|
||||||
|
//Make sure we return a JavaScript string and not a Java string.
|
||||||
|
content = String(stringBuffer.toString()); //String
|
||||||
|
} finally {
|
||||||
|
input.close();
|
||||||
|
}
|
||||||
|
callback(content);
|
||||||
|
};
|
||||||
|
} else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
|
||||||
|
typeof Components !== 'undefined' && Components.classes &&
|
||||||
|
Components.interfaces)) {
|
||||||
|
//Avert your gaze!
|
||||||
|
Cc = Components.classes,
|
||||||
|
Ci = Components.interfaces;
|
||||||
|
Components.utils['import']('resource://gre/modules/FileUtils.jsm');
|
||||||
|
|
||||||
|
text.get = function (url, callback) {
|
||||||
|
var inStream, convertStream,
|
||||||
|
readData = {},
|
||||||
|
fileObj = new FileUtils.File(url);
|
||||||
|
|
||||||
|
//XPCOM, you so crazy
|
||||||
|
try {
|
||||||
|
inStream = Cc['@mozilla.org/network/file-input-stream;1']
|
||||||
|
.createInstance(Ci.nsIFileInputStream);
|
||||||
|
inStream.init(fileObj, 1, 0, false);
|
||||||
|
|
||||||
|
convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
|
||||||
|
.createInstance(Ci.nsIConverterInputStream);
|
||||||
|
convertStream.init(inStream, "utf-8", inStream.available(),
|
||||||
|
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||||
|
|
||||||
|
convertStream.readString(inStream.available(), readData);
|
||||||
|
convertStream.close();
|
||||||
|
inStream.close();
|
||||||
|
callback(readData.value);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error((fileObj && fileObj.path || '') + ': ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
});
|
198
services/chat/public/less/chat.less
Normal file
198
services/chat/public/less/chat.less
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
@border-color: #999;
|
||||||
|
|
||||||
|
.chat-window {
|
||||||
|
border-bottom: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 10px;
|
||||||
|
height: 260px;
|
||||||
|
width: 220px;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
h3 {
|
||||||
|
background-color: rgb(40,40,40);
|
||||||
|
border: 1px solid #222;
|
||||||
|
border-bottom: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
padding: 4px 6px 5px;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
.window-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 0px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
.minimize-toggle {
|
||||||
|
width: 12px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 4px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 4px center;
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAKlmlDQ1BJQ0MgUHJvZmlsZQAASImVlgdQU+kWx7970xstEOmE3rt0KaGHIh0EGyEJnRgSgoDYEVdgRRERwQosVcG1AGJDLFhYBCzYN8iioq6LBVFReRd4hPfevJ0378yc3N+cOfO/53y538wfAHINi89PhWUASONlCEJ93OlLomPouCcAC8iAADSBEYst5DOCgwPA38anewCaft42ndb6+77/GrIcrpANABSMcBxHyE5D+CSSRWy+IAMAlBtS11mdwZ9mDsLyAmRAhLOmOWGWi6Y5bpYPzfSEh3ogjOjgySyWIAEAUidSp2eyExAdkhhhCx4niQcAGdkcuLATWYg2eXoGk7S0VdOci7BB3L/oJPybZpxEk8VKkPDsLjOB90wS8lNZ2f/ncfzvSEsVzb1DHUmyMCXMf3pn5Myy2CyvsDlO5DID5pif4R46x0kZzHBJj8g3Yo5FKRGMOU5Z5S/p58UtDpLoCz1i5jgnMTxqjjlcT685FqwKlfQLM8O85vs9Fs9xMssveI5ZgpldZpib6hM6P3OwZE5e6mLJLvECb0kPVzi/b0ZiuK+EkQ9A0p/kzZTsK/Cd108NlmgKRKGSc+DyIiSaHJan5GxBGMgGPMAGgYAFhCADcIEgg5uVMT28xyp+tiApITGDzkBuANeEzuSxzUzoVhaW1mD6Ps3+XR/uz9wTiIafr22oBsDLACkWz9fCEM36TgBoavM1rfMAyCkBcGYjWyTInK2hp38wgAikkQmVkK9BGxgAU2AFbIETcANewA8EgXAQDVYgUyeCNCAAq0Eu2AjyQSHYAXaDCnAQVIN6cBQcB23gLLgIroKboA/cBY+AGIyA12AMfAKTEAThIApEhZQgDUgXMoasIHvIBfKCAqBQKBqKhRIgHiSCcqHNUCFUAlVAh6EG6FfoNHQRug71Qw+gIWgUeg99hVEwGZaH1WA92By2hxmwPxwOL4cT4HQ4B86Dt8PlcBV8BG6FL8I34buwGH4Nj6MAioSioTRRpih7lAcqCBWDikcJUOtQBagyVBWqGdWB6kbdRolRb1Bf0Fg0FU1Hm6Kd0L7oCDQbnY5ehy5CV6Dr0a3oy+jb6CH0GPoHhoJRxRhjHDFMzBJMAmY1Jh9ThqnFnMJcwdzFjGA+YbFYGlYfa4f1xUZjk7FrsEXY/dgWbCe2HzuMHcfhcEo4Y5wzLgjHwmXg8nF7cUdwF3ADuBHcZzwJr4G3wnvjY/A8/CZ8Gb4Rfx4/gH+BnyTIEHQJjoQgAoeQTSgm1BA6CLcII4RJoixRn+hMDCcmEzcSy4nNxCvEx8QPJBJJi+RACiElkTaQyknHSNdIQ6QvZDmyEdmDvIwsIm8n15E7yQ/IHygUih7FjRJDyaBspzRQLlGeUj5LUaXMpJhSHKn1UpVSrVIDUm+lCdK60gzpFdI50mXSJ6RvSb+RIcjoyXjIsGTWyVTKnJYZlBmXpcpaygbJpskWyTbKXpd9KYeT05PzkuPI5clVy12SG6aiqNpUDyqbuplaQ71CHZHHyuvLM+WT5Qvlj8r3yo8pyCksVIhUyFKoVDinIKahaHo0Ji2VVkw7TrtH+7pAbQFjAXfBtgXNCwYWTCiqKLopchULFFsU7yp+VaIreSmlKO1UalN6ooxWNlIOUV6tfED5ivIbFXkVJxW2SoHKcZWHqrCqkWqo6hrVatUe1XE1dTUfNb7aXrVLam/Uaepu6snqpern1Uc1qBouGkkapRoXNF7RFegMeiq9nH6ZPqapqumrKdI8rNmrOamlrxWhtUmrReuJNlHbXjteu1S7S3tMR0MnUCdXp0nnoS5B1143UXePbrfuhJ6+XpTeVr02vZf6ivpM/Rz9Jv3HBhQDV4N0gyqDO4ZYQ3vDFMP9hn1GsJGNUaJRpdEtY9jY1jjJeL9xvwnGxMGEZ1JlMmhKNmWYZpo2mQ6Z0cwCzDaZtZm9NdcxjzHfad5t/sPCxiLVosbikaWcpZ/lJssOy/dWRlZsq0qrO9YUa2/r9dbt1u8WGi/kLjyw8L4N1SbQZqtNl813WztbgW2z7aidjl2s3T67QXt5+2D7IvtrDhgHd4f1DmcdvjjaOmY4Hnf8y8nUKcWp0enlIv1F3EU1i4adtZxZzoedxS50l1iXQy5iV01XlmuV6zM3bTeOW63bC4YhI5lxhPHW3cJd4H7KfcLD0WOtR6cnytPHs8Cz10vOK8Krwuupt5Z3gneT95iPjc8an05fjK+/707fQaYak81sYI752fmt9bvsT/YP86/wfxZgFCAI6AiEA/0CdwU+Xqy7mLe4LQgEMYN2BT0J1g9ODz4Tgg0JDqkMeR5qGZob2h1GDVsZ1hj2Kdw9vDj8UYRBhCiiK1I6cllkQ+RElGdUSZR4ifmStUtuRitHJ0W3x+BiImNqY8aXei3dvXRkmc2y/GX3lusvz1p+fYXyitQV51ZKr2StPBGLiY2KbYz9xgpiVbHG45hx++LG2B7sPezXHDdOKWeU68wt4b6Id44viX+Z4JywK2E00TWxLPFNkkdSRdK7ZN/kg8kTKUEpdSlTqVGpLWn4tNi00zw5Xgrv8ir1VVmr+vnG/Hy+ON0xfXf6mMBfUCuEhMuF7RnyiHHpERmItoiGMl0yKzM/r45cfSJLNouX1ZNtlL0t+0WOd84va9Br2Gu6cjVzN+YOrWWsPbwOWhe3rmu99vq89SMbfDbUbyRuTNn42yaLTSWbPm6O2tyRp5a3IW94i8+WpnypfEH+4FanrQd/Qv+U9FPvNutte7f9KOAU3Ci0KCwr/FbELrrxs+XP5T9PbY/f3ltsW3xgB3YHb8e9na4760tkS3JKhncF7motpZcWlH7cvXL39bKFZQf3EPeI9ojLA8rb9+rs3bH3W0Vixd1K98qWfar7tu2b2M/ZP3DA7UDzQbWDhQe/Hko6dP+wz+HWKr2qsmpsdWb185rImu5f7H9pqFWuLaz9XserE9eH1l9usGtoaFRtLG6Cm0RNo0eWHek76nm0vdm0+XALraXwGDgmOvbq19hf7x33P951wv5E80ndk/tOUU8VtEKt2a1jbYlt4vbo9v7Tfqe7Opw6Tp0xO1N3VvNs5TmFc8Xniefzzk9dyLkw3snvfHMx4eJw18quR5eWXLpzOeRy7xX/K9euel+91M3ovnDN+drZ647XT9+wv9F20/Zma49Nz6nfbH471Wvb23rL7lZ7n0NfR/+i/vMDrgMXb3vevnqHeefm3cV3++9F3Ls/uGxQfJ9z/+WD1AfvHmY+nHy04THmccETmSdlT1WfVv1u+HuL2FZ8bshzqOdZ2LNHw+zh138I//g2kvec8rzshcaLhpdWL8+Oeo/2vVr6auQ1//Xkm/w/Zf/c99bg7cm/3P7qGVsyNvJO8G7qfdEHpQ91Hxd+7BoPHn/6Ke3T5ETBZ6XP9V/sv3R/jfr6YnL1N9y38u+G3zt++P94PJU2NcVnCVgzVgCFJBwfD8D7OgAo0QBQ+wAgSs363ZmAZj36DIG/41lPPBO2AFQjHiTaDQBf5HkAKekgLItkMJLhbgC2tpbkP0MYb201q0VqQ6xJ2dTUB8Qj4gwB+D44NTXZNjX1vRYZ9iEAnZ9mffZ0BJgitQFfhpXV1TNrwX/GPwACZfVkyDPtLwAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90GEwkYOX1Xg14AAAAVSURBVChTY2AYBYMC/CcRkK6B5gAA7lZfobSTqEkAAAAASUVORK5CYII=);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-message-alert {
|
||||||
|
display: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
padding: 4px;
|
||||||
|
top: -16px;
|
||||||
|
left: 35px;
|
||||||
|
background-color: red;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 1px 6px;
|
||||||
|
-moz-border-radius: 3px;
|
||||||
|
-webkit-border-radius: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
&:after {
|
||||||
|
border: 3px solid red;
|
||||||
|
border-right: 3px solid transparent;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
bottom: -6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connected-users {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: #ddd;
|
||||||
|
border: 1px solid @border-color;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sent-message-area {
|
||||||
|
border: 1px solid @border-color;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 22px;
|
||||||
|
bottom: 28px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: arial, san-serif;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.chat-block {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
|
||||||
|
.message-block {
|
||||||
|
min-height: 40px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeSinceMessage {
|
||||||
|
color: @border-color;
|
||||||
|
text-align:center;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
float: left;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.load-older-messages {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #eee;
|
||||||
|
text-align:center;
|
||||||
|
display: block;
|
||||||
|
&:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@new-message-area-height: 27px;
|
||||||
|
@new-message-area-padding-top: 3px;
|
||||||
|
.new-message-area {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
height: @new-message-area-height;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
|
||||||
|
border: 1px solid @border-color;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: @new-message-area-height - 2 * @new-message-area-padding-top;
|
||||||
|
width: 212px;
|
||||||
|
border: none;
|
||||||
|
padding: @new-message-area-padding-top;
|
||||||
|
margin: 0;
|
||||||
|
resize: none;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: arial, san-serif;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-window.disconnected {
|
||||||
|
.sent-message-area {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-window.new-messages {
|
||||||
|
.header {
|
||||||
|
h3 {
|
||||||
|
background-color: #049cdb;
|
||||||
|
border-color: darken(#049cdb, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-message-alert {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
&:hover {
|
||||||
|
background-color: darken(#049cdb, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-window.minimized {
|
||||||
|
.header {
|
||||||
|
.window-controls {
|
||||||
|
.minimize-toggle {
|
||||||
|
background-image: url(data:image/png;base64,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
services/chat/rakefile.rb
Normal file
124
services/chat/rakefile.rb
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
namespace "run" do
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace "test" do
|
||||||
|
desc "Run the unit tests"
|
||||||
|
task :unit => ["compile:unittests"] do
|
||||||
|
puts "Running unit tests"
|
||||||
|
featurePath = ENV['feature']
|
||||||
|
puts featurePath
|
||||||
|
if featurePath.nil?
|
||||||
|
featurePath = ''
|
||||||
|
elsif featurePath.include? '/'
|
||||||
|
elsif !featurePath.include? '/'
|
||||||
|
featurePath +='/'
|
||||||
|
else
|
||||||
|
featurePath = ''
|
||||||
|
end
|
||||||
|
runner = ENV['MOCHA_RUNNER'] || "spec"
|
||||||
|
sh %{mocha -R #{runner} test/unit/js/#{featurePath}* --ignore-leaks} do |ok, res|
|
||||||
|
if ! ok
|
||||||
|
raise "error running unit tests : #{res}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace "compile" do
|
||||||
|
desc "Compile server and client coffeescript files"
|
||||||
|
task :app => ["compile:serverApp", "compile:clientApp"]
|
||||||
|
|
||||||
|
desc "Compile the main serverside app folder"
|
||||||
|
task :serverApp do
|
||||||
|
puts "Compiling app"
|
||||||
|
sh %{coffee -o app/js/ -c app/coffee/} do |ok, res|
|
||||||
|
if ! ok
|
||||||
|
raise "error compiling app folder: #{res}"
|
||||||
|
end
|
||||||
|
puts 'Finished server app compile'
|
||||||
|
end
|
||||||
|
sh %{coffee -c app.coffee} do |ok, res|
|
||||||
|
if ! ok
|
||||||
|
raise "error compiling app file: #{res}"
|
||||||
|
end
|
||||||
|
puts 'Finished app.coffee compile'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Compile the main client app folder"
|
||||||
|
task :clientApp do
|
||||||
|
puts "Compiling client app"
|
||||||
|
sh %{coffee -o public/js/ -c public/coffee/} do |ok, res|
|
||||||
|
if ! ok
|
||||||
|
raise "error compiling client app folder: #{res}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sh %{mkdir -p public/js/html}
|
||||||
|
sh %{mkdir -p public/js/css}
|
||||||
|
sh %{jade < public/jade/templates.jade > public/js/html/templates.html} do |ok, res|
|
||||||
|
if !ok
|
||||||
|
raise "error compiling jade templates: #{res}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sh %{lessc - < public/less/chat.less > public/js/css/chat.css} do |ok, res|
|
||||||
|
if !ok
|
||||||
|
raise "error compiling css: #{res}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "compress the js"
|
||||||
|
task :compressAndCompileJs do
|
||||||
|
sh %{node public/js/r.js -o public/app.build.js} do |ok, res|
|
||||||
|
if ! ok
|
||||||
|
raise "error compiling client app folder: #{res}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
puts "Finished client app compile"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Compile the unit tests folder"
|
||||||
|
task :unittests => ["compile:serverApp"] do
|
||||||
|
puts "Compiling Unit Tests to JS"
|
||||||
|
sh %{coffee -c -o test/unit/js/ test/unit/coffee/} do |ok, res|
|
||||||
|
if ! ok
|
||||||
|
raise "error compiling Unit tests : #{res}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace 'bootstrap' do
|
||||||
|
desc "Creates a new Feature and module, and corresponding test framework file"
|
||||||
|
task :feature, :feature_name, :module_name do |task, args|
|
||||||
|
feature_name = args[:feature_name]
|
||||||
|
module_name = args[:module_name]
|
||||||
|
FileUtils.mkdir_p("app/coffee/Features/#{feature_name}")
|
||||||
|
File.open("app/coffee/Features/#{feature_name}/#{module_name}.coffee", "w") { |f|
|
||||||
|
f.write(<<-EOS
|
||||||
|
module.exports = #{module_name} =
|
||||||
|
EOS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FileUtils.mkdir_p("test/unit/coffee/#{feature_name}")
|
||||||
|
File.open("test/unit/coffee/#{feature_name}/#{module_name}Tests.coffee", "w") { |f|
|
||||||
|
f.write(<<-EOS
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/#{feature_name}/#{module_name}.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
|
||||||
|
|
||||||
|
describe "#{module_name}", ->
|
||||||
|
beforeEach ->
|
||||||
|
@#{module_name} = SandboxedModule.require modulePath, requires:
|
||||||
|
|
||||||
|
EOS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,71 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Authentication/AuthenticationController.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
|
||||||
|
describe "AuthenticationController", ->
|
||||||
|
beforeEach ->
|
||||||
|
@AuthenticationController = SandboxedModule.require modulePath, requires:
|
||||||
|
"../WebApi/WebApiManager": @WebApiManager = {}
|
||||||
|
"../Users/UserFormatter": @UserFormatter = {}
|
||||||
|
"logger-sharelatex": @logger = { log: sinon.stub() }
|
||||||
|
@callback = sinon.stub()
|
||||||
|
|
||||||
|
describe "authClient", ->
|
||||||
|
beforeEach ->
|
||||||
|
@auth_token = "super-secret-auth-token"
|
||||||
|
@client =
|
||||||
|
params: {}
|
||||||
|
set: (key, value, callback) ->
|
||||||
|
@params[key] = value
|
||||||
|
callback()
|
||||||
|
@user =
|
||||||
|
id: "user-id-123"
|
||||||
|
email: "doug@sharelatex.com"
|
||||||
|
first_name: "Douglas"
|
||||||
|
last_name: "Adams"
|
||||||
|
@WebApiManager.getUserDetailsFromAuthToken = sinon.stub().callsArgWith(1, null, @user)
|
||||||
|
@UserFormatter.formatUserForClientSide = sinon.stub().returns({
|
||||||
|
id: @user.id
|
||||||
|
first_name: @user.first_name
|
||||||
|
last_name: @user.last_name
|
||||||
|
email: @user.email
|
||||||
|
gravatar_url: "//gravatar/url"
|
||||||
|
})
|
||||||
|
@AuthenticationController.authClient(@client, auth_token: @auth_token, @callback)
|
||||||
|
|
||||||
|
it "should get the user's data from the web api", ->
|
||||||
|
@WebApiManager.getUserDetailsFromAuthToken
|
||||||
|
.calledWith(@auth_token)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should set the user's data and auth_token on the client object", ->
|
||||||
|
@client.params.should.deep.equal {
|
||||||
|
id: @user.id
|
||||||
|
first_name: @user.first_name
|
||||||
|
last_name: @user.last_name
|
||||||
|
email: @user.email
|
||||||
|
gravatar_url: "//gravatar/url"
|
||||||
|
auth_token: @auth_token
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should call the callback with the user details (including the gravatar URL, but not the auth_token)", ->
|
||||||
|
@callback
|
||||||
|
.calledWith(null, {
|
||||||
|
id: @user.id
|
||||||
|
email: @user.email
|
||||||
|
first_name: @user.first_name
|
||||||
|
last_name: @user.last_name
|
||||||
|
gravatar_url: "//gravatar/url"
|
||||||
|
}).should.equal true
|
||||||
|
|
||||||
|
it "should log the request", ->
|
||||||
|
@logger.log
|
||||||
|
.calledWith(auth_token: @auth_token, "authenticating user")
|
||||||
|
.should.equal true
|
||||||
|
@logger.log
|
||||||
|
.calledWith(user: @user, auth_token: @auth_token, "authenticated user")
|
||||||
|
.should.equal true
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Authorization/AuthorizationManager.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
|
||||||
|
describe "AuthorizationManager", ->
|
||||||
|
beforeEach ->
|
||||||
|
@SocketManager = {}
|
||||||
|
@AuthorizationManager = SandboxedModule.require modulePath, requires:
|
||||||
|
"../WebApi/WebApiManager": @WebApiManager = {}
|
||||||
|
"../Sockets/SocketManager": @SocketManager
|
||||||
|
@callback = sinon.stub()
|
||||||
|
@user_id = "user-id-123"
|
||||||
|
@project_id = "project-id-456"
|
||||||
|
@auth_token = "auth-token-789"
|
||||||
|
@client =
|
||||||
|
params: {}
|
||||||
|
get: (key, callback = (error, value) ->) ->
|
||||||
|
callback null, @params[key]
|
||||||
|
|
||||||
|
describe "canClientJoinProjectRoom", ->
|
||||||
|
beforeEach ->
|
||||||
|
@client.params.auth_token = @auth_token
|
||||||
|
@client.params.id = @user_id
|
||||||
|
|
||||||
|
describe "when the client is a collaborator", ->
|
||||||
|
beforeEach ->
|
||||||
|
@collaborators = [
|
||||||
|
id: @user_id
|
||||||
|
]
|
||||||
|
@WebApiManager.getProjectCollaborators = sinon.stub().callsArgWith(2, null, @collaborators)
|
||||||
|
@AuthorizationManager.canClientJoinProjectRoom(@client, @project_id, @callback)
|
||||||
|
|
||||||
|
it "should get the list of collaborators from the web api", ->
|
||||||
|
@WebApiManager.getProjectCollaborators
|
||||||
|
.calledWith(@project_id, @auth_token)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return true", ->
|
||||||
|
@callback.calledWith(null, true).should.equal true
|
||||||
|
|
||||||
|
describe "when the client is not a collaborator", ->
|
||||||
|
beforeEach ->
|
||||||
|
@collaborators = [
|
||||||
|
id: "not the user id"
|
||||||
|
]
|
||||||
|
@WebApiManager.getProjectCollaborators = sinon.stub().callsArgWith(2, null, @collaborators)
|
||||||
|
@AuthorizationManager.canClientJoinProjectRoom(@client, @project_id, @callback)
|
||||||
|
|
||||||
|
it "should return false", ->
|
||||||
|
@callback.calledWith(null, false).should.equal true
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Messages/MessageController.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
tk = require "timekeeper"
|
||||||
|
ObjectId = require("mongojs").ObjectId
|
||||||
|
|
||||||
|
describe "MessageController", ->
|
||||||
|
beforeEach ->
|
||||||
|
tk.freeze(new Date())
|
||||||
|
@MessageController = SandboxedModule.require modulePath, requires:
|
||||||
|
"./MessageManager": @MessageManager = {}
|
||||||
|
"./MessageFormatter": @MessageFormatter = {}
|
||||||
|
"../Sockets/SocketManager": @SocketManager = {}
|
||||||
|
"../Authorization/AuthorizationManager": @AuthorizationManager = {}
|
||||||
|
"logger-sharelatex": @logger = { log: sinon.stub() }
|
||||||
|
"../../metrics": @metrics = {inc: sinon.stub()}
|
||||||
|
@callback = sinon.stub()
|
||||||
|
@client =
|
||||||
|
params:
|
||||||
|
id: @user_id = "user-id-123"
|
||||||
|
first_name: @first_name = "Douglas"
|
||||||
|
last_name: @last_name = "Adams"
|
||||||
|
email: @email = "doug@sharelatex.com"
|
||||||
|
gravatar_url: @gravatar_url = "//gravatar/url"
|
||||||
|
get: (key, callback = (error, value) ->) -> callback null, @params[key]
|
||||||
|
|
||||||
|
afterEach ->
|
||||||
|
tk.reset()
|
||||||
|
|
||||||
|
describe "sendMessage", ->
|
||||||
|
beforeEach ->
|
||||||
|
@MessageManager.createMessage = sinon.stub().callsArg(1)
|
||||||
|
@SocketManager.emitToRoom = sinon.stub()
|
||||||
|
@singlePopulatedMessage = {data:"here"}
|
||||||
|
@formattedMessage = {formatted:true}
|
||||||
|
@MessageFormatter.formatMessageForClientSide = sinon.stub().returns(@formattedMessage)
|
||||||
|
@MessageManager.populateMessagesWithUsers = sinon.stub().callsArgWith(1, null, [@singlePopulatedMessage])
|
||||||
|
@SocketManager.getClientAttributes = (client, attributes, callback) ->
|
||||||
|
values = (client.params[key] for key in attributes)
|
||||||
|
callback null, values
|
||||||
|
|
||||||
|
describe "when the client is authorized to send a message to the room", ->
|
||||||
|
beforeEach ->
|
||||||
|
@AuthorizationManager.canClientSendMessageToRoom = sinon.stub().callsArgWith(2, null, true)
|
||||||
|
@MessageController.sendMessage(@client, {
|
||||||
|
message:
|
||||||
|
content: @content = "Hello world"
|
||||||
|
room:
|
||||||
|
id: @room_id = "room-id-123"
|
||||||
|
}, @callback)
|
||||||
|
|
||||||
|
it "should check that the client can send a message to the room", ->
|
||||||
|
@AuthorizationManager.canClientSendMessageToRoom
|
||||||
|
.calledWith(@client, @room_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should insert the message into the database", ->
|
||||||
|
@MessageManager.createMessage
|
||||||
|
.calledWith({
|
||||||
|
content: @content
|
||||||
|
user_id: @user_id
|
||||||
|
room_id: @room_id
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
|
||||||
|
it "should format the message for the client", ->
|
||||||
|
@MessageFormatter.formatMessageForClientSide.calledWith(@singlePopulatedMessage).should.equal true
|
||||||
|
|
||||||
|
it "should send the formatted message out to the other clients in the room", ->
|
||||||
|
@SocketManager.emitToRoom.calledWith(@room_id, "messageReceived", message:@formattedMessage).should.equal true
|
||||||
|
|
||||||
|
it "should record the message as a metric", ->
|
||||||
|
@metrics.inc
|
||||||
|
.calledWith("editor.instant-message")
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback", ->
|
||||||
|
@callback.called.should.equal true
|
||||||
|
|
||||||
|
describe "when the client is not authorized", ->
|
||||||
|
beforeEach ->
|
||||||
|
@AuthorizationManager.canClientSendMessageToRoom = sinon.stub().callsArgWith(2, null, false)
|
||||||
|
@MessageController.sendMessage(@client, {
|
||||||
|
message:
|
||||||
|
content: @content = "Hello world"
|
||||||
|
room:
|
||||||
|
id: @room_id = "room-id-123"
|
||||||
|
}, @callback)
|
||||||
|
|
||||||
|
it "should not insert the message into the database", ->
|
||||||
|
@MessageManager.createMessage.called.should.equal false
|
||||||
|
|
||||||
|
it "should not send the message out to the other clients in the room", ->
|
||||||
|
@SocketManager.emitToRoom.called.should.equal false
|
||||||
|
|
||||||
|
it "should call the callback with an error that doesn't give anything away", ->
|
||||||
|
@callback.calledWith("unknown room").should.equal true
|
||||||
|
|
||||||
|
describe "getMessage", ->
|
||||||
|
beforeEach ->
|
||||||
|
@room_id = "room-id-123"
|
||||||
|
@timestamp = Date.now()
|
||||||
|
@limit = 42
|
||||||
|
|
||||||
|
describe "when the client is authorized", ->
|
||||||
|
beforeEach ->
|
||||||
|
@messages = "messages without users stub"
|
||||||
|
@messagesWithUsers = "messages with users stub"
|
||||||
|
@formattedMessages = "formatted messages stub"
|
||||||
|
@MessageManager.getMessages = sinon.stub().callsArgWith(2, null, @messages)
|
||||||
|
@MessageManager.populateMessagesWithUsers = sinon.stub().callsArgWith(1, null, @messagesWithUsers)
|
||||||
|
@AuthorizationManager.canClientReadMessagesInRoom = sinon.stub().callsArgWith(2, null, true)
|
||||||
|
@MessageFormatter.formatMessagesForClientSide = sinon.stub().returns @formattedMessages
|
||||||
|
|
||||||
|
describe "with a timestamp and limit", ->
|
||||||
|
beforeEach ->
|
||||||
|
@MessageController.getMessages(@client, {
|
||||||
|
room:
|
||||||
|
id: @room_id,
|
||||||
|
before: @timestamp,
|
||||||
|
limit: @limit
|
||||||
|
}, @callback)
|
||||||
|
|
||||||
|
it "should get the requested messages", ->
|
||||||
|
@MessageManager.getMessages
|
||||||
|
.calledWith({
|
||||||
|
timestamp: $lt: @timestamp
|
||||||
|
room_id: @room_id
|
||||||
|
}, {
|
||||||
|
limit: @limit
|
||||||
|
order_by: "timestamp"
|
||||||
|
sort_order: -1
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should populate the messages with the users", ->
|
||||||
|
@MessageManager.populateMessagesWithUsers
|
||||||
|
.calledWith(@messages)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the formatted messages", ->
|
||||||
|
@MessageFormatter.formatMessagesForClientSide
|
||||||
|
.calledWith(@messagesWithUsers)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the formatted messages", ->
|
||||||
|
@callback
|
||||||
|
.calledWith(null, @formattedMessages)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "without a timestamp or limit", ->
|
||||||
|
beforeEach ->
|
||||||
|
@MessageController.getMessages(@client, {
|
||||||
|
room:
|
||||||
|
id: @room_id,
|
||||||
|
}, @callback)
|
||||||
|
|
||||||
|
it "should get a default number of messages from the beginning", ->
|
||||||
|
@MessageManager.getMessages
|
||||||
|
.calledWith({
|
||||||
|
room_id: @room_id
|
||||||
|
}, {
|
||||||
|
limit: @MessageController.DEFAULT_MESSAGE_LIMIT
|
||||||
|
order_by: "timestamp"
|
||||||
|
sort_order: -1
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "when the client is not authorized", ->
|
||||||
|
beforeEach ->
|
||||||
|
@AuthorizationManager.canClientReadMessagesInRoom = sinon.stub().callsArgWith(2, null, false)
|
||||||
|
@MessageController.getMessages(@client, {
|
||||||
|
room:
|
||||||
|
id: @room_id,
|
||||||
|
before: @timestamp,
|
||||||
|
limit: @limit
|
||||||
|
}, @callback)
|
||||||
|
|
||||||
|
it "should call the callback with an error", ->
|
||||||
|
@callback.calledWith("unknown room").should.equal true
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Messages/MessageFormatter.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
ObjectId = require("mongojs").ObjectId
|
||||||
|
|
||||||
|
describe "MessageFormatter", ->
|
||||||
|
beforeEach ->
|
||||||
|
@MessageFormatter = SandboxedModule.require modulePath, requires: {}
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
should = require('chai').should()
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
assert = require('assert')
|
||||||
|
path = require('path')
|
||||||
|
sinon = require('sinon')
|
||||||
|
modulePath = path.join __dirname, "../../../../app/js/Features/Messages/MessageHttpController"
|
||||||
|
expect = require("chai").expect
|
||||||
|
tk = require("timekeeper")
|
||||||
|
|
||||||
|
describe "MessagesHttpController", ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
|
||||||
|
@settings = {}
|
||||||
|
@date = Date.now()
|
||||||
|
tk.freeze(@date)
|
||||||
|
@MessagesHttpController = SandboxedModule.require modulePath, requires:
|
||||||
|
"settings-sharelatex":@settings
|
||||||
|
"logger-sharelatex": log:->
|
||||||
|
"./MessageManager": @MessageManager = {}
|
||||||
|
"./MessageFormatter": @MessageFormatter = {}
|
||||||
|
"../Rooms/RoomManager": @RoomManager = {}
|
||||||
|
|
||||||
|
@req =
|
||||||
|
body:{}
|
||||||
|
@res = {}
|
||||||
|
@project_id = "12321321"
|
||||||
|
@room_id = "Asdfadf adfafd"
|
||||||
|
@user_id = "09832910838239081203981"
|
||||||
|
@content = "my message here"
|
||||||
|
|
||||||
|
|
||||||
|
afterEach ->
|
||||||
|
tk.reset()
|
||||||
|
|
||||||
|
describe "sendMessage", ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
@initialMessage = {content:@content}
|
||||||
|
@MessageManager.createMessage = sinon.stub().callsArgWith(1, null, @initialMessage)
|
||||||
|
@req.params =
|
||||||
|
project_id:@project_id
|
||||||
|
@req.body =
|
||||||
|
user_id:@user_id
|
||||||
|
content:@content
|
||||||
|
@singlePopulatedMessage = {data:"here"}
|
||||||
|
@MessageManager.populateMessagesWithUsers = sinon.stub().callsArgWith(1, null, [@singlePopulatedMessage])
|
||||||
|
@RoomManager.findOrCreateRoom = sinon.stub().callsArgWith(1, null, @room = { _id : @room_id })
|
||||||
|
@formattedMessage = {formatted:true}
|
||||||
|
@MessageFormatter.formatMessageForClientSide = sinon.stub().returns(@formattedMessage)
|
||||||
|
|
||||||
|
it "should look up the room for the project", ->
|
||||||
|
@res.send = =>
|
||||||
|
@RoomManager.findOrCreateRoom
|
||||||
|
.calledWith({
|
||||||
|
project_id: @project_id
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should create the message with the message manager", (done)->
|
||||||
|
@res.send = =>
|
||||||
|
@MessageManager.createMessage
|
||||||
|
.calledWith({
|
||||||
|
content: @content
|
||||||
|
user_id: @user_id
|
||||||
|
room_id: @room_id
|
||||||
|
timestamp: @date
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
done()
|
||||||
|
@MessagesHttpController.sendMessage @req, @res
|
||||||
|
|
||||||
|
|
||||||
|
it "should return the formetted message", (done)->
|
||||||
|
|
||||||
|
@res.send = (code, data)=>
|
||||||
|
assert.deepEqual @MessageManager.populateMessagesWithUsers.args[0][0], [@initialMessage]
|
||||||
|
code.should.equal 201
|
||||||
|
data.should.equal @formattedMessage
|
||||||
|
done()
|
||||||
|
|
||||||
|
@MessagesHttpController.sendMessage @req, @res
|
||||||
|
|
||||||
|
|
||||||
|
describe "getMessages", ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
@project_id = "room-id-123"
|
||||||
|
@timestamp = Date.now()
|
||||||
|
@limit = 42
|
||||||
|
|
||||||
|
@messages = "messages without users stub"
|
||||||
|
@messagesWithUsers = "messages with users stub"
|
||||||
|
@formattedMessages = "formatted messages stub"
|
||||||
|
@RoomManager.findOrCreateRoom = sinon.stub().callsArgWith(1, null, @room = { _id : @room_id })
|
||||||
|
@MessageManager.getMessages = sinon.stub().callsArgWith(2, null, @messages)
|
||||||
|
@MessageManager.populateMessagesWithUsers = sinon.stub().callsArgWith(1, null, @messagesWithUsers)
|
||||||
|
@MessageFormatter.formatMessagesForClientSide = sinon.stub().returns @formattedMessages
|
||||||
|
|
||||||
|
|
||||||
|
describe "with a timestamp and limit", ->
|
||||||
|
beforeEach ->
|
||||||
|
@req.params =
|
||||||
|
project_id:@project_id
|
||||||
|
@req.query =
|
||||||
|
before: @timestamp,
|
||||||
|
limit: "#{@limit}"
|
||||||
|
|
||||||
|
|
||||||
|
it "should look up the room for the project", ->
|
||||||
|
@res.send = =>
|
||||||
|
@RoomManager.findOrCreateRoom
|
||||||
|
.calledWith({
|
||||||
|
project_id: @project_id
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
done()
|
||||||
|
|
||||||
|
it "should get the requested messages", ->
|
||||||
|
@res.send = =>
|
||||||
|
@MessageManager.getMessages
|
||||||
|
.calledWith({
|
||||||
|
timestamp: $lt: @timestamp
|
||||||
|
room_id: @room_id
|
||||||
|
}, {
|
||||||
|
limit: @limit
|
||||||
|
order_by: "timestamp"
|
||||||
|
sort_order: -1
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
@MessagesHttpController.getMessages(@req, @res)
|
||||||
|
|
||||||
|
it "should populate the messages with the users", (done)->
|
||||||
|
@res.send = =>
|
||||||
|
@MessageManager.populateMessagesWithUsers.calledWith(@messages).should.equal true
|
||||||
|
done()
|
||||||
|
|
||||||
|
@MessagesHttpController.getMessages(@req, @res)
|
||||||
|
|
||||||
|
it "should return the formatted messages", (done)->
|
||||||
|
@res.send = ()=>
|
||||||
|
@MessageFormatter.formatMessagesForClientSide.calledWith(@messagesWithUsers).should.equal true
|
||||||
|
done()
|
||||||
|
@MessagesHttpController.getMessages(@req, @res)
|
||||||
|
|
||||||
|
it "should send the formated messages back with a 200", (done)->
|
||||||
|
@res.send = (code, data)=>
|
||||||
|
code.should.equal 200
|
||||||
|
data.should.equal @formattedMessages
|
||||||
|
done()
|
||||||
|
@MessagesHttpController.getMessages(@req, @res)
|
||||||
|
|
||||||
|
describe "without a timestamp or limit", ->
|
||||||
|
beforeEach ->
|
||||||
|
@req.params =
|
||||||
|
project_id:@project_id
|
||||||
|
|
||||||
|
|
||||||
|
it "should get a default number of messages from the beginning", ->
|
||||||
|
@res.send = =>
|
||||||
|
@MessageManager.getMessages
|
||||||
|
.calledWith({
|
||||||
|
room_id: @room_id
|
||||||
|
}, {
|
||||||
|
limit: @MessagesHttpController.DEFAULT_MESSAGE_LIMIT
|
||||||
|
order_by: "timestamp"
|
||||||
|
sort_order: -1
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
@MessagesHttpController.getMessages(@req, @res)
|
|
@ -0,0 +1,60 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Messages/MessageManager.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
ObjectId = require("mongojs").ObjectId
|
||||||
|
|
||||||
|
describe "MessageManager", ->
|
||||||
|
beforeEach ->
|
||||||
|
@MessageManager = SandboxedModule.require modulePath, requires:
|
||||||
|
"../WebApi/WebApiManager": @WebApiManager = {}
|
||||||
|
@callback = sinon.stub()
|
||||||
|
|
||||||
|
describe "populateMessagesWithUsers", ->
|
||||||
|
beforeEach ->
|
||||||
|
@user0 =
|
||||||
|
id: ObjectId().toString()
|
||||||
|
first_name: "Adam"
|
||||||
|
@user1 =
|
||||||
|
id: ObjectId().toString()
|
||||||
|
first_name: "Eve"
|
||||||
|
@users = {}
|
||||||
|
@users[@user0.id] = @user0
|
||||||
|
@users[@user1.id] = @user1
|
||||||
|
@messages = [{
|
||||||
|
content: "First message content"
|
||||||
|
user_id: ObjectId(@user0.id)
|
||||||
|
}, {
|
||||||
|
content: "Second message content"
|
||||||
|
user_id: ObjectId(@user0.id)
|
||||||
|
}, {
|
||||||
|
content: "Third message content"
|
||||||
|
user_id: ObjectId(@user1.id)
|
||||||
|
}]
|
||||||
|
@WebApiManager.getUserDetails = (user_id, callback = (error, user) ->) =>
|
||||||
|
callback null, @users[user_id]
|
||||||
|
sinon.spy @WebApiManager, "getUserDetails"
|
||||||
|
@MessageManager.populateMessagesWithUsers @messages, @callback
|
||||||
|
|
||||||
|
it "should insert user objects in the place of user_ids", ->
|
||||||
|
messages = @callback.args[0][1]
|
||||||
|
expect(messages).to.deep.equal [{
|
||||||
|
content: "First message content"
|
||||||
|
user: @user0
|
||||||
|
}, {
|
||||||
|
content: "Second message content"
|
||||||
|
user: @user0
|
||||||
|
}, {
|
||||||
|
content: "Third message content"
|
||||||
|
user: @user1
|
||||||
|
}]
|
||||||
|
|
||||||
|
it "should call getUserDetails once and only once for each user", ->
|
||||||
|
@WebApiManager.getUserDetails.calledWith(@user0.id).should.equal true
|
||||||
|
@WebApiManager.getUserDetails.calledWith(@user1.id).should.equal true
|
||||||
|
@WebApiManager.getUserDetails.calledTwice.should.equal true
|
||||||
|
|
||||||
|
|
240
services/chat/test/unit/coffee/Rooms/RoomControllerTests.coffee
Normal file
240
services/chat/test/unit/coffee/Rooms/RoomControllerTests.coffee
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Rooms/RoomController.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
ObjectId = require("mongojs").ObjectId
|
||||||
|
|
||||||
|
class MockClient
|
||||||
|
params: {}
|
||||||
|
get: (key, callback = (error, value) ->) ->
|
||||||
|
callback null, @params[key]
|
||||||
|
|
||||||
|
describe "RoomController", ->
|
||||||
|
beforeEach ->
|
||||||
|
@SocketManager =
|
||||||
|
getClientAttributes: sinon.stub()
|
||||||
|
|
||||||
|
|
||||||
|
@RoomController = SandboxedModule.require modulePath, requires:
|
||||||
|
"../Authorization/AuthorizationManager": @AuthorizationManager = {}
|
||||||
|
"../Sockets/SocketManager": @SocketManager
|
||||||
|
"../Rooms/RoomManager": @RoomManager = {}
|
||||||
|
"logger-sharelatex": @logger = { log: sinon.stub() }
|
||||||
|
|
||||||
|
|
||||||
|
@project_id = ObjectId().toString()
|
||||||
|
@room_id = ObjectId().toString()
|
||||||
|
@room =
|
||||||
|
_id: ObjectId(@room_id)
|
||||||
|
project_id: ObjectId(@project_id)
|
||||||
|
@callback = sinon.stub()
|
||||||
|
@client =
|
||||||
|
params: {}
|
||||||
|
get: (key, callback = (error, value) ->) -> callback null, @params[key]
|
||||||
|
|
||||||
|
describe "joinRoom", ->
|
||||||
|
describe "when the client is authorized", ->
|
||||||
|
beforeEach ->
|
||||||
|
@AuthorizationManager.canClientJoinProjectRoom = sinon.stub().callsArgWith(2, null, true)
|
||||||
|
@RoomManager.findOrCreateRoom = sinon.stub().callsArgWith(1, null, @room)
|
||||||
|
@RoomController._addClientToRoom = sinon.stub().callsArg(2)
|
||||||
|
@RoomController._getClientsInRoom = sinon.stub().callsArgWith(1, null, @clients = ["client1", "client2"])
|
||||||
|
@RoomController.joinRoom @client, { room: project_id: @project_id }, @callback
|
||||||
|
|
||||||
|
it "should check that the client can join the room", ->
|
||||||
|
@AuthorizationManager.canClientJoinProjectRoom
|
||||||
|
.calledWith(@client, @project_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should ensure that the room exists", ->
|
||||||
|
@RoomManager.findOrCreateRoom
|
||||||
|
.calledWith({ project_id: @project_id })
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should put the client into the room", ->
|
||||||
|
@RoomController._addClientToRoom
|
||||||
|
.calledWith(@client, @room_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should get the clients already in the room", ->
|
||||||
|
@RoomController._getClientsInRoom
|
||||||
|
.calledWith(@room_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback with the room id", ->
|
||||||
|
@callback.calledWith(null, {
|
||||||
|
room:
|
||||||
|
id: @room_id
|
||||||
|
connectedUsers: @clients
|
||||||
|
}).should.equal true
|
||||||
|
|
||||||
|
describe "when the client is not authorized", ->
|
||||||
|
beforeEach ->
|
||||||
|
@AuthorizationManager.canClientJoinProjectRoom = sinon.stub().callsArgWith(2, null, false)
|
||||||
|
@RoomController._addClientToRoom = sinon.stub().callsArg(2)
|
||||||
|
@RoomController.joinRoom @client, { room: project_id: @project_id }, @callback
|
||||||
|
|
||||||
|
it "should not put the client into the room", ->
|
||||||
|
@RoomController._addClientToRoom.called.should.equal false
|
||||||
|
|
||||||
|
it "should call the callback with an error that gives nothing away", ->
|
||||||
|
@callback.calledWith("unknown room").should.equal true
|
||||||
|
|
||||||
|
describe "leaveAllRooms", ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
@client = new MockClient()
|
||||||
|
@client.params =
|
||||||
|
id: "client-1-id"
|
||||||
|
first_name: "Douglas"
|
||||||
|
last_name: "Adams"
|
||||||
|
email: "doug@sharelatex.com"
|
||||||
|
gravatar_url: "//gravatar/url/1"
|
||||||
|
@room_ids = ["room-id-1", "room-id-2"]
|
||||||
|
@SocketManager.getRoomIdsClientHasJoined = sinon.stub().callsArgWith(1, null, @room_ids)
|
||||||
|
@RoomController.leaveRoom = sinon.stub().callsArg(2)
|
||||||
|
@RoomController.leaveAllRooms @client, @callback
|
||||||
|
|
||||||
|
it "should get the rooms the client has joined", ->
|
||||||
|
@SocketManager.getRoomIdsClientHasJoined
|
||||||
|
.calledWith(@client)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should leave each room", ->
|
||||||
|
for room_id in @room_ids
|
||||||
|
@RoomController.leaveRoom
|
||||||
|
.calledWith(@client, room_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback", ->
|
||||||
|
@callback.called.should.equal true
|
||||||
|
|
||||||
|
describe "leaveRoom", ->
|
||||||
|
beforeEach ->
|
||||||
|
@client = new MockClient()
|
||||||
|
@client.params =
|
||||||
|
id: "client-1-id"
|
||||||
|
first_name: "Douglas"
|
||||||
|
last_name: "Adams"
|
||||||
|
email: "doug@sharelatex.com"
|
||||||
|
gravatar_url: "//gravatar/url/1"
|
||||||
|
@RoomController._getClientAttributes = sinon.stub().callsArgWith(1, null, @client.params)
|
||||||
|
|
||||||
|
@SocketManager.removeClientFromRoom = sinon.stub().callsArg(2)
|
||||||
|
@SocketManager.emitToRoom = sinon.stub()
|
||||||
|
@RoomController.leaveRoom @client, @room_id, @callback
|
||||||
|
|
||||||
|
it "should leave the room", ->
|
||||||
|
@SocketManager.removeClientFromRoom
|
||||||
|
.calledWith(@client, @room_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should tell the other clients in the room that we have left", ->
|
||||||
|
@SocketManager.emitToRoom
|
||||||
|
.calledWith(@room_id, "userLeft", {
|
||||||
|
room:
|
||||||
|
id: @room_id
|
||||||
|
user:
|
||||||
|
id : @client.params["id"]
|
||||||
|
first_name : @client.params["first_name"]
|
||||||
|
last_name : @client.params["last_name"]
|
||||||
|
email : @client.params["email"]
|
||||||
|
gravatar_url : @client.params["gravatar_url"]
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback", ->
|
||||||
|
@callback.called.should.equal true
|
||||||
|
|
||||||
|
describe "_getClientsInRoom", ->
|
||||||
|
beforeEach ->
|
||||||
|
@client1 = new MockClient()
|
||||||
|
@client1.params =
|
||||||
|
id: "client-1-id"
|
||||||
|
first_name: "Douglas"
|
||||||
|
last_name: "Adams"
|
||||||
|
email: "doug@sharelatex.com"
|
||||||
|
gravatar_url: "//gravatar/url/1"
|
||||||
|
@client2 = new MockClient()
|
||||||
|
@client2.params =
|
||||||
|
id: "client-2-id"
|
||||||
|
first_name: "James"
|
||||||
|
last_name: "Allen"
|
||||||
|
email: "james@sharelatex.com"
|
||||||
|
gravatar_url: "//gravatar/url/2"
|
||||||
|
@clients = [ @client1, @client2 ]
|
||||||
|
callCount = 0
|
||||||
|
|
||||||
|
@RoomController._getClientAttributes = (ignore, cb)=>
|
||||||
|
if callCount == 0
|
||||||
|
callCount++
|
||||||
|
cb(null, @client1.params)
|
||||||
|
else
|
||||||
|
cb null, @client2.params
|
||||||
|
|
||||||
|
|
||||||
|
@SocketManager.getClientsInRoom = sinon.stub().callsArgWith(1, null, @clients)
|
||||||
|
@RoomController._getClientsInRoom(@room_id, @callback)
|
||||||
|
|
||||||
|
it "should get the socket.io clients in the room", ->
|
||||||
|
@SocketManager.getClientsInRoom
|
||||||
|
.calledWith(@room_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return a formatted array of clients", ->
|
||||||
|
@callback
|
||||||
|
.calledWith(null, [{
|
||||||
|
id : @client1.params["id"]
|
||||||
|
first_name : @client1.params["first_name"]
|
||||||
|
last_name : @client1.params["last_name"]
|
||||||
|
email : @client1.params["email"]
|
||||||
|
gravatar_url : @client1.params["gravatar_url"]
|
||||||
|
}, {
|
||||||
|
id : @client2.params["id"]
|
||||||
|
first_name : @client2.params["first_name"]
|
||||||
|
last_name : @client2.params["last_name"]
|
||||||
|
email : @client2.params["email"]
|
||||||
|
gravatar_url : @client2.params["gravatar_url"]
|
||||||
|
}])
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "_addClientToRoom", ->
|
||||||
|
beforeEach ->
|
||||||
|
@client = new MockClient()
|
||||||
|
@client.params =
|
||||||
|
id: "client-1-id"
|
||||||
|
first_name: "Douglas"
|
||||||
|
last_name: "Adams"
|
||||||
|
email: "doug@sharelatex.com"
|
||||||
|
gravatar_url: "//gravatar/url/1"
|
||||||
|
@RoomController._getClientAttributes = sinon.stub().callsArgWith(1, null, @client.params)
|
||||||
|
@SocketManager.addClientToRoom = sinon.stub().callsArg(2)
|
||||||
|
@SocketManager.emitToRoom = sinon.stub()
|
||||||
|
@RoomController._addClientToRoom(@client, @room_id, @callback)
|
||||||
|
|
||||||
|
it "should add the client to the room", ->
|
||||||
|
@SocketManager.addClientToRoom
|
||||||
|
.calledWith(@client, @room_id)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should tell the room that the client has been added", ->
|
||||||
|
@SocketManager.emitToRoom
|
||||||
|
.calledWith(@room_id, "userJoined", {
|
||||||
|
room:
|
||||||
|
id: @room_id
|
||||||
|
user:
|
||||||
|
id : @client.params["id"]
|
||||||
|
first_name : @client.params["first_name"]
|
||||||
|
last_name : @client.params["last_name"]
|
||||||
|
email : @client.params["email"]
|
||||||
|
gravatar_url : @client.params["gravatar_url"]
|
||||||
|
})
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should call the callback", ->
|
||||||
|
@callback.called.should.equal true
|
||||||
|
|
||||||
|
|
58
services/chat/test/unit/coffee/Rooms/RoomManagerTests.coffee
Normal file
58
services/chat/test/unit/coffee/Rooms/RoomManagerTests.coffee
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Rooms/RoomManager.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
mongojs = require "mongojs"
|
||||||
|
ObjectId = mongojs.ObjectId
|
||||||
|
|
||||||
|
describe "RoomManager", ->
|
||||||
|
beforeEach ->
|
||||||
|
@RoomManager = SandboxedModule.require modulePath, requires:
|
||||||
|
"../../mongojs":
|
||||||
|
db: @db = { rooms: {} }
|
||||||
|
ObjectId: ObjectId
|
||||||
|
@callback = sinon.stub()
|
||||||
|
|
||||||
|
describe "findOrCreateRoom", ->
|
||||||
|
describe "when the room exists", ->
|
||||||
|
beforeEach ->
|
||||||
|
@project_id = ObjectId().toString()
|
||||||
|
@room =
|
||||||
|
_id: ObjectId()
|
||||||
|
project_id: ObjectId(@project_id)
|
||||||
|
@db.rooms.findOne = sinon.stub().callsArgWith(1, null, @room)
|
||||||
|
@RoomManager.findOrCreateRoom(project_id: @project_id, @callback)
|
||||||
|
|
||||||
|
it "should look up the room based on the query", ->
|
||||||
|
@db.rooms.findOne
|
||||||
|
.calledWith(project_id: ObjectId(@project_id))
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the room in the callback", ->
|
||||||
|
@callback
|
||||||
|
.calledWith(null, @room)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "when the room does not exist", ->
|
||||||
|
beforeEach ->
|
||||||
|
@project_id = ObjectId().toString()
|
||||||
|
@room =
|
||||||
|
_id: ObjectId()
|
||||||
|
project_id: ObjectId(@project_id)
|
||||||
|
@db.rooms.findOne = sinon.stub().callsArgWith(1, null, null)
|
||||||
|
@db.rooms.save = sinon.stub().callsArgWith(1, null, @room)
|
||||||
|
@RoomManager.findOrCreateRoom(project_id: @project_id, @callback)
|
||||||
|
|
||||||
|
it "should create the room", ->
|
||||||
|
@db.rooms.save
|
||||||
|
.calledWith(project_id: ObjectId(@project_id))
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should return the room in the callback", ->
|
||||||
|
@callback
|
||||||
|
.calledWith(null, @room)
|
||||||
|
.should.equal true
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
sinon = require('sinon')
|
||||||
|
require('chai').should()
|
||||||
|
modulePath = "../../../../app/js/Features/Sockets/RealTimeEventManager.js"
|
||||||
|
|
||||||
|
describe "RealTimeEventManager", ->
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
@settings =
|
||||||
|
redis:
|
||||||
|
web:
|
||||||
|
host: "host here"
|
||||||
|
port: "port here"
|
||||||
|
password: "password here"
|
||||||
|
@RealTimeEventManager = SandboxedModule.require modulePath, requires:
|
||||||
|
"redis":
|
||||||
|
createClient: () ->
|
||||||
|
auth:->
|
||||||
|
"../../server" : io: @io = {}
|
||||||
|
"settings-sharelatex":@settings
|
||||||
|
@RealTimeEventManager.rclientPub = publish: sinon.stub()
|
||||||
|
@RealTimeEventManager.rclientSub =
|
||||||
|
subscribe: sinon.stub()
|
||||||
|
on: sinon.stub()
|
||||||
|
|
||||||
|
@room_id = "room-id-here"
|
||||||
|
@message = "message-to-chat-here"
|
||||||
|
@payload = ["argument one", 42]
|
||||||
|
|
||||||
|
describe "emitToRoom", ->
|
||||||
|
beforeEach ->
|
||||||
|
@RealTimeEventManager.emitToRoom(@room_id, @message, @payload...)
|
||||||
|
|
||||||
|
it "should publish the message to redis", ->
|
||||||
|
@RealTimeEventManager.rclientPub.publish
|
||||||
|
.calledWith("chat-events", JSON.stringify(
|
||||||
|
room_id: @room_id,
|
||||||
|
message: @message
|
||||||
|
payload: @payload
|
||||||
|
))
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "listenForChatEvents", ->
|
||||||
|
beforeEach ->
|
||||||
|
@RealTimeEventManager._processEditorEvent = sinon.stub()
|
||||||
|
@RealTimeEventManager.listenForChatEvents()
|
||||||
|
|
||||||
|
it "should subscribe to the chat-events channel", ->
|
||||||
|
@RealTimeEventManager.rclientSub.subscribe
|
||||||
|
.calledWith("chat-events")
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
it "should process the events with _processEditorEvent", ->
|
||||||
|
@RealTimeEventManager.rclientSub.on
|
||||||
|
.calledWith("message", sinon.match.func)
|
||||||
|
.should.equal true
|
||||||
|
|
||||||
|
describe "_processEditorEvent", ->
|
||||||
|
describe "with a designated room", ->
|
||||||
|
beforeEach ->
|
||||||
|
@io.sockets =
|
||||||
|
in: sinon.stub().returns(emit: @emit = sinon.stub())
|
||||||
|
data = JSON.stringify
|
||||||
|
room_id: @room_id
|
||||||
|
message: @message
|
||||||
|
payload: @payload
|
||||||
|
@RealTimeEventManager._processEditorEvent("chat-events", data)
|
||||||
|
|
||||||
|
it "should send the message to all clients in the room", ->
|
||||||
|
@io.sockets.in
|
||||||
|
.calledWith(@room_id)
|
||||||
|
.should.equal true
|
||||||
|
@emit.calledWith(@message, @payload...).should.equal true
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
sinon = require('sinon')
|
||||||
|
chai = require('chai')
|
||||||
|
should = chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
modulePath = "../../../../app/js/Features/Users/UserFormatter.js"
|
||||||
|
SandboxedModule = require('sandboxed-module')
|
||||||
|
events = require "events"
|
||||||
|
|
||||||
|
describe "UserFormatter", ->
|
||||||
|
beforeEach ->
|
||||||
|
@UserFormatter = SandboxedModule.require modulePath, requires: {}
|
||||||
|
|
Loading…
Reference in a new issue