diff --git a/.gitignore b/.gitignore index 17bbb8e5f5..4559af13e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ dump.rdb - +dump2.rdb dist/ yarn.lock npm-debug.log @@ -78,3 +78,5 @@ test.sh dump.rdb .archiver_shadow/ .snapshots/ + +.env \ No newline at end of file diff --git a/Dockerfile.railway b/Dockerfile.railway new file mode 100644 index 0000000000..10c38d4bca --- /dev/null +++ b/Dockerfile.railway @@ -0,0 +1,32 @@ +FROM mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm +COPY . /app +WORKDIR /app + +ARG NODEBB_PORT +ARG NODEBB_DB +ARG REDIS_URL +ARG NODEBB_ADMIN_USERNAME +ARG NODEBB_ADMIN_PASSWORD +ARG NODEBB_ADMIN_EMAIL +ARG NODEBB_URL +ARG NODE_ENV +SHELL ["/bin/bash", "-c"] + +RUN rm -f ./dump.rdb +RUN apt-get update \ + && apt-get install -y \ + iputils-ping \ + redis-server \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eu; \ + . ./parse-redis-url.sh \ + && curl https://qlty.sh | /bin/bash \ + && mv ~/.qlty/bin/qlty /usr/local/bin/qlty \ + && rm -rf ~/.qlty \ + && qlty --version \ + && ./nodebb setup + +EXPOSE 4567 +RUN npm i +CMD ["/bin/sh","./railway-start.sh"] \ No newline at end of file diff --git a/config2.json b/config2.json new file mode 100644 index 0000000000..5728b9a75a --- /dev/null +++ b/config2.json @@ -0,0 +1,17 @@ +{ + "url": "http://127.0.0.1:4567", + "secret": "e132a0f9-93e4-4750-91cc-7f82bcd0a4d3", + "database": "redis", + "port": "4567", + "redis": { + "host": "127.0.0.1", + "port": 6379, + "password": "", + "database": 0 + }, + "test_database": { + "host": "127.0.0.1", + "database": 1, + "port": 6379 + } +} \ No newline at end of file diff --git a/parse-redis-url.sh b/parse-redis-url.sh new file mode 100644 index 0000000000..24e92c8a38 --- /dev/null +++ b/parse-redis-url.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env sh +set -eu + +uri="${REDIS_URL:-}" + +# Drop scheme +rest="${uri#redis://}" + +# Defaults +REDIS_USERNAME="" +REDIS_PASSWORD="" + +# Split creds vs host:port +if [ "${rest#*@}" != "$rest" ]; then + creds="${rest%@*}" # user:password (or just user) + hostport="${rest#*@}" # host:port[/...] + if [ "${creds#*:}" != "$creds" ]; then + REDIS_USERNAME="${creds%%:*}" + REDIS_PASSWORD="${creds#*:}" + else + REDIS_USERNAME="$creds" + REDIS_PASSWORD="" + fi +else + hostport="$rest" +fi + +# Strip any /db suffix +hostport="${hostport%%/*}" + +REDIS_HOST="${hostport%%:*}" +REDIS_PORT="${hostport##*:}" + +# Export generic vars +# export REDIS_HOST REDIS_PORT REDIS_USERNAME REDIS_PASSWORD + +# Export NodeBB vars +export NODEBB_DB_HOST="$REDIS_HOST" +export NODEBB_DB_PORT="$REDIS_PORT" +export NODEBB_DB_USER="$REDIS_USERNAME" +export NODEBB_DB_PASSWORD="$REDIS_PASSWORD" + +exec "$@" diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 1904cded51..f02b8a5b9c 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -2,6 +2,19 @@ PostObject: description: A single post in the array returned from `Posts.getPostSummaryByPids` type: object properties: + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean pid: type: number tid: @@ -54,7 +67,8 @@ PostObject: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: type: string @@ -63,12 +77,14 @@ PostObject: type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -102,7 +118,8 @@ PostObject: type: number mainPid: type: number - description: The post id of the first post in this topic (also called the + description: + The post id of the first post in this topic (also called the "original post") teaserPid: type: number @@ -140,7 +157,8 @@ PostObject: type: string parentCid: type: number - description: The category identifier for the category that is the immediate + description: + The category identifier for the category that is the immediate ancestor of the current category bgColor: type: string @@ -160,6 +178,19 @@ PostDataObject: allOf: - type: object properties: + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean pid: type: number uid: @@ -212,7 +243,8 @@ PostDataObject: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) reputation: type: number @@ -249,12 +281,14 @@ PostDataObject: nullable: true icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -317,7 +351,8 @@ PostDataObject: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: type: string @@ -326,12 +361,14 @@ PostDataObject: description: A user identifier icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml index 30ba9d3a77..9211fa14e5 100644 --- a/public/openapi/components/schemas/TopicObject.yaml +++ b/public/openapi/components/schemas/TopicObject.yaml @@ -1,6 +1,6 @@ TopicObject: allOf: - - $ref: '#/TopicObjectSlim' + - $ref: "#/TopicObjectSlim" - type: object properties: lastposttime: @@ -48,7 +48,8 @@ TopicObject: type: string userslug: type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) reputation: type: number @@ -66,12 +67,14 @@ TopicObject: type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -109,6 +112,19 @@ TopicObject: sourceContent: type: string nullable: true + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean timestampISO: type: string description: An ISO 8601 formatted date string (complementing `timestamp`) @@ -127,19 +143,22 @@ TopicObject: type: boolean userslug: type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: nullable: true type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -239,7 +258,8 @@ TopicObjectSlim: type: number pinned: type: number - description: Whether or not this particular topic is pinned to the top of the + description: + Whether or not this particular topic is pinned to the top of the category timestamp: type: number @@ -295,4 +315,4 @@ TopicObjectSlim: type: number description: The number of thumbnails associated with this topic required: - - tid \ No newline at end of file + - tid diff --git a/public/openapi/read/categories.yaml b/public/openapi/read/categories.yaml index 76d2375ebb..92a5e1e189 100644 --- a/public/openapi/read/categories.yaml +++ b/public/openapi/read/categories.yaml @@ -76,6 +76,19 @@ get: items: type: object properties: + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean pid: type: number timestamp: @@ -96,19 +109,22 @@ get: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: nullable: true type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -134,6 +150,19 @@ get: items: type: object properties: + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean pid: type: number tid: @@ -165,19 +194,22 @@ get: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: nullable: true type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -223,34 +255,34 @@ get: user: type: object properties: - uid: - type: number - example: 1 - isLocal: - type: boolean - description: Whether the user belongs to the local installation or not. - username: - type: string - example: Dragon Fruit - userslug: - type: string - example: dragon-fruit - picture: - type: string - example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80' - nullable: true - displayname: - type: string - description: This is either username or fullname depending on forum and user settings - example: Dragon Fruit - 'icon:text': - type: string - example: D - 'icon:bgColor': - type: string - example: '#9c27b0' + uid: + type: number + example: 1 + isLocal: + type: boolean + description: Whether the user belongs to the local installation or not. + username: + type: string + example: Dragon Fruit + userslug: + type: string + example: dragon-fruit + picture: + type: string + example: "https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80" + nullable: true + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + example: Dragon Fruit + "icon:text": + type: string + example: D + "icon:bgColor": + type: string + example: "#9c27b0" imageClass: type: string - $ref: ../components/schemas/Pagination.yaml#/Pagination - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs - - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file + - $ref: ../components/schemas/CommonProps.yaml#/CommonProps diff --git a/public/openapi/read/index.yaml b/public/openapi/read/index.yaml index 4c45da4360..7dd491a9a8 100644 --- a/public/openapi/read/index.yaml +++ b/public/openapi/read/index.yaml @@ -94,19 +94,22 @@ get: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: nullable: true type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -118,7 +121,8 @@ get: description: A category identifier parentCid: type: number - description: The category identifier for the category that is the immediate + description: + The category identifier for the category that is the immediate ancestor of the current category topic: type: object @@ -127,6 +131,19 @@ get: type: string title: type: string + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean imageClass: type: string timesClicked: @@ -136,6 +153,19 @@ get: items: type: object properties: + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean pid: type: number tid: @@ -164,19 +194,22 @@ get: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: nullable: true type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -225,33 +258,33 @@ get: user: type: object properties: - uid: - type: number - example: 1 - username: - type: string - example: Dragon Fruit - userslug: - type: string - example: dragon-fruit - picture: - type: string - example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80' - nullable: true - displayname: - type: string - description: This is either username or fullname depending on forum and user settings - example: Dragon Fruit - 'icon:text': - type: string - example: D - 'icon:bgColor': - type: string - example: '#9c27b0' - isLocal: - type: boolean - description: Whether the user belongs to the local installation or not. + uid: + type: number + example: 1 + username: + type: string + example: Dragon Fruit + userslug: + type: string + example: dragon-fruit + picture: + type: string + example: "https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80" + nullable: true + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + example: Dragon Fruit + "icon:text": + type: string + example: D + "icon:bgColor": + type: string + example: "#9c27b0" + isLocal: + type: boolean + description: Whether the user belongs to the local installation or not. imageClass: type: string - $ref: ../components/schemas/Pagination.yaml#/Pagination - - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file + - $ref: ../components/schemas/CommonProps.yaml#/CommonProps diff --git a/public/openapi/read/post-queue.yaml b/public/openapi/read/post-queue.yaml index 9bd93903c4..b7e890953b 100644 --- a/public/openapi/read/post-queue.yaml +++ b/public/openapi/read/post-queue.yaml @@ -35,6 +35,19 @@ get: allOf: - type: object properties: + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean id: type: string uid: @@ -134,7 +147,8 @@ get: 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 + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: nullable: true @@ -144,12 +158,14 @@ get: description: A user identifier icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -180,4 +196,4 @@ get: type: boolean - $ref: ../components/schemas/Pagination.yaml#/Pagination - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs - - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file + - $ref: ../components/schemas/CommonProps.yaml#/CommonProps diff --git a/public/openapi/read/unread.yaml b/public/openapi/read/unread.yaml index 77e9ec44f6..a395e3fb6d 100644 --- a/public/openapi/read/unread.yaml +++ b/public/openapi/read/unread.yaml @@ -81,7 +81,8 @@ get: type: string userslug: type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) reputation: type: number @@ -99,12 +100,14 @@ get: type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -128,6 +131,19 @@ get: type: object nullable: true properties: + endorsedVotes: + type: array + items: + type: object + properties: + username: + type: string + uid: + type: number + displayname: + type: string + isLocal: + type: boolean pid: type: number uid: @@ -161,19 +177,22 @@ get: type: boolean userslug: type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces + description: + An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) picture: nullable: true type: string icon:text: type: string - description: A single-letter representation of a username. This is used in the + 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 + 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 @@ -270,4 +289,4 @@ get: type: string - $ref: ../components/schemas/Pagination.yaml#/Pagination - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs - - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file + - $ref: ../components/schemas/CommonProps.yaml#/CommonProps diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 10c5d54cf2..5cbb0334d0 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -69,8 +69,15 @@ define('forum/topic/events', [ const votes = $('[data-pid="' + data.post.pid + '"] [component="post/vote-count"]').filter(function (index, el) { return $(el).closest('[data-pid]').attr('data-pid') === String(data.post.pid); }); + const endorsedVotesComponent = $('[data-pid="' + data.post.pid + '"] [component="post/endorsed"]'); const reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); votes.html(data.post.votes).attr('data-votes', data.post.votes); + if (data.endorsedData.length) { + endorsedVotesComponent.show(); + endorsedVotesComponent.find("[component='post/endorsed/text']").text(data.endorsedData.map(user => user.username).join(', ')); + } else { + endorsedVotesComponent.hide(); + } reputationElements.html(data.user.reputation).attr('data-reputation', data.user.reputation); } diff --git a/railway-start.sh b/railway-start.sh new file mode 100644 index 0000000000..f5b948fc71 --- /dev/null +++ b/railway-start.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +./nodebb start +./nodebb log \ No newline at end of file diff --git a/src/api/helpers.js b/src/api/helpers.js index 298725e915..efbba96a57 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -73,7 +73,7 @@ exports.doTopicAction = async function (action, event, caller, { tids }) { socketHelpers.emitToUids(event, data, notifyUids); await logTopicAction(action, caller, tid, title); - switch(action) { + switch (action) { case 'delete': // falls through case 'purge': { if (utils.isNumber(cid) && parseInt(cid, 10) > 0) { @@ -141,6 +141,7 @@ exports.postCommand = async function (caller, command, eventName, notification, return await executeCommand(caller, command, eventName, notification, filteredData.data); }; +// execute voting command on a post and send a message to client on vote update async function executeCommand(caller, command, eventName, notification, data) { const result = await posts[command](data.pid, caller.uid); if (result && eventName) { diff --git a/src/api/posts.js b/src/api/posts.js index c9a570dec4..7c892dec97 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -454,13 +454,13 @@ async function canSeeVotes(uid, cids, type) { const cidToAllowed = _.zipObject(uniqCids, canRead); const checks = cids.map( (cid, index) => isAdmin || isMod[index] || - ( - cidToAllowed[cid] && ( - meta.config[type] === 'all' || - (meta.config[type] === 'loggedin' && parseInt(uid, 10) > 0) + cidToAllowed[cid] && + ( + meta.config[type] === 'all' || + (meta.config[type] === 'loggedin' && parseInt(uid, 10) > 0) + ) ) - ) ); return isArray ? checks : checks[0]; } diff --git a/src/install.js b/src/install.js index 11ef81c2e9..c6846b0e27 100644 --- a/src/install.js +++ b/src/install.js @@ -72,7 +72,6 @@ function checkSetupFlagEnv() { if (Object.keys(envConfMap).some(key => envKeys.includes(key))) { winston.info('[install/checkSetupFlagEnv] checking env vars for setup info...'); setupVal = setupVal || {}; - Object.entries(process.env).forEach(([evName, evValue]) => { // get setup values from env if (evName.startsWith('NODEBB_DB_')) { setupVal[`${process.env.NODEBB_DB}:${envConfMap[evName]}`] = evValue; diff --git a/src/posts/data.js b/src/posts/data.js index 28e6c24aaa..6d79a2b924 100644 --- a/src/posts/data.js +++ b/src/posts/data.js @@ -3,12 +3,13 @@ const db = require('../database'); const plugins = require('../plugins'); const utils = require('../utils'); - const intFields = [ 'uid', 'pid', 'tid', 'deleted', 'timestamp', 'upvotes', 'downvotes', 'deleterUid', 'edited', 'replies', 'bookmarks', 'announces', ]; +const groups = require('../groups'); +const user = require('../user'); module.exports = function (Posts) { Posts.getPostsFields = async function (pids, fields) { @@ -22,12 +23,20 @@ module.exports = function (Posts) { posts: postData, fields: fields, }); + + const endorsedVotes = await Posts.getEndorsedUsers(pids); + result.posts.forEach(post => modifyPost(post, fields)); + result.posts.forEach((post, i) => { + if (post === undefined || post === null) return; + post.endorsedVotes = endorsedVotes[i]; + }); return result.posts; }; Posts.getPostData = async function (pid) { const posts = await Posts.getPostsFields([pid], []); + return posts && posts.length ? posts[0] : null; }; @@ -53,6 +62,19 @@ module.exports = function (Posts) { await db.setObject(`post:${pid}`, data); plugins.hooks.fire('action:post.setFields', { data: { ...data, pid } }); }; + Posts.getEndorsedUsers = async function (pids) { + const upvoterIds = await db.getSetsMembers(pids.map(pid => `pid:${pid}:upvote`)); + const flattenedUpvoterIds = [... new Set(upvoterIds.flat())]; + + const userDataMap = (await user.getUsersFields(flattenedUpvoterIds, ['username'])).reduce((acc, user) => ({ ...acc, [user.uid]: user }), {}); + const userGroupsMap = (await groups.getUserGroups(flattenedUpvoterIds)) + .reduce((acc, groups, i) => ({ ...acc, [flattenedUpvoterIds[i]]: groups }), {}); + const specialUpvotes = upvoterIds.map(userIds => + userIds.filter(uid => + userGroupsMap[uid].find(group => group.slug.toLowerCase() === 'instructor' || group.slug.toLowerCase() === 'ta') !== undefined) + .map(uid => userDataMap[uid])); + return specialUpvotes; + }; }; function modifyPost(post, fields) { diff --git a/src/posts/votes.js b/src/posts/votes.js index d9487b1dc2..585ac8ea16 100644 --- a/src/posts/votes.js +++ b/src/posts/votes.js @@ -190,13 +190,13 @@ module.exports = function (Posts) { await db.sortedSetAdd(`uid:${uid}:downvote`, now, pid); } } - const postData = await Posts.getPostFields(pid, ['pid', 'uid', 'tid']); const newReputation = await user.incrementUserReputationBy(postData.uid, type === 'upvote' ? 1 : -1); await adjustPostVotes(postData, uid, type, unvote); await fireVoteHook(postData, uid, type, unvote, voteStatus); + const endorsedData = (await Posts.getEndorsedUsers([pid]))[0]; return { user: { @@ -206,6 +206,7 @@ module.exports = function (Posts) { post: postData, upvote: type === 'upvote' && !unvote, downvote: type === 'downvote' && !unvote, + endorsedData, }; } diff --git a/test/api.js b/test/api.js index d9a4062c44..6990a199f6 100644 --- a/test/api.js +++ b/test/api.js @@ -535,7 +535,7 @@ describe('API', async () => { assert( responses.hasOwnProperty('418') || Object.keys(responses).includes(String(result.response.statusCode)), - `${method.toUpperCase()} ${path} sent back unexpected HTTP status code: ${result.response.statusCode}` + `${method.toUpperCase()} ${path} sent back unexpected HTTP status code: ${result.response.statusCode} (expected: ${JSON.stringify(responses)})` ); }); @@ -697,7 +697,7 @@ describe('API', async () => { return; } - assert(schema[prop], `"${prop}" was found in response, but is not defined in schema (path: ${method} ${path}, context: ${context}), SCHEMA ${JSON.stringify(response)}`); + assert(schema[prop], `"${prop}" was found in response, but is not defined in schema (path: ${method} ${path}, context: ${context}), SCHEMA ${JSON.stringify(schema)} ${JSON.stringify(response)}`); }); } }); diff --git a/vendor/nodebb-theme-harmony-2.1.35/templates/partials/account/header.tpl b/vendor/nodebb-theme-harmony-2.1.35/templates/partials/account/header.tpl index 0d372d9c04..756936dad9 100644 --- a/vendor/nodebb-theme-harmony-2.1.35/templates/partials/account/header.tpl +++ b/vendor/nodebb-theme-harmony-2.1.35/templates/partials/account/header.tpl @@ -1,3 +1,4 @@ +
diff --git a/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topic/post.tpl b/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topic/post.tpl index 0ba7023ae7..1f1b6a4e49 100644 --- a/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topic/post.tpl +++ b/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topic/post.tpl @@ -8,6 +8,7 @@ {{{ if (./parent && !hideParent) }}} {{{ end }}} +
- - -
- {{{ if !config.theme.topMobilebar }}} - - {{{ else }}} -
- -
- {{{ end }}} - - {{{ if !isSpider }}} -
-
- -
-
- {{{ end }}} - - - - diff --git a/vendor/nodebb-theme-harmony-main/templates/groups/details.tpl b/vendor/nodebb-theme-harmony-main/templates/groups/details.tpl deleted file mode 100644 index fd392c7dbe..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/groups/details.tpl +++ /dev/null @@ -1,86 +0,0 @@ -
-
-
- {{{ if group.isOwner }}} -
- - - -
- [[groups:cover-save]] -
[[groups:cover-saving]]
- {{{ end }}} -
-
- -
-
-
-

