Browse Source

Merge branch 'master' into csh-ho-docker-issue-1338-bulk-upgrade

Christopher Hoskin 7 years ago
parent
commit
1d8e52cdd5
41 changed files with 5960 additions and 1083 deletions
  1. 75 0
      services/chat/.eslintrc
  2. 0 5
      services/chat/.gitignore
  3. 4 0
      services/chat/.prettierrc
  4. 0 97
      services/chat/Gruntfile.coffee
  5. 129 0
      services/chat/Gruntfile.js
  6. 0 13
      services/chat/app.coffee
  7. 39 0
      services/chat/app.js
  8. 0 46
      services/chat/app/coffee/Features/Messages/MessageFormatter.coffee
  9. 0 112
      services/chat/app/coffee/Features/Messages/MessageHttpController.coffee
  10. 0 76
      services/chat/app/coffee/Features/Messages/MessageManager.coffee
  11. 0 88
      services/chat/app/coffee/Features/Threads/ThreadManager.coffee
  12. 0 6
      services/chat/app/coffee/mongojs.coffee
  13. 0 39
      services/chat/app/coffee/router.coffee
  14. 0 39
      services/chat/app/coffee/server.coffee
  15. 80 0
      services/chat/app/js/Features/Messages/MessageFormatter.js
  16. 258 0
      services/chat/app/js/Features/Messages/MessageHttpController.js
  17. 146 0
      services/chat/app/js/Features/Messages/MessageManager.js
  18. 159 0
      services/chat/app/js/Features/Threads/ThreadManager.js
  19. 9 0
      services/chat/app/js/mongojs.js
  20. 84 0
      services/chat/app/js/router.js
  21. 50 0
      services/chat/app/js/server.js
  22. 0 23
      services/chat/config/settings.defaults.coffee
  23. 30 0
      services/chat/config/settings.defaults.js
  24. 53 0
      services/chat/decaffeinate.sh
  25. 4001 125
      services/chat/npm-shrinkwrap.json
  26. 0 32
      services/chat/test/acceptance/coffee/DeletingAMessageTests.coffee
  27. 0 31
      services/chat/test/acceptance/coffee/DeletingAThreadTests.coffee
  28. 0 34
      services/chat/test/acceptance/coffee/EditingAMessageTests.coffee
  29. 0 64
      services/chat/test/acceptance/coffee/GettingMessagesTests.coffee
  30. 0 72
      services/chat/test/acceptance/coffee/ResolvingAThreadTests.coffee
  31. 0 101
      services/chat/test/acceptance/coffee/SendingAMessageTests.coffee
  32. 0 20
      services/chat/test/acceptance/coffee/helpers/ChatApp.coffee
  33. 0 60
      services/chat/test/acceptance/coffee/helpers/ChatClient.coffee
  34. 73 0
      services/chat/test/acceptance/js/DeletingAMessageTests.js
  35. 63 0
      services/chat/test/acceptance/js/DeletingAThreadTests.js
  36. 72 0
      services/chat/test/acceptance/js/EditingAMessageTests.js
  37. 138 0
      services/chat/test/acceptance/js/GettingMessagesTests.js
  38. 147 0
      services/chat/test/acceptance/js/ResolvingAThreadTests.js
  39. 190 0
      services/chat/test/acceptance/js/SendingAMessageTests.js
  40. 47 0
      services/chat/test/acceptance/js/helpers/ChatApp.js
  41. 113 0
      services/chat/test/acceptance/js/helpers/ChatClient.js

+ 75 - 0
services/chat/.eslintrc

@@ -0,0 +1,75 @@
+{
+  "extends": [
+    "standard",
+    "standard-react",
+    "prettier",
+    "prettier/react",
+    "prettier/standard",
+    "plugin:jsx-a11y/recommended"
+  ],
+  "plugins": [
+    "prettier",
+    "jsx-a11y",
+    "mocha",
+    "chai-expect",
+    "chai-friendly"
+  ],
+  "env": {
+    "mocha": true
+  },
+  "globals": {
+    "expect": true,
+    "define": true,
+    "$": true,
+    "angular": true,
+    // Injected in layout.pug
+    "user_id": true
+  },
+  "settings": {
+    // Tell eslint-plugin-react which version of React we are using
+    "react": {
+      "version": "15"
+    }
+  },
+  "rules": {
+    "max-len": ["error", {
+      "ignoreUrls": true,
+      // Ignore long describe/it test blocks, long import/require statements
+      "ignorePattern": "(^\\s*(it|describe)\\s*\\(['\"]|^import\\s*.*\\s*from\\s*['\"]|^.*\\s*=\\s*require\\(['\"])"
+    }],
+
+    // Fix conflict between prettier & standard by overriding to prefer
+    // double quotes
+    "jsx-quotes": ["error", "prefer-double"],
+
+    // Override weird behaviour of jsx-a11y label-has-for (says labels must be
+    // nested *and* have for/id attributes)
+    "jsx-a11y/label-has-for": [
+      "error",
+      {
+        "required": {
+          "some": [
+            "nesting",
+            "id"
+          ]
+        }
+      }
+    ],
+
+    // Add some mocha specific rules
+    "mocha/handle-done-callback": "error",
+    "mocha/no-exclusive-tests": "error",
+    "mocha/no-global-tests": "error",
+    "mocha/no-identical-title": "error",
+    "mocha/no-nested-tests": "error",
+    "mocha/no-pending-tests": "error",
+    "mocha/no-skipped-tests": "error",
+
+    // Add some chai specific rules
+    "chai-expect/missing-assertion": "error",
+    "chai-expect/terminating-properties": "error",
+    // Swap the no-unused-expressions rule with a more chai-friendly one
+    "no-unused-expressions": 0,
+    "chai-friendly/no-unused-expressions": "error"
+  }
+}

+ 0 - 5
services/chat/.gitignore

@@ -1,12 +1,7 @@
 **.swp
 
-app.js
-app/js/
-test/unit/js/
-test/acceptance/js/
 public/build/
 
 node_modules/
 
-/public/js/chat.js
 plato/

+ 4 - 0
services/chat/.prettierrc

@@ -0,0 +1,4 @@
+{
+  "semi": false,
+  "singleQuote": true
+}

+ 0 - 97
services/chat/Gruntfile.coffee

@@ -1,97 +0,0 @@
-module.exports = (grunt) ->
-
-	# Project configuration.
-	grunt.initConfig
-		forever:
-			app:
-				options:
-					index: "app.js"
-
-		execute:
-			app:
-				src: "app.js"
-
-		coffee:
-			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'
-
-			unit_tests:
-				expand: true,
-				flatten: false,
-				cwd: 'test/unit/coffee',
-				src: ['**/*.coffee'],
-				dest: 'test/unit/js/',
-				ext: '.js'
-
-			acceptance_tests:
-				expand: true,
-				flatten: false,
-				cwd: 'test/acceptance/coffee',
-				src: ['**/*.coffee'],
-				dest: 'test/acceptance/js/',
-				ext: '.js'
-
-		watch:
-			server_coffee:
-				files: ['app/**/*.coffee', 'test/unit/**/*.coffee']
-				tasks: ['compile:server', 'compile:unit_tests', 'mochaTest']
-
-		clean: ["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/unit/**/*.js']
-			acceptance:
-				options:
-					reporter: process.env.MOCHA_RUNNER || "spec"
-					grep: grunt.option("grep")
-				src: ['test/acceptance/**/*.js']
-
-		plato:
-			your_task:
-				files: 'plato': ['app/js/**/*.js'],
-
-	grunt.loadNpmTasks 'grunt-contrib-coffee'
-	grunt.loadNpmTasks 'grunt-contrib-watch'
-	grunt.loadNpmTasks 'grunt-nodemon'
-	grunt.loadNpmTasks 'grunt-contrib-clean'
-	grunt.loadNpmTasks 'grunt-concurrent'
-	grunt.loadNpmTasks 'grunt-mocha-test'
-	grunt.loadNpmTasks 'grunt-plato'
-	grunt.loadNpmTasks 'grunt-execute'
-	grunt.loadNpmTasks 'grunt-bunyan'
-	grunt.loadNpmTasks 'grunt-forever'
-	
-
-	grunt.registerTask 'compile', ['clean', 'coffee']
-	grunt.registerTask 'install', ['compile']
-	grunt.registerTask 'default', ['compile', 'bunyan', 'execute']
-	grunt.registerTask 'test:unit', ['compile', 'mochaTest:unit']
-	grunt.registerTask 'test:acceptance', ['compile:acceptance_tests', 'mochaTest:acceptance']
-

+ 129 - 0
services/chat/Gruntfile.js

@@ -0,0 +1,129 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+module.exports = function(grunt) {
+  // Project configuration.
+  grunt.initConfig({
+    forever: {
+      app: {
+        options: {
+          index: 'app.js'
+        }
+      }
+    },
+
+    execute: {
+      app: {
+        src: 'app.js'
+      }
+    },
+
+    coffee: {
+      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'
+      },
+
+      unit_tests: {
+        expand: true,
+        flatten: false,
+        cwd: 'test/unit/coffee',
+        src: ['**/*.coffee'],
+        dest: 'test/unit/js/',
+        ext: '.js'
+      },
+
+      acceptance_tests: {
+        expand: true,
+        flatten: false,
+        cwd: 'test/acceptance/coffee',
+        src: ['**/*.coffee'],
+        dest: 'test/acceptance/js/',
+        ext: '.js'
+      }
+    },
+
+    watch: {
+      server_coffee: {
+        files: ['app/**/*.coffee', 'test/unit/**/*.coffee'],
+        tasks: ['compile:server', 'compile:unit_tests', 'mochaTest']
+      }
+    },
+
+    clean: ['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/unit/**/*.js']
+      },
+      acceptance: {
+        options: {
+          reporter: process.env.MOCHA_RUNNER || 'spec',
+          grep: grunt.option('grep')
+        },
+        src: ['test/acceptance/**/*.js']
+      }
+    },
+
+    plato: {
+      your_task: {
+        files: { plato: ['app/js/**/*.js'] }
+      }
+    }
+  })
+
+  grunt.loadNpmTasks('grunt-contrib-coffee')
+  grunt.loadNpmTasks('grunt-contrib-watch')
+  grunt.loadNpmTasks('grunt-nodemon')
+  grunt.loadNpmTasks('grunt-contrib-clean')
+  grunt.loadNpmTasks('grunt-concurrent')
+  grunt.loadNpmTasks('grunt-mocha-test')
+  grunt.loadNpmTasks('grunt-plato')
+  grunt.loadNpmTasks('grunt-execute')
+  grunt.loadNpmTasks('grunt-bunyan')
+  grunt.loadNpmTasks('grunt-forever')
+
+  grunt.registerTask('compile', ['clean', 'coffee'])
+  grunt.registerTask('install', ['compile'])
+  grunt.registerTask('default', ['compile', 'bunyan', 'execute'])
+  grunt.registerTask('test:unit', ['compile', 'mochaTest:unit'])
+  return grunt.registerTask('test:acceptance', [
+    'compile:acceptance_tests',
+    'mochaTest:acceptance'
+  ])
+}

