diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index c59b9bce29..29f31a9f62 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -164,6 +164,8 @@ paths: $ref: 'write/topics/tid/read.yaml' /topics/{tid}/bump: $ref: 'write/topics/tid/bump.yaml' + /topics/{tid}/move: + $ref: 'write/topics/tid/move.yaml' /tags/{tag}/follow: $ref: 'write/tags/tag/follow.yaml' /posts/{pid}: diff --git a/public/openapi/write/topics/tid/move.yaml b/public/openapi/write/topics/tid/move.yaml new file mode 100644 index 0000000000..6c8dedc11a --- /dev/null +++ b/public/openapi/write/topics/tid/move.yaml @@ -0,0 +1,29 @@ +put: + tags: + - topics + summary: move topic to another category + description: | + This operation moved a topic from one category to another. + + **Note**: This is a privileged call and can only be executed by administrators, global moderators, or the moderator for the category of the passed-in topic. + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id + example: 1 + responses: + '200': + description: Topic successfully moved + 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/api/topics.js b/src/api/topics.js index 7a6cabf966..2cef69215d 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -4,9 +4,12 @@ const validator = require('validator'); const user = require('../user'); const topics = require('../topics'); +const categories = require('../categories'); const posts = require('../posts'); const meta = require('../meta'); const privileges = require('../privileges'); +const events = require('../events'); +const batch = require('../batch'); const apiHelpers = require('./helpers'); @@ -298,3 +301,48 @@ topicsAPI.bump = async (caller, { tid }) => { await topics.markAsUnreadForAll(tid); topics.pushUnreadCount(caller.uid); }; + +topicsAPI.move = async (caller, { tid, cid }) => { + const canMove = await privileges.categories.isAdminOrMod(cid, caller.uid); + if (!canMove) { + throw new Error('[[error:no-privileges]]'); + } + + const tids = Array.isArray(tid) ? tid : [tid]; + const uids = await user.getUidsFromSet('users:online', 0, -1); + const cids = [parseInt(cid, 10)]; + + await batch.processArray(tids, async (tids) => { + await Promise.all(tids.map(async (tid) => { + const canMove = await privileges.topics.isAdminOrMod(tid, caller.uid); + if (!canMove) { + throw new Error('[[error:no-privileges]]'); + } + const topicData = await topics.getTopicFields(tid, ['tid', 'cid', 'slug', 'deleted']); + if (!cids.includes(topicData.cid)) { + cids.push(topicData.cid); + } + await topics.tools.move(tid, { + cid, + uid: caller.uid, + }); + + const notifyUids = await privileges.categories.filterUids('topics:read', topicData.cid, uids); + socketHelpers.emitToUids('event:topic_moved', topicData, notifyUids); + if (!topicData.deleted) { + socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic'); + } + + await events.log({ + type: `topic-move`, + uid: caller.uid, + ip: caller.ip, + tid: tid, + fromCid: topicData.cid, + toCid: cid, + }); + })); + }, { batch: 10 }); + + await categories.onTopicsMoved(cids); +}; diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index b9691a8da5..cafcec2f7f 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -207,3 +207,10 @@ Topics.bump = async (req, res) => { helpers.formatApiResponse(200, res); }; + +Topics.move = async (req, res) => { + const { cid } = req.body; + await api.topics.move(req, { cid, ...req.params }); + + helpers.formatApiResponse(200, res); +}; diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index 1a537fd56d..df10f66633 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -49,5 +49,7 @@ module.exports = function () { setupApiRoute(router, 'delete', '/:tid/read', [...middlewares, middleware.assert.topic], controllers.write.topics.markUnread); setupApiRoute(router, 'put', '/:tid/bump', [...middlewares, middleware.assert.topic], controllers.write.topics.bump); + setupApiRoute(router, 'put', '/:tid/move', [...middlewares, middleware.assert.topic], controllers.write.topics.move); + return router; }; diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js index 6c03412cc2..819ce895bf 100644 --- a/src/socket.io/topics/move.js +++ b/src/socket.io/topics/move.js @@ -8,48 +8,21 @@ const privileges = require('../../privileges'); const socketHelpers = require('../helpers'); const events = require('../../events'); +const api = require('../../api'); +const sockets = require('..'); + module.exports = function (SocketTopics) { SocketTopics.move = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/topics/:tid/move'); + if (!data || !Array.isArray(data.tids) || !data.cid) { throw new Error('[[error:invalid-data]]'); } - const canMove = await privileges.categories.isAdminOrMod(data.cid, socket.uid); - if (!canMove) { - throw new Error('[[error:no-privileges]]'); - } - - const uids = await user.getUidsFromSet('users:online', 0, -1); - const cids = [parseInt(data.cid, 10)]; - await async.eachLimit(data.tids, 10, async (tid) => { - const canMove = await privileges.topics.isAdminOrMod(tid, socket.uid); - if (!canMove) { - throw new Error('[[error:no-privileges]]'); - } - const topicData = await topics.getTopicFields(tid, ['tid', 'cid', 'slug', 'deleted']); - if (!cids.includes(topicData.cid)) { - cids.push(topicData.cid); - } - data.uid = socket.uid; - await topics.tools.move(tid, data); - - const notifyUids = await privileges.categories.filterUids('topics:read', topicData.cid, uids); - socketHelpers.emitToUids('event:topic_moved', topicData, notifyUids); - if (!topicData.deleted) { - socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'move', 'notifications:moved-your-topic'); - } - - await events.log({ - type: `topic-move`, - uid: socket.uid, - ip: socket.ip, - tid: tid, - fromCid: topicData.cid, - toCid: data.cid, - }); + await api.topics.move(socket, { + tid: data.tids, + cid: data.cid, }); - - await categories.onTopicsMoved(cids); };