{group.displayName}

-
-
- {group.descriptionParsed} -
- {{{ if group.private }}}[[groups:details.private]]{{{ end }}} - {{{ if group.hidden }}}[[groups:details.hidden]]{{{ end }}} -
-
-
-
- {{{ if loggedIn }}} - {function.membershipBtn, group} - {{{ end }}} - {{{ if isAdmin }}} - [[user:edit]] - {{{ end }}} -
-
- -
-
- {{{each widgets.left}}} - {{widgets.left.html}} - {{{end}}} -
- - -
-
-
-

[[global:posts]]

- {{{ if !posts.length }}} -
[[groups:details.has-no-posts]]
- {{{ end }}} - -
-
-

[[groups:details.members]]

- - -
- {{{ if group.isOwner }}} -
-

[[groups:details.pending]]

- -
- -
-

[[groups:details.invited]]

- -
- -
-

[[groups:details.owner-options]]

- -
- {{{ end }}} -
-
- -
- {{{each widgets.right}}} - {{widgets.right.html}} - {{{end}}} -
-
-
diff --git a/vendor/nodebb-theme-harmony-main/templates/groups/list.tpl b/vendor/nodebb-theme-harmony-main/templates/groups/list.tpl deleted file mode 100644 index 6d62e93482..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/groups/list.tpl +++ /dev/null @@ -1,58 +0,0 @@ -
- {{{each widgets.header}}} - {{widgets.header.html}} - {{{end}}} -
-
-