+ 0 - 13
services/chat/app.coffee

@@ -1,13 +0,0 @@
-logger = require 'logger-sharelatex'
-settings = require 'settings-sharelatex'
-
-Server = require "./app/js/server"
-
-if !module.parent # Called directly
-	port = settings.internal?.chat?.port or 3010
-	host = settings.internal?.chat?.host or "localhost"
-	Server.server.listen port, host, (error) ->
-		throw error if error?
-		logger.info "Chat starting up, listening on #{host}:#{port}"
-
-module.exports = Server.server

+ 39 - 0
services/chat/app.js

@@ -0,0 +1,39 @@
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS103: Rewrite code to no longer use __guard__
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const logger = require('logger-sharelatex')
+const settings = require('settings-sharelatex')
+
+const Server = require('./app/js/server')
+
+if (!module.parent) {
+  // Called directly
+  const port =
+    __guard__(
+      settings.internal != null ? settings.internal.chat : undefined,
+      x => x.port
+    ) || 3010
+  const host =
+    __guard__(
+      settings.internal != null ? settings.internal.chat : undefined,
+      x1 => x1.host
+    ) || 'localhost'
+  Server.server.listen(port, host, function(error) {
+    if (error != null) {
+      throw error
+    }
+    return logger.info(`Chat starting up, listening on ${host}:${port}`)
+  })
+}
+
+module.exports = Server.server
+
+function __guard__(value, transform) {
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}

+ 0 - 46
services/chat/app/coffee/Features/Messages/MessageFormatter.coffee

@@ -1,46 +0,0 @@
-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_id: message.user_id
-		if message.edited_at?
-			formattedMessage.edited_at = message.edited_at
-		return formattedMessage
-
-	formatMessagesForClientSide: (messages) ->
-		(@formatMessageForClientSide(message) for message in messages)
-	
-	groupMessagesByThreads: (rooms, messages) ->
-		rooms_by_id = {}
-		for room in rooms
-			rooms_by_id[room._id.toString()] = room
-
-		threads = {}
-		getThread = (room) ->
-			thread_id = room.thread_id.toString()
-			if threads[thread_id]?
-				return threads[thread_id]
-			else
-				thread = { messages: [] }
-				if room.resolved?
-					thread.resolved = true
-					thread.resolved_at = room.resolved.ts
-					thread.resolved_by_user_id = room.resolved.user_id
-				threads[thread_id] = thread
-				return thread
-			
-		for message in messages
-			room = rooms_by_id[message.room_id.toString()]
-			if room?
-				thread = getThread(room)
-				thread.messages.push MessageFormatter.formatMessageForClientSide(message)
-		
-		for thread_id, thread of threads
-			thread.messages.sort (a,b) -> a.timestamp - b.timestamp
-		
-		return threads

+ 0 - 112
services/chat/app/coffee/Features/Messages/MessageHttpController.coffee

@@ -1,112 +0,0 @@
-logger = require "logger-sharelatex"
-metrics = require "metrics-sharelatex"
-MessageManager = require "./MessageManager"
-MessageFormatter = require "./MessageFormatter"
-ThreadManager = require "../Threads/ThreadManager"
-{ObjectId} = require "../../mongojs"
-
-module.exports = MessageHttpController =
-	DEFAULT_MESSAGE_LIMIT: 50
-	MAX_MESSAGE_LENGTH: 10 * 1024 # 10kb, about 1,500 words
-	
-	getGlobalMessages: (req, res, next) ->
-		MessageHttpController._getMessages(ThreadManager.GLOBAL_THREAD, req, res, next)
-
-	sendGlobalMessage: (req, res, next) ->
-		MessageHttpController._sendMessage(ThreadManager.GLOBAL_THREAD, req, res, next)
-	
-	sendThreadMessage: (req, res, next) ->
-		MessageHttpController._sendMessage(req.params.thread_id, req, res, next)
-	
-	getAllThreads: (req, res, next) ->
-		{project_id} = req.params
-		logger.log {project_id}, "getting all threads"
-		ThreadManager.findAllThreadRooms project_id, (error, rooms) ->
-			return next(error) if error?
-			room_ids = rooms.map (r) -> r._id
-			MessageManager.findAllMessagesInRooms room_ids, (error, messages) ->
-				return next(error) if error?
-				threads = MessageFormatter.groupMessagesByThreads rooms, messages
-				res.json threads
-	
-	resolveThread: (req, res, next) ->
-		{project_id, thread_id} = req.params
-		{user_id} = req.body
-		logger.log {user_id, project_id, thread_id}, "marking thread as resolved"
-		ThreadManager.resolveThread project_id, thread_id, user_id, (error) ->
-			return next(error) if error?
-			res.send 204 # No content
-
-	reopenThread: (req, res, next) ->
-		{project_id, thread_id} = req.params
-		logger.log {project_id, thread_id}, "reopening thread"
-		ThreadManager.reopenThread project_id, thread_id, (error) ->
-			return next(error) if error?
-			res.send 204 # No content
-	
-	deleteThread: (req, res, next) ->
-		{project_id, thread_id} = req.params
-		logger.log {project_id, thread_id}, "deleting thread"
-		ThreadManager.deleteThread project_id, thread_id, (error, room_id) ->
-			return next(error) if error?
-			MessageManager.deleteAllMessagesInRoom room_id, (error) ->
-				return next(error) if error?
-				res.send 204 # No content
-	
-	editMessage: (req, res, next) ->
-		{content} = req?.body
-		{project_id, thread_id, message_id} = req.params
-		logger.log {project_id, thread_id, message_id, content}, "editing message"
-		ThreadManager.findOrCreateThread project_id, thread_id, (error, room) ->
-			return next(error) if error?
-			MessageManager.updateMessage room._id, message_id, content, Date.now(), (error) ->
-				return next(error) if error?
-				res.send(204)
-
-	deleteMessage: (req, res, next) ->
-		{project_id, thread_id, message_id} = req.params
-		logger.log {project_id, thread_id, message_id}, "deleting message"
-		ThreadManager.findOrCreateThread project_id, thread_id, (error, room) ->
-			return next(error) if error?
-			MessageManager.deleteMessage room._id, message_id, (error, message) ->
-				return next(error) if error?
-				res.send(204)
-
-	_sendMessage: (client_thread_id, req, res, next) ->
-		{user_id, content} = req?.body
-		{project_id} = req.params
-		if !ObjectId.isValid(user_id)
-			return res.send(400, "Invalid user_id")
-		if !content?
-			return res.send(400, "No content provided")
-		if content.length > @MAX_MESSAGE_LENGTH
-			return res.send(400, "Content too long (> #{@MAX_MESSAGE_LENGTH} bytes)")
-		logger.log {client_thread_id, project_id, user_id, content}, "new message received"
-		ThreadManager.findOrCreateThread project_id, client_thread_id, (error, thread) ->
-			return next(error) if error?
-			MessageManager.createMessage thread._id, user_id, content, Date.now(), (error, message) ->
-				return next(error) if error?
-				message = MessageFormatter.formatMessageForClientSide(message)
-				message.room_id = project_id
-				res.send(201, message)
-
-	_getMessages: (client_thread_id, req, res, next) ->
-		{project_id} = req.params
-		if req.query?.before?
-			before = parseInt(req.query.before, 10)
-		else
-			before = null
-		if req.query?.limit?
-			limit = parseInt(req.query.limit, 10)
-		else
-			limit = MessageHttpController.DEFAULT_MESSAGE_LIMIT
-		logger.log {limit, before, project_id, client_thread_id}, "get message request received"
-		ThreadManager.findOrCreateThread project_id, client_thread_id, (error, thread) ->
-			return next(error) if error?
-			thread_object_id = thread._id
-			logger.log {limit, before, project_id, client_thread_id, thread_object_id}, "found or created thread"
-			MessageManager.getMessages thread_object_id, limit, before, (error, messages) ->
-				return next(error) if error?
-				messages = MessageFormatter.formatMessagesForClientSide messages
-				logger.log {project_id, messages}, "got messages"
-				res.send 200, messages

+ 0 - 76
services/chat/app/coffee/Features/Messages/MessageManager.coffee

@@ -1,76 +0,0 @@
-mongojs = require "../../mongojs"
-db = mongojs.db
-ObjectId = mongojs.ObjectId
-async = require "async"
-metrics = require 'metrics-sharelatex'
-logger = require 'logger-sharelatex'
-
-module.exports = MessageManager =
-	createMessage: (room_id, user_id, content, timestamp, callback = (error, message) ->) ->
-		newMessageOpts = 
-			content: content
-			room_id: room_id
-			user_id: user_id
-			timestamp: timestamp
-		newMessageOpts = @_ensureIdsAreObjectIds(newMessageOpts)
-		db.messages.save newMessageOpts, callback
-
-	getMessages: (room_id, limit, before, callback = (error, messages) ->) ->
-		query =
-			room_id: room_id
-		if before?
-			query.timestamp = { $lt: before }
-		query = @_ensureIdsAreObjectIds(query)
-		cursor = db.messages.find(query).sort({ timestamp: -1 }).limit(limit)
-		cursor.toArray callback
-	
-	findAllMessagesInRooms: (room_ids, callback = (error, messages) ->) ->
-		db.messages.find {
-			room_id: { $in: room_ids }
-		}, callback
-
-	deleteAllMessagesInRoom: (room_id, callback = (error) ->) ->
-		db.messages.remove {
-			room_id: room_id
-		}, callback
-	
-	updateMessage: (room_id, message_id, content, timestamp, callback = (error, message) ->) ->
-		query = @_ensureIdsAreObjectIds(
-			_id: message_id
-			room_id: room_id
-		)
-		db.messages.update query, {
-			$set:
-				content: content
-				edited_at: timestamp
-		}, (error) ->
-			return callback(error) if error?
-			return callback()
-
-	deleteMessage: (room_id, message_id, callback = (error) ->) ->
-		query = @_ensureIdsAreObjectIds(
-			_id: message_id
-			room_id: room_id
-		)
-		db.messages.remove query, (error) ->
-			return callback(error) if error?
-			return 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)
-		if query._id? and query._id not instanceof ObjectId
-			query._id = ObjectId(query._id)
-		return query
-
-
-[
-	'createMessage',
-	'getMessages',
-	'findAllMessagesInRooms',
-	'updateMessage',
-	'deleteMessage'
-].map (method) ->
-	metrics.timeAsyncMethod(MessageManager, method, 'mongo.MessageManager', logger)

