From da25ce4d09f73dc9ead102c5e43b94d7cea994f5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 2 Oct 2020 16:35:45 -0400 Subject: [PATCH] feat: topic delete/restore/purge/(un)pin/(un)lock --- public/openapi/write.yaml | 129 +++++++++++++++++++++++++ public/src/client/topic/threadTools.js | 19 ++-- src/controllers/write/topics.js | 85 ++++++++++++++++ src/routes/write/topics.js | 59 ++--------- src/socket.io/topics/tools.js | 8 ++ 5 files changed, 238 insertions(+), 62 deletions(-) diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 623c6a136f..224eafd53d 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -534,6 +534,135 @@ paths: $ref: '#/components/schemas/Status' response: $ref: components/schemas/PostsObject.yaml#/PostsObject + delete: + tags: + - topics + summary: Delete a topic + description: This operation purges a topic and all of its posts (careful, there is no confirmation!) + responses: + '200': + description: Topic successfully purged + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} + /topics/{tid}/state: + delete: + tags: + - topics + summary: Delete a topic + description: This operation deletes an existing topic. + responses: + '200': + description: Topic successfully deleted + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} + put: + tags: + - topics + summary: Restore a topic + description: This operation restores a topic. + responses: + '200': + description: Topic successfully restored + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} + /topics/{tid}/lock: + delete: + tags: + - topics + summary: Lock a topic + description: This operation locks an existing topic. + responses: + '200': + description: Topic successfully locked + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} + put: + tags: + - topics + summary: Unlock a topic + description: This operation unlocks a topic. + responses: + '200': + description: Topic successfully unlocked + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} + /topics/{tid}/state: + delete: + tags: + - topics + summary: Pin a topic + description: This operation pins an existing topic. + responses: + '200': + description: Topic successfully pinned + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} + put: + tags: + - topics + summary: Unpin a topic + description: This operation unpins a topic. + responses: + '200': + description: Topic successfully unpinned + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + properties: {} components: schemas: Status: diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index 03044352c7..6388aacf3c 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -5,7 +5,8 @@ define('forum/topic/threadTools', [ 'components', 'translator', 'handleBack', -], function (components, translator, handleBack) { + 'api', +], function (components, translator, handleBack, api) { var ThreadTools = {}; ThreadTools.init = function (tid, topicContainer) { @@ -27,22 +28,22 @@ define('forum/topic/threadTools', [ }); topicContainer.on('click', '[component="topic/lock"]', function () { - socket.emit('topics.lock', { tids: [tid], cid: ajaxify.data.cid }); + api.put(`/topics/${tid}/lock`); return false; }); topicContainer.on('click', '[component="topic/unlock"]', function () { - socket.emit('topics.unlock', { tids: [tid], cid: ajaxify.data.cid }); + api.del(`/topics/${tid}/lock`); return false; }); topicContainer.on('click', '[component="topic/pin"]', function () { - socket.emit('topics.pin', { tids: [tid], cid: ajaxify.data.cid }); + api.put(`/topics/${tid}/pin`); return false; }); topicContainer.on('click', '[component="topic/unpin"]', function () { - socket.emit('topics.unpin', { tids: [tid], cid: ajaxify.data.cid }); + api.del(`/topics/${tid}/pin`); return false; }); @@ -176,11 +177,9 @@ define('forum/topic/threadTools', [ return; } - socket.emit('topics.' + command, { tids: [tid], cid: ajaxify.data.cid }, function (err) { - if (err) { - app.alertError(err.message); - } - }); + const method = command === 'restore' ? 'put' : 'del'; + const suffix = command !== 'purge' ? '/state' : ''; + api[method](`/topics/${tid}${suffix}`, undefined, undefined, err => app.alertError(err.status.message)); }); }); } diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index 008ff44b45..3d174367b7 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -4,6 +4,8 @@ const topics = require('../../topics'); const posts = require('../../posts'); const user = require('../../user'); const meta = require('../../meta'); +const events = require('../../events'); +const privileges = require('../../privileges'); const helpers = require('../helpers'); const socketHelpers = require('../../socket.io/helpers'); @@ -71,3 +73,86 @@ Topics.reply = async (req, res) => { user.updateOnlineUsers(req.user.uid); socketHelpers.notifyNew(req.user.uid, 'newPost', result); }; + +Topics.delete = async (req, res) => { + await doTopicAction('delete', 'event:topic_deleted', req, { + tids: [req.params.tid], + }); + helpers.formatApiResponse(200, res); +}; + +Topics.restore = async (req, res) => { + await doTopicAction('restore', 'event:topic_restored', req, { + tids: [req.params.tid], + }); + helpers.formatApiResponse(200, res); +}; + +Topics.purge = async (req, res) => { + await doTopicAction('purge', 'event:topic_purged', req, { + tids: [req.params.tid], + }); + helpers.formatApiResponse(200, res); +}; + +Topics.pin = async (req, res) => { + await doTopicAction('pin', 'event:topic_pinned', req, { + tids: [req.params.tid], + }); + helpers.formatApiResponse(200, res); +}; + +Topics.unpin = async (req, res) => { + await doTopicAction('unpin', 'event:topic_unpinned', req, { + tids: [req.params.tid], + }); + helpers.formatApiResponse(200, res); +}; + +Topics.lock = async (req, res) => { + await doTopicAction('lock', 'event:topic_locked', req, { + tids: [req.params.tid], + }); + helpers.formatApiResponse(200, res); +}; + +Topics.unlock = async (req, res) => { + await doTopicAction('unlock', 'event:topic_unlocked', req, { + tids: [req.params.tid], + }); + helpers.formatApiResponse(200, res); +}; + +async function doTopicAction(action, event, socket, { tids }) { + if (!Array.isArray(tids)) { + throw new Error('[[error:invalid-tid]]'); + } + + if (typeof topics.tools[action] !== 'function') { + return; + } + + const uids = await user.getUidsFromSet('users:online', 0, -1); + + await Promise.all(tids.map(async function (tid) { + const title = await topics.getTopicField(tid, 'title'); + const data = await topics.tools[action](tid, socket.uid); + const notifyUids = await privileges.categories.filterUids('topics:read', data.cid, uids); + socketHelpers.emitToTopicAndCategory(event, data, notifyUids); + await logTopicAction(action, socket, tid, title); + })); +} + +async function logTopicAction(action, req, tid, title) { + var actionsToLog = ['delete', 'restore', 'purge']; + if (!actionsToLog.includes(action)) { + return; + } + await events.log({ + type: 'topic-' + action, + uid: req.uid, + ip: req.ip, + tid: tid, + title: String(title), + }); +} diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index e6a620c30e..1e805f4df6 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -12,50 +12,17 @@ module.exports = function () { setupApiRoute(router, '/', middleware, [...middlewares, middleware.checkRequired.bind(null, ['cid', 'title', 'content'])], 'post', controllers.write.topics.create); setupApiRoute(router, '/:tid', middleware, [...middlewares, middleware.checkRequired.bind(null, ['content']), middleware.assertTopic], 'post', controllers.write.topics.reply); - // setupApiRoute(router, '/:cid', middleware, [...middlewares, middleware.isAdmin], 'put', controllers.write.categories.update); - // setupApiRoute(router, '/:cid', middleware, [...middlewares, middleware.isAdmin], 'delete', controllers.write.categories.delete); + setupApiRoute(router, '/:tid', middleware, [...middlewares, middleware.assertTopic], 'delete', controllers.write.topics.purge); - // app.route('/:tid') - // .delete(apiMiddleware.requireUser, apiMiddleware.validateTid, function(req, res) { - // Topics.purgePostsAndTopic(req.params.tid, req.params._uid, function(err) { - // errorHandler.handle(err, res); - // }); - // }) - // .put(apiMiddleware.requireUser, function(req, res) { - // if (!utils.checkRequired(['pid', 'content'], req, res)) { - // return false; - // } + setupApiRoute(router, '/:tid/state', middleware, [...middlewares, middleware.assertTopic], 'put', controllers.write.topics.restore); + setupApiRoute(router, '/:tid/state', middleware, [...middlewares, middleware.assertTopic], 'delete', controllers.write.topics.delete); - // var payload = { - // uid: req.user.uid, - // pid: req.body.pid, - // content: req.body.content, - // options: {} - // }; - // console.log(payload); + setupApiRoute(router, '/:tid/pin', middleware, [...middlewares, middleware.assertTopic], 'put', controllers.write.topics.pin); + setupApiRoute(router, '/:tid/pin', middleware, [...middlewares, middleware.assertTopic], 'delete', controllers.write.topics.unpin); - // // Maybe a "set if available" utils method may come in handy - // if (req.body.handle) { payload.handle = req.body.handle; } - // if (req.body.title) { payload.title = req.body.title; } - // if (req.body.topic_thumb) { payload.options.topic_thumb = req.body.topic_thumb; } - // if (req.body.tags) { payload.options.tags = req.body.tags; } + setupApiRoute(router, '/:tid/lock', middleware, [...middlewares, middleware.assertTopic], 'put', controllers.write.topics.lock); + setupApiRoute(router, '/:tid/lock', middleware, [...middlewares, middleware.assertTopic], 'delete', controllers.write.topics.unlock); - // Posts.edit(payload, function(err, returnData) { - // errorHandler.handle(err, res, returnData); - // }); - // }); - - // app.route('/:tid/state') - // .put(apiMiddleware.requireUser, apiMiddleware.validateTid, function (req, res) { - // Topics.restore(req.params.tid, req.params._uid, function (err) { - // errorHandler.handle(err, res); - // }); - // }) - // .delete(apiMiddleware.requireUser, apiMiddleware.validateTid, function (req, res) { - // Topics.delete(req.params.tid, req.params._uid, function (err) { - // errorHandler.handle(err, res); - // }); - // }); // app.route('/:tid/follow') // .put(apiMiddleware.requireUser, apiMiddleware.validateTid, function(req, res) { @@ -85,17 +52,5 @@ module.exports = function () { // }); // }); - // app.route('/:tid/pin') - // .put(apiMiddleware.requireUser, apiMiddleware.validateTid, function(req, res) { - // Topics.tools.pin(req.params.tid, req.user.uid, function(err) { - // errorHandler.handle(err, res); - // }); - // }) - // .delete(apiMiddleware.requireUser, apiMiddleware.validateTid, function(req, res) { - // Topics.tools.unpin(req.params.tid, req.user.uid, function(err) { - // errorHandler.handle(err, res); - // }); - // }); - return router; }; diff --git a/src/socket.io/topics/tools.js b/src/socket.io/topics/tools.js index 1b176a83be..e04ffd94b2 100644 --- a/src/socket.io/topics/tools.js +++ b/src/socket.io/topics/tools.js @@ -6,6 +6,7 @@ const events = require('../../events'); const privileges = require('../../privileges'); const plugins = require('../../plugins'); const socketHelpers = require('../helpers'); +const sockets = require('..'); module.exports = function (SocketTopics) { SocketTopics.loadTopicTools = async function (socket, data) { @@ -34,30 +35,37 @@ module.exports = function (SocketTopics) { }; SocketTopics.delete = async function (socket, data) { + sockets.warnDeprecated(socket, 'DELETE /api/v1/topics/state'); await SocketTopics.doTopicAction('delete', 'event:topic_deleted', socket, data); }; SocketTopics.restore = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v1/topics/state'); await SocketTopics.doTopicAction('restore', 'event:topic_restored', socket, data); }; SocketTopics.purge = async function (socket, data) { + sockets.warnDeprecated(socket, 'DELETE /api/v1/topics'); await SocketTopics.doTopicAction('purge', 'event:topic_purged', socket, data); }; SocketTopics.lock = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v1/topics/lock'); await SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, data); }; SocketTopics.unlock = async function (socket, data) { + sockets.warnDeprecated(socket, 'DELETE /api/v1/topics/lock'); await SocketTopics.doTopicAction('unlock', 'event:topic_unlocked', socket, data); }; SocketTopics.pin = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v1/topics/pin'); await SocketTopics.doTopicAction('pin', 'event:topic_pinned', socket, data); }; SocketTopics.unpin = async function (socket, data) { + sockets.warnDeprecated(socket, 'DELETE /api/v1/topics/pin'); await SocketTopics.doTopicAction('unpin', 'event:topic_unpinned', socket, data); };