[[pages:groups]]

-
- -
-
-
- {{{ if allowGroupCreation }}} - - {{{ end }}} - -
-
-
- - -
-
-
-
-
- -
- -
- {{{ if groups.length }}} - - {{{ else }}} -
-
- [[groups:no-groups-found]] -
-
- {{{ end }}} -
- - -
diff --git a/vendor/nodebb-theme-harmony-main/templates/groups/members.tpl b/vendor/nodebb-theme-harmony-main/templates/groups/members.tpl deleted file mode 100644 index ffec941ab3..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/groups/members.tpl +++ /dev/null @@ -1,10 +0,0 @@ - -
-
- {{{ each users }}} - - {{{ end }}} -
- - -
\ No newline at end of file diff --git a/vendor/nodebb-theme-harmony-main/templates/header.tpl b/vendor/nodebb-theme-harmony-main/templates/header.tpl deleted file mode 100644 index 412826c93a..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/header.tpl +++ /dev/null @@ -1,39 +0,0 @@ - - - - {browserTitle} - {{{each metaTags}}}{function.buildMetaTag}{{{end}}} - - {{{each linkTags}}}{function.buildLinkTag}{{{end}}} - - - - {{{if useCustomHTML}}} - {{customHTML}} - {{{end}}} - {{{if useCustomCSS}}} - - {{{end}}} - - - - [[global:skip-to-content]] - - {{{ if config.theme.topMobilebar }}} - - {{{ end }}} - -
- - -
- -
- - diff --git a/vendor/nodebb-theme-harmony-main/templates/notifications.tpl b/vendor/nodebb-theme-harmony-main/templates/notifications.tpl deleted file mode 100644 index 3e6673d701..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/notifications.tpl +++ /dev/null @@ -1,32 +0,0 @@ -
- - -
- -
-
- -
-
-
    - -