+ 0 - 88
services/chat/app/coffee/Features/Threads/ThreadManager.coffee

@@ -1,88 +0,0 @@
-mongojs = require("../../mongojs")
-db = mongojs.db
-ObjectId = mongojs.ObjectId
-logger = require('logger-sharelatex')
-metrics = require('metrics-sharelatex')
-
-module.exports = ThreadManager =
-	GLOBAL_THREAD: "GLOBAL"
-
-	findOrCreateThread: (project_id, thread_id, callback = (error, thread) ->) ->
-		project_id = ObjectId(project_id.toString())
-		if thread_id != ThreadManager.GLOBAL_THREAD
-			thread_id = ObjectId(thread_id.toString())
-
-		if thread_id == ThreadManager.GLOBAL_THREAD
-			query = {
-				project_id: project_id
-				thread_id: { $exists: false }
-			}
-			update = {
-				project_id: project_id
-			}
-		else
-			query = {
-				project_id: project_id
-				thread_id: thread_id
-			}
-			update = {
-				project_id: project_id
-				thread_id: thread_id
-			}
-		
-		db.rooms.update query, update, { upsert: true }, (error) ->
-			return callback(error) if error?
-			db.rooms.find query, (error, rooms = []) ->
-				return callback(error) if error?
-				return callback null, rooms[0]
-	
-	findAllThreadRooms: (project_id, callback = (error, rooms) ->) ->
-		db.rooms.find {
-			project_id: ObjectId(project_id.toString())
-			thread_id: { $exists: true }
-		}, {
-			thread_id: 1,
-			resolved: 1
-		}, callback
-	
-	resolveThread: (project_id, thread_id, user_id, callback = (error) ->) ->
-		db.rooms.update {
-			project_id: ObjectId(project_id.toString())
-			thread_id: ObjectId(thread_id.toString())
-		}, {
-			$set: {
-				resolved: {
-					user_id: user_id
-					ts: new Date()
-				}
-			}
-		}, callback
-	
-	reopenThread: (project_id, thread_id, callback = (error) ->) ->
-		db.rooms.update {
-			project_id: ObjectId(project_id.toString())
-			thread_id: ObjectId(thread_id.toString())
-		}, {
-			$unset: {
-				resolved: true
-			}
-		}, callback
-
-	deleteThread: (project_id, thread_id, callback = (error, room_id) ->) ->
-		@findOrCreateThread project_id, thread_id, (error, room) ->
-			return callback(error) if error?
-			db.rooms.remove {
-				_id: room._id
-			}, (error) ->
-				return callback(error) if error?
-				return callback null, room._id
-
-
-[
-	 'findOrCreateThread',
-	 'findAllThreadRooms',
-	 'resolveThread',
-	 'reopenThread',
-	 'deleteThread',
-].map (method) ->
-	metrics.timeAsyncMethod(ThreadManager, method, 'mongo.ThreadManager', logger)

+ 0 - 6
services/chat/app/coffee/mongojs.coffee

@@ -1,6 +0,0 @@
-Settings = require("settings-sharelatex")
-mongojs = require "mongojs"
-db = mongojs(Settings.mongo.url, ["rooms", "messages"])
-module.exports =
-	db: db
-	ObjectId: mongojs.ObjectId

+ 0 - 39
services/chat/app/coffee/router.coffee

@@ -1,39 +0,0 @@
-MessageHttpController = require('./Features/Messages/MessageHttpController')
-{ObjectId} = require "./mongojs"
-
-module.exports = Router =
-	route: (app) ->
-		app.param 'project_id', (req, res, next, project_id) ->
-			if ObjectId.isValid(project_id)
-				next()
-			else
-				res.send 400, "Invalid project_id"
-
-		app.param 'thread_id', (req, res, next, thread_id) ->
-			if ObjectId.isValid(thread_id)
-				next()
-			else
-				res.send 400, "Invalid thread_id"
-
-		# These are for backwards compatibility
-		app.get  "/room/:project_id/messages", MessageHttpController.getGlobalMessages
-		app.post "/room/:project_id/messages", MessageHttpController.sendGlobalMessage
-
-		app.get  "/project/:project_id/messages", MessageHttpController.getGlobalMessages
-		app.post "/project/:project_id/messages", MessageHttpController.sendGlobalMessage
-
-		app.post "/project/:project_id/thread/:thread_id/messages", MessageHttpController.sendThreadMessage
-		app.get  "/project/:project_id/threads", MessageHttpController.getAllThreads
-
-		app.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", MessageHttpController.editMessage
-		app.del  "/project/:project_id/thread/:thread_id/messages/:message_id", MessageHttpController.deleteMessage
-
-		app.post "/project/:project_id/thread/:thread_id/resolve", MessageHttpController.resolveThread
-		app.post "/project/:project_id/thread/:thread_id/reopen", MessageHttpController.reopenThread
-		app.del  "/project/:project_id/thread/:thread_id", MessageHttpController.deleteThread
-
-		app.get "/status", (req, res, next) ->
-			res.send("chat is alive")
-
-
-			

+ 0 - 39
services/chat/app/coffee/server.coffee

@@ -1,39 +0,0 @@
-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)
-Router = require "./router"
-
-app.use express.bodyParser()
-app.use metrics.http.monitor(logger)
-
-if (app.get 'env') == 'development'
-	console.log "Development Enviroment"
-	app.use express.errorHandler({ dumpExceptions: true, showStack: true })
-
-if (app.get 'env') == 'production'
-	console.log "Production Enviroment"
-	app.use express.logger()
-	app.use express.errorHandler()
-	
-profiler = require "v8-profiler"
-app.get "/profile", (req, res) ->
-	time = parseInt(req.query.time || "1000")
-	profiler.startProfiling("test")
-	setTimeout () ->
-		profile = profiler.stopProfiling("test")
-		res.json(profile)
-	, time
-
-Router.route(app)
-
-module.exports = {
-	server: server
-	app: app
-}
-
-

+ 80 - 0
services/chat/app/js/Features/Messages/MessageFormatter.js

