Initial Open Source commit

This commit is contained in:
James Allen
2014-08-15 10:50:36 +01:00
commit 8cb71e8fad
52 changed files with 41511 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
**.swp
app.js
app/js/
test/unit/js/
public/build/
node_modules/
/public/js/chat.js
plato/
+124
View 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
View 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
@@ -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
@@ -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
@@ -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
View 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
View 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
View 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
View 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()
@@ -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
View 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
View 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
View 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
@@ -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
@@ -0,0 +1,5 @@
define [
"libs/backbone"
], (Backbone) ->
Message = Backbone.Model.extend {}
@@ -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
@@ -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
@@ -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)
@@ -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
View 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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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));
});
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+373
View 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
View 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,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);
}
}
.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
View 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
@@ -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
@@ -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: {}