Merge pull request #11051 from overleaf/revert-10983-revert-10901-lg-openapi-chat

Migrate chat service to OpenAPI : (Revert of the Revert)

GitOrigin-RevId: 77a34fef20f77344e01461d42b531c623cf28203
This commit is contained in:
Lucie Germain
2023-01-05 09:05:00 +00:00
committed by Copybot
parent 29576fc1fd
commit afa66aef49
11 changed files with 802 additions and 150 deletions
+3 -2
View File
@@ -1,13 +1,14 @@
import logger from '@overleaf/logger'
import settings from '@overleaf/settings'
import { mongoClient } from './app/js/mongodb.js'
import { server } from './app/js/server.js'
import { createServer } from './app/js/server.js'
const port = settings.internal.chat.port
const host = settings.internal.chat.host
mongoClient
.connect()
.then(() => {
.then(async () => {
const { server } = await createServer()
server.listen(port, host, function (err) {
if (err) {
logger.fatal({ err }, `Cannot bind to ${host}:${port}. Exiting.`)
@@ -3,24 +3,105 @@ import * as MessageManager from './MessageManager.js'
import * as MessageFormatter from './MessageFormatter.js'
import * as ThreadManager from '../Threads/ThreadManager.js'
import { ObjectId } from '../../mongodb.js'
import { expressify } from '../../util/promises.js'
const DEFAULT_MESSAGE_LIMIT = 50
const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words
export const getGlobalMessages = expressify(async (req, res) => {
async function readContext(context, req) {
req.body = context.requestBody
req.params = context.params.path
req.query = context.params.query
if (typeof req.params.projectId !== 'undefined') {
if (!ObjectId.isValid(req.params.projectId)) {
context.res.status(400).setBody('Invalid projectId')
}
}
if (typeof req.params.threadId !== 'undefined') {
if (!ObjectId.isValid(req.params.threadId)) {
context.res.status(400).setBody('Invalid threadId')
}
}
}
export async function callMessageHttpController(context, ControllerMethod) {
const req = {}
readContext(context, req)
if (context.res.statusCode !== 400) {
return await ControllerMethod(req, context.res)
} else {
return context.res.body
}
}
export async function getGlobalMessages(context) {
return await callMessageHttpController(context, _getGlobalMessages)
}
export async function sendGlobalMessage(context) {
return await callMessageHttpController(context, _sendGlobalMessage)
}
export async function sendMessage(context) {
return await callMessageHttpController(context, _sendThreadMessage)
}
export async function getThreads(context) {
return await callMessageHttpController(context, _getAllThreads)
}
export async function resolveThread(context) {
return await callMessageHttpController(context, _resolveThread)
}
export async function reopenThread(context) {
return await callMessageHttpController(context, _reopenThread)
}
export async function deleteThread(context) {
return await callMessageHttpController(context, _deleteThread)
}
export async function editMessage(context) {
return await callMessageHttpController(context, _editMessage)
}
export async function deleteMessage(context) {
return await callMessageHttpController(context, _deleteMessage)
}
export async function destroyProject(context) {
return await callMessageHttpController(context, _destroyProject)
}
export async function getStatus(context) {
const message = 'chat is alive'
context.res.status(200).setBody(message)
return message
}
const _getGlobalMessages = async (req, res) => {
await _getMessages(ThreadManager.GLOBAL_THREAD, req, res)
})
}
export const sendGlobalMessage = expressify(async (req, res) => {
await _sendMessage(ThreadManager.GLOBAL_THREAD, req, res)
})
async function _sendGlobalMessage(req, res) {
const { user_id: userId, content } = req.body
const { projectId } = req.params
return await _sendMessage(
userId,
projectId,
content,
ThreadManager.GLOBAL_THREAD,
res
)
}
export const sendThreadMessage = expressify(async (req, res) => {
await _sendMessage(req.params.threadId, req, res)
})
async function _sendThreadMessage(req, res) {
const { user_id: userId, content } = req.body
const { projectId, threadId } = req.params
return await _sendMessage(userId, projectId, content, threadId, res)
}
export const getAllThreads = expressify(async (req, res) => {
const _getAllThreads = async (req, res) => {
const { projectId } = req.params
logger.debug({ projectId }, 'getting all threads')
const rooms = await ThreadManager.findAllThreadRooms(projectId)
@@ -28,32 +109,32 @@ export const getAllThreads = expressify(async (req, res) => {
const messages = await MessageManager.findAllMessagesInRooms(roomIds)
const threads = MessageFormatter.groupMessagesByThreads(rooms, messages)
res.json(threads)
})
}
export const resolveThread = expressify(async (req, res) => {
const _resolveThread = async (req, res) => {
const { projectId, threadId } = req.params
const { user_id: userId } = req.body
logger.debug({ userId, projectId, threadId }, 'marking thread as resolved')
await ThreadManager.resolveThread(projectId, threadId, userId)
res.sendStatus(204)
})
res.status(204)
}
export const reopenThread = expressify(async (req, res) => {
const _reopenThread = async (req, res) => {
const { projectId, threadId } = req.params
logger.debug({ projectId, threadId }, 'reopening thread')
await ThreadManager.reopenThread(projectId, threadId)
res.sendStatus(204)
})
res.status(204)
}
export const deleteThread = expressify(async (req, res) => {
const _deleteThread = async (req, res) => {
const { projectId, threadId } = req.params
logger.debug({ projectId, threadId }, 'deleting thread')
const roomId = await ThreadManager.deleteThread(projectId, threadId)
await MessageManager.deleteAllMessagesInRoom(roomId)
res.sendStatus(204)
})
res.status(204)
}
export const editMessage = expressify(async (req, res) => {
const _editMessage = async (req, res) => {
const { content, userId } = req.body
const { projectId, threadId, messageId } = req.params
logger.debug({ projectId, threadId, messageId, content }, 'editing message')
@@ -66,20 +147,21 @@ export const editMessage = expressify(async (req, res) => {
Date.now()
)
if (!found) {
return res.sendStatus(404)
res.status(404)
return
}
res.sendStatus(204)
})
res.status(204)
}
export const deleteMessage = expressify(async (req, res) => {
const _deleteMessage = async (req, res) => {
const { projectId, threadId, messageId } = req.params
logger.debug({ projectId, threadId, messageId }, 'deleting message')
const room = await ThreadManager.findOrCreateThread(projectId, threadId)
await MessageManager.deleteMessage(room._id, messageId)
res.sendStatus(204)
})
res.status(204)
}
export const destroyProject = expressify(async (req, res) => {
const _destroyProject = async (req, res) => {
const { projectId } = req.params
logger.debug({ projectId }, 'destroying project')
const rooms = await ThreadManager.findAllThreadRoomsAndGlobalThread(projectId)
@@ -88,22 +170,24 @@ export const destroyProject = expressify(async (req, res) => {
await MessageManager.deleteAllMessagesInRooms(roomIds)
logger.debug({ projectId }, 'deleting all threads in project')
await ThreadManager.deleteAllThreadsInProject(projectId)
res.sendStatus(204)
})
res.status(204)
}
async function _sendMessage(clientThreadId, req, res) {
const { user_id: userId, content } = req.body
const { projectId } = req.params
async function _sendMessage(userId, projectId, content, clientThreadId, res) {
if (!ObjectId.isValid(userId)) {
return res.status(400).send('Invalid userId')
const message = 'Invalid userId'
res.status(400).setBody(message)
return message
}
if (!content) {
return res.status(400).send('No content provided')
const message = 'No content provided'
res.status(400).setBody(message)
return message
}
if (content.length > MAX_MESSAGE_LENGTH) {
return res
.status(400)
.send(`Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`)
const message = `Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
res.status(400).setBody(message)
return message
}
logger.debug(
{ clientThreadId, projectId, userId, content },
@@ -121,7 +205,7 @@ async function _sendMessage(clientThreadId, req, res) {
)
message = MessageFormatter.formatMessageForClientSide(message)
message.room_id = projectId
res.status(201).send(message)
res.status(201).setBody(message)
}
async function _getMessages(clientThreadId, req, res) {
@@ -153,5 +237,5 @@ async function _getMessages(clientThreadId, req, res) {
let messages = await MessageManager.getMessages(threadObjectId, limit, before)
messages = MessageFormatter.formatMessagesForClientSide(messages)
logger.debug({ projectId, messages }, 'got messages')
res.status(200).send(messages)
res.status(200).setBody(messages)
}
-65
View File
@@ -1,65 +0,0 @@
import * as MessageHttpController from './Features/Messages/MessageHttpController.js'
import { ObjectId } from './mongodb.js'
export function route(app) {
app.param('projectId', function (req, res, next, projectId) {
if (ObjectId.isValid(projectId)) {
next()
} else {
res.status(400).send('Invalid projectId')
}
})
app.param('threadId', function (req, res, next, threadId) {
if (ObjectId.isValid(threadId)) {
next()
} else {
res.status(400).send('Invalid threadId')
}
})
// These are for backwards compatibility
app.get('/room/:projectId/messages', MessageHttpController.getGlobalMessages)
app.post('/room/:projectId/messages', MessageHttpController.sendGlobalMessage)
app.get(
'/project/:projectId/messages',
MessageHttpController.getGlobalMessages
)
app.post(
'/project/:projectId/messages',
MessageHttpController.sendGlobalMessage
)
app.post(
'/project/:projectId/thread/:threadId/messages',
MessageHttpController.sendThreadMessage
)
app.get('/project/:projectId/threads', MessageHttpController.getAllThreads)
app.post(
'/project/:projectId/thread/:threadId/messages/:messageId/edit',
MessageHttpController.editMessage
)
app.delete(
'/project/:projectId/thread/:threadId/messages/:messageId',
MessageHttpController.deleteMessage
)
app.post(
'/project/:projectId/thread/:threadId/resolve',
MessageHttpController.resolveThread
)
app.post(
'/project/:projectId/thread/:threadId/reopen',
MessageHttpController.reopenThread
)
app.delete(
'/project/:projectId/thread/:threadId',
MessageHttpController.deleteThread
)
app.delete('/project/:projectId', MessageHttpController.destroyProject)
app.get('/status', (req, res, next) => res.send('chat is alive'))
}
+36 -9
View File
@@ -2,18 +2,45 @@ import http from 'http'
import metrics from '@overleaf/metrics'
import logger from '@overleaf/logger'
import express from 'express'
import bodyParser from 'body-parser'
import * as Router from './router.js'
import exegesisExpress from 'exegesis-express'
import path from 'path'
import { fileURLToPath } from 'url'
import * as messagesController from './Features/Messages/MessageHttpController.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
metrics.initialize('chat')
logger.initialize('chat')
export const app = express()
export const server = http.createServer(app)
export async function createServer() {
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(metrics.http.monitor(logger))
metrics.injectMetricsRoute(app)
// See https://github.com/exegesis-js/exegesis/blob/master/docs/Options.md
const options = {
controllers: { messagesController },
ignoreServers: true,
allowMissingControllers: false,
}
Router.route(app)
// const exegesisMiddleware = await exegesisExpress.middleware(
const exegesisMiddleware = await exegesisExpress.middleware(
path.resolve(__dirname, '../../chat.yaml'),
options
)
// If you have any body parsers, this should go before them.
app.use(exegesisMiddleware)
// Return a 404
app.use((req, res) => {
res.status(404).json({ message: `Not found` })
})
// Handle any unexpected errors
app.use((err, req, res, next) => {
res.status(500).json({ message: `Internal error: ${err.message}` })
})
const server = http.createServer(app)
return { app, server }
}
+317
View File
@@ -0,0 +1,317 @@
openapi: 3.1.0
x-stoplight:
id: okoe8mh50pjec
info:
title: chat
version: '1.0'
servers:
- url: 'http://chat:3010'
x-exegesis-controller: messagesController
paths:
'/project/{projectId}/messages':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
get:
summary: Get Global messages
tags: []
responses:
'201':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Message'
operationId: getGlobalMessages
description: Get global messages for the project with Project ID provided
parameters:
- schema:
type: string
in: query
name: before
- schema:
type: string
in: query
name: limit
post:
summary: Send Global message
operationId: sendGlobalMessage
responses:
'201':
description: OK
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
examples:
example-1:
value:
user_id: string
content: string
description: 'UserID and Content of the message to be posted. '
description: Send global message for the project with Project ID provided
'/project/{projectId}/thread/{threadId}/messages':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
- schema:
type: string
name: threadId
in: path
required: true
post:
summary: Send message
operationId: sendMessage
responses:
'201':
description: Created
description: Add a message to the thread with thread ID provided from the Project with Project ID provided.
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
description: |-
JSON object with :
- user_id: Id of the user
- content: Content of the message
'/project/{projectId}/threads':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
get:
summary: Get Threads
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Thread'
examples: {}
'404':
description: Not Found
operationId: getThreads
description: Get the list of threads for the project with Project ID provided
'/project/{projectId}/thread/{threadId}/messages/{messageId}/edit':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
- schema:
type: string
name: threadId
in: path
required: true
- schema:
type: string
name: messageId
in: path
required: true
post:
summary: Edit message
operationId: editMessage
responses:
'204':
description: No Content
'404':
description: Not Found
requestBody:
content:
application/json:
schema:
type: object
properties:
content:
type: string
user_id:
type: string
readOnly: true
required:
- content
examples: {}
description: |-
JSON object with :
- content: Content of the message to edit
- user_id: Id of the user (optional)
description: |
Update message with Message ID provided from the Thread ID and Project ID provided
'/project/{projectId}/thread/{threadId}/messages/{messageId}':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
- schema:
type: string
name: threadId
in: path
required: true
- schema:
type: string
name: messageId
in: path
required: true
delete:
summary: Delete message
operationId: deleteMessage
responses:
'204':
description: No Content
description: 'Delete message with Message ID provided, from the Thread with ThreadID and ProjectID provided'
'/project/{projectId}/thread/{threadId}/resolve':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
- schema:
type: string
name: threadId
in: path
required: true
post:
summary: Resolve Thread
operationId: resolveThread
responses:
'204':
description: No Content
requestBody:
content:
application/json:
schema:
type: object
properties:
user_id:
type: string
required:
- user_id
description: |-
JSON object with :
- user_id: Id of the user.
description: Mark Thread with ThreadID and ProjectID provided owned by the user with UserID provided as resolved.
'/project/{projectId}/thread/{threadId}/reopen':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
- schema:
type: string
name: threadId
in: path
required: true
post:
summary: Reopen Thread
operationId: reopenThread
responses:
'204':
description: No Content
description: |-
Reopen Thread with ThreadID and ProjectID provided.
i.e unmark it as resolved.
'/project/{projectId}/thread/{threadId}':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
- schema:
type: string
name: threadId
in: path
required: true
delete:
summary: Delete thread
operationId: deleteThread
responses:
'204':
description: No Content
description: Delete thread with ThreadID and ProjectID provided
'/project/{projectId}':
parameters:
- schema:
type: string
name: projectId
in: path
required: true
delete:
summary: Destroy project
operationId: destroyProject
responses:
'204':
description: No Content
description: 'Delete all threads from Project with Project ID provided, and all messages in those threads.'
/status:
get:
summary: Check status
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: string
description: chat is alive
operationId: getStatus
description: Check that the Chat service is alive
head:
summary: Check status
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: string
description: chat is alive
operationId: getStatus
description: Check that the Chat service is alive
components:
schemas:
Message:
title: Message
x-stoplight:
id: ue9n1vvezlutw
type: object
examples:
- user_id: string
- content: string
properties:
user_id:
type: string
content:
type: string
required:
- user_id
- content
Thread:
title: Thread
x-stoplight:
id: 0ppt3jw4h5bua
type: array
items:
$ref: '#/components/schemas/Message'
+9 -1
View File
@@ -22,6 +22,7 @@
"@overleaf/settings": "*",
"async": "^3.2.2",
"body-parser": "^1.19.0",
"exegesis-express": "^4.0.0",
"express": "4.17.1",
"mongodb": "^4.11.0"
},
@@ -35,5 +36,12 @@
"sandboxed-module": "^2.0.4",
"sinon": "^9.2.4",
"timekeeper": "^2.2.0"
}
},
"version": "1.0.0",
"directories": {
"test": "test"
},
"keywords": [],
"author": "",
"license": "AGPL-3.0"
}
@@ -28,6 +28,9 @@ describe('Getting messages', async function () {
content2
)
expect(response2.statusCode).to.equal(201)
const { response: response3, body } = await ChatClient.checkStatus()
expect(response3.statusCode).to.equal(200)
expect(body).to.equal('chat is alive')
})
it('should contain the messages and populated users when getting the messages', async function () {
@@ -122,7 +122,9 @@ describe('Sending a message', async function () {
null
)
expect(response.statusCode).to.equal(400)
expect(body).to.equal('No content provided')
// Exegesis is responding with validation errors. I can´t find a way to choose the validation error yet.
// expect(body).to.equal('No content provided')
expect(body.message).to.equal('Validation errors')
})
})
@@ -1,20 +1,15 @@
import { server } from '../../../../app/js/server.js'
import { createServer } from '../../../../app/js/server.js'
import { promisify } from 'util'
export { db } from '../../../../app/js/mongodb.js'
let serverPromise = null
function startServer(resolve, reject) {
server.listen(3010, 'localhost', error => {
if (error) {
return reject(error)
}
resolve()
})
}
export async function ensureRunning() {
if (!serverPromise) {
serverPromise = new Promise(startServer)
const { app } = await createServer()
const startServer = promisify(app.listen.bind(app))
serverPromise = startServer(3010, 'localhost')
}
return serverPromise
}
@@ -64,20 +64,6 @@ export async function resolveThread(projectId, threadId, userId) {
})
}
export async function reopenThread(projectId, threadId) {
return asyncRequest({
method: 'post',
url: `/project/${projectId}/thread/${threadId}/reopen`,
})
}
export async function deleteThread(projectId, threadId) {
return asyncRequest({
method: 'delete',
url: `/project/${projectId}/thread/${threadId}`,
})
}
export async function editMessage(projectId, threadId, messageId, content) {
return asyncRequest({
method: 'post',
@@ -105,6 +91,28 @@ export async function editMessageWithUser(
})
}
export async function checkStatus() {
return asyncRequest({
method: 'get',
url: `/status`,
json: true,
})
}
export async function reopenThread(projectId, threadId) {
return asyncRequest({
method: 'post',
url: `/project/${projectId}/thread/${threadId}/reopen`,
})
}
export async function deleteThread(projectId, threadId) {
return asyncRequest({
method: 'delete',
url: `/project/${projectId}/thread/${threadId}`,
})
}
export async function deleteMessage(projectId, threadId, messageId) {
return asyncRequest({
method: 'delete',