@@ -0,0 +1,80 @@
+/* eslint-disable
+    camelcase,
+    max-len,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let MessageFormatter
+module.exports = MessageFormatter = {
+  formatMessageForClientSide(message) {
+    if (message._id != null) {
+      message.id = message._id.toString()
+      delete message._id
+    }
+    const formattedMessage = {
+      id: message.id,
+      content: message.content,
+      timestamp: message.timestamp,
+      user_id: message.user_id
+    }
+    if (message.edited_at != null) {
+      formattedMessage.edited_at = message.edited_at
+    }
+    return formattedMessage
+  },
+
+  formatMessagesForClientSide(messages) {
+    return Array.from(messages).map(message =>
+      this.formatMessageForClientSide(message)
+    )
+  },
+
+  groupMessagesByThreads(rooms, messages) {
+    let room, thread
+    const rooms_by_id = {}
+    for (room of Array.from(rooms)) {
+      rooms_by_id[room._id.toString()] = room
+    }
+
+    const threads = {}
+    const getThread = function(room) {
+      const thread_id = room.thread_id.toString()
+      if (threads[thread_id] != null) {
+        return threads[thread_id]
+      } else {
+        const thread = { messages: [] }
+        if (room.resolved != null) {
+          thread.resolved = true
+          thread.resolved_at = room.resolved.ts
+          thread.resolved_by_user_id = room.resolved.user_id
+        }
+        threads[thread_id] = thread
+        return thread
+      }
+    }
+
+    for (let message of Array.from(messages)) {
+      room = rooms_by_id[message.room_id.toString()]
+      if (room != null) {
+        thread = getThread(room)
+        thread.messages.push(
+          MessageFormatter.formatMessageForClientSide(message)
+        )
+      }
+    }
+
+    for (let thread_id in threads) {
+      thread = threads[thread_id]
+      thread.messages.sort((a, b) => a.timestamp - b.timestamp)
+    }
+
+    return threads
+  }
+}

+ 258 - 0
services/chat/app/js/Features/Messages/MessageHttpController.js

@@ -0,0 +1,258 @@
+/* eslint-disable
+    camelcase,
+    max-len,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let MessageHttpController
+const logger = require('logger-sharelatex')
+const metrics = require('metrics-sharelatex')
+const MessageManager = require('./MessageManager')
+const MessageFormatter = require('./MessageFormatter')
+const ThreadManager = require('../Threads/ThreadManager')
+const { ObjectId } = require('../../mongojs')
+
+module.exports = MessageHttpController = {
+  DEFAULT_MESSAGE_LIMIT: 50,
+  MAX_MESSAGE_LENGTH: 10 * 1024, // 10kb, about 1,500 words
+
+  getGlobalMessages(req, res, next) {
+    return MessageHttpController._getMessages(
+      ThreadManager.GLOBAL_THREAD,
+      req,
+      res,
+      next
+    )
+  },
+
+  sendGlobalMessage(req, res, next) {
+    return MessageHttpController._sendMessage(
+      ThreadManager.GLOBAL_THREAD,
+      req,
+      res,
+      next
+    )
+  },
+
+  sendThreadMessage(req, res, next) {
+    return MessageHttpController._sendMessage(
+      req.params.thread_id,
+      req,
+      res,
+      next
+    )
+  },
+
+  getAllThreads(req, res, next) {
+    const { project_id } = req.params
+    logger.log({ project_id }, 'getting all threads')
+    return ThreadManager.findAllThreadRooms(project_id, function(error, rooms) {
+      if (error != null) {
+        return next(error)
+      }
+      const room_ids = rooms.map(r => r._id)
+      return MessageManager.findAllMessagesInRooms(room_ids, function(
+        error,
+        messages
+      ) {
+        if (error != null) {
+          return next(error)
+        }
+        const threads = MessageFormatter.groupMessagesByThreads(rooms, messages)
+        return res.json(threads)
+      })
+    })
+  },
+
+  resolveThread(req, res, next) {
+    const { project_id, thread_id } = req.params
+    const { user_id } = req.body
+    logger.log({ user_id, project_id, thread_id }, 'marking thread as resolved')
+    return ThreadManager.resolveThread(project_id, thread_id, user_id, function(
+      error
+    ) {
+      if (error != null) {
+        return next(error)
+      }
+      return res.send(204)
+    })
+  }, // No content
+
+  reopenThread(req, res, next) {
+    const { project_id, thread_id } = req.params
+    logger.log({ project_id, thread_id }, 'reopening thread')
+    return ThreadManager.reopenThread(project_id, thread_id, function(error) {
+      if (error != null) {
+        return next(error)
+      }
+      return res.send(204)
+    })
+  }, // No content
+
+  deleteThread(req, res, next) {
+    const { project_id, thread_id } = req.params
+    logger.log({ project_id, thread_id }, 'deleting thread')
+    return ThreadManager.deleteThread(project_id, thread_id, function(
+      error,
+      room_id
+    ) {
+      if (error != null) {
+        return next(error)
+      }
+      return MessageManager.deleteAllMessagesInRoom(room_id, function(error) {
+        if (error != null) {
+          return next(error)
+        }
+        return res.send(204)
+      })
+    })
+  }, // No content
+
+  editMessage(req, res, next) {
+    const { content } = req != null ? req.body : undefined
+    const { project_id, thread_id, message_id } = req.params
+    logger.log(
+      { project_id, thread_id, message_id, content },
+      'editing message'
+    )
+    return ThreadManager.findOrCreateThread(project_id, thread_id, function(
+      error,
+      room
+    ) {
+      if (error != null) {
+        return next(error)
+      }
+      return MessageManager.updateMessage(
+        room._id,
+        message_id,
+        content,
+        Date.now(),
+        function(error) {
+          if (error != null) {
+            return next(error)
+          }
+          return res.send(204)
+        }
+      )
+    })
+  },
+
+  deleteMessage(req, res, next) {
+    const { project_id, thread_id, message_id } = req.params
+    logger.log({ project_id, thread_id, message_id }, 'deleting message')
+    return ThreadManager.findOrCreateThread(project_id, thread_id, function(
+      error,
+      room
+    ) {
+      if (error != null) {
+        return next(error)
+      }
+      return MessageManager.deleteMessage(room._id, message_id, function(
+        error,
+        message
+      ) {
+        if (error != null) {
+          return next(error)
+        }
+        return res.send(204)
+      })
+    })
+  },
+
+  _sendMessage(client_thread_id, req, res, next) {
+    const { user_id, content } = req != null ? req.body : undefined
+    const { project_id } = req.params
+    if (!ObjectId.isValid(user_id)) {
+      return res.send(400, 'Invalid user_id')
+    }
+    if (content == null) {
+      return res.send(400, 'No content provided')
+    }
+    if (content.length > this.MAX_MESSAGE_LENGTH) {
+      return res.send(
+        400,
+        `Content too long (> ${this.MAX_MESSAGE_LENGTH} bytes)`
+      )
+    }
+    logger.log(
+      { client_thread_id, project_id, user_id, content },
+      'new message received'
+    )
+    return ThreadManager.findOrCreateThread(
+      project_id,
+      client_thread_id,
+      function(error, thread) {
+        if (error != null) {
+          return next(error)
+        }
+        return MessageManager.createMessage(
+          thread._id,
+          user_id,
+          content,
+          Date.now(),
+          function(error, message) {
+            if (error != null) {
+              return next(error)
+            }
+            message = MessageFormatter.formatMessageForClientSide(message)
+            message.room_id = project_id
+            return res.send(201, message)
+          }
+        )
+      }
+    )
+  },
+
+  _getMessages(client_thread_id, req, res, next) {
+    let before, limit
+    const { project_id } = req.params
+    if ((req.query != null ? req.query.before : undefined) != null) {
+      before = parseInt(req.query.before, 10)
+    } else {
+      before = null
+    }
+    if ((req.query != null ? req.query.limit : undefined) != null) {
+      limit = parseInt(req.query.limit, 10)
+    } else {
+      limit = MessageHttpController.DEFAULT_MESSAGE_LIMIT
+    }
+    logger.log(
+      { limit, before, project_id, client_thread_id },
+      'get message request received'
+    )
+    return ThreadManager.findOrCreateThread(
+      project_id,
+      client_thread_id,
+      function(error, thread) {
+        if (error != null) {
+          return next(error)
+        }
+        const thread_object_id = thread._id
+        logger.log(
+          { limit, before, project_id, client_thread_id, thread_object_id },
+          'found or created thread'
+        )
+        return MessageManager.getMessages(
+          thread_object_id,
+          limit,
+          before,
+          function(error, messages) {
+            if (error != null) {
+              return next(error)
+            }
+            messages = MessageFormatter.formatMessagesForClientSide(messages)
+            logger.log({ project_id, messages }, 'got messages')
+            return res.send(200, messages)
+          }
+        )
+      }
+    )
+  }
+}

+ 146 - 0
services/chat/app/js/Features/Messages/MessageManager.js

@@ -0,0 +1,146 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    max-len,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let MessageManager
+const mongojs = require('../../mongojs')
+const { db } = mongojs
+const { ObjectId } = mongojs
+const async = require('async')
+const metrics = require('metrics-sharelatex')
+const logger = require('logger-sharelatex')
+
+module.exports = MessageManager = {
+  createMessage(room_id, user_id, content, timestamp, callback) {
+    if (callback == null) {
+      callback = function(error, message) {}
+    }
+    let newMessageOpts = {
+      content,
+      room_id,
+      user_id,
+      timestamp
+    }
+    newMessageOpts = this._ensureIdsAreObjectIds(newMessageOpts)
+    return db.messages.save(newMessageOpts, callback)
+  },
+
+  getMessages(room_id, limit, before, callback) {
+    if (callback == null) {
+      callback = function(error, messages) {}
+    }
+    let query = { room_id }
+    if (before != null) {
+      query.timestamp = { $lt: before }
+    }
+    query = this._ensureIdsAreObjectIds(query)
+    const cursor = db.messages
+      .find(query)
+      .sort({ timestamp: -1 })
+      .limit(limit)
+    return cursor.toArray(callback)
+  },
+
+  findAllMessagesInRooms(room_ids, callback) {
+    if (callback == null) {
+      callback = function(error, messages) {}
+    }
+    return db.messages.find(
+      {
+        room_id: { $in: room_ids }
+      },
+      callback
+    )
+  },
+
+  deleteAllMessagesInRoom(room_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return db.messages.remove(
+      {
+        room_id
+      },
+      callback
+    )
+  },
+
+  updateMessage(room_id, message_id, content, timestamp, callback) {
+    if (callback == null) {
+      callback = function(error, message) {}
+    }
+    const query = this._ensureIdsAreObjectIds({
+      _id: message_id,
+      room_id
+    })
+    return db.messages.update(
+      query,
+      {
+        $set: {
+          content,
+          edited_at: timestamp
+        }
+      },
+      function(error) {
+        if (error != null) {
+          return callback(error)
+        }
+        return callback()
+      }
+    )
+  },
+
+  deleteMessage(room_id, message_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    const query = this._ensureIdsAreObjectIds({
+      _id: message_id,
+      room_id
+    })
+    return db.messages.remove(query, function(error) {
+      if (error != null) {
+        return callback(error)
+      }
+      return callback()
+    })
+  },
+
+  _ensureIdsAreObjectIds(query) {
+    if (query.user_id != null && !(query.user_id instanceof ObjectId)) {
+      query.user_id = ObjectId(query.user_id)
+    }
+    if (query.room_id != null && !(query.room_id instanceof ObjectId)) {
+      query.room_id = ObjectId(query.room_id)
+    }
+    if (query._id != null && !(query._id instanceof ObjectId)) {
+      query._id = ObjectId(query._id)
+    }
+    return query
+  }
+}
+
+;[
+  'createMessage',
+  'getMessages',
+  'findAllMessagesInRooms',
+  'updateMessage',
+  'deleteMessage'
+].map(method =>
+  metrics.timeAsyncMethod(
+    MessageManager,
+    method,
+    'mongo.MessageManager',
+    logger
+  )
+)

+ 159 - 0
services/chat/app/js/Features/Threads/ThreadManager.js

@@ -0,0 +1,159 @@
+/* eslint-disable
+    camelcase,
+    handle-callback-err,
+    max-len,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let ThreadManager
+const mongojs = require('../../mongojs')
+const { db } = mongojs
+const { ObjectId } = mongojs
+const logger = require('logger-sharelatex')
+const metrics = require('metrics-sharelatex')
+
+module.exports = ThreadManager = {
+  GLOBAL_THREAD: 'GLOBAL',
+
+  findOrCreateThread(project_id, thread_id, callback) {
+    let query, update
+    if (callback == null) {
+      callback = function(error, thread) {}
+    }
+    project_id = ObjectId(project_id.toString())
+    if (thread_id !== ThreadManager.GLOBAL_THREAD) {
+      thread_id = ObjectId(thread_id.toString())
+    }
+
+    if (thread_id === ThreadManager.GLOBAL_THREAD) {
+      query = {
+        project_id,
+        thread_id: { $exists: false }
+      }
+      update = {
+        project_id
+      }
+    } else {
+      query = {
+        project_id,
+        thread_id
+      }
+      update = {
+        project_id,
+        thread_id
+      }
+    }
+
+    return db.rooms.update(query, update, { upsert: true }, function(error) {
+      if (error != null) {
+        return callback(error)
+      }
+      return db.rooms.find(query, function(error, rooms) {
+        if (rooms == null) {
+          rooms = []
+        }
+        if (error != null) {
+          return callback(error)
+        }
+        return callback(null, rooms[0])
+      })
+    })
+  },
+
+  findAllThreadRooms(project_id, callback) {
+    if (callback == null) {
+      callback = function(error, rooms) {}
+    }
+    return db.rooms.find(
+      {
+        project_id: ObjectId(project_id.toString()),
+        thread_id: { $exists: true }
+      },
+      {
+        thread_id: 1,
+        resolved: 1
+      },
+      callback
+    )
+  },
+
+  resolveThread(project_id, thread_id, user_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return db.rooms.update(
+      {
+        project_id: ObjectId(project_id.toString()),
+        thread_id: ObjectId(thread_id.toString())
+      },
+      {
+        $set: {
+          resolved: {
+            user_id,
+            ts: new Date()
+          }
+        }
+      },
+      callback
+    )
+  },
+
+  reopenThread(project_id, thread_id, callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    return db.rooms.update(
+      {
+        project_id: ObjectId(project_id.toString()),
+        thread_id: ObjectId(thread_id.toString())
+      },
+      {
+        $unset: {
+          resolved: true
+        }
+      },
+      callback
+    )
+  },
+
+  deleteThread(project_id, thread_id, callback) {
+    if (callback == null) {
+      callback = function(error, room_id) {}
+    }
+    return this.findOrCreateThread(project_id, thread_id, function(
+      error,
+      room
+    ) {
+      if (error != null) {
+        return callback(error)
+      }
+      return db.rooms.remove(
+        {
+          _id: room._id
+        },
+        function(error) {
+          if (error != null) {
+            return callback(error)
+          }
+          return callback(null, room._id)
+        }
+      )
+    })
+  }
+}
+
+;[
+  'findOrCreateThread',
+  'findAllThreadRooms',
+  'resolveThread',
+  'reopenThread',
+  'deleteThread'
+].map(method =>
+  metrics.timeAsyncMethod(ThreadManager, method, 'mongo.ThreadManager', logger)
+)

+ 9 - 0
services/chat/app/js/mongojs.js

@@ -0,0 +1,9 @@
+// TODO: This file was created by bulk-decaffeinate.
+// Sanity-check the conversion and remove this comment.
+const Settings = require('settings-sharelatex')
+const mongojs = require('mongojs')
+const db = mongojs(Settings.mongo.url, ['rooms', 'messages'])
+module.exports = {
+  db,
+  ObjectId: mongojs.ObjectId
+}

+ 84 - 0
services/chat/app/js/router.js

@@ -0,0 +1,84 @@
+/* eslint-disable
+    camelcase,
+    max-len,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+let Router
+const MessageHttpController = require('./Features/Messages/MessageHttpController')
+const { ObjectId } = require('./mongojs')
+
+module.exports = Router = {
+  route(app) {
+    app.param('project_id', function(req, res, next, project_id) {
+      if (ObjectId.isValid(project_id)) {
+        return next()
+      } else {
+        return res.send(400, 'Invalid project_id')
+      }
+    })
+
+    app.param('thread_id', function(req, res, next, thread_id) {
+      if (ObjectId.isValid(thread_id)) {
+        return next()
+      } else {
+        return res.send(400, 'Invalid thread_id')
+      }
+    })
+
+    // These are for backwards compatibility
+    app.get(
+      '/room/:project_id/messages',
+      MessageHttpController.getGlobalMessages
+    )
+    app.post(
+      '/room/:project_id/messages',
+      MessageHttpController.sendGlobalMessage
+    )
+
+    app.get(
+      '/project/:project_id/messages',
+      MessageHttpController.getGlobalMessages
+    )
+    app.post(
+      '/project/:project_id/messages',
+      MessageHttpController.sendGlobalMessage
+    )
+
+    app.post(
+      '/project/:project_id/thread/:thread_id/messages',
+      MessageHttpController.sendThreadMessage
+    )
+    app.get('/project/:project_id/threads', MessageHttpController.getAllThreads)
+
+    app.post(
+      '/project/:project_id/thread/:thread_id/messages/:message_id/edit',
+      MessageHttpController.editMessage
+    )
+    app.del(
+      '/project/:project_id/thread/:thread_id/messages/:message_id',
+      MessageHttpController.deleteMessage
+    )
+
+    app.post(
+      '/project/:project_id/thread/:thread_id/resolve',
+      MessageHttpController.resolveThread
+    )
+    app.post(
+      '/project/:project_id/thread/:thread_id/reopen',
+      MessageHttpController.reopenThread
+    )
+    app.del(
+      '/project/:project_id/thread/:thread_id',
+      MessageHttpController.deleteThread
+    )
+
+    return app.get('/status', (req, res, next) => res.send('chat is alive'))
+  }
+}

+ 50 - 0
services/chat/app/js/server.js

@@ -0,0 +1,50 @@
+/* eslint-disable
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const logger = require('logger-sharelatex')
+logger.initialize('chat-sharelatex')
+const metrics = require('metrics-sharelatex')
+metrics.initialize('chat')
+const Path = require('path')
+const express = require('express')
+const app = express()
+const server = require('http').createServer(app)
+const Router = require('./router')
+
+app.use(express.bodyParser())
+app.use(metrics.http.monitor(logger))
+
+if (app.get('env') === 'development') {
+  console.log('Development Enviroment')
+  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }))
+}
+
+if (app.get('env') === 'production') {
+  console.log('Production Enviroment')
+  app.use(express.logger())
+  app.use(express.errorHandler())
+}
+
+const profiler = require('v8-profiler')
+app.get('/profile', function(req, res) {
+  const time = parseInt(req.query.time || '1000')
+  profiler.startProfiling('test')
+  return setTimeout(function() {
+    const profile = profiler.stopProfiling('test')
+    return res.json(profile)
+  }, time)
+})
+
+Router.route(app)
+
+module.exports = {
+  server,
+  app
+}

+ 0 - 23
services/chat/config/settings.defaults.coffee

@@ -1,23 +0,0 @@
-settings =
-	internal:
-		chat:
-			host: process.env['LISTEN_ADDRESS'] or "localhost"
-			port: 3010
-	
-	apis:
-		web:
-			url: "http://#{process.env['WEB_HOST'] || "localhost"}:3000"
-			user: "sharelatex"
-			pass: "password"
-			
-	mongo:
-		url: process.env['MONGO_CONNECTION_STRING'] or "mongodb://#{process.env["MONGO_HOST"] or "localhost"}/sharelatex"
-
-
-	redis:
-		web:
-			host: process.env['REDIS_HOST'] || "localhost"
-			port: "6379"
-			password: process.env['REDIS_PASSWORD'] || ""
-
-module.exports = settings

+ 30 - 0
services/chat/config/settings.defaults.js

@@ -0,0 +1,30 @@
+module.exports = {
+  internal: {
+    chat: {
+      host: process.env['LISTEN_ADDRESS'] || 'localhost',
+      port: 3010
+    }
+  },
+
+  apis: {
+    web: {
+      url: `http://${process.env['WEB_HOST'] || 'localhost'}:${process.env[
+        'WEB_PORT'
+      ] || 3000}`,
+      user: 'sharelatex',
+      pass: 'password'
+    }
+  },
+
+  mongo: {
+    url: process.env['MONGO_CONNECTION_STRING'] || `mongodb://${process.env['MONGO_HOST'] || 'localhost'}/sharelatex`
+  },
+
+  redis: {
+    web: {
+      host: process.env['REDIS_HOST'] || 'localhost',
+      port: '6379',
+      password: process.env['REDIS_PASSWORD'] || ''
+    }
+  }
+}

+ 53 - 0
services/chat/decaffeinate.sh

@@ -0,0 +1,53 @@
+set -ex
+
+curl -o .eslintrc https://raw.githubusercontent.com/sharelatex/web-sharelatex/master/.eslintrc
+curl -o .prettierrc https://raw.githubusercontent.com/sharelatex/web-sharelatex/master/.prettierrc
+
+git add .
+git commit -m "Decaffeinate: add eslint and prettier rc files"
+
+npx bulk-decaffeinate convert --dir app/coffee
+
+npx bulk-decaffeinate clean
+
+git mv app/coffee app/js
+
+git commit -m "Rename app/coffee dir to app/js"
+
+npx prettier-eslint 'app/js/**/*.js' --write
+
+git add .
+git commit -m "Prettier: convert app/js decaffeinated files to Prettier format"
+
+npx bulk-decaffeinate convert --dir test/acceptance/coffee
+
+npx bulk-decaffeinate clean
+
+git mv test/acceptance/coffee test/acceptance/js
+
+git commit -m "Rename test/acceptance/coffee to test/acceptance/js"
+
+npx prettier-eslint 'test/acceptance/js/**/*.js' --write
+
+git add .
+git commit -m "Prettier: convert test/acceptance decaffeinated files to Prettier format"
+
+git mv app.coffee app.js
+git mv Gruntfile.coffee Gruntfile.js
+git mv config/settings.defaults.coffee config/settings.defaults.js
+
+git commit -m "Rename individual coffee files to js files"
+
+decaffeinate app.js
+decaffeinate Gruntfile.js
+decaffeinate config/settings.defaults.js
+
+git add .
+git commit -m "Decaffeinate: convert individual files to js"
+
+npx prettier-eslint 'app.js' 'Gruntfile.js' 'config/settings.defaults.js' --write
+
+git add .
+git commit -m "Prettier: convert individual decaffeinated files to Prettier format"
+
+echo "done"

