diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 7866cc8e5e..6f70f55a20 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -164,6 +164,8 @@ paths: $ref: 'write/posts/pid/diffs/since.yaml' /posts/{pid}/diffs/{timestamp}: $ref: 'write/posts/pid/diffs/timestamp.yaml' + /posts/{pid}/replies: + $ref: 'write/posts/pid/replies.yaml' /chats/: $ref: 'write/chats.yaml' /chats/{roomId}: diff --git a/public/openapi/write/posts/pid/replies.yaml b/public/openapi/write/posts/pid/replies.yaml new file mode 100644 index 0000000000..b021eec14e --- /dev/null +++ b/public/openapi/write/posts/pid/replies.yaml @@ -0,0 +1,234 @@ +get: + tags: + - posts + summary: get post replies + description: This operation retrieves a post's direct replies + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + responses: + '200': + description: Post replies successfully retrieved. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + replies: + type: array + items: + type: object + properties: + pid: + type: number + uid: + type: number + description: A user identifier + tid: + type: number + description: A topic identifier + content: + type: string + timestamp: + type: number + votes: + type: number + deleted: + type: number + upvotes: + type: number + downvotes: + type: number + bookmarks: + type: number + deleterUid: + type: number + edited: + type: number + timestampISO: + type: string + description: An ISO 8601 formatted date string (complementing `timestamp`) + editedISO: + type: string + index: + type: number + user: + type: object + properties: + uid: + type: number + description: A user identifier + username: + type: string + description: A friendly name for a given user account + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces + removed, etc.) + reputation: + type: number + postcount: + type: number + topiccount: + type: number + picture: + type: string + nullable: true + signature: + type: string + banned: + type: number + banned:expire: + type: number + status: + type: string + lastonline: + type: number + groupTitle: + nullable: true + type: string + groupTitleArray: + type: array + items: + type: string + muted: + type: boolean + description: Whether or not the user has been muted. + mutedUntil: + type: number + description: A UNIX timestamp representing the moment a muted state will be lifted. + nullable: true + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users without + an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's auto-generated + icon + example: "#f44336" + lastonlineISO: + type: string + banned_until: + type: number + banned_until_readable: + type: string + selectedGroups: + type: array + items: + type: object + properties: + name: + type: string + slug: + type: string + labelColor: + type: string + textColor: + type: string + icon: + type: string + userTitle: + type: string + custom_profile_info: + type: array + items: + type: object + properties: + content: + type: string + description: HTML that is injected into `topic.tpl` of themes that support custom profile info + editor: + nullable: true + bookmarked: + type: boolean + upvoted: + type: boolean + downvoted: + type: boolean + replies: + type: object + properties: + hasMore: + type: boolean + users: + type: array + items: + type: object + properties: + username: + type: string + description: A friendly name for a given user account + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces + removed, etc.) + picture: + type: string + uid: + type: number + description: A user identifier + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users without + an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's auto-generated + icon + example: "#f44336" + administrator: + type: boolean + text: + type: string + count: + type: number + selfPost: + type: boolean + events: + type: array + items: + type: object + properties: + type: + type: string + id: + type: number + timestamp: + type: number + timestampISO: + type: string + topicOwnerPost: + type: boolean + display_edit_tools: + type: boolean + display_delete_tools: + type: boolean + display_moderator_tools: + type: boolean + display_move_tools: + type: boolean + display_post_menu: + type: boolean + flagId: + type: number + description: The flag identifier, if this particular post has been flagged before diff --git a/src/api/posts.js b/src/api/posts.js index 4bba610aed..06639255a1 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -386,3 +386,25 @@ postsAPI.deleteDiff = async (caller, { pid, timestamp }) => { await posts.diffs.delete(pid, timestamp, caller.uid); }; + +postsAPI.getReplies = async (caller, { pid }) => { + const { uid } = caller; + const canRead = await privileges.posts.can('topics:read', pid, caller.uid); + if (!canRead) { + return null; + } + + const { topicPostSort } = await user.getSettings(uid); + const pids = await posts.getPidsFromSet(`pid:${pid}:replies`, 0, -1, topicPostSort === 'newest_to_oldest'); + + let [postData, postPrivileges] = await Promise.all([ + posts.getPostsByPids(pids, uid), + privileges.posts.get(pids, uid), + ]); + postData = await topics.addPostData(postData, uid); + postData.forEach((postData, index) => posts.modifyPostByPrivilege(postData, postPrivileges[index])); + postData = postData.filter((postData, index) => postData && postPrivileges[index].read); + postData = await user.blocks.filter(uid, postData); + + return postData; +}; diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index d2a2cb9b9d..529eabfe44 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -160,3 +160,12 @@ Posts.deleteDiff = async (req, res) => { helpers.formatApiResponse(200, res, await api.posts.getDiffs(req, { ...req.params })); }; + +Posts.getReplies = async (req, res) => { + const replies = await api.posts.getReplies(req, { ...req.params }); + if (replies === null) { + return helpers.formatApiResponse(404, res, new Error('[[error:no-post]]')); + } + + helpers.formatApiResponse(200, res, { replies }); +}; diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index ed6bd62ad8..a834d26088 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -35,6 +35,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:pid/diffs/:since', middlewares, controllers.write.posts.restoreDiff); setupApiRoute(router, 'delete', '/:pid/diffs/:timestamp', middlewares, controllers.write.posts.deleteDiff); + setupApiRoute(router, 'get', '/:pid/replies', [middleware.assert.post], controllers.write.posts.getReplies); + // Shorthand route to access post routes by topic index router.all('/+byIndex/:index*?', [middleware.checkRequired.bind(null, ['tid'])], controllers.write.posts.redirectByIndex); diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index b297bb9579..9eef769cb3 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -8,7 +8,6 @@ const privileges = require('../privileges'); const plugins = require('../plugins'); const meta = require('../meta'); const topics = require('../topics'); -const user = require('../user'); const notifications = require('../notifications'); const utils = require('../utils'); const events = require('../events'); @@ -91,21 +90,13 @@ SocketPosts.getPidIndex = async function (socket, data) { }; SocketPosts.getReplies = async function (socket, pid) { + sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/replies'); + if (!utils.isNumber(pid)) { throw new Error('[[error:invalid-data]]'); } - const { topicPostSort } = await user.getSettings(socket.uid); - const pids = await posts.getPidsFromSet(`pid:${pid}:replies`, 0, -1, topicPostSort === 'newest_to_oldest'); - let [postData, postPrivileges] = await Promise.all([ - posts.getPostsByPids(pids, socket.uid), - privileges.posts.get(pids, socket.uid), - ]); - postData = await topics.addPostData(postData, socket.uid); - postData.forEach((postData, index) => posts.modifyPostByPrivilege(postData, postPrivileges[index])); - postData = postData.filter((postData, index) => postData && postPrivileges[index].read); - postData = await user.blocks.filter(socket.uid, postData); - return postData; + return await api.posts.getReplies(socket, { pid }); }; SocketPosts.accept = async function (socket, data) {