Browse Source

Merge pull request #17 from sharelatex/ns-decaffeinate

decaffeinate this service
nate stemen 7 years ago
parent
commit
c9a9ad06a7
50 changed files with 3519 additions and 1199 deletions
  1. 75 0
      services/chat/.eslintrc
  2. 38 0
      services/chat/.github/ISSUE_TEMPLATE.md
  3. 45 0
      services/chat/.github/PULL_REQUEST_TEMPLATE.md
  4. 0 5
      services/chat/.gitignore
  5. 4 0
      services/chat/.prettierrc
  6. 0 97
      services/chat/Gruntfile.coffee
  7. 129 0
      services/chat/Gruntfile.js
  8. 41 15
      services/chat/Jenkinsfile
  9. 17 11
      services/chat/Makefile
  10. 0 13
      services/chat/app.coffee
  11. 39 0
      services/chat/app.js
  12. 0 46
      services/chat/app/coffee/Features/Messages/MessageFormatter.coffee
  13. 0 112
      services/chat/app/coffee/Features/Messages/MessageHttpController.coffee
  14. 0 76
      services/chat/app/coffee/Features/Messages/MessageManager.coffee
  15. 0 88
      services/chat/app/coffee/Features/Threads/ThreadManager.coffee
  16. 0 6
      services/chat/app/coffee/mongojs.coffee
  17. 0 39
      services/chat/app/coffee/router.coffee
  18. 0 39
      services/chat/app/coffee/server.coffee
  19. 80 0
      services/chat/app/js/Features/Messages/MessageFormatter.js
  20. 258 0
      services/chat/app/js/Features/Messages/MessageHttpController.js
  21. 146 0
      services/chat/app/js/Features/Messages/MessageManager.js
  22. 159 0
      services/chat/app/js/Features/Threads/ThreadManager.js
  23. 9 0
      services/chat/app/js/mongojs.js
  24. 84 0
      services/chat/app/js/router.js
  25. 50 0
      services/chat/app/js/server.js
  26. 9 0
      services/chat/buildscript.txt
  27. 0 20
      services/chat/config/settings.defaults.coffee
  28. 30 0
      services/chat/config/settings.defaults.js
  29. 53 0
      services/chat/decaffeinate.sh
  30. 9 4
      services/chat/docker-compose.ci.yml
  31. 13 5
      services/chat/docker-compose.yml
  32. 6 3
      services/chat/nodemon.json
  33. 1360 201
      services/chat/npm-shrinkwrap.json
  34. 22 5
      services/chat/package.json
  35. 0 32
      services/chat/test/acceptance/coffee/DeletingAMessageTests.coffee
  36. 0 31
      services/chat/test/acceptance/coffee/DeletingAThreadTests.coffee
  37. 0 34
      services/chat/test/acceptance/coffee/EditingAMessageTests.coffee
  38. 0 64
      services/chat/test/acceptance/coffee/GettingMessagesTests.coffee
  39. 0 72
      services/chat/test/acceptance/coffee/ResolvingAThreadTests.coffee
  40. 0 101
      services/chat/test/acceptance/coffee/SendingAMessageTests.coffee
  41. 0 20
      services/chat/test/acceptance/coffee/helpers/ChatApp.coffee
  42. 0 60
      services/chat/test/acceptance/coffee/helpers/ChatClient.coffee
  43. 73 0
      services/chat/test/acceptance/js/DeletingAMessageTests.js
  44. 63 0
      services/chat/test/acceptance/js/DeletingAThreadTests.js
  45. 72 0
      services/chat/test/acceptance/js/EditingAMessageTests.js
  46. 138 0
      services/chat/test/acceptance/js/GettingMessagesTests.js
  47. 147 0
      services/chat/test/acceptance/js/ResolvingAThreadTests.js
  48. 190 0
      services/chat/test/acceptance/js/SendingAMessageTests.js
  49. 47 0
      services/chat/test/acceptance/js/helpers/ChatApp.js
  50. 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"
+  }
+}

+ 38 - 0
services/chat/.github/ISSUE_TEMPLATE.md