File diff suppressed because it is too large
+ 4001 - 125
services/chat/npm-shrinkwrap.json


+ 0 - 32
services/chat/test/acceptance/coffee/DeletingAMessageTests.coffee

@@ -1,32 +0,0 @@
-{ObjectId} = require "../../../app/js/mongojs"
-expect = require("chai").expect
-
-ChatClient = require "./helpers/ChatClient"
-ChatApp = require "./helpers/ChatApp"
-
-describe "Deleting a message", ->
-	before (done) ->
-		@project_id = ObjectId().toString()
-		@user_id = ObjectId().toString()
-		@thread_id = ObjectId().toString()
-		ChatApp.ensureRunning done
-
-	describe "in a thread", ->
-		before (done) ->
-			ChatClient.sendMessage @project_id, @thread_id, @user_id, "first message", (error, response, @message) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				ChatClient.sendMessage @project_id, @thread_id, @user_id, "deleted message", (error, response, @message) =>
-					expect(error).to.be.null
-					expect(response.statusCode).to.equal 201
-					ChatClient.deleteMessage @project_id, @thread_id, @message.id, (error, response, body) =>
-						expect(error).to.be.null
-						expect(response.statusCode).to.equal 204
-						done()
-		
-		it "should then remove the message from the threads", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(threads[@thread_id].messages.length).to.equal 1
-				done()

+ 0 - 31
services/chat/test/acceptance/coffee/DeletingAThreadTests.coffee

@@ -1,31 +0,0 @@
-{ObjectId} = require "../../../app/js/mongojs"
-expect = require("chai").expect
-crypto = require "crypto"
-
-ChatClient = require "./helpers/ChatClient"
-ChatApp = require "./helpers/ChatApp"
-
-describe "Deleting a thread", ->
-	before (done) ->
-		@project_id = ObjectId().toString()
-		@user_id = ObjectId().toString()
-		ChatApp.ensureRunning done
-
-	describe "with a thread that is deleted", ->
-		before (done) ->
-			@thread_id = ObjectId().toString()
-			@content = "deleted thread message"
-			ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				ChatClient.deleteThread @project_id, @thread_id, (error, response, body) =>
-					expect(error).to.be.null
-					expect(response.statusCode).to.equal 204
-					done()
-		
-		it "should then not list the thread for the project", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(Object.keys(threads).length).to.equal 0
-				done()

