From 54c9d9d04cd5d2f77bde363027ee22e2f665c5c7 Mon Sep 17 00:00:00 2001 From: Evelynn Chen Date: Sat, 21 Sep 2024 11:44:08 -0400 Subject: [PATCH 1/2] attempted implementation of endorsement, unendorsement is not working --- .../components/schemas/PostObject.yaml | 3 ++ public/openapi/write/posts/pid.yaml | 15 +++++++ public/openapi/write/posts/pid/endorse.yaml | 26 ++++++++++++ src/posts/data.js | 3 ++ src/posts/summary.js | 2 +- src/posts/votes.js | 17 ++++++++ test/posts.js | 40 +++++++++++++++++++ 7 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 public/openapi/write/posts/pid/endorse.yaml diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index ea91579cc6..c1554d4cb2 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -22,6 +22,9 @@ PostObject: type: number votes: type: number + endorsedBy: + type: number + nullable: true timestampISO: type: string description: An ISO 8601 formatted date string (complementing `timestamp`) diff --git a/public/openapi/write/posts/pid.yaml b/public/openapi/write/posts/pid.yaml index 593a7acd01..dac2033227 100644 --- a/public/openapi/write/posts/pid.yaml +++ b/public/openapi/write/posts/pid.yaml @@ -64,6 +64,21 @@ get: type: boolean downvoted: type: boolean + endorsedBy: + type: number + description: ID of the user who endorsed the post + required: + - pid + - uid + - tid + - content + - timestamp + - upvotes + - downvotes + - timestampISO + - upvoted + - downvoted + - endorsedBy put: tags: - posts diff --git a/public/openapi/write/posts/pid/endorse.yaml b/public/openapi/write/posts/pid/endorse.yaml new file mode 100644 index 0000000000..df58774cbd --- /dev/null +++ b/public/openapi/write/posts/pid/endorse.yaml @@ -0,0 +1,26 @@ +put: + tags: + - posts + summary: endorse a post + description: This operation endorses a post. + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + responses: + '200': + description: Post successfully endorsed + 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/posts/data.js b/src/posts/data.js index 3a4d303ff5..b40f793752 100644 --- a/src/posts/data.js +++ b/src/posts/data.js @@ -67,5 +67,8 @@ function modifyPost(post, fields) { if (post.hasOwnProperty('edited')) { post.editedISO = post.edited !== 0 ? utils.toISOString(post.edited) : ''; } + if (post.hasOwnProperty('endorsedBy')) { + post.endorsedBy = post.endorsedBy ? post.endorsedBy : null; + } } } diff --git a/src/posts/summary.js b/src/posts/summary.js index 364baad1f7..ce1d19f86c 100644 --- a/src/posts/summary.js +++ b/src/posts/summary.js @@ -20,7 +20,7 @@ module.exports = function (Posts) { options.parse = options.hasOwnProperty('parse') ? options.parse : true; options.extraFields = options.hasOwnProperty('extraFields') ? options.extraFields : []; - const fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted', 'upvotes', 'downvotes', 'replies', 'handle'].concat(options.extraFields); + const fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted', 'upvotes', 'downvotes', 'replies', 'handle', 'endorsedBy'].concat(options.extraFields); let posts = await Posts.getPostsFields(pids, fields); posts = posts.filter(Boolean); diff --git a/src/posts/votes.js b/src/posts/votes.js index bfe5e1e47f..a7c97b31ec 100644 --- a/src/posts/votes.js +++ b/src/posts/votes.js @@ -98,6 +98,23 @@ module.exports = function (Posts) { return await db.getSetsMembers(pids.map(pid => `pid:${pid}:upvote`)); }; + Posts.endorse = async function (pid, uid) { + const postData = await Posts.getPostFields(pid, ['endorsedBy']); + if (postData.endorsedBy) { + throw new Error('[[error:post-already-endorsed]]'); + } + await Posts.setPostField(pid, 'endorsedBy', uid); + }; + + Posts.unendorse = async function (pid, uid) { + const postData = await Posts.getPostFields(pid, ['endorsedBy']); + if (postData.endorsedBy === uid) { + await Posts.setPostField(pid, 'endorsedBy', null); + } else { + throw new Error('[[error:not-endorsed-by-user]]'); + } + }; + function voteInProgress(pid, uid) { return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].includes(parseInt(pid, 10)); } diff --git a/test/posts.js b/test/posts.js index 20403e24cf..1b03c99899 100644 --- a/test/posts.js +++ b/test/posts.js @@ -132,6 +132,46 @@ describe('Post\'s', () => { }); }); + describe('endorsing', () => { + it('should allow a user to endorse a post', async () => { + await posts.endorse(postData.pid, voterUid); + const updatedPost = await posts.getPostFields(postData.pid, ['endorsedBy']); + assert.equal(updatedPost.endorsedBy, voterUid); + }); + + it('should not allow a user to endorse the same post again', async () => { + try { + await posts.endorse(postData.pid, voterUid); + assert.fail('Expected error not thrown'); + } catch (err) { + assert.equal(err.message, '[[error:post-already-endorsed]]'); + } + }); + + it('should allow the user to unendorse the post', async () => { + await posts.unendorse(postData.pid, voterUid); + const updatedPost = await posts.getPostFields(postData.pid, ['endorsedBy']); + console.log(updatedPost); + assert.equal(updatedPost.endorsedBy, null); + }); + + it('should not allow a user to unendorse a post they have not endorsed', async () => { + try { + await posts.unendorse(postData.pid, voterUid); + assert.fail('Expected error not thrown'); + } catch (err) { + assert.equal(err.message, '[[error:not-endorsed-by-user]]'); + } + }); + + it('should allow a different user to endorse the post after it has been unendorsed', async () => { + const newVoterUid = await user.create({ username: 'newEndorser' }); + await posts.endorse(postData.pid, newVoterUid); + const updatedPost = await posts.getPostFields(postData.pid, ['endorsedBy']); + assert.equal(updatedPost.endorsedBy, newVoterUid); + }); + }); + describe('voting', () => { it('should fail to upvote post if group does not have upvote permission', async () => { await privileges.categories.rescind(['groups:posts:upvote', 'groups:posts:downvote'], cid, 'registered-users'); From 2253c3f7edbd0c79e303b902fcd9e7f2a0d1ed43 Mon Sep 17 00:00:00 2001 From: Evelynn Chen Date: Sun, 22 Sep 2024 01:33:17 -0400 Subject: [PATCH 2/2] added api routes to src/api/posts.js; added api endpoints; emulated socket.io handlers like votes --- src/api/posts.js | 14 ++++++++++++++ src/controllers/write/posts.js | 12 ++++++++++++ src/posts/endorsements.js | 20 ++++++++++++++++++++ src/routes/write/posts.js | 2 ++ src/socket.io/posts.js | 1 + src/socket.io/posts/endorsements.js | 22 ++++++++++++++++++++++ 6 files changed, 71 insertions(+) create mode 100644 src/posts/endorsements.js create mode 100644 src/socket.io/posts/endorsements.js diff --git a/src/api/posts.js b/src/api/posts.js index 4e3917a008..9c4bcd7d44 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -308,6 +308,20 @@ postsAPI.unvote = async function (caller, data) { return await apiHelpers.postCommand(caller, 'unvote', 'voted', '', data); }; +postsAPI.endorse = async function (caller, data) { + if (!data || !data.pid || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + await posts.endorse(data.pid, data.uid); +}; + +postsAPI.unendorse = async function (caller, data) { + if (!data || !data.pid || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + await posts.unendorse(data.pid, data.uid); +}; + postsAPI.getVoters = async function (caller, data) { if (!data || !data.pid) { throw new Error('[[error:invalid-data]]'); diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 1dc8cf6800..bafc762a69 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -153,6 +153,18 @@ Posts.unbookmark = async (req, res) => { helpers.formatApiResponse(200, res); }; +Posts.endorse = async (req, res) => { + const { pid, uid } = req.body; + await api.posts.endorse(req, { pid, uid }); + helpers.formatApiResponse(200, res); +}; + +Posts.unendorse = async (req, res) => { + const { pid, uid } = req.body; + await api.posts.unendorse(req, { pid, uid }); + helpers.formatApiResponse(200, res); +}; + Posts.getDiffs = async (req, res) => { helpers.formatApiResponse(200, res, await api.posts.getDiffs(req, { ...req.params })); }; diff --git a/src/posts/endorsements.js b/src/posts/endorsements.js new file mode 100644 index 0000000000..45f632667f --- /dev/null +++ b/src/posts/endorsements.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = function (Posts) { + Posts.endorse = async function (pid, uid) { + const postData = await Posts.getPostFields(pid, ['endorsedBy']); + if (postData.endorsedBy) { + throw new Error('[[error:post-already-endorsed]]'); + } + await Posts.setPostField(pid, 'endorsedBy', uid); + }; + + Posts.unendorse = async function (pid, uid) { + const postData = await Posts.getPostFields(pid, ['endorsedBy']); + if (postData.endorsedBy === uid) { + await Posts.setPostField(pid, 'endorsedBy', null); + } else { + throw new Error('[[error:not-endorsed-by-user]]'); + } + }; +}; diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index e573bbb9b0..b56554f136 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -32,6 +32,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:pid/bookmark', middlewares, controllers.write.posts.bookmark); setupApiRoute(router, 'delete', '/:pid/bookmark', middlewares, controllers.write.posts.unbookmark); + setupApiRoute(router, 'post', '/:pid/endorse', middlewares, controllers.write.posts.endorse); + setupApiRoute(router, 'post', '/:pid/unendorse', middlewares, controllers.write.posts.unendorse); setupApiRoute(router, 'get', '/:pid/diffs', [middleware.assert.post], controllers.write.posts.getDiffs); setupApiRoute(router, 'get', '/:pid/diffs/:since', [middleware.assert.post], controllers.write.posts.loadDiff); setupApiRoute(router, 'put', '/:pid/diffs/:since', middlewares, controllers.write.posts.restoreDiff); diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index a684d95783..cc14eeb188 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -20,6 +20,7 @@ const SocketPosts = module.exports; require('./posts/votes')(SocketPosts); require('./posts/tools')(SocketPosts); +require('./posts/endorsements')(SocketPosts); SocketPosts.getRawPost = async function (socket, pid) { sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/raw'); diff --git a/src/socket.io/posts/endorsements.js b/src/socket.io/posts/endorsements.js new file mode 100644 index 0000000000..df97f38652 --- /dev/null +++ b/src/socket.io/posts/endorsements.js @@ -0,0 +1,22 @@ +'use strict'; + +const api = require('../../api'); +const sockets = require('../index'); + +module.exports = function (SocketPosts) { + SocketPosts.endorse = async function (socket, data) { + if (!data || !data.pid || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + sockets.warnDeprecated(socket, 'POST /api/v3/posts/:pid/endorse'); + return await api.posts.endorse(socket, { pid: data.pid, uid: data.uid }); + }; + + SocketPosts.unendorse = async function (socket, data) { + if (!data || !data.pid || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + sockets.warnDeprecated(socket, 'POST /api/v3/posts/:pid/unendorse'); + return await api.posts.unendorse(socket, { pid: data.pid, uid: data.uid }); + }; +};