@@ -0,0 +1,38 @@
+<!-- BUG REPORT TEMPLATE -->
+
+## Steps to Reproduce
+<!-- Describe the steps leading up to when / where you found the bug. -->
+<!-- Screenshots may be helpful here. -->
+
+1.
+2.
+3.
+
+## Expected Behaviour
+<!-- What should have happened when you completed the steps above? -->
+
+## Observed Behaviour
+<!-- What actually happened when you completed the steps above? -->
+<!-- Screenshots may be helpful here. -->
+
+## Context
+<!-- How has this issue affected you? What were you trying to accomplish? -->
+
+## Technical Info
+<!-- Provide any technical details that may be applicable (or N/A if not applicable). -->
+
+* URL:
+* Browser Name and version:
+* Operating System and version (desktop or mobile):
+* Signed in as:
+* Project and/or file:
+
+## Analysis
+<!--- Optionally, document investigation of / suggest a fix for the bug, e.g. 'comes from this line / commit' -->
+
+## Who Needs to Know?
+<!-- If you want to bring this to the attention of particular people, @-mention them below. -->
+<!-- If a user reported this bug and should be notified when it is fixed, provide the Front conversation link. -->
+
+-
+-

+ 45 - 0
services/chat/.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,45 @@
+<!-- Please review https://github.com/overleaf/write_latex/blob/master/.github/CONTRIBUTING.md for guidance on what is expected in each section. -->
+
+### Description
+
+
+
+#### Screenshots
+
+
+
+#### Related Issues / PRs
+
+
+
+### Review
+
+
+
+#### Potential Impact
+
+
+
+#### Manual Testing Performed
+
+- [ ]
+- [ ]
+
+#### Accessibility
+
+
+
+### Deployment
+
+
+
+#### Deployment Checklist
+
+- [ ] Update documentation not included in the PR (if any)
+- [ ]
+
+#### Metrics and Monitoring
+
+
+
+#### Who Needs to Know?

+ 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'
+  ])
+}

+ 41 - 15
services/chat/Jenkinsfile

@@ -3,6 +3,13 @@ String cron_string = BRANCH_NAME == "master" ? "@daily" : ""
 pipeline {
   agent any
 
+  environment {
+    GIT_PROJECT = "chat-sharelatex"
+    JENKINS_WORKFLOW = "chat-sharelatex"
+    TARGET_URL = "${env.JENKINS_URL}blue/organizations/jenkins/${JENKINS_WORKFLOW}/detail/$BRANCH_NAME/$BUILD_NUMBER/pipeline"
+    GIT_API_URL = "https://api.github.com/repos/sharelatex/${GIT_PROJECT}/statuses/$GIT_COMMIT"
+  }
+
   triggers {
     pollSCM('* * * * *')
     cron(cron_string)
@@ -18,9 +25,18 @@ pipeline {
         }
       }
       steps {
-        // we need to disable logallrefupdates, else git clones 
-        // during the npm install will require git to lookup the 
-        // user id which does not exist in the container's 
+        withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
+          sh "curl $GIT_API_URL \
+            --data '{ \
+            \"state\" : \"pending\", \
+            \"target_url\": \"$TARGET_URL\", \
+            \"description\": \"Your build is underway\", \
+            \"context\": \"ci/jenkins\" }' \
+            -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD"
+        }
+        // we need to disable logallrefupdates, else git clones
+        // during the npm install will require git to lookup the
+        // user id which does not exist in the container's
         // /etc/passwd file, causing the clone to fail.
         sh 'git config --global core.logallrefupdates false'
         sh 'rm -rf node_modules'
@@ -28,18 +44,6 @@ pipeline {
       }
     }
 
-    stage('Compile') {
-      agent {
-        docker {
-          image 'node:6.14.1'
-          reuseNode true
-        }
-      }
-      steps {
-        sh 'npm run compile:all'
-      }
-    }
-
     stage('Unit Tests') {
       steps {
         sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_unit'
@@ -77,6 +81,19 @@ pipeline {
   post {
     always {
       sh 'DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" make test_clean'
+      sh 'make clean'
+    }
+
+    success {
+      withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
+        sh "curl $GIT_API_URL \
+          --data '{ \
+          \"state\" : \"success\", \
+          \"target_url\": \"$TARGET_URL\", \
+          \"description\": \"Your build succeeded!\", \
+          \"context\": \"ci/jenkins\" }' \
+          -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD"
+      }
     }
 
     failure {
@@ -84,6 +101,15 @@ pipeline {
            to: "${EMAIL_ALERT_TO}",
            subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}",
            body: "Build: ${BUILD_URL}")
+      withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
+        sh "curl $GIT_API_URL \
+          --data '{ \
+          \"state\" : \"failure\", \
+          \"target_url\": \"$TARGET_URL\", \
+          \"description\": \"Your build failed\", \
+          \"context\": \"ci/jenkins\" }' \
+          -u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD"
+      }
     }
   }
 