+ 0 - 34
services/chat/test/acceptance/coffee/EditingAMessageTests.coffee

@@ -1,34 +0,0 @@
-{ObjectId} = require "../../../app/js/mongojs"
-expect = require("chai").expect
-
-ChatClient = require "./helpers/ChatClient"
-ChatApp = require "./helpers/ChatApp"
-
-describe "Editing a message", ->
-	before (done) ->
-		@project_id = ObjectId().toString()
-		@user_id = ObjectId().toString()
-		@thread_id = ObjectId().toString()
-		ChatApp.ensureRunning done
-
-	describe "in a thread", ->
-		before (done) ->
-			@content = "thread message"
-			@new_content = "updated thread message"
-			ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, @message) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				expect(@message.id).to.exist
-				expect(@message.content).to.equal @content
-				ChatClient.editMessage @project_id, @thread_id, @message.id, @new_content, (error, response, @new_message) =>
-					expect(error).to.be.null
-					expect(response.statusCode).to.equal 204
-					done()
-		
-		it "should then list the updated message in the threads", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(threads[@thread_id].messages.length).to.equal 1
-				expect(threads[@thread_id].messages[0].content).to.equal @new_content
-				done()

+ 0 - 64
services/chat/test/acceptance/coffee/GettingMessagesTests.coffee

@@ -1,64 +0,0 @@
-{ObjectId} = require "../../../app/js/mongojs"
-expect = require("chai").expect
-async = require "async"
-crypto = require "crypto"
-
-ChatClient = require "./helpers/ChatClient"
-ChatApp = require "./helpers/ChatApp"
-
-describe "Getting messages", ->
-	before (done) ->
-		@user_id1 = ObjectId().toString()
-		@user_id2 = ObjectId().toString()
-		@content1 = "foo bar"
-		@content2 = "hello world"
-		ChatApp.ensureRunning done
-
-	describe "globally", ->
-		before (done) ->
-			@project_id = ObjectId().toString()
-			async.series [
-				(cb) => ChatClient.sendGlobalMessage @project_id, @user_id1, @content1, cb
-				(cb) => ChatClient.sendGlobalMessage @project_id, @user_id2, @content2, cb
-			], done
-		
-		it "should contain the messages and populated users when getting the messages", (done) ->
-			ChatClient.getGlobalMessages @project_id, (error, response, messages) =>
-				expect(messages.length).to.equal 2
-				messages.reverse()
-				expect(messages[0].content).to.equal @content1
-				expect(messages[0].user_id).to.equal @user_id1
-				expect(messages[1].content).to.equal @content2
-				expect(messages[1].user_id).to.equal @user_id2
-				done()
-
-	describe "from all the threads", ->
-		before (done) ->
-			@project_id = ObjectId().toString()
-			@thread_id1 = ObjectId().toString()
-			@thread_id2 = ObjectId().toString()
-			async.series [
-				(cb) => ChatClient.sendMessage @project_id, @thread_id1, @user_id1, "one", cb
-				(cb) => ChatClient.sendMessage @project_id, @thread_id2, @user_id2, "two", cb
-				(cb) => ChatClient.sendMessage @project_id, @thread_id1, @user_id1, "three", cb
-				(cb) => ChatClient.sendMessage @project_id, @thread_id2, @user_id2, "four", cb
-			], done
-		
-		it "should contain a dictionary of threads with messages with populated users", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(Object.keys(threads).length).to.equal 2
-				thread1 = threads[@thread_id1]
-				expect(thread1.messages.length).to.equal 2
-				thread2 = threads[@thread_id2]
-				expect(thread2.messages.length).to.equal 2
-				
-				expect(thread1.messages[0].content).to.equal "one"
-				expect(thread1.messages[0].user_id).to.equal @user_id1
-				expect(thread1.messages[1].content).to.equal "three"
-				expect(thread1.messages[1].user_id).to.equal @user_id1
-
-				expect(thread2.messages[0].content).to.equal "two"
-				expect(thread2.messages[0].user_id).to.equal @user_id2
-				expect(thread2.messages[1].content).to.equal "four"
-				expect(thread2.messages[1].user_id).to.equal @user_id2
-				done()

+ 0 - 72
services/chat/test/acceptance/coffee/ResolvingAThreadTests.coffee

@@ -1,72 +0,0 @@
-{ObjectId} = require "../../../app/js/mongojs"
-expect = require("chai").expect
-crypto = require "crypto"
-
-ChatClient = require "./helpers/ChatClient"
-ChatApp = require "./helpers/ChatApp"
-
-describe "Resolving a thread", ->
-	before (done) ->
-		@project_id = ObjectId().toString()
-		@user_id = ObjectId().toString()
-		ChatApp.ensureRunning done
-
-	describe "with a resolved thread", ->
-		before (done) ->
-			@thread_id = ObjectId().toString()
-			@content = "resolved message"
-			ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				ChatClient.resolveThread @project_id, @thread_id, @user_id, (error, response, body) =>
-					expect(error).to.be.null
-					expect(response.statusCode).to.equal 204
-					done()
-		
-		it "should then list the thread as resolved", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(threads[@thread_id].resolved).to.equal true
-				expect(threads[@thread_id].resolved_by_user_id).to.equal @user_id
-				resolved_at = new Date(threads[@thread_id].resolved_at)
-				expect(new Date() - resolved_at).to.be.below 1000
-				done()
-
-	describe "when a thread is not resolved", ->
-		before (done) ->
-			@thread_id = ObjectId().toString()
-			@content = "open message"
-			ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				done()
-		
-		it "should not list the thread as resolved", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(threads[@thread_id].resolved).to.be.undefined
-				done()
-	
-	describe "when a thread is resolved then reopened", ->
-		before (done) ->
-			@thread_id = ObjectId().toString()
-			@content = "resolved message"
-			ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				ChatClient.resolveThread @project_id, @thread_id, @user_id, (error, response, body) =>
-					expect(error).to.be.null
-					expect(response.statusCode).to.equal 204
-					ChatClient.reopenThread @project_id, @thread_id, (error, response, body) =>
-						expect(error).to.be.null
-						expect(response.statusCode).to.equal 204
-						done()
-				
-		it "should not list the thread as resolved", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(threads[@thread_id].resolved).to.be.undefined
-				done()

+ 0 - 101
services/chat/test/acceptance/coffee/SendingAMessageTests.coffee

@@ -1,101 +0,0 @@
-{ObjectId} = require "../../../app/js/mongojs"
-expect = require("chai").expect
-
-ChatClient = require "./helpers/ChatClient"
-ChatApp = require "./helpers/ChatApp"
-
-describe "Sending a message", ->
-	before (done) ->
-		ChatApp.ensureRunning done
-
-	describe "globally", ->
-		before (done) ->
-			@project_id = ObjectId().toString()
-			@user_id = ObjectId().toString()
-			@content = "global message"
-			ChatClient.sendGlobalMessage @project_id, @user_id, @content, (error, response, body) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				expect(body.content).to.equal @content
-				expect(body.user_id).to.equal @user_id
-				expect(body.room_id).to.equal @project_id
-				done()
-		
-		it "should then list the message in the project messages", (done) ->
-			ChatClient.getGlobalMessages @project_id, (error, response, messages) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(messages.length).to.equal 1
-				expect(messages[0].content).to.equal @content
-				done()
-
-	describe "to a thread", ->
-		before (done) ->
-			@project_id = ObjectId().toString()
-			@user_id = ObjectId().toString()
-			@thread_id = ObjectId().toString()
-			@content = "thread message"
-			ChatClient.sendMessage @project_id, @thread_id, @user_id, @content, (error, response, body) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 201
-				expect(body.content).to.equal @content
-				expect(body.user_id).to.equal @user_id
-				expect(body.room_id).to.equal @project_id
-				done()
-		
-		it "should then list the message in the threads", (done) ->
-			ChatClient.getThreads @project_id, (error, response, threads) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(threads[@thread_id].messages.length).to.equal 1
-				expect(threads[@thread_id].messages[0].content).to.equal @content
-				done()
-		
-		it "should not appear in the global messages", (done) ->
-			ChatClient.getGlobalMessages @project_id, (error, response, messages) =>
-				expect(error).to.be.null
-				expect(response.statusCode).to.equal 200
-				expect(messages.length).to.equal 0
-				done()
-	
-	describe "failure cases", ->
-		before () ->
-			@project_id = ObjectId().toString()
-			@user_id = ObjectId().toString()
-			@thread_id = ObjectId().toString()
-		
-		describe "with a malformed user_id", ->
-			it "should return a graceful error", (done) ->
-				ChatClient.sendMessage @project_id, @thread_id, "malformed-user", "content", (error, response, body) =>
-					expect(response.statusCode).to.equal 400
-					expect(body).to.equal "Invalid user_id"
-					done()
-		
-		describe "with a malformed project_id", ->
-			it "should return a graceful error", (done) ->
-				ChatClient.sendMessage "malformed-project", @thread_id, @user_id, "content", (error, response, body) =>
-					expect(response.statusCode).to.equal 400
-					expect(body).to.equal "Invalid project_id"
-					done()
-		
-		describe "with a malformed thread_id", ->
-			it "should return a graceful error", (done) ->
-				ChatClient.sendMessage @project_id, "malformed-thread-id", @user_id, "content", (error, response, body) =>
-					expect(response.statusCode).to.equal 400
-					expect(body).to.equal "Invalid thread_id"
-					done()
-		
-		describe "with no content", ->
-			it "should return a graceful error", (done) ->
-				ChatClient.sendMessage @project_id, @thread_id, @user_id, null, (error, response, body) =>
-					expect(response.statusCode).to.equal 400
-					expect(body).to.equal "No content provided"
-					done()
-		
-		describe "with very long content", ->
-			it "should return a graceful error", (done) ->
-				content = new Buffer(10240).toString("hex")
-				ChatClient.sendMessage @project_id, @thread_id, @user_id, content, (error, response, body) =>
-					expect(response.statusCode).to.equal 400
-					expect(body).to.equal "Content too long (> 10240 bytes)"
-					done()

+ 0 - 20
services/chat/test/acceptance/coffee/helpers/ChatApp.coffee

@@ -1,20 +0,0 @@
-app = require('../../../../app')
-require("logger-sharelatex").logger.level("error")
-
-module.exports =
-	running: false
-	initing: false
-	callbacks: []
-	ensureRunning: (callback = (error) ->) ->
-		if @running
-			return callback()
-		else if @initing
-			@callbacks.push callback
-		else
-			@initing = true
-			@callbacks.push callback
-			app.listen 3010, "localhost", (error) => 
-				throw error if error?
-				@running = true
-				for callback in @callbacks
-					callback()

