diff --git a/public/openapi/write/chats/roomId.yaml b/public/openapi/write/chats/roomId.yaml index 3869d212ca..143eaf2b6e 100644 --- a/public/openapi/write/chats/roomId.yaml +++ b/public/openapi/write/chats/roomId.yaml @@ -94,3 +94,53 @@ get: type: boolean isAdminOrGlobalMod: type: boolean +post: + tags: + - chats + summary: send a chat message + description: This operation sends a chat message to a chat room + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid chat room id + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: This is a test message + required: + - message + responses: + '200': + description: message successfully sent + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + allOf: + - $ref: ../../components/schemas/Chats.yaml#/MessageObject + - type: object + properties: + self: + type: number + description: Whether or not the message was sent by the calling user (which if you're using this route, will always be 1) + newSet: + type: boolean + description: Whether the message is considered part of a new "set" of messages. It is used in the frontend UI for explicitly denoting that a time gap existed between messages. + cleanedContent: + type: string + mid: + type: number \ No newline at end of file diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index 66a09ee31d..e18b3d6ea7 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -2,8 +2,9 @@ define('forum/chats/messages', [ - 'components', 'translator', 'benchpress', 'hooks', 'bootbox', 'alerts', 'messages', -], function (components, translator, Benchpress, hooks, bootbox, alerts, messagesModule) { + 'components', 'translator', 'benchpress', 'hooks', + 'bootbox', 'alerts', 'messages', 'api', +], function (components, translator, Benchpress, hooks, bootbox, alerts, messagesModule, api) { const messages = {}; messages.sendMessage = function (roomId, inputEl) { @@ -24,25 +25,22 @@ define('forum/chats/messages', [ }); if (!mid) { - socket.emit('modules.chats.send', { - roomId: roomId, + api.post(`/chats/${roomId}`, { message: msg, - }, function (err) { - if (err) { - inputEl.val(msg); - messages.updateRemainingLength(inputEl.parent()); - if (err.message === '[[error:email-not-confirmed-chat]]') { - return messagesModule.showEmailConfirmWarning(err.message); - } - - return alerts.alert({ - alert_id: 'chat_spam_error', - title: '[[global:alert.error]]', - message: err.message, - type: 'danger', - timeout: 10000, - }); + }).catch((err) => { + inputEl.val(msg); + messages.updateRemainingLength(inputEl.parent()); + if (err.message === '[[error:email-not-confirmed-chat]]') { + return messagesModule.showEmailConfirmWarning(err.message); } + + return alerts.alert({ + alert_id: 'chat_spam_error', + title: '[[global:alert.error]]', + message: err.message, + type: 'danger', + timeout: 10000, + }); }); } else { socket.emit('modules.chats.edit', { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 836e278879..78b1e67902 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -1,8 +1,8 @@ 'use strict'; define('chat', [ - 'components', 'taskbar', 'translator', 'hooks', 'bootbox', 'alerts', -], function (components, taskbar, translator, hooks, bootbox, alerts) { + 'components', 'taskbar', 'translator', 'hooks', 'bootbox', 'alerts', 'api', +], function (components, taskbar, translator, hooks, bootbox, alerts, api) { const module = {}; let newMessage = false; @@ -20,27 +20,24 @@ define('chat', [ if (module.modalExists(roomId)) { loadAndCenter(module.getModal(roomId)); } else { - socket.emit('modules.chats.loadRoom', { roomId: roomId, uid: uid || app.user.uid }, function (err, roomData) { - if (err) { - return alerts.error(err); - } + api.get(`/chats/${roomId}`, { + uid: uid || app.user.uid, + }).then((roomData) => { roomData.users = roomData.users.filter(function (user) { return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10); }); roomData.uid = uid || app.user.uid; roomData.isSelf = true; module.createModal(roomData, loadAndCenter); - }); + }).catch(alerts.error); } }; module.newChat = function (touid, callback) { function createChat() { - socket.emit('modules.chats.newRoom', { touid: touid }, function (err, roomId) { - if (err) { - return alerts.error(err); - } - + api.post(`/chats`, { + uids: [touid], + }).then(({ roomId }) => { if (!ajaxify.data.template.chats) { module.openChat(roomId); } else { @@ -48,7 +45,7 @@ define('chat', [ } callback(null, roomId); - }); + }).catch(alerts.error); } callback = callback || function () { }; @@ -130,13 +127,7 @@ define('chat', [ if (module.modalExists(data.roomId)) { addMessageToModal(data); } else if (!ajaxify.data.template.chats) { - socket.emit('modules.chats.loadRoom', { - roomId: data.roomId, - }, function (err, roomData) { - if (err) { - return alerts.error(err); - } - + api.get(`/chats/${data.roomId}`, {}).then((roomData) => { roomData.users = roomData.users.filter(function (user) { return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10); }); @@ -144,7 +135,7 @@ define('chat', [ roomData.uid = app.user.uid; roomData.isSelf = isSelf; module.createModal(roomData); - }); + }).catch(alerts.error); } }; diff --git a/src/api/chats.js b/src/api/chats.js index 9e793577a4..edcd86bee7 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -1,17 +1,17 @@ 'use strict'; +const user = require('../user'); const meta = require('../meta'); -const privileges = require('../privileges'); const messaging = require('../messaging'); +const plugins = require('../plugins'); - -const websockets = require('../socket.io'); -const socketHelpers = require('../socket.io/helpers'); +// const websockets = require('../socket.io'); +// const socketHelpers = require('../socket.io/helpers'); const chatsAPI = module.exports; function rateLimitExceeded(caller) { - const session = caller.request ? caller.request.session : caller.session; // socket vs req + const session = caller.request ? caller.request.session : caller.session; // socket vs req const now = Date.now(); session.lastChatMessageTime = session.lastChatMessageTime || 0; if (now - session.lastChatMessageTime < meta.config.chatMessageDelay) { @@ -35,3 +35,27 @@ chatsAPI.create = async function (caller, data) { return await messaging.getRoomData(roomId); }; + +chatsAPI.post = async (caller, data) => { + if (rateLimitExceeded(caller)) { + throw new Error('[[error:too-many-messages]]'); + } + + ({ data } = await plugins.hooks.fire('filter:messaging.send', { + data, + uid: caller.uid, + })); + + await messaging.canMessageRoom(caller.uid, data.roomId); + const message = await messaging.sendMessage({ + uid: caller.uid, + roomId: data.roomId, + content: data.message, + timestamp: Date.now(), + ip: caller.ip, + }); + messaging.notifyUsersInRoom(caller.uid, data.roomId, message); + user.updateOnlineUsers(caller.uid); + + return message; +}; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index cac8cb074f..0dccc7ecdd 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -36,7 +36,12 @@ Chats.get = async (req, res) => { }; Chats.post = async (req, res) => { - // ... + const messageObj = await api.chats.post(req, { + ...req.body, + roomId: req.params.roomId, + }); + + helpers.formatApiResponse(200, res, messageObj); }; Chats.users = async (req, res) => { diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 5c60e4080d..bcd9195898 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -15,7 +15,7 @@ module.exports = function () { setupApiRoute(router, 'head', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.exists); setupApiRoute(router, 'get', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.get); - // setupApiRoute(router, 'post', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.post); + setupApiRoute(router, 'post', '/:roomId', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['message'])], controllers.write.chats.post); // // no route for room deletion, reserved just in case... // setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index f6c5002131..a5129904a2 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -59,46 +59,20 @@ SocketModules.chats.newRoom = async function (socket, data) { }; SocketModules.chats.send = async function (socket, data) { + sockets.warnDeprecated(socket, 'POST /api/v3/chats/:roomId'); + if (!data || !data.roomId || !socket.uid) { throw new Error('[[error:invalid-data]]'); } - if (rateLimitExceeded(socket)) { - throw new Error('[[error:too-many-messages]]'); - } const canChat = await privileges.global.can('chat', socket.uid); if (!canChat) { throw new Error('[[error:no-privileges]]'); } - const results = await plugins.hooks.fire('filter:messaging.send', { - data: data, - uid: socket.uid, - }); - data = results.data; - await Messaging.canMessageRoom(socket.uid, data.roomId); - const message = await Messaging.sendMessage({ - uid: socket.uid, - roomId: data.roomId, - content: data.message, - timestamp: Date.now(), - ip: socket.ip, - }); - Messaging.notifyUsersInRoom(socket.uid, data.roomId, message); - user.updateOnlineUsers(socket.uid); - return message; + return api.chats.post(socket, data); }; -function rateLimitExceeded(socket) { - const now = Date.now(); - socket.lastChatMessageTime = socket.lastChatMessageTime || 0; - if (now - socket.lastChatMessageTime < meta.config.chatMessageDelay) { - return true; - } - socket.lastChatMessageTime = now; - return false; -} - SocketModules.chats.loadRoom = async function (socket, data) { sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId'); diff --git a/test/api.js b/test/api.js index 114f9ccfd0..64857041f2 100644 --- a/test/api.js +++ b/test/api.js @@ -135,6 +135,7 @@ describe('API', async () => { }); meta.config.allowTopicsThumbnail = 1; meta.config.termsOfUse = 'I, for one, welcome our new test-driven overlords'; + meta.config.chatMessageDelay = 0; // Create a category const testCategory = await categories.create({ name: 'test' });