From edff0d66746843bcf578abdf4bcc1d4e6d582796 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 30 Sep 2022 15:40:18 -0400 Subject: [PATCH] refactor: track created chat rooms, v3 admin calls to retrieve and delete chat rooms, chat room deletion methods --- public/openapi/write.yaml | 4 ++ public/openapi/write/admin/chats.yaml | 46 ++++++++++++++++++++ public/openapi/write/admin/chats/roomId.yaml | 26 +++++++++++ src/controllers/write/admin.js | 28 ++++++++++++ src/events.js | 1 + src/messaging/rooms.js | 25 +++++++++++ src/routes/write/admin.js | 3 ++ src/upgrades/3.0.0/save_rooms_zset.js | 19 ++++++++ 8 files changed, 152 insertions(+) create mode 100644 public/openapi/write/admin/chats.yaml create mode 100644 public/openapi/write/admin/chats/roomId.yaml create mode 100644 src/upgrades/3.0.0/save_rooms_zset.js diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 6f55dbcfc0..6b4b57d48e 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -172,6 +172,10 @@ paths: $ref: 'write/admin/analytics.yaml' /admin/analytics/{set}: $ref: 'write/admin/analytics/set.yaml' + /admin/chats: + $ref: 'write/admin/chats.yaml' + /admin/chats/{roomId}: + $ref: 'write/admin/chats/roomId.yaml' /files/: $ref: 'write/files.yaml' /files/folder: diff --git a/public/openapi/write/admin/chats.yaml b/public/openapi/write/admin/chats.yaml new file mode 100644 index 0000000000..db09f44398 --- /dev/null +++ b/public/openapi/write/admin/chats.yaml @@ -0,0 +1,46 @@ +get: + tags: + - admin + summary: get chat rooms + description: This operation returns all chat rooms managed by NodeBB. **For privacy reasons**, only chat room metadata is shown. + parameters: + - in: query + name: perPage + schema: + type: number + description: The number of chat rooms displayed per page + example: 20 + - in: query + name: page + schema: + type: number + description: The page number + example: 1 + responses: + '200': + description: Chat rooms retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + rooms: + type: array + items: + type: object + properties: + owner: + type: number + roomId: + type: number + userCount: + type: number + roomName: + type: string + groupChat: + type: boolean \ No newline at end of file diff --git a/public/openapi/write/admin/chats/roomId.yaml b/public/openapi/write/admin/chats/roomId.yaml new file mode 100644 index 0000000000..a7d2317cd2 --- /dev/null +++ b/public/openapi/write/admin/chats/roomId.yaml @@ -0,0 +1,26 @@ +delete: + tags: + - admin + summary: delete chat room + description: This operation deletes a chat room from the database + parameters: + - in: path + name: roomId + schema: + type: number + description: The roomId to be deleted + example: 1 + required: true + responses: + '200': + description: Chat room deleted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} \ No newline at end of file diff --git a/src/controllers/write/admin.js b/src/controllers/write/admin.js index 8b9faa55ef..c1616904bd 100644 --- a/src/controllers/write/admin.js +++ b/src/controllers/write/admin.js @@ -1,8 +1,11 @@ 'use strict'; +const db = require('../../database'); const meta = require('../../meta'); const privileges = require('../../privileges'); const analytics = require('../../analytics'); +const messaging = require('../../messaging'); +const events = require('../../events'); const helpers = require('../helpers'); @@ -40,3 +43,28 @@ Admin.getAnalyticsData = async (req, res) => { const getStats = req.query.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; helpers.formatApiResponse(200, res, await getStats(`analytics:${req.params.set}`, parseInt(req.query.until, 10) || Date.now(), req.query.amount)); }; + +Admin.chats = {}; + +Admin.chats.getRooms = async (req, res) => { + const page = (isFinite(req.query.page) && parseInt(req.query.page, 10)) || 1; + const perPage = (isFinite(req.query.perPage) && parseInt(req.query.perPage, 10)) || 20; + const start = Math.max(0, page - 1) * perPage; + const stop = start + perPage; + const roomIds = await db.getSortedSetRevRange('chat:rooms', start, stop); + + helpers.formatApiResponse(200, res, { + rooms: await messaging.getRoomsData(roomIds), + }); +}; + +Admin.chats.deleteRoom = async (req, res) => { + await messaging.deleteRooms([req.params.roomId]); + + events.log({ + type: 'chat-room-deleted', + uid: req.uid, + ip: req.ip, + }); + helpers.formatApiResponse(200, res); +}; diff --git a/src/events.js b/src/events.js index 637f53acf6..09c9062ae8 100644 --- a/src/events.js +++ b/src/events.js @@ -75,6 +75,7 @@ events.types = [ 'export:uploads', 'account-locked', 'getUsersCSV', + 'chat-room-deleted', // To add new types from plugins, just Array.push() to this array ]; diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 948fd88027..249bf83896 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -21,6 +21,12 @@ module.exports = function (Messaging) { Messaging.getRoomsData = async (roomIds) => { const roomData = await db.getObjects(roomIds.map(roomId => `chat:room:${roomId}`)); + + const userCounts = await db.sortedSetsCard(roomIds.map(roomId => `chat:room:${roomId}:uids`)); + userCounts.forEach((count, idx) => { + roomData[idx].userCount = count; + }); + modifyRoomData(roomData); return roomData; }; @@ -47,6 +53,7 @@ module.exports = function (Messaging) { await Promise.all([ db.setObject(`chat:room:${roomId}`, room), + db.sortedSetAdd('chat:rooms', now, roomId), db.sortedSetAdd(`chat:room:${roomId}:uids`, now, uid), ]); await Promise.all([ @@ -59,6 +66,24 @@ module.exports = function (Messaging) { return roomId; }; + Messaging.deleteRooms = async (roomIds) => { + // warning: uid::chat:room::mids is left behind, along with each message: obj + // deleting them from db requires iterating through all messages; not performant + if (!roomIds) { + throw new Error('[[error:invalid-data]]'); + } + + if (!Array.isArray(roomIds)) { + roomIds = [roomIds]; + } + + await Promise.all(roomIds.map(async (roomId) => { + const uids = await db.getSortedSetMembers(`chat:room:${roomId}:uids`); + await Messaging.leaveRoom(uids, roomId); + await db.delete(`chat:room:${roomId}`); + })); + }; + Messaging.isUserInRoom = async (uid, roomId) => { const inRoom = await db.isSortedSetMember(`chat:room:${roomId}:uids`, uid); const data = await plugins.hooks.fire('filter:messaging.isUserInRoom', { uid: uid, roomId: roomId, inRoom: inRoom }); diff --git a/src/routes/write/admin.js b/src/routes/write/admin.js index 0cda6327fb..ca3661cf19 100644 --- a/src/routes/write/admin.js +++ b/src/routes/write/admin.js @@ -15,5 +15,8 @@ module.exports = function () { setupApiRoute(router, 'get', '/analytics', [...middlewares], controllers.write.admin.getAnalyticsKeys); setupApiRoute(router, 'get', '/analytics/:set', [...middlewares], controllers.write.admin.getAnalyticsData); + setupApiRoute(router, 'get', '/chats', [...middlewares], controllers.write.admin.chats.getRooms); + setupApiRoute(router, 'delete', '/chats/:roomId', [...middlewares, middleware.assert.room], controllers.write.admin.chats.deleteRoom); + return router; }; diff --git a/src/upgrades/3.0.0/save_rooms_zset.js b/src/upgrades/3.0.0/save_rooms_zset.js new file mode 100644 index 0000000000..8c7f8a1442 --- /dev/null +++ b/src/upgrades/3.0.0/save_rooms_zset.js @@ -0,0 +1,19 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Store list of chat rooms', + timestamp: Date.UTC(2022, 8, 30), + method: async () => { + const lastRoomId = await db.getObjectField('global', 'nextChatRoomId'); + let keys = []; + for (let x = 1; x <= lastRoomId; x++) { + keys.push(`chat:room:${x}`); + } + + const exists = await db.exists(keys); + keys = keys.filter((_, idx) => exists[idx]); + await db.sortedSetAdd('chat:rooms', keys.map(Date.now), keys.map(key => key.slice(10))); + }, +};