+ 0 - 60
services/chat/test/acceptance/coffee/helpers/ChatClient.coffee

@@ -1,60 +0,0 @@
-request = require("request").defaults({baseUrl: "http://localhost:3010"})
-
-module.exports =
-	sendGlobalMessage: (project_id, user_id, content, callback) ->
-		request.post {
-			url: "/project/#{project_id}/messages"
-			json:
-				user_id: user_id
-				content: content
-		}, callback
-	
-	getGlobalMessages: (project_id, callback) ->
-		request.get {
-			url: "/project/#{project_id}/messages",
-			json: true
-		}, callback
-
-	sendMessage: (project_id, thread_id, user_id, content, callback) ->
-		request.post {
-			url: "/project/#{project_id}/thread/#{thread_id}/messages"
-			json:
-				user_id: user_id
-				content: content
-		}, callback
-	
-	getThreads: (project_id, callback) ->
-		request.get {
-			url: "/project/#{project_id}/threads",
-			json: true
-		}, callback
-	
-	resolveThread: (project_id, thread_id, user_id, callback) ->
-		request.post {
-			url: "/project/#{project_id}/thread/#{thread_id}/resolve",
-			json: {
-				user_id: user_id
-			}
-		}, callback
-
-	reopenThread: (project_id, thread_id, callback) ->
-		request.post {
-			url: "/project/#{project_id}/thread/#{thread_id}/reopen",
-		}, callback
-
-	deleteThread: (project_id, thread_id, callback) ->
-		request.del {
-			url: "/project/#{project_id}/thread/#{thread_id}",
-		}, callback
-
-	editMessage: (project_id, thread_id, message_id, content, callback) ->
-		request.post {
-			url: "/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}/edit"
-			json:
-				content: content
-		}, callback
-
-	deleteMessage: (project_id, thread_id, message_id, callback) ->
-		request.del {
-			url: "/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}",
-		}, callback

+ 73 - 0
services/chat/test/acceptance/js/DeletingAMessageTests.js

