mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' into csh-ho-docker-issue-1338-bulk-upgrade
This commit is contained in:
commit
1d8e52cdd5
41 changed files with 10150 additions and 1641 deletions
75
services/chat/.eslintrc
Normal file
75
services/chat/.eslintrc
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
5
services/chat/.gitignore
vendored
5
services/chat/.gitignore
vendored
|
@ -1,12 +1,7 @@
|
||||||
**.swp
|
**.swp
|
||||||
|
|
||||||
app.js
|
|
||||||
app/js/
|
|
||||||
test/unit/js/
|
|
||||||
test/acceptance/js/
|
|
||||||
public/build/
|
public/build/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
/public/js/chat.js
|
|
||||||
plato/
|
plato/
|
||||||
|
|
4
services/chat/.prettierrc
Normal file
4
services/chat/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
|
@ -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
services/chat/Gruntfile.js
Normal file
129
services/chat/Gruntfile.js
Normal file
|
@ -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'
|
||||||
|
])
|
||||||
|
}
|
|
@ -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
services/chat/app.js
Normal file
39
services/chat/app.js
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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
|
|
|
@ -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")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
services/chat/app/js/Features/Messages/MessageFormatter.js
Normal file
80
services/chat/app/js/Features/Messages/MessageFormatter.js
Normal file
|
@ -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
services/chat/app/js/Features/Messages/MessageHttpController.js
Normal file
258
services/chat/app/js/Features/Messages/MessageHttpController.js
Normal file
|
@ -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
services/chat/app/js/Features/Messages/MessageManager.js
Normal file
146
services/chat/app/js/Features/Messages/MessageManager.js
Normal file
|
@ -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
services/chat/app/js/Features/Threads/ThreadManager.js
Normal file
159
services/chat/app/js/Features/Threads/ThreadManager.js
Normal file
|
@ -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
services/chat/app/js/mongojs.js
Normal file
9
services/chat/app/js/mongojs.js
Normal file
|
@ -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
services/chat/app/js/router.js
Normal file
84
services/chat/app/js/router.js
Normal file
|
@ -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
services/chat/app/js/server.js
Normal file
50
services/chat/app/js/server.js
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
settings =
|
|
||||||
internal:
|
|
||||||
chat:
|
|
||||||
host: process.env['LISTEN_ADDRESS'] or "localhost"
|
|
||||||
port: 3010
|
|
||||||
|
|
||||||
apis:
|
|
||||||
web:
|
|
||||||
url: "http://#{process.env['WEB_HOST'] || "localhost"}:3000"
|
|
||||||
user: "sharelatex"
|
|
||||||
pass: "password"
|
|
||||||
|
|
||||||
mongo:
|
|
||||||
url: process.env['MONGO_CONNECTION_STRING'] or "mongodb://#{process.env["MONGO_HOST"] or "localhost"}/sharelatex"
|
|
||||||
|
|
||||||
|
|
||||||
redis:
|
|
||||||
web:
|
|
||||||
host: process.env['REDIS_HOST'] || "localhost"
|
|
||||||
port: "6379"
|
|
||||||
password: process.env['REDIS_PASSWORD'] || ""
|
|
||||||
|
|
||||||
module.exports = settings
|
|
30
services/chat/config/settings.defaults.js
Normal file
30
services/chat/config/settings.defaults.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
module.exports = {
|
||||||
|
internal: {
|
||||||
|
chat: {
|
||||||
|
host: process.env['LISTEN_ADDRESS'] || 'localhost',
|
||||||
|
port: 3010
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
apis: {
|
||||||
|
web: {
|
||||||
|
url: `http://${process.env['WEB_HOST'] || 'localhost'}:${process.env[
|
||||||
|
'WEB_PORT'
|
||||||
|
] || 3000}`,
|
||||||
|
user: 'sharelatex',
|
||||||
|
pass: 'password'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mongo: {
|
||||||
|
url: process.env['MONGO_CONNECTION_STRING'] || `mongodb://${process.env['MONGO_HOST'] || 'localhost'}/sharelatex`
|
||||||
|
},
|
||||||
|
|
||||||
|
redis: {
|
||||||
|
web: {
|
||||||
|
host: process.env['REDIS_HOST'] || 'localhost',
|
||||||
|
port: '6379',
|
||||||
|
password: process.env['REDIS_PASSWORD'] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
services/chat/decaffeinate.sh
Executable file
53
services/chat/decaffeinate.sh
Executable file
|
@ -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"
|
8922
services/chat/npm-shrinkwrap.json
generated
8922
services/chat/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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
services/chat/test/acceptance/js/DeletingAMessageTests.js
Normal file
73
services/chat/test/acceptance/js/DeletingAMessageTests.js
Normal file
|
@ -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
services/chat/test/acceptance/js/DeletingAThreadTests.js
Normal file
63
services/chat/test/acceptance/js/DeletingAThreadTests.js
Normal file
|
@ -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
services/chat/test/acceptance/js/EditingAMessageTests.js
Normal file
72
services/chat/test/acceptance/js/EditingAMessageTests.js
Normal file
|
@ -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
services/chat/test/acceptance/js/GettingMessagesTests.js
Normal file
138
services/chat/test/acceptance/js/GettingMessagesTests.js
Normal file
|
@ -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
services/chat/test/acceptance/js/ResolvingAThreadTests.js
Normal file
147
services/chat/test/acceptance/js/ResolvingAThreadTests.js
Normal file
|
@ -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
services/chat/test/acceptance/js/SendingAMessageTests.js
Normal file
190
services/chat/test/acceptance/js/SendingAMessageTests.js
Normal file
|
@ -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
services/chat/test/acceptance/js/helpers/ChatApp.js
Normal file
47
services/chat/test/acceptance/js/helpers/ChatApp.js
Normal file
|
@ -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
services/chat/test/acceptance/js/helpers/ChatClient.js
Normal file
113
services/chat/test/acceptance/js/helpers/ChatClient.js
Normal file
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue