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
|
||||
|
||||
app.js
|
||||
app/js/
|
||||
test/unit/js/
|
||||
test/acceptance/js/
|
||||
public/build/
|
||||
|
||||
node_modules/
|
||||
|
||||
/public/js/chat.js
|
||||
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