@@ -0,0 +1,73 @@
+/* eslint-disable
+    max-len,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const { ObjectId } = require('../../../app/js/mongojs')
+const { expect } = require('chai')
+
+const ChatClient = require('./helpers/ChatClient')
+const ChatApp = require('./helpers/ChatApp')
+
+describe('Deleting a message', function() {
+  before(function(done) {
+    this.project_id = ObjectId().toString()
+    this.user_id = ObjectId().toString()
+    this.thread_id = ObjectId().toString()
+    return ChatApp.ensureRunning(done)
+  })
+
+  return describe('in a thread', function() {
+    before(function(done) {
+      return ChatClient.sendMessage(
+        this.project_id,
+        this.thread_id,
+        this.user_id,
+        'first message',
+        (error, response, message) => {
+          this.message = message
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          return ChatClient.sendMessage(
+            this.project_id,
+            this.thread_id,
+            this.user_id,
+            'deleted message',
+            (error, response, message1) => {
+              this.message = message1
+              expect(error).to.be.null
+              expect(response.statusCode).to.equal(201)
+              return ChatClient.deleteMessage(
+                this.project_id,
+                this.thread_id,
+                this.message.id,
+                (error, response, body) => {
+                  expect(error).to.be.null
+                  expect(response.statusCode).to.equal(204)
+                  return done()
+                }
+              )
+            }
+          )
+        }
+      )
+    })
+
+    return it('should then remove the message from the threads', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(threads[this.thread_id].messages.length).to.equal(1)
+          return done()
+        }
+      )
+    })
+  })
+})

+ 63 - 0
services/chat/test/acceptance/js/DeletingAThreadTests.js

@@ -0,0 +1,63 @@
+/* eslint-disable
+    max-len,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const { ObjectId } = require('../../../app/js/mongojs')
+const { expect } = require('chai')
+const crypto = require('crypto')
+
+const ChatClient = require('./helpers/ChatClient')
+const ChatApp = require('./helpers/ChatApp')
+
+describe('Deleting a thread', function() {
+  before(function(done) {
+    this.project_id = ObjectId().toString()
+    this.user_id = ObjectId().toString()
+    return ChatApp.ensureRunning(done)
+  })
+
+  return describe('with a thread that is deleted', function() {
+    before(function(done) {
+      this.thread_id = ObjectId().toString()
+      this.content = 'deleted thread message'
+      return ChatClient.sendMessage(
+        this.project_id,
+        this.thread_id,
+        this.user_id,
+        this.content,
+        (error, response, body) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          return ChatClient.deleteThread(
+            this.project_id,
+            this.thread_id,
+            (error, response, body) => {
+              expect(error).to.be.null
+              expect(response.statusCode).to.equal(204)
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    return it('should then not list the thread for the project', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(Object.keys(threads).length).to.equal(0)
+          return done()
+        }
+      )
+    })
+  })
+})

+ 72 - 0
services/chat/test/acceptance/js/EditingAMessageTests.js

@@ -0,0 +1,72 @@
+/* eslint-disable
+    camelcase,
+    max-len,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const { ObjectId } = require('../../../app/js/mongojs')
+const { expect } = require('chai')
+
+const ChatClient = require('./helpers/ChatClient')
+const ChatApp = require('./helpers/ChatApp')
+
+describe('Editing a message', function() {
+  before(function(done) {
+    this.project_id = ObjectId().toString()
+    this.user_id = ObjectId().toString()
+    this.thread_id = ObjectId().toString()
+    return ChatApp.ensureRunning(done)
+  })
+
+  return describe('in a thread', function() {
+    before(function(done) {
+      this.content = 'thread message'
+      this.new_content = 'updated thread message'
+      return ChatClient.sendMessage(
+        this.project_id,
+        this.thread_id,
+        this.user_id,
+        this.content,
+        (error, response, message) => {
+          this.message = message
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          expect(this.message.id).to.exist
+          expect(this.message.content).to.equal(this.content)
+          return ChatClient.editMessage(
+            this.project_id,
+            this.thread_id,
+            this.message.id,
+            this.new_content,
+            (error, response, new_message) => {
+              this.new_message = new_message
+              expect(error).to.be.null
+              expect(response.statusCode).to.equal(204)
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    return it('should then list the updated message in the threads', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(threads[this.thread_id].messages.length).to.equal(1)
+          expect(threads[this.thread_id].messages[0].content).to.equal(
+            this.new_content
+          )
+          return done()
+        }
+      )
+    })
+  })
+})

+ 138 - 0
services/chat/test/acceptance/js/GettingMessagesTests.js

@@ -0,0 +1,138 @@
+/* eslint-disable
+    handle-callback-err,
+    max-len,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const { ObjectId } = require('../../../app/js/mongojs')
+const { expect } = require('chai')
+const async = require('async')
+const crypto = require('crypto')
+
+const ChatClient = require('./helpers/ChatClient')
+const ChatApp = require('./helpers/ChatApp')
+
+describe('Getting messages', function() {
+  before(function(done) {
+    this.user_id1 = ObjectId().toString()
+    this.user_id2 = ObjectId().toString()
+    this.content1 = 'foo bar'
+    this.content2 = 'hello world'
+    return ChatApp.ensureRunning(done)
+  })
+
+  describe('globally', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      return async.series(
+        [
+          cb =>
+            ChatClient.sendGlobalMessage(
+              this.project_id,
+              this.user_id1,
+              this.content1,
+              cb
+            ),
+          cb =>
+            ChatClient.sendGlobalMessage(
+              this.project_id,
+              this.user_id2,
+              this.content2,
+              cb
+            )
+        ],
+        done
+      )
+    })
+
+    return it('should contain the messages and populated users when getting the messages', function(done) {
+      return ChatClient.getGlobalMessages(
+        this.project_id,
+        (error, response, messages) => {
+          expect(messages.length).to.equal(2)
+          messages.reverse()
+          expect(messages[0].content).to.equal(this.content1)
+          expect(messages[0].user_id).to.equal(this.user_id1)
+          expect(messages[1].content).to.equal(this.content2)
+          expect(messages[1].user_id).to.equal(this.user_id2)
+          return done()
+        }
+      )
+    })
+  })
+
+  return describe('from all the threads', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.thread_id1 = ObjectId().toString()
+      this.thread_id2 = ObjectId().toString()
+      return async.series(
+        [
+          cb =>
+            ChatClient.sendMessage(
+              this.project_id,
+              this.thread_id1,
+              this.user_id1,
+              'one',
+              cb
+            ),
+          cb =>
+            ChatClient.sendMessage(
+              this.project_id,
+              this.thread_id2,
+              this.user_id2,
+              'two',
+              cb
+            ),
+          cb =>
+            ChatClient.sendMessage(
+              this.project_id,
+              this.thread_id1,
+              this.user_id1,
+              'three',
+              cb
+            ),
+          cb =>
+            ChatClient.sendMessage(
+              this.project_id,
+              this.thread_id2,
+              this.user_id2,
+              'four',
+              cb
+            )
+        ],
+        done
+      )
+    })
+
+    return it('should contain a dictionary of threads with messages with populated users', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(Object.keys(threads).length).to.equal(2)
+          const thread1 = threads[this.thread_id1]
+          expect(thread1.messages.length).to.equal(2)
+          const thread2 = threads[this.thread_id2]
+          expect(thread2.messages.length).to.equal(2)
+
+          expect(thread1.messages[0].content).to.equal('one')
+          expect(thread1.messages[0].user_id).to.equal(this.user_id1)
+          expect(thread1.messages[1].content).to.equal('three')
+          expect(thread1.messages[1].user_id).to.equal(this.user_id1)
+
+          expect(thread2.messages[0].content).to.equal('two')
+          expect(thread2.messages[0].user_id).to.equal(this.user_id2)
+          expect(thread2.messages[1].content).to.equal('four')
+          expect(thread2.messages[1].user_id).to.equal(this.user_id2)
+          return done()
+        }
+      )
+    })
+  })
+})

+ 147 - 0
services/chat/test/acceptance/js/ResolvingAThreadTests.js

@@ -0,0 +1,147 @@
+/* eslint-disable
+    camelcase,
+    max-len,
+    no-unused-vars,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const { ObjectId } = require('../../../app/js/mongojs')
+const { expect } = require('chai')
+const crypto = require('crypto')
+
+const ChatClient = require('./helpers/ChatClient')
+const ChatApp = require('./helpers/ChatApp')
+
+describe('Resolving a thread', function() {
+  before(function(done) {
+    this.project_id = ObjectId().toString()
+    this.user_id = ObjectId().toString()
+    return ChatApp.ensureRunning(done)
+  })
+
+  describe('with a resolved thread', function() {
+    before(function(done) {
+      this.thread_id = ObjectId().toString()
+      this.content = 'resolved message'
+      return ChatClient.sendMessage(
+        this.project_id,
+        this.thread_id,
+        this.user_id,
+        this.content,
+        (error, response, body) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          return ChatClient.resolveThread(
+            this.project_id,
+            this.thread_id,
+            this.user_id,
+            (error, response, body) => {
+              expect(error).to.be.null
+              expect(response.statusCode).to.equal(204)
+              return done()
+            }
+          )
+        }
+      )
+    })
+
+    return it('should then list the thread as resolved', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(threads[this.thread_id].resolved).to.equal(true)
+          expect(threads[this.thread_id].resolved_by_user_id).to.equal(
+            this.user_id
+          )
+          const resolved_at = new Date(threads[this.thread_id].resolved_at)
+          expect(new Date() - resolved_at).to.be.below(1000)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('when a thread is not resolved', function() {
+    before(function(done) {
+      this.thread_id = ObjectId().toString()
+      this.content = 'open message'
+      return ChatClient.sendMessage(
+        this.project_id,
+        this.thread_id,
+        this.user_id,
+        this.content,
+        (error, response, body) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          return done()
+        }
+      )
+    })
+
+    return it('should not list the thread as resolved', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(threads[this.thread_id].resolved).to.be.undefined
+          return done()
+        }
+      )
+    })
+  })
+
+  return describe('when a thread is resolved then reopened', function() {
+    before(function(done) {
+      this.thread_id = ObjectId().toString()
+      this.content = 'resolved message'
+      return ChatClient.sendMessage(
+        this.project_id,
+        this.thread_id,
+        this.user_id,
+        this.content,
+        (error, response, body) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          return ChatClient.resolveThread(
+            this.project_id,
+            this.thread_id,
+            this.user_id,
+            (error, response, body) => {
+              expect(error).to.be.null
+              expect(response.statusCode).to.equal(204)
+              return ChatClient.reopenThread(
+                this.project_id,
+                this.thread_id,
+                (error, response, body) => {
+                  expect(error).to.be.null
+                  expect(response.statusCode).to.equal(204)
+                  return done()
+                }
+              )
+            }
+          )
+        }
+      )
+    })
+
+    return it('should not list the thread as resolved', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(threads[this.thread_id].resolved).to.be.undefined
+          return done()
+        }
+      )
+    })
+  })
+})

+ 190 - 0
services/chat/test/acceptance/js/SendingAMessageTests.js

@@ -0,0 +1,190 @@
+/* eslint-disable
+    handle-callback-err,
+    max-len,
+    no-return-assign,
+    node/no-deprecated-api,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const { ObjectId } = require('../../../app/js/mongojs')
+const { expect } = require('chai')
+
+const ChatClient = require('./helpers/ChatClient')
+const ChatApp = require('./helpers/ChatApp')
+
+describe('Sending a message', function() {
+  before(done => ChatApp.ensureRunning(done))
+
+  describe('globally', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      this.content = 'global message'
+      return ChatClient.sendGlobalMessage(
+        this.project_id,
+        this.user_id,
+        this.content,
+        (error, response, body) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          expect(body.content).to.equal(this.content)
+          expect(body.user_id).to.equal(this.user_id)
+          expect(body.room_id).to.equal(this.project_id)
+          return done()
+        }
+      )
+    })
+
+    return it('should then list the message in the project messages', function(done) {
+      return ChatClient.getGlobalMessages(
+        this.project_id,
+        (error, response, messages) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(messages.length).to.equal(1)
+          expect(messages[0].content).to.equal(this.content)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('to a thread', function() {
+    before(function(done) {
+      this.project_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      this.thread_id = ObjectId().toString()
+      this.content = 'thread message'
+      return ChatClient.sendMessage(
+        this.project_id,
+        this.thread_id,
+        this.user_id,
+        this.content,
+        (error, response, body) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(201)
+          expect(body.content).to.equal(this.content)
+          expect(body.user_id).to.equal(this.user_id)
+          expect(body.room_id).to.equal(this.project_id)
+          return done()
+        }
+      )
+    })
+
+    it('should then list the message in the threads', function(done) {
+      return ChatClient.getThreads(
+        this.project_id,
+        (error, response, threads) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(threads[this.thread_id].messages.length).to.equal(1)
+          expect(threads[this.thread_id].messages[0].content).to.equal(
+            this.content
+          )
+          return done()
+        }
+      )
+    })
+
+    return it('should not appear in the global messages', function(done) {
+      return ChatClient.getGlobalMessages(
+        this.project_id,
+        (error, response, messages) => {
+          expect(error).to.be.null
+          expect(response.statusCode).to.equal(200)
+          expect(messages.length).to.equal(0)
+          return done()
+        }
+      )
+    })
+  })
+
+  return describe('failure cases', function() {
+    before(function() {
+      this.project_id = ObjectId().toString()
+      this.user_id = ObjectId().toString()
+      return (this.thread_id = ObjectId().toString())
+    })
+
+    describe('with a malformed user_id', () =>
+      it('should return a graceful error', function(done) {
+        return ChatClient.sendMessage(
+          this.project_id,
+          this.thread_id,
+          'malformed-user',
+          'content',
+          (error, response, body) => {
+            expect(response.statusCode).to.equal(400)
+            expect(body).to.equal('Invalid user_id')
+            return done()
+          }
+        )
+      }))
+
+    describe('with a malformed project_id', () =>
+      it('should return a graceful error', function(done) {
+        return ChatClient.sendMessage(
+          'malformed-project',
+          this.thread_id,
+          this.user_id,
+          'content',
+          (error, response, body) => {
+            expect(response.statusCode).to.equal(400)
+            expect(body).to.equal('Invalid project_id')
+            return done()
+          }
+        )
+      }))
+
+    describe('with a malformed thread_id', () =>
+      it('should return a graceful error', function(done) {
+        return ChatClient.sendMessage(
+          this.project_id,
+          'malformed-thread-id',
+          this.user_id,
+          'content',
+          (error, response, body) => {
+            expect(response.statusCode).to.equal(400)
+            expect(body).to.equal('Invalid thread_id')
+            return done()
+          }
+        )
+      }))
+
+    describe('with no content', () =>
+      it('should return a graceful error', function(done) {
+        return ChatClient.sendMessage(
+          this.project_id,
+          this.thread_id,
+          this.user_id,
+          null,
+          (error, response, body) => {
+            expect(response.statusCode).to.equal(400)
+            expect(body).to.equal('No content provided')
+            return done()
+          }
+        )
+      }))
+
+    return describe('with very long content', () =>
+      it('should return a graceful error', function(done) {
+        const content = new Buffer(10240).toString('hex')
+        return ChatClient.sendMessage(
+          this.project_id,
+          this.thread_id,
+          this.user_id,
+          content,
+          (error, response, body) => {
+            expect(response.statusCode).to.equal(400)
+            expect(body).to.equal('Content too long (> 10240 bytes)')
+            return done()
+          }
+        )
+      }))
+  })
+})

+ 47 - 0
services/chat/test/acceptance/js/helpers/ChatApp.js

@@ -0,0 +1,47 @@
+/* eslint-disable
+    handle-callback-err,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS101: Remove unnecessary use of Array.from
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS205: Consider reworking code to avoid use of IIFEs
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const app = require('../../../../app')
+require('logger-sharelatex').logger.level('error')
+
+module.exports = {
+  running: false,
+  initing: false,
+  callbacks: [],
+  ensureRunning(callback) {
+    if (callback == null) {
+      callback = function(error) {}
+    }
+    if (this.running) {
+      return callback()
+    } else if (this.initing) {
+      return this.callbacks.push(callback)
+    } else {
+      this.initing = true
+      this.callbacks.push(callback)
+      return app.listen(3010, 'localhost', error => {
+        if (error != null) {
+          throw error
+        }
+        this.running = true
+        return (() => {
+          const result = []
+          for (callback of Array.from(this.callbacks)) {
+            result.push(callback())
+          }
+          return result
+        })()
+      })
+    }
+  }
+}

+ 113 - 0
services/chat/test/acceptance/js/helpers/ChatClient.js

@@ -0,0 +1,113 @@
+/* eslint-disable
+    camelcase,
+    max-len,
+*/
+// TODO: This file was created by bulk-decaffeinate.
+// Fix any style issues and re-enable lint.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+const request = require('request').defaults({
+  baseUrl: 'http://localhost:3010'
+})
+
+module.exports = {
+  sendGlobalMessage(project_id, user_id, content, callback) {
+    return request.post(
+      {
+        url: `/project/${project_id}/messages`,
+        json: {
+          user_id,
+          content
+        }
+      },
+      callback
+    )
+  },
+
+  getGlobalMessages(project_id, callback) {
+    return request.get(
+      {
+        url: `/project/${project_id}/messages`,
+        json: true
+      },
+      callback
+    )
+  },
+
+  sendMessage(project_id, thread_id, user_id, content, callback) {
+    return request.post(
+      {
+        url: `/project/${project_id}/thread/${thread_id}/messages`,
+        json: {
+          user_id,
+          content
+        }
+      },
+      callback
+    )
+  },
+
+  getThreads(project_id, callback) {
+    return request.get(
+      {
+        url: `/project/${project_id}/threads`,
+        json: true
+      },
+      callback
+    )
+  },
+
+  resolveThread(project_id, thread_id, user_id, callback) {
+    return request.post(
+      {
+        url: `/project/${project_id}/thread/${thread_id}/resolve`,
+        json: {
+          user_id
+        }
+      },
+      callback
+    )
+  },
+
+  reopenThread(project_id, thread_id, callback) {
+    return request.post(
+      {
+        url: `/project/${project_id}/thread/${thread_id}/reopen`
+      },
+      callback
+    )
+  },
+
+  deleteThread(project_id, thread_id, callback) {
+    return request.del(
+      {
+        url: `/project/${project_id}/thread/${thread_id}`
+      },
+      callback
+    )
+  },
+
+  editMessage(project_id, thread_id, message_id, content, callback) {
+    return request.post(
+      {
+        url: `/project/${project_id}/thread/${thread_id}/messages/${message_id}/edit`,
+        json: {
+          content
+        }
+      },
+      callback
+    )
+  },
+
+  deleteMessage(project_id, thread_id, message_id, callback) {
+    return request.del(
+      {
+        url: `/project/${project_id}/thread/${thread_id}/messages/${message_id}`
+      },
+      callback
+    )
+  }
+}

Some files were not shown because too many files changed in this diff