+ 17 - 11
services/chat/Makefile

@@ -1,29 +1,35 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.0.0
+# Version: 1.1.10
 
 BUILD_NUMBER ?= local
 BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
 PROJECT_NAME = chat
 DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
-DOCKER_COMPOSE := docker-compose ${DOCKER_COMPOSE_FLAGS}
+DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \
+	BRANCH_NAME=$(BRANCH_NAME) \
+	PROJECT_NAME=$(PROJECT_NAME) \
+	MOCHA_GREP=${MOCHA_GREP} \
+	docker-compose ${DOCKER_COMPOSE_FLAGS}
+
 
 clean:
-	rm -f app.js
-	rm -rf app/js
-	rm -rf test/unit/js
-	rm -rf test/acceptance/js
+lint:
+	$(DOCKER_COMPOSE) run --rm test_unit npm run lint
 
-test: test_unit test_acceptance
+test: lint test_unit test_acceptance
 
 test_unit:
-	@[ -d test/unit ] && $(DOCKER_COMPOSE) run --rm test_unit -- ${MOCHA_ARGS} || echo "chat has no unit tests"
+	@[ ! -d test/unit ] && echo "chat has no unit tests" || $(DOCKER_COMPOSE) run --rm test_unit
 
-test_acceptance: test_clean # clear the database before each acceptance test run
-	@[ -d test/acceptance ] && $(DOCKER_COMPOSE) run --rm test_acceptance -- ${MOCHA_ARGS} || echo "chat has no acceptance tests"
+test_acceptance: test_clean test_acceptance_pre_run # clear the database before each acceptance test run
+	@[ ! -d test/acceptance ] && echo "chat has no acceptance tests" || $(DOCKER_COMPOSE) run --rm test_acceptance
 
 test_clean:
-	$(DOCKER_COMPOSE) down
+	$(DOCKER_COMPOSE) down -v -t 0
+
+test_acceptance_pre_run:
+	@[ ! -f test/acceptance/scripts/pre-run ] && echo "chat has no pre acceptance tests task" || $(DOCKER_COMPOSE) run --rm test_acceptance test/acceptance/scripts/pre-run
 
 .PHONY: clean test test_unit test_acceptance test_clean build publish

+ 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
+}

+ 9 - 0
services/chat/buildscript.txt

@@ -0,0 +1,9 @@
+chat
+--node-version=6.14.1
+--script-version=1.1.10
+--build-target=native
+--kube=false
+--dependencies=redis,mongo
+--language=es
+--docker-repos=quay.io/sharelatex
+--acceptance-creds=None

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

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

+ 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: `mongodb://${process.env['MONGO_HOST'] || 'localhost'}/sharelatex`
+  },
+
+  redis: {
+    web: {
+      host: process.env['REDIS_HOST'] || 'localhost',
+      port: '6379',
+      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"

+ 9 - 4
services/chat/docker-compose.ci.yml

@@ -1,7 +1,7 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.0.0
+# Version: 1.1.10
 
 version: "2"
 
@@ -11,7 +11,8 @@ services:
     volumes:
       - .:/app
     working_dir: /app
-    entrypoint: npm run test:unit:_run
+    user: node
+    command: npm run test:unit:_run
 
   test_acceptance:
     image: node:6.14.1
@@ -19,12 +20,16 @@ services:
       - .:/app
     working_dir: /app
     environment:
+      ELASTIC_SEARCH_DSN: es:9200
       REDIS_HOST: redis
       MONGO_HOST: mongo
+      POSTGRES_HOST: postgres
+      MOCHA_GREP: ${MOCHA_GREP}
     depends_on:
-      - redis
       - mongo
-    entrypoint: npm run test:acceptance:_run
+      - redis
+    user: node
+    command: npm run test:acceptance:_run
 
   redis:
     image: redis

+ 13 - 5
services/chat/docker-compose.yml

@@ -1,7 +1,7 @@
 # This file was auto-generated, do not edit it directly.
 # Instead run bin/update_build_scripts from
 # https://github.com/sharelatex/sharelatex-dev-environment
-# Version: 1.0.0
+# Version: 1.1.10
 
 version: "2"
 
@@ -11,23 +11,31 @@ services:
     volumes:
       - .:/app
     working_dir: /app
-    entrypoint: npm run test:unit
+    environment:
+      MOCHA_GREP: ${MOCHA_GREP}
+    command: npm run test:unit
+    user: node
 
   test_acceptance:
     image: node:6.14.1
     volumes:
       - .:/app
+    working_dir: /app
     environment:
+      ELASTIC_SEARCH_DSN: es:9200
       REDIS_HOST: redis
       MONGO_HOST: mongo
+      POSTGRES_HOST: postgres
+      MOCHA_GREP: ${MOCHA_GREP}
+    user: node
     depends_on:
-      - redis
       - mongo
-    working_dir: /app
-    entrypoint: npm run test:acceptance
+      - redis
+    command: npm run test:acceptance
 
   redis:
     image: redis
 
   mongo:
     image: mongo:3.4
+

+ 6 - 3
services/chat/nodemon.json

@@ -4,12 +4,15 @@
     "node_modules/"
   ],
   "verbose": true,
+  "legacyWatch": true,
   "execMap": {
     "js": "npm run start"
   },
+
   "watch": [
-    "app/coffee/",
-    "app.coffee"
+    "app/js/",
+    "app.js",
+    "config/"
   ],
-  "ext": "coffee"
+  "ext": "js"
 }

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


+ 22 - 5
services/chat/package.json

@@ -8,15 +8,16 @@
   },
   "scripts": {
     "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee",
-    "start": "npm run compile:app && node app.js",
-    "test:acceptance": "npm run compile:app && npm run compile:acceptance_tests && npm run test:acceptance:_run -- $@",
-    "test:unit": "npm run compile:app && npm run compile:unit_tests && npm run test:unit:_run -- $@",
+    "start": "node $NODE_APP_OPTIONS app.js",
+    "test:acceptance": "npm run test:acceptance:_run -- --grep=$MOCHA_GREP",
+    "test:unit": "npm run test:unit:_run -- --grep=$MOCHA_GREP",
     "compile:unit_tests": "[ -e test/unit ] && coffee -o test/unit/js -c test/unit/coffee || echo 'No unit tests to compile'",
     "compile:acceptance_tests": "[ -e test/acceptance ] && coffee -o test/acceptance/js -c test/acceptance/coffee || echo 'No acceptance tests to compile'",
     "compile:all": "npm run compile:app && npm run compile:unit_tests && npm run compile:acceptance_tests",
     "nodemon": "nodemon --config nodemon.json",
     "test:acceptance:_run": "mocha --recursive --reporter spec --timeout 15000 --exit $@ test/acceptance/js",
-    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js"
+    "test:unit:_run": "mocha --recursive --reporter spec $@ test/unit/js",
+    "lint": "node_modules/.bin/eslint ."
   },
   "dependencies": {
     "async": "0.2.9",
@@ -27,12 +28,27 @@
     "mongojs": "2.4.0",
     "redis": "~0.10.1",
     "request": "^2.79.0",
-    "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
+    "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#1.1.0",
     "v8-profiler": "^5.6.5"
   },
   "devDependencies": {
     "bunyan": "^1.0.0",
     "chai": "",
+    "eslint": "^5.10.0",
+    "eslint-config-prettier": "^3.3.0",
+    "eslint-config-standard": "^12.0.0",
+    "eslint-config-standard-jsx": "^6.0.2",
+    "eslint-config-standard-react": "^7.0.2",
+    "eslint-plugin-chai-expect": "^2.0.1",
+    "eslint-plugin-chai-friendly": "^0.4.1",
+    "eslint-plugin-import": "^2.14.0",
+    "eslint-plugin-jsx-a11y": "^6.1.2",
+    "eslint-plugin-mocha": "^5.2.0",
+    "eslint-plugin-node": "^8.0.0",
+    "eslint-plugin-prettier": "^3.0.0",
+    "eslint-plugin-promise": "^4.0.1",
+    "eslint-plugin-react": "^7.11.1",
+    "eslint-plugin-standard": "^4.0.0",
     "grunt": "~0.4.1",
     "grunt-bunyan": "^0.5.0",
     "grunt-concurrent": "~0.4.2",
@@ -47,6 +63,7 @@
     "grunt-plato": "~0.2.1",
     "mocha": "^4.1.0",
     "nodemon": "^1.14.11",
+    "prettier-eslint-cli": "^4.7.1",
     "sandboxed-module": "",
     "sinon": "",
     "timekeeper": ""

+ 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