diff --git a/services/real-time/app/coffee/Router.coffee b/services/real-time/app/coffee/Router.coffee index f8aedbf46b..43ec7976f0 100644 --- a/services/real-time/app/coffee/Router.coffee +++ b/services/real-time/app/coffee/Router.coffee @@ -5,6 +5,16 @@ HttpController = require "./HttpController" Utils = require "./Utils" module.exports = Router = + _handleError: (callback, error, client, method, extraAttrs = {}) -> + Utils.getClientAttributes client, ["project_id", "doc_id", "user_id"], (_, attrs) -> + for key, value of extraAttrs + attrs[key] = value + attrs.client_id = client.id + attrs.err = error + logger.error attrs, "server side error in #{method}" + # Don't return raw error to prevent leaking server side info + return callback {message: "Something went wrong"} + configure: (app, io, session) -> app.set("io", io) app.get "/clients", HttpController.getConnectedClients @@ -29,9 +39,7 @@ module.exports = Router = client.on "joinProject", (data = {}, callback) -> WebsocketController.joinProject client, user, data.project_id, (err, args...) -> if err? - logger.error {err, user_id: user?.id, project_id: data.project_id}, "server side error in joinProject" - # Don't return raw error to prevent leaking server side info - return callback {message: "Something went wrong"} + Router._handleError callback, err, client, "joinProject", {project_id: data.project_id, user_id: user?.id} else callback(null, args...) @@ -44,30 +52,27 @@ module.exports = Router = WebsocketController.joinDoc client, doc_id, fromVersion, (err, args...) -> if err? - Utils.getClientAttributes client, ["project_id", "user_id"], (_, {project_id, user_id}) -> - logger.error {err, client_id: client.id, user_id, project_id, doc_id, fromVersion}, "server side error in joinDoc" - # Don't return raw error to prevent leaking server side info - return callback {message: "Something went wrong"} + Router._handleError callback, err, client, "joinDoc", {doc_id, fromVersion} else callback(null, args...) client.on "leaveDoc", (doc_id, callback) -> WebsocketController.leaveDoc client, doc_id, (err, args...) -> if err? - Utils.getClientAttributes client, ["project_id", "user_id"], (_, {project_id, user_id}) -> - logger.error {err, client_id: client.id, user_id, project_id, doc_id}, "server side error in leaveDoc" - # Don't return raw error to prevent leaking server side info - return callback {message: "Something went wrong"} + Router._handleError callback, err, client, "leaveDoc" else callback(null, args...) - client.on "getConnectedUsers", (callback = (error, users) ->) -> + client.on "clientTracking.getConnectedUsers", (callback = (error, users) ->) -> WebsocketController.getConnectedUsers client, (err, users) -> if err? - Utils.getClientAttributes client, ["project_id", "user_id", "doc_id"], (_, {project_id, user_id, doc_id}) -> - logger.error {err, client_id: client.id, user_id, project_id, doc_id}, "server side error in getConnectedUsers" - # Don't return raw error to prevent leaking server side info - return callback {message: "Something went wrong"} + Router._handleError callback, err, client, "clientTracking.getConnectedUsers" else callback(null, users) - \ No newline at end of file + + client.on "clientTracking.updatePosition", (cursorData, callback = (error) ->) -> + WebsocketController.updateClientPosition client, cursorData, (err) -> + if err? + Router._handleError callback, err, client, "clientTracking.updatePosition" + else + callback() \ No newline at end of file diff --git a/services/real-time/app/coffee/WebsocketController.coffee b/services/real-time/app/coffee/WebsocketController.coffee index 7fa4ca6f01..1e7860daf0 100644 --- a/services/real-time/app/coffee/WebsocketController.coffee +++ b/services/real-time/app/coffee/WebsocketController.coffee @@ -70,7 +70,37 @@ module.exports = WebsocketController = client.leave doc_id callback() + updateClientPosition: (client, cursorData, callback = (error) ->) -> + Utils.getClientAttributes client, [ + "project_id", "first_name", "last_name", "email", "user_id" + ], (error, {project_id, first_name, last_name, email, user_id}) -> + return callback(error) if error? + logger.log {user_id, project_id, client_id: client.id, cursorData: cursorData}, "updating client position" + cursorData.id = client.id + cursorData.user_id = user_id if user_id? + cursorData.email = email if email? + if first_name? and last_name? + cursorData.name = first_name + " " + last_name + ConnectedUsersManager.updateUserPosition(project_id, client.id, { + first_name: first_name, + last_name: last_name, + email: email, + user_id: user_id + }, { + row: cursorData.row, + column: cursorData.column, + doc_id: cursorData.doc_id + }, callback) + else + cursorData.name = "Anonymous" + callback() + #EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData) + #callback() + getConnectedUsers: (client, callback = (error, users) ->) -> + Utils.getClientAttributes client, ["project_id", "user_id"], (error, {project_id, user_id}) -> + logger.log {user_id, project_id, client_id: client.id}, "getting connected users" + AuthorizationManager.assertClientCanViewProject client, (error) -> return callback(error) if error? client.get "project_id", (error, project_id) -> diff --git a/services/real-time/test/acceptance/coffee/ClientTrackingTests.coffee b/services/real-time/test/acceptance/coffee/ClientTrackingTests.coffee new file mode 100644 index 0000000000..391030a1c5 --- /dev/null +++ b/services/real-time/test/acceptance/coffee/ClientTrackingTests.coffee @@ -0,0 +1,61 @@ +chai = require("chai") +expect = chai.expect +chai.should() + +RealTimeClient = require "./helpers/RealTimeClient" +MockWebServer = require "./helpers/MockWebServer" +FixturesManager = require "./helpers/FixturesManager" + +describe "clientTracking", -> + before (done) -> + FixturesManager.setUpProject { + privilegeLevel: "owner" + project: { + name: "Test Project" + } + }, (error, data) => + throw error if error? + {@user_id, @project_id} = data + @clientA = RealTimeClient.connect() + @clientB = RealTimeClient.connect() + @clientA.emit "joinProject", { + project_id: @project_id + }, (error) => + throw error if error? + @clientB.emit "joinProject", { + project_id: @project_id + }, (error) => + throw error if error? + done() + + describe "when a client updates its cursor location", -> + before (done) -> + @updates = [] + @clientB.on "clientTracking.clientUpdated", (data) -> + @updates.push data + + @clientA.emit "clientTracking.updatePosition", { + row: @row = 42 + column: @column = 36 + doc_id: @doc_id = "mock-doc-id" + }, (error) -> + throw error if error? + done() + + it "should tell other clients about the update" + + it "should record the update in getConnectedUsers", (done) -> + @clientB.emit "clientTracking.getConnectedUsers", (error, users) => + for user in users + if user.client_id == @clientA.socket.sessionid + expect(user.cursorData).to.deep.equal({ + row: @row + column: @column + doc_id: @doc_id + }) + return done() + throw new Error("user was never found") + + + describe "anonymous users", -> + it "should test something..." diff --git a/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee b/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee index 48b8732080..63633dec24 100644 --- a/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee +++ b/services/real-time/test/acceptance/coffee/JoinProjectTests.coffee @@ -47,7 +47,7 @@ describe "joinProject", -> done() it "should have marked the user as connected", (done) -> - @client.emit "getConnectedUsers", (error, users) => + @client.emit "clientTracking.getConnectedUsers", (error, users) => connected = false for user in users if user.client_id == @client.socket.sessionid and user.user_id == @user_id diff --git a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee index 5c2eb5fd3a..ad584ce608 100644 --- a/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee +++ b/services/real-time/test/acceptance/coffee/helpers/FixturesManager.coffee @@ -16,7 +16,11 @@ module.exports = FixturesManager = MockWebServer.run (error) => throw error if error? RealTimeClient.setSession { - user: { _id: user_id } + user: { + _id: user_id + first_name: "Joe" + last_name: "Bloggs" + } }, (error) => throw error if error? callback null, {project_id, user_id, privilegeLevel, project} diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee b/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee index 976e113291..8aedca2366 100644 --- a/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee +++ b/services/real-time/test/unit/coffee/WebsocketControllerTests.coffee @@ -217,4 +217,72 @@ describe 'WebsocketController', -> it "should return an error", -> @callback.calledWith(@err).should.equal true - \ No newline at end of file + describe "updateClientPosition", -> + beforeEach -> + #@EditorRealTimeController.emitToRoom = sinon.stub() + @ConnectedUsersManager.updateUserPosition = sinon.stub().callsArgWith(4) + @update = { + doc_id: @doc_id = "doc-id-123" + row: @row = 42 + column: @column = 37 + } + + describe "with a logged in user", -> + beforeEach -> + @clientParams = { + project_id: @project_id + first_name: @first_name = "Douglas" + last_name: @last_name = "Adams" + email: @email = "joe@example.com" + user_id: @user_id = "user-id-123" + } + @client.get = (param, callback) => callback null, @clientParams[param] + @WebsocketController.updateClientPosition @client, @update + + @populatedCursorData = + doc_id: @doc_id, + id: @client.id + name: "#{@first_name} #{@last_name}" + row: @row + column: @column + email: @email + user_id: @user_id + + # it "should send the update to the project room with the user's name", -> + # @EditorRealTimeController.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true + + it "should send the cursor data to the connected user manager", (done)-> + @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.id, { + user_id: @user_id, + email: @email, + first_name: @first_name, + last_name: @last_name + }, { + row: @row + column: @column + doc_id: @doc_id + }).should.equal true + done() + + describe "with an anonymous user", -> + beforeEach -> + @clientParams = { + project_id: @project_id + } + @client.get = (param, callback) => callback null, @clientParams[param] + @WebsocketController.updateClientPosition @client, @update + + # it "should send the update to the project room with an anonymous name", -> + # @EditorRealTimeController.emitToRoom + # .calledWith(@project_id, "clientTracking.clientUpdated", { + # doc_id: @doc_id, + # id: @client.id + # name: "Anonymous" + # row: @row + # column: @column + # }) + # .should.equal true + + it "should not send cursor data to the connected user manager", (done)-> + @ConnectedUsersManager.updateUserPosition.called.should.equal false + done() \ No newline at end of file