- -
-
-
- - diff --git a/vendor/nodebb-theme-harmony-main/templates/partials/account/admin-menu.tpl b/vendor/nodebb-theme-harmony-main/templates/partials/account/admin-menu.tpl deleted file mode 100644 index 2941c91505..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/partials/account/admin-menu.tpl +++ /dev/null @@ -1,36 +0,0 @@ -
- - -
diff --git a/vendor/nodebb-theme-harmony-main/templates/partials/account/category-item.tpl b/vendor/nodebb-theme-harmony-main/templates/partials/account/category-item.tpl deleted file mode 100644 index e7f9568a44..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/partials/account/category-item.tpl +++ /dev/null @@ -1,22 +0,0 @@ -
  • - -
    -
    -
    - {buildCategoryIcon(@value, "24px", "rounded-1")} -
    -
    -
    - -
    - {{{ if ./descriptionParsed }}} -
    {./descriptionParsed}
    - {{{ end }}} -
    -
    -
    - -
    -
    -
    -
  • diff --git a/vendor/nodebb-theme-harmony-main/templates/partials/account/footer.tpl b/vendor/nodebb-theme-harmony-main/templates/partials/account/footer.tpl deleted file mode 100644 index e3c5167c91..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/partials/account/footer.tpl +++ /dev/null @@ -1,3 +0,0 @@ -
    -
    -
    \ No newline at end of file diff --git a/vendor/nodebb-theme-harmony-main/templates/partials/account/header.tpl b/vendor/nodebb-theme-harmony-main/templates/partials/account/header.tpl deleted file mode 100644 index 0d372d9c04..0000000000 --- a/vendor/nodebb-theme-harmony-main/templates/partials/account/header.tpl +++ /dev/null @@ -1,98 +0,0 @@ -