From 19dc1025d4cb7c7081948e55d1a62c3c83aa34b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:04:26 -0400 Subject: [PATCH 001/141] fix(deps): update dependency winston to v3.18.3 (#13687) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 9f957c9ae5..5d8a2b5443 100644 --- a/install/package.json +++ b/install/package.json @@ -151,7 +151,7 @@ "validator": "13.15.15", "webpack": "5.102.0", "webpack-merge": "6.0.1", - "winston": "3.17.0", + "winston": "3.18.3", "workerpool": "9.3.4", "xml": "1.0.1", "xregexp": "5.1.2", From eb06bda8d8d1c70040b8c8b1300ddf8089276a5a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:04:55 -0400 Subject: [PATCH 002/141] chore(deps): update dependency @commitlint/cli to v20.1.0 (#13686) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 5d8a2b5443..46e67d5d56 100644 --- a/install/package.json +++ b/install/package.json @@ -160,7 +160,7 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@commitlint/cli": "20.0.0", + "@commitlint/cli": "20.1.0", "@commitlint/config-angular": "20.0.0", "coveralls": "3.1.1", "@eslint/js": "9.36.0", From c7696667372858e0d6c65246c1360035b79715de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:05:14 -0400 Subject: [PATCH 003/141] chore(deps): update dependency mocha to v11.7.4 (#13685) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 46e67d5d56..27bb269af0 100644 --- a/install/package.json +++ b/install/package.json @@ -172,7 +172,7 @@ "husky": "8.0.3", "jsdom": "27.0.0", "lint-staged": "16.2.3", - "mocha": "11.7.2", + "mocha": "11.7.4", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", From 4640a63e4bbbfdc6e844be5a1ba9945eb8d772b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:47:23 -0400 Subject: [PATCH 004/141] chore(deps): update redis docker tag to v8.2.2 (#13692) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- docker-compose-pgsql.yml | 2 +- docker-compose-redis.yml | 2 +- docker-compose.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a94157f446..a405e8a749 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: - 5432:5432 redis: - image: 'redis:8.2.1' + image: 'redis:8.2.2' # Set health checks to wait until redis has started options: >- --health-cmd "redis-cli ping" diff --git a/docker-compose-pgsql.yml b/docker-compose-pgsql.yml index 3e32023939..f7a9e0bc0a 100644 --- a/docker-compose-pgsql.yml +++ b/docker-compose-pgsql.yml @@ -24,7 +24,7 @@ services: - postgres-data:/var/lib/postgresql/data redis: - image: redis:8.2.1-alpine + image: redis:8.2.2-alpine restart: unless-stopped command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning'] # command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # uncomment if you want to use snapshotting instead of AOF diff --git a/docker-compose-redis.yml b/docker-compose-redis.yml index 98bceb6721..2cd7197231 100644 --- a/docker-compose-redis.yml +++ b/docker-compose-redis.yml @@ -14,7 +14,7 @@ services: - ./install/docker/setup.json:/usr/src/app/setup.json redis: - image: redis:8.2.1-alpine + image: redis:8.2.2-alpine restart: unless-stopped command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning'] # command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # uncomment if you want to use snapshotting instead of AOF diff --git a/docker-compose.yml b/docker-compose.yml index ec096eb060..32b02159f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,7 @@ services: - mongo-data:/data/db - ./install/docker/mongodb-user-init.js:/docker-entrypoint-initdb.d/user-init.js redis: - image: redis:8.2.1-alpine + image: redis:8.2.2-alpine restart: unless-stopped command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning'] # command: ['redis-server', '--save', '60', '1', '--loglevel', 'warning'] # uncomment if you want to use snapshotting instead of AOF From 9b6e9b2ac39520b51270fb1cc2ec92008bd3bef4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:50:50 -0400 Subject: [PATCH 005/141] fix(deps): update dependency redis to v5.8.3 (#13691) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 27bb269af0..fb6a3ca3ec 100644 --- a/install/package.json +++ b/install/package.json @@ -123,7 +123,7 @@ "pretty": "^2.0.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", - "redis": "5.8.2", + "redis": "5.8.3", "rimraf": "6.0.1", "rss": "1.2.2", "rtlcss": "4.3.0", From 66285ef53eba7aec0152eab5c642477ccde18094 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 5 Oct 2025 09:20:02 +0000 Subject: [PATCH 006/141] Latest translations and fallbacks --- public/language/zh-CN/admin/manage/categories.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/language/zh-CN/admin/manage/categories.json b/public/language/zh-CN/admin/manage/categories.json index 07c53a18aa..410f44b0c8 100644 --- a/public/language/zh-CN/admin/manage/categories.json +++ b/public/language/zh-CN/admin/manage/categories.json @@ -4,7 +4,7 @@ "add-local-category": "添加本地版块", "add-remote-category": "添加远程版块", "remove": "移除", - "rename": "Rename", + "rename": "重命名", "jump-to": "跳转…", "settings": "版块设置", "edit-category": "编辑版块", @@ -112,9 +112,9 @@ "alert.create": "创建一个版块", "alert.add": "添加一个版块", "alert.add-help": "可以通过指定其句柄将远程版块添加到版块列表中。

注: — 远程版块可能无法反映所有已发布的主题,除非至少有一名本地用户关注或订阅该版块。", - "alert.rename": "Rename a Remote Category", - "alert.rename-help": "Please enter a new name for this category. Leave blank to restore original name.", - "alert.confirm-remove": "Do you really want to remove this category? You can add it back at any time.", + "alert.rename": "重命名远程版块", + "alert.rename-help": "请为该版块输入新名称。留空则恢复原名。", + "alert.confirm-remove": "您确定要删除此版块吗?您可以随时将其重新添加回来。", "alert.confirm-purge": "

您确定要清除此版块“%1”吗?

警告! 版块将被清除!

清除版块将删除所有主题和帖子,并从数据库中删除版块。 如果您想暂时移除版块,请使用停用版块。

", "alert.purge-success": "版块已删除!", "alert.copy-success": "设置已复制!", From 5dc9f2c5d45a0a11ef06b7f16feee71e0dc682e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:13:26 -0400 Subject: [PATCH 007/141] fix(deps): update dependency nodemailer to v7.0.7 (#13694) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index fb6a3ca3ec..45ca7d4109 100644 --- a/install/package.json +++ b/install/package.json @@ -111,7 +111,7 @@ "nodebb-theme-peace": "2.2.48", "nodebb-theme-persona": "14.1.14", "nodebb-widget-essentials": "7.0.40", - "nodemailer": "7.0.6", + "nodemailer": "7.0.7", "nprogress": "0.2.0", "passport": "0.7.0", "passport-http-bearer": "1.0.1", From d73892aedab0eb2e0d2e6a2bd5cc2a935c966cb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:13:40 -0400 Subject: [PATCH 008/141] chore(deps): update dependency @eslint/js to v9.37.0 (#13693) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 45ca7d4109..79cc502eda 100644 --- a/install/package.json +++ b/install/package.json @@ -163,7 +163,7 @@ "@commitlint/cli": "20.1.0", "@commitlint/config-angular": "20.0.0", "coveralls": "3.1.1", - "@eslint/js": "9.36.0", + "@eslint/js": "9.37.0", "@stylistic/eslint-plugin": "5.4.0", "eslint-config-nodebb": "1.1.11", "eslint-plugin-import": "2.32.0", From 923ddbc1f1a464772a5f858b3318fae07106ce17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:28:44 -0400 Subject: [PATCH 009/141] chore(deps): update postgres docker tag to v18 (#13679) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- docker-compose-pgsql.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a405e8a749..89e7d91fe7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -48,7 +48,7 @@ jobs: services: postgres: - image: 'postgres:17-alpine' + image: 'postgres:18-alpine' env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres diff --git a/docker-compose-pgsql.yml b/docker-compose-pgsql.yml index f7a9e0bc0a..3c55eb6c3b 100644 --- a/docker-compose-pgsql.yml +++ b/docker-compose-pgsql.yml @@ -14,7 +14,7 @@ services: - ./install/docker/setup.json:/usr/src/app/setup.json postgres: - image: postgres:17.6-alpine + image: postgres:18.0-alpine restart: unless-stopped environment: POSTGRES_USER: nodebb diff --git a/docker-compose.yml b/docker-compose.yml index 32b02159f8..ee7a18ceb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: - redis postgres: - image: postgres:17.6-alpine + image: postgres:18.0-alpine restart: unless-stopped environment: POSTGRES_USER: nodebb From 93b6cb598402c5ba430a46712fc2459c654dcdd1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 6 Oct 2025 13:45:40 -0400 Subject: [PATCH 010/141] feat: federate Delete on post delete as well as purge, topic deletion federates Announce(Delete(Object)) --- src/api/activitypub.js | 59 +++++++++++++++++++++++++++++++++++++++++- src/posts/delete.js | 1 + src/topics/delete.js | 2 ++ src/topics/tools.js | 8 ++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/api/activitypub.js b/src/api/activitypub.js index 7e4ae7da18..19e9087539 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -350,7 +350,7 @@ activitypubApi.announce = {}; activitypubApi.announce.note = enabledCheck(async (caller, { tid }) => { const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); - // Only remote posts can be announced to real categories + // Only remote posts can be announced to local categories if (utils.isNumber(pid) || parseInt(cid, 10) === -1) { return; } @@ -379,6 +379,63 @@ activitypubApi.announce.note = enabledCheck(async (caller, { tid }) => { }); }); +activitypubApi.announce.delete = enabledCheck(async ({ uid }, { tid }) => { + const now = new Date(); + const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); + + // Only local categories + if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { + return; + } + + const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`); + return; + } + + const { to, cc, targets } = await activitypub.buildRecipients({ + to: [activitypub._constants.publicAddress], + cc: [`${nconf.get('url')}/category/${cid}/followers`], + }, { cid }); + + const deleteTpl = { + id: `${nconf.get('url')}/topic/${tid}#activity/delete/${now.getTime()}`, + type: 'Delete', + actor: `${nconf.get('url')}/category/${cid}`, + to, + cc, + origin: `${nconf.get('url')}/category/${cid}`, + }; + + // 7888 variant + await activitypub.send('cid', cid, Array.from(targets), { + id: `${nconf.get('url')}/topic/${tid}#activity/announce/delete/${now.getTime()}`, + type: 'Announce', + actor: `${nconf.get('url')}/category/${cid}`, + to, + cc, + object: { + ...deleteTpl, + object: `${nconf.get('url')}/topic/${tid}`, + }, + }); + + // 1b12 variant + await activitypub.send('cid', cid, Array.from(targets), { + id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/delete/${now.getTime()}`, + type: 'Announce', + actor: `${nconf.get('url')}/category/${cid}`, + to, + cc, + object: { + ...deleteTpl, + actor: `${nconf.get('url')}/uid/${uid}`, + object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, + }, + }); +}); + activitypubApi.undo = {}; // activitypubApi.undo.follow = diff --git a/src/posts/delete.js b/src/posts/delete.js index 5668ac3750..dddb33e6a9 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -34,6 +34,7 @@ module.exports = function (Posts) { await Promise.all([ topics.updateLastPostTimeFromLastPid(postData.tid), topics.updateTeaser(postData.tid), + isDeleting ? activitypub.notes.delete(pid) : null, isDeleting ? db.sortedSetRemove(`cid:${topicData.cid}:pids`, pid) : db.sortedSetAdd(`cid:${topicData.cid}:pids`, postData.timestamp, pid), diff --git a/src/topics/delete.js b/src/topics/delete.js index e97fd0a98e..d07a3aee5b 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -8,6 +8,7 @@ const categories = require('../categories'); const flags = require('../flags'); const plugins = require('../plugins'); const batch = require('../batch'); +const api = require('../api'); const utils = require('../utils'); module.exports = function (Topics) { @@ -80,6 +81,7 @@ module.exports = function (Topics) { } deletedTopic.tags = tags; await deleteFromFollowersIgnorers(tid); + await api.activitypub.announce.delete({ uid }, { tid }), await Promise.all([ db.deleteAll([ diff --git a/src/topics/tools.js b/src/topics/tools.js index 294615b38a..65505bde67 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -7,6 +7,7 @@ const topics = require('.'); const categories = require('../categories'); const user = require('../user'); const plugins = require('../plugins'); +const api = require('../api'); const privileges = require('../privileges'); const utils = require('../utils'); @@ -277,6 +278,13 @@ module.exports = function (Topics) { const oldCid = topicData.cid; await categories.moveRecentReplies(tid, oldCid, cid); + // AP: Announce(Delete(Object)) + if (cid === -1) { + await api.activitypub.announce.delete({ uid: data.uid }, { tid }); + } else { + // tbd: api.activitypub.announce.move + } + await Promise.all([ Topics.setTopicFields(tid, { cid: cid, From 4d24309a069cd04fe589fb493b40e230cf64f663 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 7 Oct 2025 11:35:36 -0400 Subject: [PATCH 011/141] feat: federate topic deletion on topic deletion as well as purge --- src/topics/delete.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/topics/delete.js b/src/topics/delete.js index d07a3aee5b..3557349d9d 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -25,6 +25,7 @@ module.exports = function (Topics) { deleterUid: uid, deletedTimestamp: Date.now(), }), + api.activitypub.announce.delete({ uid }, { tid }), ]); await categories.updateRecentTidForCid(cid); From d3b3720915f5846e8f5a8e0bee9c17b3ff233902 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 9 Oct 2025 13:56:59 -0400 Subject: [PATCH 012/141] refactor: move post attachment handling directly into posts.create --- src/activitypub/notes.js | 10 +++------- src/posts/create.js | 5 ++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 130cb1874f..8e11898f26 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -210,7 +210,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { activitypub.helpers.log(`[notes/assert] ${count} new note(s) found.`); if (!hasTid) { - const { to, cc, attachment } = mainPost._activitypub; + const { to, cc } = mainPost._activitypub; const tags = await Notes._normalizeTags(mainPost._activitypub.tag || []); try { @@ -239,7 +239,6 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { id: tid, path: mainPost._activitypub.image, }) : null, - posts.attachments.update(mainPid, attachment), ]); if (context) { @@ -249,16 +248,13 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { } for (const post of unprocessed) { - const { to, cc, attachment } = post._activitypub; + const { to, cc } = post._activitypub; try { // eslint-disable-next-line no-await-in-loop await topics.reply(post); // eslint-disable-next-line no-await-in-loop - await Promise.all([ - Notes.updateLocalRecipients(post.pid, { to, cc }), - posts.attachments.update(post.pid, attachment), - ]); + await Notes.updateLocalRecipients(post.pid, { to, cc }); } catch (e) { activitypub.helpers.log(`[activitypub/notes.assert] Could not add reply (${post.pid}): ${e.message}`); } diff --git a/src/posts/create.js b/src/posts/create.js index 656ae68ab0..5d56c05d26 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -71,6 +71,8 @@ module.exports = function (Posts) { const topicData = await topics.getTopicFields(tid, ['cid', 'pinned']); postData.cid = topicData.cid; + const hasAttachment = _activitypub.attachment && _activitypub.attachment.length; + await Promise.all([ db.sortedSetAdd('posts:pid', timestamp, postData.pid), utils.isNumber(pid) ? db.incrObjectField('global', 'postCount') : null, @@ -79,7 +81,8 @@ module.exports = function (Posts) { categories.onNewPostMade(topicData.cid, topicData.pinned, postData), groups.onNewPostMade(postData), addReplyTo(postData, timestamp), - Posts.uploads.sync(postData.pid), + Posts.uploads.sync(pid), + hasAttachment ? Posts.attachments.update(pid, _activitypub.attachment) : null, ]); const result = await plugins.hooks.fire('filter:post.get', { post: postData, uid: data.uid }); From 07bed55e333805921463b1415f4317a4b1313e83 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 9 Oct 2025 13:57:21 -0400 Subject: [PATCH 013/141] fix: add attachments to retrieved post data onNewPost --- src/topics/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/topics/create.js b/src/topics/create.js index 8f347c736a..098fba2d41 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -248,7 +248,7 @@ module.exports = function (Topics) { async function onNewPost({ pid, tid, uid: postOwner }, { uid, handle }) { const [[postData], [userInfo]] = await Promise.all([ - posts.getPostSummaryByPids([pid], uid, {}), + posts.getPostSummaryByPids([pid], uid, { extraFields: ['attachments'] }), posts.getUserInfoForPosts([postOwner], uid), ]); await Promise.all([ From e7bdf6bc31b7c7ae60e1cc38b78aec6c393e66bc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 9 Oct 2025 14:00:30 -0400 Subject: [PATCH 014/141] feat: bundle link-preview plugin --- install/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 79cc502eda..5402c223ef 100644 --- a/install/package.json +++ b/install/package.json @@ -101,6 +101,7 @@ "nodebb-plugin-dbsearch": "6.3.2", "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", + "nodebb-plugin-link-preview": "2.1.5", "nodebb-plugin-markdown": "13.2.1", "nodebb-plugin-mentions": "4.7.6", "nodebb-plugin-spam-be-gone": "2.3.2", @@ -202,4 +203,4 @@ "url": "https://github.com/barisusakli" } ] -} \ No newline at end of file +} From b153941cf389732b3a93ca30e5e8de65aaae2809 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 9 Oct 2025 14:01:08 -0400 Subject: [PATCH 015/141] feat: auto-enable link-preview plugin on new installations --- src/install.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/install.js b/src/install.js index 064d5d77ff..23346cbdee 100644 --- a/src/install.js +++ b/src/install.js @@ -534,6 +534,7 @@ async function enableDefaultPlugins() { 'nodebb-rewards-essentials', 'nodebb-plugin-emoji', 'nodebb-plugin-emoji-android', + 'nodebb-plugin-link-preview', ]; let customDefaults = nconf.get('defaultplugins') || nconf.get('defaultPlugins'); From bb7b65eaa13f9ff6fc8a284f50161f7be2a97165 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:02:20 -0400 Subject: [PATCH 016/141] fix(deps): update dependency webpack to v5.102.1 (#13698) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 5402c223ef..3fc5172197 100644 --- a/install/package.json +++ b/install/package.json @@ -150,7 +150,7 @@ "tough-cookie": "6.0.0", "undici": "^7.10.0", "validator": "13.15.15", - "webpack": "5.102.0", + "webpack": "5.102.1", "webpack-merge": "6.0.1", "winston": "3.18.3", "workerpool": "9.3.4", From a2892f60bc018d16bb6794f32398141fa80324d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:02:30 -0400 Subject: [PATCH 017/141] fix(deps): update dependency semver to v7.7.3 (#13697) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 3fc5172197..61336c75c0 100644 --- a/install/package.json +++ b/install/package.json @@ -132,7 +132,7 @@ "sass": "1.93.2", "satori": "0.18.3", "sbd": "^1.0.19", - "semver": "7.7.2", + "semver": "7.7.3", "serve-favicon": "2.5.1", "sharp": "0.34.4", "sitemap": "8.0.0", From 5d3709f002c089679f148eeedd50e148d069f6d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:02:42 -0400 Subject: [PATCH 018/141] fix(deps): update dependency nodemailer to v7.0.9 (#13695) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 61336c75c0..6c46043871 100644 --- a/install/package.json +++ b/install/package.json @@ -112,7 +112,7 @@ "nodebb-theme-peace": "2.2.48", "nodebb-theme-persona": "14.1.14", "nodebb-widget-essentials": "7.0.40", - "nodemailer": "7.0.7", + "nodemailer": "7.0.9", "nprogress": "0.2.0", "passport": "0.7.0", "passport-http-bearer": "1.0.1", From d7657538faeefbe70a237b61a1f6414bc018acf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 11 Oct 2025 20:39:14 -0400 Subject: [PATCH 019/141] Revert "feat: auto-enable link-preview plugin on new installations" This reverts commit b153941cf389732b3a93ca30e5e8de65aaae2809. --- src/install.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/install.js b/src/install.js index 23346cbdee..064d5d77ff 100644 --- a/src/install.js +++ b/src/install.js @@ -534,7 +534,6 @@ async function enableDefaultPlugins() { 'nodebb-rewards-essentials', 'nodebb-plugin-emoji', 'nodebb-plugin-emoji-android', - 'nodebb-plugin-link-preview', ]; let customDefaults = nconf.get('defaultplugins') || nconf.get('defaultPlugins'); From 6c2100684b7a1630e618560a13901e28a2750bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 11 Oct 2025 20:54:00 -0400 Subject: [PATCH 020/141] fix: crash in tests --- src/posts/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/posts/create.js b/src/posts/create.js index 5d56c05d26..e74299c27c 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -71,7 +71,7 @@ module.exports = function (Posts) { const topicData = await topics.getTopicFields(tid, ['cid', 'pinned']); postData.cid = topicData.cid; - const hasAttachment = _activitypub.attachment && _activitypub.attachment.length; + const hasAttachment = _activitypub && _activitypub.attachment && _activitypub.attachment.length; await Promise.all([ db.sortedSetAdd('posts:pid', timestamp, postData.pid), From 49a293259435b9db640737abfe278f8d6dae739e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:03:34 -0400 Subject: [PATCH 021/141] fix(deps): update dependency nodebb-theme-harmony to v2.1.21 (#13700) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 6c46043871..6237eae76b 100644 --- a/install/package.json +++ b/install/package.json @@ -107,7 +107,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.5", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.20", + "nodebb-theme-harmony": "2.1.21", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.48", "nodebb-theme-persona": "14.1.14", From fa18287d037759a91ba11adc499b36fc2a5e24f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:03:42 -0400 Subject: [PATCH 022/141] fix(deps): update dependency nodebb-theme-persona to v14.1.15 (#13701) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 6237eae76b..bc0b9481a9 100644 --- a/install/package.json +++ b/install/package.json @@ -110,7 +110,7 @@ "nodebb-theme-harmony": "2.1.21", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.48", - "nodebb-theme-persona": "14.1.14", + "nodebb-theme-persona": "14.1.15", "nodebb-widget-essentials": "7.0.40", "nodemailer": "7.0.9", "nprogress": "0.2.0", From f608c7c7a78e85d8cec27ad118f06f5c3945decf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:03:52 -0400 Subject: [PATCH 023/141] chore(deps): update dependency lint-staged to v16.2.4 (#13699) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index bc0b9481a9..526909d7e8 100644 --- a/install/package.json +++ b/install/package.json @@ -172,7 +172,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "27.0.0", - "lint-staged": "16.2.3", + "lint-staged": "16.2.4", "mocha": "11.7.4", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From 238600a0ec85f45e3306c1877168d1b8ff92ebad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:04:03 -0400 Subject: [PATCH 024/141] chore(deps): update dependency smtp-server to v3.15.0 (#13702) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 526909d7e8..1154fa0451 100644 --- a/install/package.json +++ b/install/package.json @@ -177,7 +177,7 @@ "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", - "smtp-server": "3.14.0" + "smtp-server": "3.15.0" }, "optionalDependencies": { "sass-embedded": "1.93.2" From af5efbd71d6ad9bedbf1ad74e1ede162f3a7fe31 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 14 Oct 2025 11:21:39 -0400 Subject: [PATCH 025/141] fix: regression caused by d3b3720915f5846e8f5a8e0bee9c17b3ff233902 --- src/posts/create.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/posts/create.js b/src/posts/create.js index e74299c27c..fa7ca1d071 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -18,6 +18,7 @@ module.exports = function (Posts) { const content = data.content.toString(); const timestamp = data.timestamp || Date.now(); const isMain = data.isMain || false; + let hasAttachment = false; if (!uid && parseInt(uid, 10) !== 0) { throw new Error('[[error:invalid-uid]]'); @@ -46,23 +47,25 @@ module.exports = function (Posts) { if (_activitypub.audience) { postData.audience = _activitypub.audience; } - } - // Rewrite emoji references to inline image assets - if (_activitypub && _activitypub.tag && Array.isArray(_activitypub.tag)) { - _activitypub.tag - .filter(tag => tag.type === 'Emoji' && - tag.icon && tag.icon.type === 'Image') - .forEach((tag) => { - if (!tag.name.startsWith(':')) { - tag.name = `:${tag.name}`; - } - if (!tag.name.endsWith(':')) { - tag.name = `${tag.name}:`; - } + // Rewrite emoji references to inline image assets + if (_activitypub && _activitypub.tag && Array.isArray(_activitypub.tag)) { + _activitypub.tag + .filter(tag => tag.type === 'Emoji' && + tag.icon && tag.icon.type === 'Image') + .forEach((tag) => { + if (!tag.name.startsWith(':')) { + tag.name = `:${tag.name}`; + } + if (!tag.name.endsWith(':')) { + tag.name = `${tag.name}:`; + } - postData.content = postData.content.replace(new RegExp(tag.name, 'g'), ``); - }); + postData.content = postData.content.replace(new RegExp(tag.name, 'g'), ``); + }); + } + + hasAttachment = _activitypub && _activitypub.attachment && _activitypub.attachment.length; } ({ post: postData } = await plugins.hooks.fire('filter:post.create', { post: postData, data: data })); @@ -71,8 +74,6 @@ module.exports = function (Posts) { const topicData = await topics.getTopicFields(tid, ['cid', 'pinned']); postData.cid = topicData.cid; - const hasAttachment = _activitypub && _activitypub.attachment && _activitypub.attachment.length; - await Promise.all([ db.sortedSetAdd('posts:pid', timestamp, postData.pid), utils.isNumber(pid) ? db.incrObjectField('global', 'postCount') : null, From bf37c7bd77cd4a391d8b9c80d16915ba5d1f0608 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:24:26 -0400 Subject: [PATCH 026/141] fix(deps): update dependency chart.js to v4.5.1 (#13704) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 1154fa0451..0dcf15858c 100644 --- a/install/package.json +++ b/install/package.json @@ -50,7 +50,7 @@ "bootstrap": "5.3.8", "bootswatch": "5.3.8", "chalk": "4.1.2", - "chart.js": "4.5.0", + "chart.js": "4.5.1", "cli-graph": "3.2.2", "clipboard": "2.0.11", "commander": "14.0.1", From febe0ae01aa14db94e2ac7c2f41f6ca99f90c5ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:24:44 -0400 Subject: [PATCH 027/141] chore(deps): update actions/setup-node action to v6 (#13708) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 89e7d91fe7..03dbbf8f3c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -86,7 +86,7 @@ jobs: - run: cp install/package.json package.json - name: Install Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} From 41b7a91d8f3e02cf7836086aee8a189f8a35df3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:10:55 -0400 Subject: [PATCH 028/141] fix(deps): update dependency esbuild to v0.25.11 (#13710) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 0dcf15858c..7da467536c 100644 --- a/install/package.json +++ b/install/package.json @@ -66,7 +66,7 @@ "csrf-sync": "4.2.1", "daemon": "1.1.0", "diff": "8.0.2", - "esbuild": "0.25.10", + "esbuild": "0.25.11", "express": "4.21.2", "express-session": "1.18.2", "express-useragent": "1.0.15", From 9583f0d49b040d82130051f9958b64fb50c476a9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 11:24:08 -0400 Subject: [PATCH 029/141] feat: execute 1b12 rebroadcast logic on all tids even if not posted to a local cid --- src/activitypub/feps.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/activitypub/feps.js b/src/activitypub/feps.js index b8bf765bef..cc49f2a8aa 100644 --- a/src/activitypub/feps.js +++ b/src/activitypub/feps.js @@ -3,6 +3,7 @@ const nconf = require('nconf'); const posts = require('../posts'); +const topics = require('../topics'); const utils = require('../utils'); const activitypub = module.parent.exports; @@ -13,8 +14,16 @@ Feps.announce = async function announce(id, activity) { if (String(id).startsWith(nconf.get('url'))) { ({ id: localId } = await activitypub.helpers.resolveLocalId(id)); } - const cid = await posts.getCidByPid(localId || id); - if (cid === -1 || !utils.isNumber(cid)) { // local cids only + + /** + * Re-broadcasting occurs on + * - local cids (for all tids), and + * - local tids (posted to remote cids) only + */ + const tid = await posts.getPostField(localId || id, 'tid'); + const cid = await topics.getTopicField(tid, 'cid'); + const shouldAnnounce = (utils.isNumber(cid) && cid > 0) || utils.isNumber(tid); + if (!shouldAnnounce) { // inverse conditionals can kiss my ass. return; } From c25c629023959e078630bcaa0d2fa618936b3fc8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 11:52:47 -0400 Subject: [PATCH 030/141] fix(deps): bump dbsearch --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 7da467536c..90d5f97518 100644 --- a/install/package.json +++ b/install/package.json @@ -98,7 +98,7 @@ "nconf": "0.13.0", "nodebb-plugin-2factor": "7.6.0", "nodebb-plugin-composer-default": "10.3.1", - "nodebb-plugin-dbsearch": "6.3.2", + "nodebb-plugin-dbsearch": "6.3.3", "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-link-preview": "2.1.5", From 79d088536a590671765bebdf08f90218c28ba445 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 12:03:26 -0400 Subject: [PATCH 031/141] fix: update 1b12 rebroadcast logic to send as application actor if post is in remote cid --- src/activitypub/feps.js | 9 +++++++-- src/activitypub/helpers.js | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/activitypub/feps.js b/src/activitypub/feps.js index cc49f2a8aa..36b600283f 100644 --- a/src/activitypub/feps.js +++ b/src/activitypub/feps.js @@ -21,12 +21,17 @@ Feps.announce = async function announce(id, activity) { * - local tids (posted to remote cids) only */ const tid = await posts.getPostField(localId || id, 'tid'); - const cid = await topics.getTopicField(tid, 'cid'); - const shouldAnnounce = (utils.isNumber(cid) && cid > 0) || utils.isNumber(tid); + let cid = await topics.getTopicField(tid, 'cid'); + const localCid = utils.isNumber(cid) && cid > 0; + const shouldAnnounce = localCid || utils.isNumber(tid); if (!shouldAnnounce) { // inverse conditionals can kiss my ass. return; } + if (!localCid) { + cid = 0; // override cid to 0 so application actor sends the announce + } + let relays = await activitypub.relays.list(); relays = relays.reduce((memo, { state, url }) => { if (state === 2) { diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index e6eb2e1c08..90701dd58d 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -225,7 +225,7 @@ Helpers.resolveActor = (type, id) => { case 'category': case 'cid': { - return `${nconf.get('url')}/category/${id}`; + return `${nconf.get('url')}${id > 0 ? `/category/${id}` : '/actor'}`; } default: From 58a9e1c4f9ec6cba94d5422b36005cf508f7a09d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 12:08:29 -0400 Subject: [PATCH 032/141] fix: update targets in 1b12 rebroadcast when cid is remote --- src/activitypub/feps.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/activitypub/feps.js b/src/activitypub/feps.js index 36b600283f..12b99d8fc8 100644 --- a/src/activitypub/feps.js +++ b/src/activitypub/feps.js @@ -21,17 +21,13 @@ Feps.announce = async function announce(id, activity) { * - local tids (posted to remote cids) only */ const tid = await posts.getPostField(localId || id, 'tid'); - let cid = await topics.getTopicField(tid, 'cid'); + const cid = await topics.getTopicField(tid, 'cid'); const localCid = utils.isNumber(cid) && cid > 0; const shouldAnnounce = localCid || utils.isNumber(tid); if (!shouldAnnounce) { // inverse conditionals can kiss my ass. return; } - if (!localCid) { - cid = 0; // override cid to 0 so application actor sends the announce - } - let relays = await activitypub.relays.list(); relays = relays.reduce((memo, { state, url }) => { if (state === 2) { @@ -39,7 +35,7 @@ Feps.announce = async function announce(id, activity) { } return memo; }, []); - const followers = await activitypub.notes.getCategoryFollowers(cid); + const followers = localCid ? await activitypub.notes.getCategoryFollowers(cid) : [cid]; const targets = relays.concat(followers); if (!targets.length) { return; @@ -54,7 +50,7 @@ Feps.announce = async function announce(id, activity) { const isMain = await posts.isMain(localId || id); if (isMain) { activitypub.helpers.log(`[activitypub/inbox.announce(1b12)] Announcing plain object (${activity.id}) to followers of cid ${cid} and ${relays.length} relays`); - await activitypub.send('cid', cid, targets, { + await activitypub.send('cid', localCid ? cid : 0, targets, { id: `${nconf.get('url')}/post/${encodeURIComponent(id)}#activity/announce/${now}`, type: 'Announce', actor: `${nconf.get('url')}/category/${cid}`, @@ -66,7 +62,7 @@ Feps.announce = async function announce(id, activity) { } activitypub.helpers.log(`[activitypub/inbox.announce(1b12)] Announcing ${activity.type} (${activity.id}) to followers of cid ${cid} and ${relays.length} relays`); - await activitypub.send('cid', cid, targets, { + await activitypub.send('cid', localCid ? cid : 0, targets, { id: `${nconf.get('url')}/post/${encodeURIComponent(id)}#activity/announce/${now + 1}`, type: 'Announce', actor: `${nconf.get('url')}/category/${cid}`, From a45f6f9c4cd1014202dd4900051068d87c836cbb Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 12:24:42 -0400 Subject: [PATCH 033/141] fix: update getPrivateKey to send application actor key when cid 0 --- src/activitypub/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 5971c69326..7b84148600 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -201,7 +201,7 @@ ActivityPub.getPrivateKey = async (type, id) => { if (type === 'uid') { keyId = `${nconf.get('url')}${id > 0 ? `/uid/${id}` : '/actor'}#key`; } else { - keyId = `${nconf.get('url')}/category/${id}#key`; + keyId = `${nconf.get('url')}${id > 0 ? `/category/${id}` : '/actor'}#key`; } return { key: privateKey, keyId }; From d4695f1085e356507ab6a1b17329abc4b082cc72 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 12:31:55 -0400 Subject: [PATCH 034/141] fix: broken category urls in to, cc --- src/activitypub/feps.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/activitypub/feps.js b/src/activitypub/feps.js index 12b99d8fc8..a84600ca65 100644 --- a/src/activitypub/feps.js +++ b/src/activitypub/feps.js @@ -46,6 +46,12 @@ Feps.announce = async function announce(id, activity) { targets.unshift(actor); } const now = Date.now(); + const to = [localCid ? `${nconf.get('url')}/category/${cid}/followers` : cid]; + const cc = [activitypub._constants.publicAddress]; + if (localCid) { + cc.unshift(actor); + } + if (activity.type === 'Create') { const isMain = await posts.isMain(localId || id); if (isMain) { @@ -53,9 +59,9 @@ Feps.announce = async function announce(id, activity) { await activitypub.send('cid', localCid ? cid : 0, targets, { id: `${nconf.get('url')}/post/${encodeURIComponent(id)}#activity/announce/${now}`, type: 'Announce', - actor: `${nconf.get('url')}/category/${cid}`, - to: [`${nconf.get('url')}/category/${cid}/followers`], - cc: [actor, activitypub._constants.publicAddress], + actor: localCid ? `${nconf.get('url')}/category/${cid}` : `${nconf.get('url')}/actor`, + to, + cc, object: activity.object, }); } @@ -65,9 +71,9 @@ Feps.announce = async function announce(id, activity) { await activitypub.send('cid', localCid ? cid : 0, targets, { id: `${nconf.get('url')}/post/${encodeURIComponent(id)}#activity/announce/${now + 1}`, type: 'Announce', - actor: `${nconf.get('url')}/category/${cid}`, - to: [`${nconf.get('url')}/category/${cid}/followers`], - cc: [actor, activitypub._constants.publicAddress], + actor: localCid ? `${nconf.get('url')}/category/${cid}` : `${nconf.get('url')}/actor`, + to, + cc, object: activity, }); }; From 3fa74d4cecc130fc584a6f9989fbaeb2da5a47dc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 12:33:57 -0400 Subject: [PATCH 035/141] fix: do not include actor from reflected activity when rebroadcasting remote cid --- src/activitypub/feps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activitypub/feps.js b/src/activitypub/feps.js index a84600ca65..9fb0e28680 100644 --- a/src/activitypub/feps.js +++ b/src/activitypub/feps.js @@ -42,7 +42,7 @@ Feps.announce = async function announce(id, activity) { } const { actor } = activity; - if (actor && !actor.startsWith(nconf.get('url'))) { + if (localCid && actor && !actor.startsWith(nconf.get('url'))) { targets.unshift(actor); } const now = Date.now(); From fadac6165e362e4bbfd53ac364a9d18e32d27451 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Oct 2025 15:02:23 -0400 Subject: [PATCH 036/141] fix: move Announce(Delete) out of topics.move and into topics API method --- src/api/topics.js | 11 ++++++++++- src/topics/tools.js | 8 -------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/api/topics.js b/src/api/topics.js index d44fffae04..e0f149e392 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -320,7 +320,16 @@ topicsAPI.move = async (caller, { tid, cid }) => { socketHelpers.emitToUids('event:topic_moved', topicData, notifyUids); if (!topicData.deleted) { socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic'); - activitypubApi.announce.note(caller, { tid }); + + // AP: Announce(Delete(Object)) + if (cid === -1) { + await activitypubApi.announce.delete({ uid: caller.uid }, { tid }); + // tbd: activitypubApi.undo.announce? + } else { + // tbd: some kind of plain object announce by the category... + activitypubApi.announce.note(caller, { tid }); // user announce, remove when discrete announces are a thing + // tbd: api.activitypub.announce.move + } } await events.log({ diff --git a/src/topics/tools.js b/src/topics/tools.js index 65505bde67..294615b38a 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -7,7 +7,6 @@ const topics = require('.'); const categories = require('../categories'); const user = require('../user'); const plugins = require('../plugins'); -const api = require('../api'); const privileges = require('../privileges'); const utils = require('../utils'); @@ -278,13 +277,6 @@ module.exports = function (Topics) { const oldCid = topicData.cid; await categories.moveRecentReplies(tid, oldCid, cid); - // AP: Announce(Delete(Object)) - if (cid === -1) { - await api.activitypub.announce.delete({ uid: data.uid }, { tid }); - } else { - // tbd: api.activitypub.announce.move - } - await Promise.all([ Topics.setTopicFields(tid, { cid: cid, From 4d5005b972664150ba1f38b652304d4c30b2856a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Oct 2025 11:12:00 -0400 Subject: [PATCH 037/141] feat: handle incoming Announce(Delete), closes #13712 --- src/activitypub/inbox.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 9b5f85db69..9f8f3c5ebe 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -321,6 +321,12 @@ inbox.announce = async (req) => { break; } + case object.type === 'Delete': { + req.body = object; + await inbox.delete(req); + break; + } + case object.type === 'Create': { object = object.object; // falls through From 2b2028e4469702547184784aa43568b820dae86f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Oct 2025 11:27:21 -0400 Subject: [PATCH 038/141] refactor: inbox announce(delete) handling to also handle context deletion, #13712 --- src/activitypub/contexts.js | 4 ++++ src/activitypub/inbox.js | 34 ++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/activitypub/contexts.js b/src/activitypub/contexts.js index 9d6c948a5e..2e346facc0 100644 --- a/src/activitypub/contexts.js +++ b/src/activitypub/contexts.js @@ -81,6 +81,10 @@ Contexts.getItems = async (uid, id, options) => { } if (items) { + if (options.returnRootId) { + return items.pop(); + } + items = await Promise.all(items .map(async item => (activitypub.helpers.isUri(item) ? parseString(uid, item) : parseItem(uid, item)))); items = items.filter(Boolean); diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 9f8f3c5ebe..77fd2bf4b5 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -188,14 +188,14 @@ inbox.delete = async (req) => { throw new Error('[[error:invalid-pid]]'); } } - const pid = object.id || object; + const id = object.id || object; let type = object.type || undefined; // Deletes don't have their objects resolved automatically let method = 'purge'; try { if (!type) { - ({ type } = await activitypub.get('uid', 0, pid)); + ({ type } = await activitypub.get('uid', 0, id)); } if (type === 'Tombstone') { @@ -208,27 +208,41 @@ inbox.delete = async (req) => { // Deletions must be made by an actor of the same origin const actorHostname = new URL(actor).hostname; - const objectHostname = new URL(pid).hostname; + const objectHostname = new URL(id).hostname; if (actorHostname !== objectHostname) { return reject('Delete', object, actor); } - const [isNote/* , isActor */] = await Promise.all([ - posts.exists(pid), + const [isNote, isContext/* , isActor */] = await Promise.all([ + posts.exists(id), + activitypub.contexts.getItems(0, id, { returnRootId: true }), // db.isSortedSetMember('usersRemote:lastCrawled', object.id), ]); switch (true) { case isNote: { - const cid = await posts.getCidByPid(pid); + const cid = await posts.getCidByPid(id); const allowed = await privileges.categories.can('posts:edit', cid, activitypub._constants.uid); if (!allowed) { return reject('Delete', object, actor); } - const uid = await posts.getPostField(pid, 'uid'); - await activitypub.feps.announce(pid, req.body); - await api.posts[method]({ uid }, { pid }); + const uid = await posts.getPostField(id, 'uid'); + await activitypub.feps.announce(id, req.body); + await api.posts[method]({ uid }, { pid: id }); + break; + } + + case !!isContext: { + const pid = isContext; + const exists = await posts.exists(pid); + if (!exists) { + activitypub.helpers.log(`[activitypub/inbox.delete] Context main pid (${pid}) not found locally. Doing nothing.`); + return; + } + const { tid, uid } = await posts.getPostFields(pid, ['tid', 'uid']); + activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`); + await api.topics[method]({ uid }, { tids: [tid] }); break; } @@ -238,7 +252,7 @@ inbox.delete = async (req) => { // } default: { - activitypub.helpers.log(`[activitypub/inbox.delete] Object (${pid}) does not exist locally. Doing nothing.`); + activitypub.helpers.log(`[activitypub/inbox.delete] Object (${id}) does not exist locally. Doing nothing.`); break; } } From 1d529473b4ca1123ddc73bd480044e32f63042d3 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Oct 2025 12:17:52 -0400 Subject: [PATCH 039/141] fix: rebroadcasting logic should only execute for local tids if the remote cid is not addressed already --- src/activitypub/feps.js | 3 ++- src/activitypub/helpers.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/activitypub/feps.js b/src/activitypub/feps.js index 9fb0e28680..01fc3d46d0 100644 --- a/src/activitypub/feps.js +++ b/src/activitypub/feps.js @@ -23,7 +23,8 @@ Feps.announce = async function announce(id, activity) { const tid = await posts.getPostField(localId || id, 'tid'); const cid = await topics.getTopicField(tid, 'cid'); const localCid = utils.isNumber(cid) && cid > 0; - const shouldAnnounce = localCid || utils.isNumber(tid); + const addressed = activitypub.helpers.addressed(cid, activity); + const shouldAnnounce = localCid || (utils.isNumber(tid) && !addressed); if (!shouldAnnounce) { // inverse conditionals can kiss my ass. return; } diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 90701dd58d..6628b3ec55 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -526,3 +526,20 @@ Helpers.generateDigest = (set) => { return result.toString('hex'); }); }; + +Helpers.addressed = (id, activity) => { + // Returns Boolean for if id is found in addressing fields (to, cc, etc.) + if (!id || !activity || typeof activity !== 'object') { + return false; + } + + const combined = new Set([ + ...(activity.to || []), + ...(activity.cc || []), + ...(activity.bto || []), + ...(activity.bcc || []), + ...(activity.audience || []), + ]); + + return combined.has(id); +}; From e09bb8b611acca5348a18bd19d3ed6c2a70129b9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Oct 2025 15:57:01 -0400 Subject: [PATCH 040/141] refactor: user announces no longer occur on topic move. Instead, the new category announces. Only occurs when topic moved to local categories. --- src/api/activitypub.js | 35 ++++++++++++++++++++++++++++++++++- src/api/topics.js | 3 +-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/api/activitypub.js b/src/api/activitypub.js index 19e9087539..3d1d3ff546 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -347,7 +347,40 @@ activitypubApi.like.note = enabledCheck(async (caller, { pid }) => { activitypubApi.announce = {}; -activitypubApi.announce.note = enabledCheck(async (caller, { tid }) => { +activitypubApi.announce.category = enabledCheck(async (_, { tid }) => { + const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); + + // Only local categories can announce + if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { + return; + } + + const uid = await posts.getPostField(pid, 'uid'); // author + const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`); + return; + } + + const { to, cc, targets } = await activitypub.buildRecipients({ + id: pid, + to: [activitypub._constants.publicAddress], + cc: [`${nconf.get('url')}/category/${cid}/followers`, uid], + }, { cid, uid: utils.isNumber(uid) ? uid : undefined }); + + await activitypub.send('cid', cid, Array.from(targets), { + id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/${Date.now()}`, + type: 'Announce', + actor: `${nconf.get('url')}/category/${cid}`, + to, + cc, + object: pid, + target: `${nconf.get('url')}/category/${cid}`, + }); +}); + +activitypubApi.announce.user = enabledCheck(async (caller, { tid }) => { + // ORPHANED, but will re-use when user announces are a thing. const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); // Only remote posts can be announced to local categories diff --git a/src/api/topics.js b/src/api/topics.js index e0f149e392..38468f5956 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -326,8 +326,7 @@ topicsAPI.move = async (caller, { tid, cid }) => { await activitypubApi.announce.delete({ uid: caller.uid }, { tid }); // tbd: activitypubApi.undo.announce? } else { - // tbd: some kind of plain object announce by the category... - activitypubApi.announce.note(caller, { tid }); // user announce, remove when discrete announces are a thing + activitypubApi.announce.category(caller, { tid }); // tbd: api.activitypub.announce.move } } From f98a7216a3feed00c80a91ee6276126bfc51158b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Oct 2025 16:23:27 -0400 Subject: [PATCH 041/141] feat: handle Delete(Context) as a move to cid -1 if the remote context still exists --- src/activitypub/inbox.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 77fd2bf4b5..9fe4a52a4d 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -199,7 +199,9 @@ inbox.delete = async (req) => { } if (type === 'Tombstone') { - method = 'delete'; + method = 'delete'; // soft delete + } else if (activitypub._constants.acceptable.contextTypes.includes(type)) { + method = 'move'; // move to cid -1 } } catch (e) { // probably 410/404 @@ -215,10 +217,15 @@ inbox.delete = async (req) => { const [isNote, isContext/* , isActor */] = await Promise.all([ posts.exists(id), - activitypub.contexts.getItems(0, id, { returnRootId: true }), + activitypub.contexts.getItems(0, id, { returnRootId: true }), // ⚠️ unreliable, needs better logic (Contexts.is?) // db.isSortedSetMember('usersRemote:lastCrawled', object.id), ]); + // 'move' method only applicable for contexts + if (method === 'move' && !isContext) { + return reject('Delete', object, actor); + } + switch (true) { case isNote: { const cid = await posts.getCidByPid(id); @@ -241,8 +248,13 @@ inbox.delete = async (req) => { return; } const { tid, uid } = await posts.getPostFields(pid, ['tid', 'uid']); - activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`); - await api.topics[method]({ uid }, { tids: [tid] }); + if (method === 'move') { + activitypub.helpers.log(`[activitypub/inbox.delete] Moving tid ${tid} to cid -1.`); + await api.topics.move({ uid }, { tid, cid: -1 }); + } else { + activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`); + await api.topics[method]({ uid }, { tids: [tid] }); + } break; } From 603068aebb792dfbf4696c5e7a158bedf9484289 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 17 Oct 2025 11:11:04 -0400 Subject: [PATCH 042/141] fix: do not include image or icon props if they are falsy values --- src/activitypub/mocks.js | 54 +++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 1e04fb99ab..81db4b7a01 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -486,35 +486,37 @@ Mocks.actors.user = async (uid) => { }); return { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - ], - id: `${nconf.get('url')}/uid/${uid}`, - url: `${nconf.get('url')}/user/${userslug}`, - followers: `${nconf.get('url')}/uid/${uid}/followers`, - following: `${nconf.get('url')}/uid/${uid}/following`, - inbox: `${nconf.get('url')}/uid/${uid}/inbox`, - outbox: `${nconf.get('url')}/uid/${uid}/outbox`, + ...{ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ], + id: `${nconf.get('url')}/uid/${uid}`, + url: `${nconf.get('url')}/user/${userslug}`, + followers: `${nconf.get('url')}/uid/${uid}/followers`, + following: `${nconf.get('url')}/uid/${uid}/following`, + inbox: `${nconf.get('url')}/uid/${uid}/inbox`, + outbox: `${nconf.get('url')}/uid/${uid}/outbox`, - type: 'Person', - name: username !== displayname ? fullname : username, // displayname is escaped, fullname is not - preferredUsername: userslug, - summary: aboutmeParsed, - icon: picture, - image: cover, - published: new Date(joindate).toISOString(), - attachment, + type: 'Person', + name: username !== displayname ? fullname : username, // displayname is escaped, fullname is not + preferredUsername: userslug, + summary: aboutmeParsed, + published: new Date(joindate).toISOString(), + attachment, - publicKey: { - id: `${nconf.get('url')}/uid/${uid}#key`, - owner: `${nconf.get('url')}/uid/${uid}`, - publicKeyPem: publicKey, - }, - - endpoints: { - sharedInbox: `${nconf.get('url')}/inbox`, + publicKey: { + id: `${nconf.get('url')}/uid/${uid}#key`, + owner: `${nconf.get('url')}/uid/${uid}`, + publicKeyPem: publicKey, + }, + + endpoints: { + sharedInbox: `${nconf.get('url')}/inbox`, + }, }, + ...(picture && { icon: picture }), + ...(cover && { image: cover }), }; }; From 2425f3b671ef1f286740f21cac788c206cef8546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 17 Oct 2025 16:23:50 -0400 Subject: [PATCH 043/141] https://github.com/NodeBB/NodeBB/issues/13713 --- public/src/client/topic.js | 6 ++++++ public/src/modules/helpers.common.js | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 5ee1f91f60..bdebc50264 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -206,6 +206,12 @@ define('forum/topic', [ }); } }); + + $('[component="topic/thumb/list/expand"]').on('click', function () { + const btn = $(this); + btn.parents('[component="topic/thumb/list"]').removeClass('thumbs-collapsed'); + btn.remove(); + }); } function addBlockQuoteHandler() { diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 896d0b485b..6df32ea76e 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -25,6 +25,8 @@ module.exports = function (utils, Benchpress, relative_path) { userAgentIcons, buildAvatar, increment, + lessthan, + greaterthan, generateWroteReplied, generateRepliedTo, generateWrote, @@ -328,6 +330,14 @@ module.exports = function (utils, Benchpress, relative_path) { return String(value + parseInt(inc, 10)); } + function lessthan(a, b) { + return parseInt(a, 10) < parseInt(b, 10); + } + + function greaterthan(a, b) { + return parseInt(a, 10) > parseInt(b, 10); + } + function generateWroteReplied(post, timeagoCutoff) { if (post.toPid) { return generateRepliedTo(post, timeagoCutoff); From 52c56bc5453bd68d64082cf4ce5bfcd728a9cb1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 17 Oct 2025 22:02:57 -0400 Subject: [PATCH 044/141] chore: up themes --- install/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/package.json b/install/package.json index 90d5f97518..7706ad1192 100644 --- a/install/package.json +++ b/install/package.json @@ -101,16 +101,16 @@ "nodebb-plugin-dbsearch": "6.3.3", "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", - "nodebb-plugin-link-preview": "2.1.5", + "nodebb-plugin-link-preview": "2.1.5", "nodebb-plugin-markdown": "13.2.1", "nodebb-plugin-mentions": "4.7.6", "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.5", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.21", + "nodebb-theme-harmony": "2.1.22", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.48", - "nodebb-theme-persona": "14.1.15", + "nodebb-theme-persona": "14.1.16", "nodebb-widget-essentials": "7.0.40", "nodemailer": "7.0.9", "nprogress": "0.2.0", From 27a0dc731bddf58db8406a0bf9f619d5a69e056e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:10:24 -0400 Subject: [PATCH 045/141] fix(deps): update dependency ace-builds to v1.43.4 (#13714) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 97897399eb..6563a7f2d9 100644 --- a/install/package.json +++ b/install/package.json @@ -39,7 +39,7 @@ "@textcomplete/contenteditable": "0.1.13", "@textcomplete/core": "0.1.13", "@textcomplete/textarea": "0.1.13", - "ace-builds": "1.43.3", + "ace-builds": "1.43.4", "archiver": "7.0.1", "async": "3.2.6", "autoprefixer": "10.4.21", From 7fd9e89495cebba1fca0aa63b1ba1585572129c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:25:08 -0400 Subject: [PATCH 046/141] chore(deps): update dependency @eslint/js to v9.38.0 (#13716) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 6563a7f2d9..4489a095a3 100644 --- a/install/package.json +++ b/install/package.json @@ -164,7 +164,7 @@ "@commitlint/cli": "20.1.0", "@commitlint/config-angular": "20.0.0", "coveralls": "3.1.1", - "@eslint/js": "9.37.0", + "@eslint/js": "9.38.0", "@stylistic/eslint-plugin": "5.4.0", "eslint-config-nodebb": "1.1.11", "eslint-plugin-import": "2.32.0", From 1d9d7fc56b222ddd26e2661f5d0407fc8d99c9d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:32:52 -0400 Subject: [PATCH 047/141] fix(deps): update dependency sitemap to v8.0.1 (#13720) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 4489a095a3..589f1d75ad 100644 --- a/install/package.json +++ b/install/package.json @@ -135,7 +135,7 @@ "semver": "7.7.3", "serve-favicon": "2.5.1", "sharp": "0.34.4", - "sitemap": "8.0.0", + "sitemap": "8.0.1", "socket.io": "4.8.1", "socket.io-client": "4.8.1", "@socket.io/redis-adapter": "8.3.0", From 9d2b83f5636f88befa9f5a6a7e45bf0db16aca8d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:33:11 -0400 Subject: [PATCH 048/141] chore(deps): update dependency jsdom to v27.0.1 (#13718) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 589f1d75ad..ab61a659e3 100644 --- a/install/package.json +++ b/install/package.json @@ -171,7 +171,7 @@ "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", - "jsdom": "27.0.0", + "jsdom": "27.0.1", "lint-staged": "16.2.4", "mocha": "11.7.4", "mocha-lcov-reporter": "1.3.0", From 93d46c842ea4f94218783bb35e19da283b8c86ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:44:31 -0400 Subject: [PATCH 049/141] chore(deps): update dependency @stylistic/eslint-plugin to v5.5.0 (#13717) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index ab61a659e3..adc2a69305 100644 --- a/install/package.json +++ b/install/package.json @@ -165,7 +165,7 @@ "@commitlint/config-angular": "20.0.0", "coveralls": "3.1.1", "@eslint/js": "9.38.0", - "@stylistic/eslint-plugin": "5.4.0", + "@stylistic/eslint-plugin": "5.5.0", "eslint-config-nodebb": "1.1.11", "eslint-plugin-import": "2.32.0", "grunt": "1.6.1", From 97e59fbe0430f40ec28ee8df0668b2d1218b7fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 21 Oct 2025 10:11:18 -0400 Subject: [PATCH 050/141] feat: add new setting to control posts uploads being shown as thumbs --- install/data/defaults.json | 1 + public/language/en-GB/admin/settings/uploads.json | 1 + src/topics/thumbs.js | 5 +++-- src/views/admin/settings/uploads.tpl | 5 +++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/install/data/defaults.json b/install/data/defaults.json index 574a8bfe01..67557efabe 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -38,6 +38,7 @@ "maximumTagLength": 15, "undoTimeout": 0, "allowTopicsThumbnail": 1, + "showPostUploadsAsThumbnails": 1, "registrationType": "normal", "registrationApprovalType": "normal", "allowAccountDelete": 1, diff --git a/public/language/en-GB/admin/settings/uploads.json b/public/language/en-GB/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/en-GB/admin/settings/uploads.json +++ b/public/language/en-GB/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index 22fa2c48bd..3239ab0fdc 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -33,8 +33,9 @@ Thumbs.load = async function (topicData) { const topicsWithThumbs = topicData.filter((tid, idx) => hasThumbs[idx]); const tidsWithThumbs = topicsWithThumbs.map(t => t.tid); - - const thumbs = await loadFromTopicData(topicsWithThumbs); + const thumbs = await loadFromTopicData(topicsWithThumbs, { + thumbsOnly: meta.config.showPostUploadsAsThumbnails !== 1, + }); const tidToThumbs = _.zipObject(tidsWithThumbs, thumbs); return topicData.map(t => (t && t.tid ? (tidToThumbs[t.tid] || []) : [])); diff --git a/src/views/admin/settings/uploads.tpl b/src/views/admin/settings/uploads.tpl index aa138c8c67..400058eb20 100644 --- a/src/views/admin/settings/uploads.tpl +++ b/src/views/admin/settings/uploads.tpl @@ -88,6 +88,11 @@ +
+ + +
+
From e7498e8fb5d2c852752a0d1128279655985420e3 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 21 Oct 2025 14:11:49 +0000 Subject: [PATCH 051/141] chore(i18n): fallback strings for new resources: nodebb.admin-settings-uploads --- public/language/ar/admin/settings/uploads.json | 1 + public/language/az/admin/settings/uploads.json | 1 + public/language/bg/admin/settings/uploads.json | 1 + public/language/bn/admin/settings/uploads.json | 1 + public/language/cs/admin/settings/uploads.json | 1 + public/language/da/admin/settings/uploads.json | 1 + public/language/de/admin/settings/uploads.json | 1 + public/language/el/admin/settings/uploads.json | 1 + public/language/en-US/admin/settings/uploads.json | 1 + public/language/en-x-pirate/admin/settings/uploads.json | 1 + public/language/es/admin/settings/uploads.json | 1 + public/language/et/admin/settings/uploads.json | 1 + public/language/fa-IR/admin/settings/uploads.json | 1 + public/language/fi/admin/settings/uploads.json | 1 + public/language/fr/admin/settings/uploads.json | 1 + public/language/gl/admin/settings/uploads.json | 1 + public/language/he/admin/settings/uploads.json | 1 + public/language/hr/admin/settings/uploads.json | 1 + public/language/hu/admin/settings/uploads.json | 1 + public/language/hy/admin/settings/uploads.json | 1 + public/language/id/admin/settings/uploads.json | 1 + public/language/it/admin/settings/uploads.json | 1 + public/language/ja/admin/settings/uploads.json | 1 + public/language/ko/admin/settings/uploads.json | 1 + public/language/lt/admin/settings/uploads.json | 1 + public/language/lv/admin/settings/uploads.json | 1 + public/language/ms/admin/settings/uploads.json | 1 + public/language/nb/admin/settings/uploads.json | 1 + public/language/nl/admin/settings/uploads.json | 1 + public/language/nn-NO/admin/settings/uploads.json | 1 + public/language/pl/admin/settings/uploads.json | 1 + public/language/pt-BR/admin/settings/uploads.json | 1 + public/language/pt-PT/admin/settings/uploads.json | 1 + public/language/ro/admin/settings/uploads.json | 1 + public/language/ru/admin/settings/uploads.json | 1 + public/language/rw/admin/settings/uploads.json | 1 + public/language/sc/admin/settings/uploads.json | 1 + public/language/sk/admin/settings/uploads.json | 1 + public/language/sl/admin/settings/uploads.json | 1 + public/language/sq-AL/admin/settings/uploads.json | 1 + public/language/sr/admin/settings/uploads.json | 1 + public/language/sv/admin/settings/uploads.json | 1 + public/language/th/admin/settings/uploads.json | 1 + public/language/tr/admin/settings/uploads.json | 1 + public/language/uk/admin/settings/uploads.json | 1 + public/language/ur/admin/settings/uploads.json | 1 + public/language/vi/admin/settings/uploads.json | 1 + public/language/zh-CN/admin/settings/uploads.json | 1 + public/language/zh-TW/admin/settings/uploads.json | 1 + 49 files changed, 49 insertions(+) diff --git a/public/language/ar/admin/settings/uploads.json b/public/language/ar/admin/settings/uploads.json index b8d85be443..af3efb16f7 100644 --- a/public/language/ar/admin/settings/uploads.json +++ b/public/language/ar/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "السماح للاعضاء برفع الصور المصغرة للموضوع", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "حجم الصورة المصغرة للموضوع", "allowed-file-extensions": "إمتدادات الملفات المسموح بها", "allowed-file-extensions-help": "أدخل قائمة بامتدادات الملفات مفصولة بفواصل (مثال: pdf,xls,doc). القائمة الفارغة تعني أن كل الامتدادات مسموح بها.", diff --git a/public/language/az/admin/settings/uploads.json b/public/language/az/admin/settings/uploads.json index ceed4ae396..a403a54e34 100644 --- a/public/language/az/admin/settings/uploads.json +++ b/public/language/az/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maksimum şəklin hündürlüyü (piksellə)", "reject-image-height-help": "Bu dəyərdən yüksək olan şəkillər rədd ediləcək.", "allow-topic-thumbnails": "İstifadəçilərə mövzu miniatürlərini yükləməyə icazə ver", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Mövzu thumb ölçüsü", "allowed-file-extensions": "İcazə verilən fayl uzantıları", "allowed-file-extensions-help": "Fayl uzantılarının vergüllə ayrılmış siyahısını buraya daxil edin (məsələn, pdf, xls, doc). Boş siyahı bütün genişləndirmələrə icazə verildiyini bildirir.", diff --git a/public/language/bg/admin/settings/uploads.json b/public/language/bg/admin/settings/uploads.json index 4820730824..0cbfc9f7b4 100644 --- a/public/language/bg/admin/settings/uploads.json +++ b/public/language/bg/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Максимална височина на изображенията (в пиксели)", "reject-image-height-help": "Изображенията, чиято височина е по-голяма от тази стойност, ще бъдат отхвърляни.", "allow-topic-thumbnails": "Позволяване на потребителите да качват миниатюрни изображения за темите", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Размер на миниатюрите за темите", "allowed-file-extensions": "Разрешени файлови разширения", "allowed-file-extensions-help": "Въведете файловите разширения, разделени със запетаи (пример: pdf,xls,doc). Ако списъкът е празен, всички файлови разширения ще бъдат разрешени.", diff --git a/public/language/bn/admin/settings/uploads.json b/public/language/bn/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/bn/admin/settings/uploads.json +++ b/public/language/bn/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/cs/admin/settings/uploads.json b/public/language/cs/admin/settings/uploads.json index dea6aa44df..90d14e88d8 100644 --- a/public/language/cs/admin/settings/uploads.json +++ b/public/language/cs/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximální výška obrázku (v pixelech)", "reject-image-height-help": "Vyšší obrázek než tato hodnota bude zamítnut.", "allow-topic-thumbnails": "Povolit uživatelům nahrát miniatury témat", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Velikost miniatury tématu", "allowed-file-extensions": "Povolené přípony souborů", "allowed-file-extensions-help": "Zadejte seznam přípon souborů oddělených čárkou (např.: pdf, xls, doc). Prázdný seznam znamená, že všechny přípony jsou povoleny.", diff --git a/public/language/da/admin/settings/uploads.json b/public/language/da/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/da/admin/settings/uploads.json +++ b/public/language/da/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/de/admin/settings/uploads.json b/public/language/de/admin/settings/uploads.json index 269bfb8178..75a5223588 100644 --- a/public/language/de/admin/settings/uploads.json +++ b/public/language/de/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximale Bildhöhe (in Pixeln)", "reject-image-height-help": "Höhere Bilder werden abgelehnt.", "allow-topic-thumbnails": "Nutzern erlauben Themen Thumbnails hochzuladen", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Thema Thumbnailgröße", "allowed-file-extensions": "Erlaubte Dateiendungen", "allowed-file-extensions-help": "Komma-getrennte Liste der Dateiendungen hier einfügen (z.B. pdf,xls,doc). Eine leere Liste bedeutet, dass alle Dateiendungen erlaubt sind.", diff --git a/public/language/el/admin/settings/uploads.json b/public/language/el/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/el/admin/settings/uploads.json +++ b/public/language/el/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/en-US/admin/settings/uploads.json b/public/language/en-US/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/en-US/admin/settings/uploads.json +++ b/public/language/en-US/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/en-x-pirate/admin/settings/uploads.json b/public/language/en-x-pirate/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/en-x-pirate/admin/settings/uploads.json +++ b/public/language/en-x-pirate/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/es/admin/settings/uploads.json b/public/language/es/admin/settings/uploads.json index 60273aa8ac..d92fe07210 100644 --- a/public/language/es/admin/settings/uploads.json +++ b/public/language/es/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Altura máxima de la imágen (en píxeles)", "reject-image-height-help": "Las imágenes más altas que este valor serán rechazadas.", "allow-topic-thumbnails": "Permitir a los usuarios subir imágenes en miniatura para los temas", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Tamaño de la Imagen en Miniatura para el Tema", "allowed-file-extensions": "Permitir Extensiones de Archivo", "allowed-file-extensions-help": "Introduzca una lista de extensiones de archivos, separadas por comas, aquí (por ejemplo: pdf,xls,doc). Una lista vacía significa que se permiten todas las extensiones.", diff --git a/public/language/et/admin/settings/uploads.json b/public/language/et/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/et/admin/settings/uploads.json +++ b/public/language/et/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/fa-IR/admin/settings/uploads.json b/public/language/fa-IR/admin/settings/uploads.json index baf90ba0e4..507f64c898 100644 --- a/public/language/fa-IR/admin/settings/uploads.json +++ b/public/language/fa-IR/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/fi/admin/settings/uploads.json b/public/language/fi/admin/settings/uploads.json index a1412b0ed2..792ccd9d3d 100644 --- a/public/language/fi/admin/settings/uploads.json +++ b/public/language/fi/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Kuvan suurin sallittu korkeus (kuvapisteinä)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/fr/admin/settings/uploads.json b/public/language/fr/admin/settings/uploads.json index 453b6be283..6662c0d074 100644 --- a/public/language/fr/admin/settings/uploads.json +++ b/public/language/fr/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Hauteur maximale des images (en pixels)", "reject-image-height-help": "Les images plus grandes que cette valeur seront rejetées.", "allow-topic-thumbnails": "Autoriser les utilisateurs à téléverser des miniatures de sujet", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Miniature du sujet", "allowed-file-extensions": "Extensions de fichiers autorisées", "allowed-file-extensions-help": "Entrer une liste d’extensions de fichier séparées par une virgule (ex : pdf,xls,doc). Une liste vide signifie que toutes les extensions sont autorisées.", diff --git a/public/language/gl/admin/settings/uploads.json b/public/language/gl/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/gl/admin/settings/uploads.json +++ b/public/language/gl/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/he/admin/settings/uploads.json b/public/language/he/admin/settings/uploads.json index 55286bd76d..3b4c72a82b 100644 --- a/public/language/he/admin/settings/uploads.json +++ b/public/language/he/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "גובה תמונה מקסימלי (בפיקסלים)", "reject-image-height-help": "תמונות גבוהות יותר מערך זה יידחו", "allow-topic-thumbnails": "אפשרו למשתמשים להעלות תמונה ממוזערת לנושא", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "גודל תמונה ממוזערת לנושא", "allowed-file-extensions": "סיומות קבצים מאושרים", "allowed-file-extensions-help": "הכניסו כאן רשימת פורמטי קבצים מאושרים (לדוגמא. pdf,xls,doc). השארת השורה ללא תוכן פירושו שכל הקבצים יהיו מאושרים.", diff --git a/public/language/hr/admin/settings/uploads.json b/public/language/hr/admin/settings/uploads.json index 1efba861e4..e206adf803 100644 --- a/public/language/hr/admin/settings/uploads.json +++ b/public/language/hr/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Dozvoli korisnicima da učitaju sliku teme", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Veličina slike teme", "allowed-file-extensions": "Dozvoljene ekstenzije datoteka", "allowed-file-extensions-help": "Unesite popis dozvoljenih ekstenzija datoteka sa zarezima između (npr. pdf,xls,doc ).Prazan popis znači da su sve ekstenzije dozvoljene.", diff --git a/public/language/hu/admin/settings/uploads.json b/public/language/hu/admin/settings/uploads.json index fb103aea58..8a575d432f 100644 --- a/public/language/hu/admin/settings/uploads.json +++ b/public/language/hu/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Képek maximális magassága (pixelben)", "reject-image-height-help": "Azon képek, amik magasabbak ennél az értéknél visszautasításra kerülnek.", "allow-topic-thumbnails": "Kis képek feltöltésének engedélyezése témakörhöz a felhasználók számára", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Témakörkép mérete", "allowed-file-extensions": "Megengedett fájlkiterjesztések", "allowed-file-extensions-help": "Itt adj meg fájlkiterjesztési listát, vesszővel elválasztva (pl. pdf,xls,doc). Az üres lista azt jelenti, hogy minden kiterjesztés megengedett.", diff --git a/public/language/hy/admin/settings/uploads.json b/public/language/hy/admin/settings/uploads.json index 6fa6f789fa..388ddc3c09 100644 --- a/public/language/hy/admin/settings/uploads.json +++ b/public/language/hy/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Նկարի առավելագույն բարձրությունը (պիքսելներով)", "reject-image-height-help": "Այս արժեքից բարձր նկարները կմերժվեն:", "allow-topic-thumbnails": "Թույլ տվեք օգտատերերին վերբեռնել թեմայի մանրապատկերները", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Թեմայի Thumb չափ", "allowed-file-extensions": "Թույլատրված ֆայլերի ընդարձակումներ", "allowed-file-extensions-help": "Մուտքագրեք ստորակետերով բաժանված ֆայլերի ընդարձակման ցանկն այստեղ (օրինակ՝ pdf, xls, doc): Դատարկ ցուցակը նշանակում է, որ բոլոր ընդլայնումները թույլատրված են:", diff --git a/public/language/id/admin/settings/uploads.json b/public/language/id/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/id/admin/settings/uploads.json +++ b/public/language/id/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/it/admin/settings/uploads.json b/public/language/it/admin/settings/uploads.json index a2ba8602e9..f063b36ac9 100644 --- a/public/language/it/admin/settings/uploads.json +++ b/public/language/it/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Lunghezza Massima Immagine (in pixel)", "reject-image-height-help": "Le immagini più alte di questo valore saranno rifiutate.", "allow-topic-thumbnails": "Consenti agli utenti di caricare le miniature degli argomenti", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Dimensione miniatura Argomento", "allowed-file-extensions": "Abilita Estensioni File", "allowed-file-extensions-help": "Inserisci una lista di estensioni separati da virgola quì (es. pdf,xls,doc). Una lista vuota indica che tutte le estensioni sono abilitate.", diff --git a/public/language/ja/admin/settings/uploads.json b/public/language/ja/admin/settings/uploads.json index 84b216ba93..1e7ff65241 100644 --- a/public/language/ja/admin/settings/uploads.json +++ b/public/language/ja/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "ユーザーがスレッドのサムネイルをアップロードできるようにする", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "スレッドのサムネイルの大きさ", "allowed-file-extensions": "ファイル拡張子が有効になりました。", "allowed-file-extensions-help": "ここにファイル拡張子のカンマ区切りリストを入力します(例: pdf,xls,doc )。空のリストは、すべての拡張が許可されていることを意味します。", diff --git a/public/language/ko/admin/settings/uploads.json b/public/language/ko/admin/settings/uploads.json index f222e5fe80..c6ae4c5a87 100644 --- a/public/language/ko/admin/settings/uploads.json +++ b/public/language/ko/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "최대 이미지 높이(픽셀 단위)", "reject-image-height-help": "이 값보다 큰 이미지는 등록할 수 없습니다.", "allow-topic-thumbnails": "사용자가 토픽 썸네일 업로드 허용", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "토픽 썸네일 크기", "allowed-file-extensions": "허용된 파일 확장자", "allowed-file-extensions-help": "허용된 파일 확장자를 쉼표로 구분하여 입력하세요 (예: pdf,xls,doc). 비어 있는 목록은 모든 확장자가 허용됨을 의미합니다.", diff --git a/public/language/lt/admin/settings/uploads.json b/public/language/lt/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/lt/admin/settings/uploads.json +++ b/public/language/lt/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/lv/admin/settings/uploads.json b/public/language/lv/admin/settings/uploads.json index 0b79f6bb9a..94082ee915 100644 --- a/public/language/lv/admin/settings/uploads.json +++ b/public/language/lv/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maksimālais bildes augstums (pikseļos)", "reject-image-height-help": "Bildes, kas ir augstākas par šo vērtību, tiks noraidītas.", "allow-topic-thumbnails": "Atļaut lietotājiem augšupielādēt tematu sīktēlus", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Tematu sīktēlu lielums", "allowed-file-extensions": "Atļautie failu paplašinājumi", "allowed-file-extensions-help": "Ievadīt ar komatu atdalītu failu paplašinājumu sarakstu (piemērām pdf,xls,doc). Tukšais saraksts nozīmē, ka visi failu paplašinājumi ir atļauti.", diff --git a/public/language/ms/admin/settings/uploads.json b/public/language/ms/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/ms/admin/settings/uploads.json +++ b/public/language/ms/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/nb/admin/settings/uploads.json b/public/language/nb/admin/settings/uploads.json index ff5386fedb..8209e108e4 100644 --- a/public/language/nb/admin/settings/uploads.json +++ b/public/language/nb/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maksimal bildehøyde (i piksler)", "reject-image-height-help": "Bilder høyere enn denne verdien vil bli avvist.", "allow-topic-thumbnails": "Tillat brukere å laste opp emneminiatyrbilder", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Størrelse på emneminiatyrbilder", "allowed-file-extensions": "Tillatte filtyper", "allowed-file-extensions-help": "Skriv inn kommaseparerte filtyper her (f.eks. pdf,xls,doc). En tom liste betyr at alle filtyper er tillatt.", diff --git a/public/language/nl/admin/settings/uploads.json b/public/language/nl/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/nl/admin/settings/uploads.json +++ b/public/language/nl/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/nn-NO/admin/settings/uploads.json b/public/language/nn-NO/admin/settings/uploads.json index c729541aff..c7730e9264 100644 --- a/public/language/nn-NO/admin/settings/uploads.json +++ b/public/language/nn-NO/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Avvis bilete høgde", "reject-image-height-help": "Angi maksimal høgde for bilete som vert avvist ved opplasting.", "allow-topic-thumbnails": "Tillat emne-miniatyrbilete", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Storleik på emne-miniatyr", "allowed-file-extensions": "Tillatne filtypar", "allowed-file-extensions-help": "Angi kva filtypar som er tillatne ved opplasting.", diff --git a/public/language/pl/admin/settings/uploads.json b/public/language/pl/admin/settings/uploads.json index 10d0606239..561a29afc0 100644 --- a/public/language/pl/admin/settings/uploads.json +++ b/public/language/pl/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maksymalna wysokość obrazu (w pikselach)", "reject-image-height-help": "Obrazy o wysokości przekraczającej tę wartość zostaną odrzucone.", "allow-topic-thumbnails": "Zezwalaj użytkownikom na ustawianie miniaturek tematów", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Rozmiar miniatury tematu", "allowed-file-extensions": "Dozwolone typy plików", "allowed-file-extensions-help": "Wprowadź rozdzielone przecinkami rozszerzenia plików (np. pdf,xls,doc). Pusta lista oznacza, że wszystkie rozszerzenia są dozwolone.", diff --git a/public/language/pt-BR/admin/settings/uploads.json b/public/language/pt-BR/admin/settings/uploads.json index c203d2cd23..5f0b8362c9 100644 --- a/public/language/pt-BR/admin/settings/uploads.json +++ b/public/language/pt-BR/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Altura Máxima das Imagens (em pixels)", "reject-image-height-help": "Imagens com uma altura maior do que este valor serão rejeitadas.", "allow-topic-thumbnails": "Permitir usuários de enviar miniaturas de tópico", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Tamanho da Miniatura de Tópico", "allowed-file-extensions": "Extensões de Arquivo Permitidas", "allowed-file-extensions-help": "Digite uma lista, separada por vírgulas, de extensões de arquivos aqui (por exemplo: pdf,xls,doc). Uma lista vazia significa que todas as extensões são permitidas.", diff --git a/public/language/pt-PT/admin/settings/uploads.json b/public/language/pt-PT/admin/settings/uploads.json index 91c89d7a46..593df275f8 100644 --- a/public/language/pt-PT/admin/settings/uploads.json +++ b/public/language/pt-PT/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Altura Máxima da Imagem (em píxeis)", "reject-image-height-help": "Imagens mais altas que este valor vão ser rejeitadas.", "allow-topic-thumbnails": "Permitir aos utilizadores enviar miniaturas de tópicos", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Tamanho da Miniatura do Tópico", "allowed-file-extensions": "Extensões de Ficheiro Permitidas", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/ro/admin/settings/uploads.json b/public/language/ro/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/ro/admin/settings/uploads.json +++ b/public/language/ro/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/ru/admin/settings/uploads.json b/public/language/ru/admin/settings/uploads.json index 8725c70db8..c26cbe27da 100644 --- a/public/language/ru/admin/settings/uploads.json +++ b/public/language/ru/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Макс. высота изображения (в пикселях)", "reject-image-height-help": "Загрузка изображений выше указанного значения будет отклонена.", "allow-topic-thumbnails": "Разрешить пользователям загружать миниатюры для тем", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Размер миниатюр", "allowed-file-extensions": "Допустимые расширения файлов", "allowed-file-extensions-help": "Укажите через запятую список расширений файлов, например pdf,xls,doc. Оставьте поле пустым, чтобы разрешить любые загрузки.", diff --git a/public/language/rw/admin/settings/uploads.json b/public/language/rw/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/rw/admin/settings/uploads.json +++ b/public/language/rw/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/sc/admin/settings/uploads.json b/public/language/sc/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/sc/admin/settings/uploads.json +++ b/public/language/sc/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/sk/admin/settings/uploads.json b/public/language/sk/admin/settings/uploads.json index f9a18dcb4c..fdabe75093 100644 --- a/public/language/sk/admin/settings/uploads.json +++ b/public/language/sk/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Povoliť používateľom nahrať miniatúry tém", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Veľkosť miniatúry témy", "allowed-file-extensions": "Predvolené prípony súborov", "allowed-file-extensions-help": "Zadajte zoznam prípon súborov oddelených čiarkou (napr.: pdf, xls, doc). Prázdny zoznam znamená, že všetky prípony sú povolené.", diff --git a/public/language/sl/admin/settings/uploads.json b/public/language/sl/admin/settings/uploads.json index fc43ca3793..8d185fe531 100644 --- a/public/language/sl/admin/settings/uploads.json +++ b/public/language/sl/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Dovoljene pripone datoteke", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/sq-AL/admin/settings/uploads.json b/public/language/sq-AL/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/sq-AL/admin/settings/uploads.json +++ b/public/language/sq-AL/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/sr/admin/settings/uploads.json b/public/language/sr/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/sr/admin/settings/uploads.json +++ b/public/language/sr/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/sv/admin/settings/uploads.json b/public/language/sv/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/sv/admin/settings/uploads.json +++ b/public/language/sv/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/th/admin/settings/uploads.json b/public/language/th/admin/settings/uploads.json index 22046915d9..e91a7bee36 100644 --- a/public/language/th/admin/settings/uploads.json +++ b/public/language/th/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/tr/admin/settings/uploads.json b/public/language/tr/admin/settings/uploads.json index f9b369f66a..b9d2884b62 100644 --- a/public/language/tr/admin/settings/uploads.json +++ b/public/language/tr/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maksimum Görsel Yüksekliği (piksel)", "reject-image-height-help": "Bu değerden daha uzun olan görseller reddedilecektir.", "allow-topic-thumbnails": "Kullanıcıların konulara küçük resim yüklemesine izin ver", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Konu Küçük Resim Boyutu", "allowed-file-extensions": "İzin Verilen Dosya Uzantıları", "allowed-file-extensions-help": "Virgül ile ayrılmış dosya uzantıları listesini buraya girin (ör. pdf, xls, doc). Boş bir liste, tüm uzantılara izin verildiği anlamına gelir.", diff --git a/public/language/uk/admin/settings/uploads.json b/public/language/uk/admin/settings/uploads.json index f54640cb29..18dda7a1ca 100644 --- a/public/language/uk/admin/settings/uploads.json +++ b/public/language/uk/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Дозволити користувачам завантажувати мініатюри тем", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Розмір мініатюри теми", "allowed-file-extensions": "Допустимі розширення файлів", "allowed-file-extensions-help": "Вкажіть розширеня файлів розділені комою (наприклад, pdf,xls,doc). Пустий список дає дозвіл на будь-які розширення.", diff --git a/public/language/ur/admin/settings/uploads.json b/public/language/ur/admin/settings/uploads.json index f44da0e354..289d295862 100644 --- a/public/language/ur/admin/settings/uploads.json +++ b/public/language/ur/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "تصاویر کی زیادہ سے زیادہ اونچائی (پکسلز میں)", "reject-image-height-help": "جن تصاویر کی اونچائی اس قیمت سے زیادہ ہوگی وہ مسترد کر دی جائیں گی۔", "allow-topic-thumbnails": "صارفین کو موضوعات کے لیے تھمب نیلز اپ لوڈ کرنے کی اجازت دیں", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "موضوعات کے تھمب نیلز کا سائز", "allowed-file-extensions": "اجازت شدہ فائل ایکسٹینشنز", "allowed-file-extensions-help": "فائل ایکسٹینشنز کوموں سے الگ کرکے درج کریں (مثال: pdf,xls,doc)۔ اگر فہرست خالی ہو تو تمام فائل ایکسٹینشنز کی اجازت ہوگی۔", diff --git a/public/language/vi/admin/settings/uploads.json b/public/language/vi/admin/settings/uploads.json index f89c13a955..055f3dcf47 100644 --- a/public/language/vi/admin/settings/uploads.json +++ b/public/language/vi/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "Chiều Cao Ảnh Tối Đa (pixel)", "reject-image-height-help": "Hình ảnh cao hơn giá trị này sẽ bị từ chối.", "allow-topic-thumbnails": "Cho phép người dùng tải lên ảnh thumbnails chủ đề", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "Kích Cỡ Ảnh Thumbnails Chủ Đề", "allowed-file-extensions": "Cho Phép Phần Mở Rộng Tệp", "allowed-file-extensions-help": "Nhập danh sách phần mở rộng tệp phân tách bằng dấu phẩy ở đây (VD: pdf,xls,doc). Để trống là cho phép tất cả.", diff --git a/public/language/zh-CN/admin/settings/uploads.json b/public/language/zh-CN/admin/settings/uploads.json index 382195d94a..0c3cee593d 100644 --- a/public/language/zh-CN/admin/settings/uploads.json +++ b/public/language/zh-CN/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "图片最大高度值(单位:像素)", "reject-image-height-help": "高于此数值大小的图片将会被拒绝", "allow-topic-thumbnails": "允许用户上传主题缩略图", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "主题缩略图大小", "allowed-file-extensions": "允许的文件扩展名", "allowed-file-extensions-help": "在此处输入以逗号分隔的文件扩展名列表 (例如 pdf,xls,doc )。 为空则表示允许所有扩展名。", diff --git a/public/language/zh-TW/admin/settings/uploads.json b/public/language/zh-TW/admin/settings/uploads.json index fc3bddd9ca..a72caffbc2 100644 --- a/public/language/zh-TW/admin/settings/uploads.json +++ b/public/language/zh-TW/admin/settings/uploads.json @@ -22,6 +22,7 @@ "reject-image-height": "圖片最大高度值(單位:像素)", "reject-image-height-help": "高於此數值大小的圖片將會被拒絕", "allow-topic-thumbnails": "允許使用者上傳主題縮圖", + "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", "topic-thumb-size": "主題縮圖大小", "allowed-file-extensions": "允許的副檔名", "allowed-file-extensions-help": "在此處輸入以逗號分隔的副檔名列表 (例如 pdf,xls,doc )。 為空則表示允許所有副檔名。", From 83a172c9a49bb499e6bb84dd42bdf63829ad313d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:13:58 -0400 Subject: [PATCH 052/141] chore(deps): update dependency lint-staged to v16.2.5 (#13721) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index adc2a69305..45db72c361 100644 --- a/install/package.json +++ b/install/package.json @@ -172,7 +172,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "27.0.1", - "lint-staged": "16.2.4", + "lint-staged": "16.2.5", "mocha": "11.7.4", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From 34e95e6d46b670df2a3973c842d7b667bbe9d0ea Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 21 Oct 2025 12:00:01 -0400 Subject: [PATCH 053/141] feat: context removal logic (aka moving topics to uncategorized, and federating this to other NodeBBs) Squashed commit of the following: commit 3309117eb1c08f3a1bcfa2c56fa81a81427c0f0c Author: Julian Lam Date: Tue Oct 21 11:48:12 2025 -0400 fix: activitypubApi.remove.context to use oldCid instead of cid commit e90c5f79eb42fc17c5329c7083dcf5d462bb5d0a Author: Julian Lam Date: Tue Oct 21 11:41:05 2025 -0400 fix: parseInt cid in cid detection for api.topics.move commit ab6561e60f1d05e0010ae28868d62070d6857855 Author: Julian Lam Date: Mon Oct 20 14:03:45 2025 -0400 feat: inbox handler for Remove(Context) commit 30dc527cc0ec5bfbe4c0cfd459ef5a517a645871 Author: Julian Lam Date: Mon Oct 20 12:17:23 2025 -0400 feat: unwind announce(delete), federate out Remove(Context) on delete, but not on purge --- src/activitypub/inbox.js | 59 +++++++++++++++++--------- src/api/activitypub.js | 92 +++++++++++++++------------------------- src/api/topics.js | 8 ++-- src/topics/delete.js | 3 +- 4 files changed, 79 insertions(+), 83 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 9fe4a52a4d..9a9381f43f 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -13,6 +13,7 @@ const notifications = require('../notifications'); const messaging = require('../messaging'); const flags = require('../flags'); const api = require('../api'); +const utils = require('../utils'); const activitypub = require('.'); const socketHelpers = require('../socket.io/helpers'); @@ -78,6 +79,42 @@ inbox.add = async (req) => { } }; +inbox.remove = async (req) => { + const { actor, object } = req.body; + + const isContext = activitypub._constants.acceptable.contextTypes.has(object.type); + if (!isContext) { + return; // don't know how to handle other types + } + console.log('isContext?', isContext); + + const mainPid = await activitypub.contexts.getItems(0, object.id, { returnRootId: true }); + const exists = await posts.exists(mainPid); + if (!exists) { + return; // post not cached; do nothing. + } + console.log('mainPid is', mainPid); + + // Ensure that cid is same-origin as the actor + const tid = await posts.getPostField(mainPid, 'tid'); + const cid = await topics.getTopicField(tid, 'cid'); + if (utils.isNumber(cid)) { + // remote removal of topic in local cid; what?? + return; + } + const actorHostname = new URL(actor).hostname; + const cidHostname = new URL(cid).hostname; + if (actorHostname !== cidHostname) { + throw new Error('[[error:activitypub.origin-mismatch]]'); + } + + activitypub.helpers.log(`[activitypub/inbox/remove] Removing topic ${tid} from ${cid}`); + await topics.tools.move(tid, { + cid: -1, + uid: 'system', + }); +}; + inbox.update = async (req) => { const { actor, object } = req.body; const isPublic = publiclyAddressed([...(object.to || []), ...(object.cc || [])]); @@ -200,8 +237,6 @@ inbox.delete = async (req) => { if (type === 'Tombstone') { method = 'delete'; // soft delete - } else if (activitypub._constants.acceptable.contextTypes.includes(type)) { - method = 'move'; // move to cid -1 } } catch (e) { // probably 410/404 @@ -221,11 +256,6 @@ inbox.delete = async (req) => { // db.isSortedSetMember('usersRemote:lastCrawled', object.id), ]); - // 'move' method only applicable for contexts - if (method === 'move' && !isContext) { - return reject('Delete', object, actor); - } - switch (true) { case isNote: { const cid = await posts.getCidByPid(id); @@ -248,13 +278,8 @@ inbox.delete = async (req) => { return; } const { tid, uid } = await posts.getPostFields(pid, ['tid', 'uid']); - if (method === 'move') { - activitypub.helpers.log(`[activitypub/inbox.delete] Moving tid ${tid} to cid -1.`); - await api.topics.move({ uid }, { tid, cid: -1 }); - } else { - activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`); - await api.topics[method]({ uid }, { tids: [tid] }); - } + activitypub.helpers.log(`[activitypub/inbox.delete] Deleting tid ${tid}.`); + await api.topics[method]({ uid }, { tids: [tid] }); break; } @@ -347,12 +372,6 @@ inbox.announce = async (req) => { break; } - case object.type === 'Delete': { - req.body = object; - await inbox.delete(req); - break; - } - case object.type === 'Create': { object = object.object; // falls through diff --git a/src/api/activitypub.js b/src/api/activitypub.js index 3d1d3ff546..12c1989238 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -412,63 +412,6 @@ activitypubApi.announce.user = enabledCheck(async (caller, { tid }) => { }); }); -activitypubApi.announce.delete = enabledCheck(async ({ uid }, { tid }) => { - const now = new Date(); - const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); - - // Only local categories - if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { - return; - } - - const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid); - if (!allowed) { - activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`); - return; - } - - const { to, cc, targets } = await activitypub.buildRecipients({ - to: [activitypub._constants.publicAddress], - cc: [`${nconf.get('url')}/category/${cid}/followers`], - }, { cid }); - - const deleteTpl = { - id: `${nconf.get('url')}/topic/${tid}#activity/delete/${now.getTime()}`, - type: 'Delete', - actor: `${nconf.get('url')}/category/${cid}`, - to, - cc, - origin: `${nconf.get('url')}/category/${cid}`, - }; - - // 7888 variant - await activitypub.send('cid', cid, Array.from(targets), { - id: `${nconf.get('url')}/topic/${tid}#activity/announce/delete/${now.getTime()}`, - type: 'Announce', - actor: `${nconf.get('url')}/category/${cid}`, - to, - cc, - object: { - ...deleteTpl, - object: `${nconf.get('url')}/topic/${tid}`, - }, - }); - - // 1b12 variant - await activitypub.send('cid', cid, Array.from(targets), { - id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/delete/${now.getTime()}`, - type: 'Announce', - actor: `${nconf.get('url')}/category/${cid}`, - to, - cc, - object: { - ...deleteTpl, - actor: `${nconf.get('url')}/uid/${uid}`, - object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, - }, - }); -}); - activitypubApi.undo = {}; // activitypubApi.undo.follow = @@ -573,3 +516,38 @@ activitypubApi.undo.flag = enabledCheck(async (caller, flag) => { }); await db.sortedSetRemove(`flag:${flag.flagId}:remote`, caller.uid); }); + +activitypubApi.remove = {}; + +activitypubApi.remove.context = enabledCheck(async ({ uid }, { tid }) => { + // Federates Remove(Context); where Context is the tid + const now = new Date(); + const cid = await topics.getTopicField(tid, 'oldCid'); + + // Only local categories + if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { + return; + } + + const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating deletion of tid ${tid} to the fediverse due to privileges.`); + return; + } + + const { to, cc, targets } = await activitypub.buildRecipients({ + to: [activitypub._constants.publicAddress], + cc: [`${nconf.get('url')}/category/${cid}/followers`], + }, { cid }); + + // Remove(Context) + await activitypub.send('uid', uid, Array.from(targets), { + id: `${nconf.get('url')}/topic/${tid}#activity/remove/${now.getTime()}`, + type: 'Remove', + actor: `${nconf.get('url')}/uid/${uid}`, + to, + cc, + object: `${nconf.get('url')}/topic/${tid}`, + origin: `${nconf.get('url')}/category/${cid}`, + }); +}); \ No newline at end of file diff --git a/src/api/topics.js b/src/api/topics.js index 38468f5956..964c49e2ec 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -8,6 +8,7 @@ const meta = require('../meta'); const privileges = require('../privileges'); const events = require('../events'); const batch = require('../batch'); +const utils = require('../utils'); const activitypubApi = require('./activitypub'); const apiHelpers = require('./helpers'); @@ -321,13 +322,12 @@ topicsAPI.move = async (caller, { tid, cid }) => { if (!topicData.deleted) { socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic'); - // AP: Announce(Delete(Object)) - if (cid === -1) { - await activitypubApi.announce.delete({ uid: caller.uid }, { tid }); + if (utils.isNumber(cid) && parseInt(cid, 10) === -1) { + activitypubApi.remove.context(caller, { tid }); // tbd: activitypubApi.undo.announce? } else { + // tbd: activitypubApi.move activitypubApi.announce.category(caller, { tid }); - // tbd: api.activitypub.announce.move } } diff --git a/src/topics/delete.js b/src/topics/delete.js index 3557349d9d..ee9d39e107 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -25,7 +25,7 @@ module.exports = function (Topics) { deleterUid: uid, deletedTimestamp: Date.now(), }), - api.activitypub.announce.delete({ uid }, { tid }), + api.activitypub.remove.context({ uid }, { tid }), ]); await categories.updateRecentTidForCid(cid); @@ -82,7 +82,6 @@ module.exports = function (Topics) { } deletedTopic.tags = tags; await deleteFromFollowersIgnorers(tid); - await api.activitypub.announce.delete({ uid }, { tid }), await Promise.all([ db.deleteAll([ From 3df4970ce112970cba895253465186403c8e3849 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 21 Oct 2025 12:16:20 -0400 Subject: [PATCH 054/141] fix: call api.topics method on topic move during note assertion, have category announce new topic on note assertion --- src/activitypub/notes.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 8e11898f26..14e0976986 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -15,6 +15,7 @@ const notifications = require('../notifications'); const user = require('../user'); const topics = require('../topics'); const posts = require('../posts'); +const api = require('../api'); const utils = require('../utils'); const activitypub = module.parent.exports; @@ -106,7 +107,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { if (options.cid && cid === -1) { // Move topic if currently uncategorized - await topics.tools.move(tid, { cid: options.cid, uid: 'system' }); + await api.topics.move({ uid: 'system' }, { tid, cid: options.cid }); } const exists = await posts.exists(chain.map(p => p.pid)); @@ -261,6 +262,12 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { } await Notes.syncUserInboxes(tid, uid); + + if (!hasTid && options.cid) { + // New topic, have category announce it + api.activitypub.announce.category({}, { tid }); + } + return { tid, count }; } catch (e) { winston.warn(`[activitypub/notes.assert] Could not assert ${id} (${e.message}).`); From 5a6c209770c3439edf7564f6a18ed693a6b6b683 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:23:22 -0400 Subject: [PATCH 055/141] fix(deps): update dependency workerpool to v10 (#13723) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 45db72c361..c0b6bc44f8 100644 --- a/install/package.json +++ b/install/package.json @@ -153,7 +153,7 @@ "webpack": "5.102.1", "webpack-merge": "6.0.1", "winston": "3.18.3", - "workerpool": "9.3.4", + "workerpool": "10.0.0", "xml": "1.0.1", "xregexp": "5.1.2", "yargs": "17.7.2", From bb34b8c7a3b2fd617bc7bb8000df38ae897baea0 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 22 Oct 2025 09:20:27 +0000 Subject: [PATCH 056/141] Latest translations and fallbacks --- public/language/bg/admin/settings/uploads.json | 2 +- public/language/it/admin/settings/uploads.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/bg/admin/settings/uploads.json b/public/language/bg/admin/settings/uploads.json index 0cbfc9f7b4..0cb47ec123 100644 --- a/public/language/bg/admin/settings/uploads.json +++ b/public/language/bg/admin/settings/uploads.json @@ -22,7 +22,7 @@ "reject-image-height": "Максимална височина на изображенията (в пиксели)", "reject-image-height-help": "Изображенията, чиято височина е по-голяма от тази стойност, ще бъдат отхвърляни.", "allow-topic-thumbnails": "Позволяване на потребителите да качват миниатюрни изображения за темите", - "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", + "show-post-uploads-as-thumbnails": "Показване на качените файлове в публикациите като миниатюрни изображения", "topic-thumb-size": "Размер на миниатюрите за темите", "allowed-file-extensions": "Разрешени файлови разширения", "allowed-file-extensions-help": "Въведете файловите разширения, разделени със запетаи (пример: pdf,xls,doc). Ако списъкът е празен, всички файлови разширения ще бъдат разрешени.", diff --git a/public/language/it/admin/settings/uploads.json b/public/language/it/admin/settings/uploads.json index f063b36ac9..d40563b47f 100644 --- a/public/language/it/admin/settings/uploads.json +++ b/public/language/it/admin/settings/uploads.json @@ -22,7 +22,7 @@ "reject-image-height": "Lunghezza Massima Immagine (in pixel)", "reject-image-height-help": "Le immagini più alte di questo valore saranno rifiutate.", "allow-topic-thumbnails": "Consenti agli utenti di caricare le miniature degli argomenti", - "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", + "show-post-uploads-as-thumbnails": "Mostra i post caricati come miniature", "topic-thumb-size": "Dimensione miniatura Argomento", "allowed-file-extensions": "Abilita Estensioni File", "allowed-file-extensions-help": "Inserisci una lista di estensioni separati da virgola quì (es. pdf,xls,doc). Una lista vuota indica che tutte le estensioni sono abilitate.", From 3ede64d8a12a1ecc1970f0d9218ea7bcae9100e1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 22 Oct 2025 12:51:50 -0400 Subject: [PATCH 057/141] refactor: move all methods in src/api/activitypub.js to src/activitypub.out.js --- src/activitypub/index.js | 1 + src/activitypub/notes.js | 2 +- .../activitypub.js => activitypub/out.js} | 712 ++++++++---------- src/api/categories.js | 5 +- src/api/helpers.js | 6 +- src/api/index.js | 1 - src/api/posts.js | 6 +- src/api/topics.js | 10 +- src/controllers/write/categories.js | 15 +- src/controllers/write/users.js | 13 +- src/flags.js | 10 +- src/messaging/delete.js | 4 +- src/messaging/edit.js | 4 +- src/messaging/notifications.js | 4 +- src/topics/delete.js | 4 +- src/topics/scheduled.js | 4 +- src/user/categories.js | 10 +- src/user/profile.js | 4 +- 18 files changed, 364 insertions(+), 451 deletions(-) rename src/{api/activitypub.js => activitypub/out.js} (65%) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 7b84148600..e215d68c5d 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -68,6 +68,7 @@ ActivityPub.instances = require('./instances'); ActivityPub.feps = require('./feps'); ActivityPub.rules = require('./rules'); ActivityPub.relays = require('./relays'); +ActivityPub.out = require('./out'); ActivityPub.startJobs = () => { ActivityPub.helpers.log('[activitypub/jobs] Registering jobs.'); diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 14e0976986..b0efa76e08 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -265,7 +265,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { if (!hasTid && options.cid) { // New topic, have category announce it - api.activitypub.announce.category({}, { tid }); + activitypub.out.announce.category(tid); } return { tid, count }; diff --git a/src/api/activitypub.js b/src/activitypub/out.js similarity index 65% rename from src/api/activitypub.js rename to src/activitypub/out.js index 12c1989238..da6c62f361 100644 --- a/src/api/activitypub.js +++ b/src/activitypub/out.js @@ -1,34 +1,34 @@ 'use strict'; /** - * DEVELOPMENT NOTE + * This method deals unilaterally with federating activities outward. + * There _shouldn't_ be any activities sent out that don't go through this file + * This _should_ be the only file that calls activitypub.send() * - * THIS FILE IS UNDER ACTIVE DEVELOPMENT AND IS EXPLICITLY EXCLUDED FROM IMMUTABILITY GUARANTEES - * - * If you use api methods in this file, be prepared that they may be removed or modified with no warning. + * YMMV. */ -const nconf = require('nconf'); const winston = require('winston'); +const nconf = require('nconf'); const db = require('../database'); const user = require('../user'); const categories = require('../categories'); const meta = require('../meta'); const privileges = require('../privileges'); -const activitypub = require('../activitypub'); -const posts = require('../posts'); const topics = require('../topics'); +const posts = require('../posts'); const messaging = require('../messaging'); const utils = require('../utils'); +const activitypub = module.parent.exports; -const activitypubApi = module.exports; +const Out = module.exports; function enabledCheck(next) { - return async function (caller, params) { + return async function (...args) { if (meta.config.activitypubEnabled) { try { - await next(caller, params); + await next.apply(null, args); } catch (e) { winston.error(`[activitypub/api] Error\n${e.stack}`); } @@ -36,7 +36,7 @@ function enabledCheck(next) { }; } -activitypubApi.follow = enabledCheck(async (caller, { type, id, actor } = {}) => { +Out.follow = enabledCheck(async (type, id, actor) => { // Privilege checks should be done upstream const acceptedTypes = ['uid', 'cid']; const assertion = await activitypub.actors.assert(actor); @@ -73,8 +73,302 @@ activitypubApi.follow = enabledCheck(async (caller, { type, id, actor } = {}) => } }); -// should be .undo.follow -activitypubApi.unfollow = enabledCheck(async (caller, { type, id, actor }) => { +Out.create = {}; + +Out.create.note = enabledCheck(async (uid, post) => { + if (utils.isNumber(post)) { + post = (await posts.getPostSummaryByPids([post], uid, { stripTags: false })).pop(); + if (!post) { + return; + } + } + const { pid } = post; + const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating creation of pid ${pid} to the fediverse due to privileges.`); + return; + } + + const { activity, targets } = await activitypub.mocks.activities.create(pid, uid, post); + + await Promise.all([ + activitypub.send('uid', uid, Array.from(targets), activity), + activitypub.feps.announce(pid, activity), + // utils.isNumber(post.cid) ? activitypubApi.add(caller, { pid }) : undefined, + ]); +}); + +Out.create.privateNote = enabledCheck(async (messageObj) => { + const { roomId } = messageObj; + let targets = await messaging.getUidsInRoom(roomId, 0, -1); + targets = targets.filter(uid => !utils.isNumber(uid)); // remote uids only + + const object = await activitypub.mocks.notes.private({ messageObj }); + + const payload = { + id: `${object.id}#activity/create/${Date.now()}`, + type: 'Create', + actor: object.attributedTo, + to: object.to, + object, + }; + + await activitypub.send('uid', messageObj.fromuid, targets, payload); +}); + +Out.update = {}; + +Out.update.profile = enabledCheck(async (uid, actorUid) => { + // Local users only + if (!utils.isNumber(uid)) { + return; + } + + const [object, targets] = await Promise.all([ + activitypub.mocks.actors.user(uid), + db.getSortedSetMembers(`followersRemote:${uid}`), + ]); + + await activitypub.send('uid', actorUid || uid, targets, { + id: `${object.id}#activity/update/${Date.now()}`, + type: 'Update', + actor: object.id, + to: [activitypub._constants.publicAddress], + cc: [], + object, + }); +}); + +Out.update.category = enabledCheck(async (cid) => { + // Local categories only + if (!utils.isNumber(cid)) { + return; + } + + const [object, targets] = await Promise.all([ + activitypub.mocks.actors.category(cid), + activitypub.notes.getCategoryFollowers(cid), + ]); + + await activitypub.send('cid', cid, targets, { + id: `${object.id}#activity/update/${Date.now()}`, + type: 'Update', + actor: object.id, + to: [activitypub._constants.publicAddress], + cc: [], + object, + }); +}); + +Out.update.note = enabledCheck(async (uid, post) => { + // Only applies to local posts + if (!utils.isNumber(post.pid)) { + return; + } + + const object = await activitypub.mocks.notes.public(post); + const { to, cc, targets } = await activitypub.buildRecipients(object, { pid: post.pid, uid: post.user.uid }); + object.to = to; + object.cc = cc; + + const allowed = await privileges.posts.can('topics:read', post.pid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating update of pid ${post.pid} to the fediverse due to privileges.`); + return; + } + + const payload = { + id: `${object.id}#activity/update/${post.edited || Date.now()}`, + type: 'Update', + actor: object.attributedTo, + to, + cc, + object, + }; + + await Promise.all([ + activitypub.send('uid', uid, Array.from(targets), payload), + activitypub.feps.announce(post.pid, payload), + ]); +}); + +Out.update.privateNote = enabledCheck(async (uid, messageObj) => { + if (!utils.isNumber(messageObj.mid)) { + return; + } + + const { roomId } = messageObj; + let uids = await messaging.getUidsInRoom(roomId, 0, -1); + uids = uids.filter(uid => String(uid) !== String(messageObj.fromuid)); // no author + const to = uids.map(uid => (utils.isNumber(uid) ? `${nconf.get('url')}/uid/${uid}` : uid)); + const targets = uids.filter(uid => !utils.isNumber(uid)); // remote uids only + + const object = await activitypub.mocks.notes.private({ messageObj }); + + const payload = { + id: `${object.id}#activity/create/${Date.now()}`, + type: 'Update', + actor: object.attributedTo, + to, + object, + }; + + await activitypub.send('uid', uid, targets, payload); +}); + +Out.delete = {}; + +Out.delete.note = enabledCheck(async (uid, pid) => { + // Only applies to local posts + if (!utils.isNumber(pid)) { + return; + } + + const id = `${nconf.get('url')}/post/${pid}`; + const post = (await posts.getPostSummaryByPids([pid], uid, { stripTags: false })).pop(); + const object = await activitypub.mocks.notes.public(post); + const { to, cc, targets } = await activitypub.buildRecipients(object, { pid, uid: post.user.uid }); + + const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating update of pid ${pid} to the fediverse due to privileges.`); + return; + } + + const payload = { + id: `${id}#activity/delete/${Date.now()}`, + type: 'Delete', + actor: object.attributedTo, + to, + cc, + object: id, + origin: object.context, + }; + + await Promise.all([ + activitypub.send('uid', uid, Array.from(targets), payload), + activitypub.feps.announce(pid, payload), + ]); +}); + +Out.like = {}; + +Out.like.note = enabledCheck(async (uid, pid) => { + const payload = { + id: `${nconf.get('url')}/uid/${uid}#activity/like/${encodeURIComponent(pid)}`, + type: 'Like', + actor: `${nconf.get('url')}/uid/${uid}`, + object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, + }; + + if (!activitypub.helpers.isUri(pid)) { // only 1b12 announce for local likes + await activitypub.feps.announce(pid, payload); + return; + } + + const recipient = await posts.getPostField(pid, 'uid'); + if (!activitypub.helpers.isUri(recipient)) { + return; + } + + await Promise.all([ + activitypub.send('uid', uid, [recipient], payload), + activitypub.feps.announce(pid, payload), + ]); +}); + +Out.announce = {}; + +Out.announce.category = enabledCheck(async (tid) => { + const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); + + // Only local categories can announce + if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { + return; + } + + const uid = await posts.getPostField(pid, 'uid'); // author + const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`); + return; + } + + const { to, cc, targets } = await activitypub.buildRecipients({ + id: pid, + to: [activitypub._constants.publicAddress], + cc: [`${nconf.get('url')}/category/${cid}/followers`, uid], + }, { cid, uid: utils.isNumber(uid) ? uid : undefined }); + + await activitypub.send('cid', cid, Array.from(targets), { + id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/${Date.now()}`, + type: 'Announce', + actor: `${nconf.get('url')}/category/${cid}`, + to, + cc, + object: pid, + target: `${nconf.get('url')}/category/${cid}`, + }); +}); + +Out.flag = enabledCheck(async (uid, flag) => { + if (!activitypub.helpers.isUri(flag.targetId)) { + return; + } + const reportedIds = [flag.targetId]; + if (flag.type === 'post' && activitypub.helpers.isUri(flag.targetUid)) { + reportedIds.push(flag.targetUid); + } + const reason = flag.reason || + (flag.reports && flag.reports.filter(report => report.reporter.uid === uid).at(-1).value); + await activitypub.send('uid', uid, reportedIds, { + id: `${nconf.get('url')}/${flag.type}/${encodeURIComponent(flag.targetId)}#activity/flag/${uid}`, + type: 'Flag', + actor: `${nconf.get('url')}/uid/${uid}`, + object: reportedIds, + content: reason, + }); + await db.sortedSetAdd(`flag:${flag.flagId}:remote`, Date.now(), uid); +}); + +Out.remove = {}; + +Out.remove.context = enabledCheck(async (uid, tid) => { + // Federates Remove(Context); where Context is the tid + const now = new Date(); + const cid = await topics.getTopicField(tid, 'oldCid'); + + // Only local categories + if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { + return; + } + + const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating deletion of tid ${tid} to the fediverse due to privileges.`); + return; + } + + const { to, cc, targets } = await activitypub.buildRecipients({ + to: [activitypub._constants.publicAddress], + cc: [`${nconf.get('url')}/category/${cid}/followers`], + }, { cid }); + + // Remove(Context) + await activitypub.send('uid', uid, Array.from(targets), { + id: `${nconf.get('url')}/topic/${tid}#activity/remove/${now.getTime()}`, + type: 'Remove', + actor: `${nconf.get('url')}/uid/${uid}`, + to, + cc, + object: `${nconf.get('url')}/topic/${tid}`, + origin: `${nconf.get('url')}/category/${cid}`, + }); +}); + +Out.undo = {}; + +Out.undo.follow = enabledCheck(async (type, id, actor) => { const acceptedTypes = ['uid', 'cid']; const assertion = await activitypub.actors.assert(actor); if (!acceptedTypes.includes(type) || !assertion) { @@ -139,312 +433,35 @@ activitypubApi.unfollow = enabledCheck(async (caller, { type, id, actor }) => { } }); -activitypubApi.create = {}; - -activitypubApi.create.note = enabledCheck(async (caller, { pid, post }) => { - if (!post) { - post = (await posts.getPostSummaryByPids([pid], caller.uid, { stripTags: false })).pop(); - if (!post) { - return; - } - } else { - pid = post.pid; - } - - const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); - if (!allowed) { - activitypub.helpers.log(`[activitypub/api] Not federating creation of pid ${pid} to the fediverse due to privileges.`); - return; - } - - const { activity, targets } = await activitypub.mocks.activities.create(pid, caller.uid, post); - - await Promise.all([ - activitypub.send('uid', caller.uid, Array.from(targets), activity), - activitypub.feps.announce(pid, activity), - // utils.isNumber(post.cid) ? activitypubApi.add(caller, { pid }) : undefined, - ]); -}); - -activitypubApi.create.privateNote = enabledCheck(async (caller, { messageObj }) => { - const { roomId } = messageObj; - let targets = await messaging.getUidsInRoom(roomId, 0, -1); - targets = targets.filter(uid => !utils.isNumber(uid)); // remote uids only - - const object = await activitypub.mocks.notes.private({ messageObj }); - - const payload = { - id: `${object.id}#activity/create/${Date.now()}`, - type: 'Create', - actor: object.attributedTo, - to: object.to, - object, - }; - - await activitypub.send('uid', messageObj.fromuid, targets, payload); -}); - -activitypubApi.update = {}; - -activitypubApi.update.profile = enabledCheck(async (caller, { uid }) => { - // Local users only - if (!utils.isNumber(uid)) { - return; - } - - const [object, targets] = await Promise.all([ - activitypub.mocks.actors.user(uid), - db.getSortedSetMembers(`followersRemote:${caller.uid}`), - ]); - - await activitypub.send('uid', caller.uid, targets, { - id: `${object.id}#activity/update/${Date.now()}`, - type: 'Update', - actor: object.id, - to: [activitypub._constants.publicAddress], - cc: [], - object, - }); -}); - -activitypubApi.update.category = enabledCheck(async (caller, { cid }) => { - // Local categories only - if (!utils.isNumber(cid)) { - return; - } - - const [object, targets] = await Promise.all([ - activitypub.mocks.actors.category(cid), - activitypub.notes.getCategoryFollowers(cid), - ]); - - await activitypub.send('cid', cid, targets, { - id: `${object.id}#activity/update/${Date.now()}`, - type: 'Update', - actor: object.id, - to: [activitypub._constants.publicAddress], - cc: [], - object, - }); -}); - -activitypubApi.update.note = enabledCheck(async (caller, { post }) => { - // Only applies to local posts - if (!utils.isNumber(post.pid)) { - return; - } - - const object = await activitypub.mocks.notes.public(post); - const { to, cc, targets } = await activitypub.buildRecipients(object, { pid: post.pid, uid: post.user.uid }); - object.to = to; - object.cc = cc; - - const allowed = await privileges.posts.can('topics:read', post.pid, activitypub._constants.uid); - if (!allowed) { - activitypub.helpers.log(`[activitypub/api] Not federating update of pid ${post.pid} to the fediverse due to privileges.`); - return; - } - - const payload = { - id: `${object.id}#activity/update/${post.edited || Date.now()}`, - type: 'Update', - actor: object.attributedTo, - to, - cc, - object, - }; - - await Promise.all([ - activitypub.send('uid', caller.uid, Array.from(targets), payload), - activitypub.feps.announce(post.pid, payload), - ]); -}); - -activitypubApi.update.privateNote = enabledCheck(async (caller, { messageObj }) => { - if (!utils.isNumber(messageObj.mid)) { - return; - } - - const { roomId } = messageObj; - let uids = await messaging.getUidsInRoom(roomId, 0, -1); - uids = uids.filter(uid => String(uid) !== String(messageObj.fromuid)); // no author - const to = uids.map(uid => (utils.isNumber(uid) ? `${nconf.get('url')}/uid/${uid}` : uid)); - const targets = uids.filter(uid => !utils.isNumber(uid)); // remote uids only - - const object = await activitypub.mocks.notes.private({ messageObj }); - - const payload = { - id: `${object.id}#activity/create/${Date.now()}`, - type: 'Update', - actor: object.attributedTo, - to, - object, - }; - - await activitypub.send('uid', caller.uid, targets, payload); -}); - -activitypubApi.delete = {}; - -activitypubApi.delete.note = enabledCheck(async (caller, { pid }) => { - // Only applies to local posts - if (!utils.isNumber(pid)) { - return; - } - - const id = `${nconf.get('url')}/post/${pid}`; - const post = (await posts.getPostSummaryByPids([pid], caller.uid, { stripTags: false })).pop(); - const object = await activitypub.mocks.notes.public(post); - const { to, cc, targets } = await activitypub.buildRecipients(object, { pid, uid: post.user.uid }); - - const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); - if (!allowed) { - activitypub.helpers.log(`[activitypub/api] Not federating update of pid ${pid} to the fediverse due to privileges.`); - return; - } - - const payload = { - id: `${id}#activity/delete/${Date.now()}`, - type: 'Delete', - actor: object.attributedTo, - to, - cc, - object: id, - origin: object.context, - }; - - await Promise.all([ - activitypub.send('uid', caller.uid, Array.from(targets), payload), - activitypub.feps.announce(pid, payload), - ]); -}); - -activitypubApi.like = {}; - -activitypubApi.like.note = enabledCheck(async (caller, { pid }) => { - const payload = { - id: `${nconf.get('url')}/uid/${caller.uid}#activity/like/${encodeURIComponent(pid)}`, - type: 'Like', - actor: `${nconf.get('url')}/uid/${caller.uid}`, - object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, - }; - - if (!activitypub.helpers.isUri(pid)) { // only 1b12 announce for local likes - await activitypub.feps.announce(pid, payload); - return; - } - - const uid = await posts.getPostField(pid, 'uid'); - if (!activitypub.helpers.isUri(uid)) { - return; - } - - await Promise.all([ - activitypub.send('uid', caller.uid, [uid], payload), - activitypub.feps.announce(pid, payload), - ]); -}); - -activitypubApi.announce = {}; - -activitypubApi.announce.category = enabledCheck(async (_, { tid }) => { - const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); - - // Only local categories can announce - if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { - return; - } - - const uid = await posts.getPostField(pid, 'uid'); // author - const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); - if (!allowed) { - activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`); - return; - } - - const { to, cc, targets } = await activitypub.buildRecipients({ - id: pid, - to: [activitypub._constants.publicAddress], - cc: [`${nconf.get('url')}/category/${cid}/followers`, uid], - }, { cid, uid: utils.isNumber(uid) ? uid : undefined }); - - await activitypub.send('cid', cid, Array.from(targets), { - id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/${Date.now()}`, - type: 'Announce', - actor: `${nconf.get('url')}/category/${cid}`, - to, - cc, - object: pid, - target: `${nconf.get('url')}/category/${cid}`, - }); -}); - -activitypubApi.announce.user = enabledCheck(async (caller, { tid }) => { - // ORPHANED, but will re-use when user announces are a thing. - const { mainPid: pid, cid } = await topics.getTopicFields(tid, ['mainPid', 'cid']); - - // Only remote posts can be announced to local categories - if (utils.isNumber(pid) || parseInt(cid, 10) === -1) { - return; - } - - const uid = await posts.getPostField(pid, 'uid'); // author - const allowed = await privileges.posts.can('topics:read', pid, activitypub._constants.uid); - if (!allowed) { - activitypub.helpers.log(`[activitypub/api] Not federating announce of pid ${pid} to the fediverse due to privileges.`); - return; - } - - const { to, cc, targets } = await activitypub.buildRecipients({ - id: pid, - to: [activitypub._constants.publicAddress], - cc: [`${nconf.get('url')}/uid/${caller.uid}/followers`, uid], - }, { uid: caller.uid }); - - await activitypub.send('uid', caller.uid, Array.from(targets), { - id: `${nconf.get('url')}/post/${encodeURIComponent(pid)}#activity/announce/${Date.now()}`, - type: 'Announce', - actor: `${nconf.get('url')}/uid/${caller.uid}`, - to, - cc, - object: pid, - target: `${nconf.get('url')}/category/${cid}`, - }); -}); - -activitypubApi.undo = {}; - -// activitypubApi.undo.follow = - -activitypubApi.undo.like = enabledCheck(async (caller, { pid }) => { +Out.undo.like = enabledCheck(async (uid, pid) => { if (!activitypub.helpers.isUri(pid)) { return; } - const uid = await posts.getPostField(pid, 'uid'); - if (!activitypub.helpers.isUri(uid)) { + const author = await posts.getPostField(pid, 'uid'); + if (!activitypub.helpers.isUri(author)) { return; } const payload = { - id: `${nconf.get('url')}/uid/${caller.uid}#activity/undo:like/${encodeURIComponent(pid)}/${Date.now()}`, + id: `${nconf.get('url')}/uid/${uid}#activity/undo:like/${encodeURIComponent(pid)}/${Date.now()}`, type: 'Undo', - actor: `${nconf.get('url')}/uid/${caller.uid}`, + actor: `${nconf.get('url')}/uid/${uid}`, object: { - actor: `${nconf.get('url')}/uid/${caller.uid}`, - id: `${nconf.get('url')}/uid/${caller.uid}#activity/like/${encodeURIComponent(pid)}`, + actor: `${nconf.get('url')}/uid/${uid}`, + id: `${nconf.get('url')}/uid/${uid}#activity/like/${encodeURIComponent(pid)}`, type: 'Like', object: pid, }, }; await Promise.all([ - activitypub.send('uid', caller.uid, [uid], payload), + activitypub.send('uid', uid, [author], payload), activitypub.feps.announce(pid, payload), ]); }); -activitypubApi.flag = enabledCheck(async (caller, flag) => { +Out.undo.flag = enabledCheck(async (uid, flag) => { if (!activitypub.helpers.isUri(flag.targetId)) { return; } @@ -453,101 +470,18 @@ activitypubApi.flag = enabledCheck(async (caller, flag) => { reportedIds.push(flag.targetUid); } const reason = flag.reason || - (flag.reports && flag.reports.filter(report => report.reporter.uid === caller.uid).at(-1).value); - await activitypub.send('uid', caller.uid, reportedIds, { - id: `${nconf.get('url')}/${flag.type}/${encodeURIComponent(flag.targetId)}#activity/flag/${caller.uid}`, - type: 'Flag', - actor: `${nconf.get('url')}/uid/${caller.uid}`, - object: reportedIds, - content: reason, - }); - await db.sortedSetAdd(`flag:${flag.flagId}:remote`, Date.now(), caller.uid); -}); - -/* -activitypubApi.add = enabledCheck((async (_, { pid }) => { - let localId; - if (String(pid).startsWith(nconf.get('url'))) { - ({ id: localId } = await activitypub.helpers.resolveLocalId(pid)); - } - - const tid = await posts.getPostField(localId || pid, 'tid'); - const cid = await posts.getCidByPid(localId || pid); - if (!utils.isNumber(tid) || cid <= 0) { // `Add` only federated on categorized topics started locally - return; - } - - let to = [activitypub._constants.publicAddress]; - let cc = []; - let targets; - ({ to, cc, targets } = await activitypub.buildRecipients({ to, cc }, { pid: localId || pid, cid })); - - await activitypub.send('cid', cid, Array.from(targets), { - id: `${nconf.get('url')}/post/${encodeURIComponent(localId || pid)}#activity/add/${Date.now()}`, - type: 'Add', - to, - cc, - object: utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid, - target: `${nconf.get('url')}/topic/${tid}`, - }); -})); -*/ -activitypubApi.undo.flag = enabledCheck(async (caller, flag) => { - if (!activitypub.helpers.isUri(flag.targetId)) { - return; - } - const reportedIds = [flag.targetId]; - if (flag.type === 'post' && activitypub.helpers.isUri(flag.targetUid)) { - reportedIds.push(flag.targetUid); - } - const reason = flag.reason || - (flag.reports && flag.reports.filter(report => report.reporter.uid === caller.uid).at(-1).value); - await activitypub.send('uid', caller.uid, reportedIds, { - id: `${nconf.get('url')}/${flag.type}/${encodeURIComponent(flag.targetId)}#activity/undo:flag/${caller.uid}/${Date.now()}`, + (flag.reports && flag.reports.filter(report => report.reporter.uid === uid).at(-1).value); + await activitypub.send('uid', uid, reportedIds, { + id: `${nconf.get('url')}/${flag.type}/${encodeURIComponent(flag.targetId)}#activity/undo:flag/${uid}/${Date.now()}`, type: 'Undo', - actor: `${nconf.get('url')}/uid/${caller.uid}`, + actor: `${nconf.get('url')}/uid/${uid}`, object: { - id: `${nconf.get('url')}/${flag.type}/${encodeURIComponent(flag.targetId)}#activity/flag/${caller.uid}`, - actor: `${nconf.get('url')}/uid/${caller.uid}`, + id: `${nconf.get('url')}/${flag.type}/${encodeURIComponent(flag.targetId)}#activity/flag/${uid}`, + actor: `${nconf.get('url')}/uid/${uid}`, type: 'Flag', object: reportedIds, content: reason, }, }); - await db.sortedSetRemove(`flag:${flag.flagId}:remote`, caller.uid); -}); - -activitypubApi.remove = {}; - -activitypubApi.remove.context = enabledCheck(async ({ uid }, { tid }) => { - // Federates Remove(Context); where Context is the tid - const now = new Date(); - const cid = await topics.getTopicField(tid, 'oldCid'); - - // Only local categories - if (!utils.isNumber(cid) || parseInt(cid, 10) < 1) { - return; - } - - const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid); - if (!allowed) { - activitypub.helpers.log(`[activitypub/api] Not federating deletion of tid ${tid} to the fediverse due to privileges.`); - return; - } - - const { to, cc, targets } = await activitypub.buildRecipients({ - to: [activitypub._constants.publicAddress], - cc: [`${nconf.get('url')}/category/${cid}/followers`], - }, { cid }); - - // Remove(Context) - await activitypub.send('uid', uid, Array.from(targets), { - id: `${nconf.get('url')}/topic/${tid}#activity/remove/${now.getTime()}`, - type: 'Remove', - actor: `${nconf.get('url')}/uid/${uid}`, - to, - cc, - object: `${nconf.get('url')}/topic/${tid}`, - origin: `${nconf.get('url')}/category/${cid}`, - }); + await db.sortedSetRemove(`flag:${flag.flagId}:remote`, uid); }); \ No newline at end of file diff --git a/src/api/categories.js b/src/api/categories.js index 693f8f15ee..476a0d4d9d 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -7,10 +7,9 @@ const events = require('../events'); const user = require('../user'); const groups = require('../groups'); const privileges = require('../privileges'); +const activitypub = require('../activitypub'); const utils = require('../utils'); -const activitypubApi = require('./activitypub'); - const categoriesAPI = module.exports; const hasAdminPrivilege = async (uid, privilege = 'categories') => { @@ -66,7 +65,7 @@ categoriesAPI.update = async function (caller, data) { const payload = {}; payload[cid] = values; await categories.update(payload); - activitypubApi.update.category(caller, { cid }); // background + activitypub.out.update.category(cid); // background }; categoriesAPI.delete = async function (caller, { cid }) { diff --git a/src/api/helpers.js b/src/api/helpers.js index 7df860a569..3422b4a6f9 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -6,6 +6,7 @@ const topics = require('../topics'); const posts = require('../posts'); const privileges = require('../privileges'); const plugins = require('../plugins'); +const activitypub = require('../activitypub'); const socketHelpers = require('../socket.io/helpers'); const websockets = require('../socket.io'); const events = require('../events'); @@ -129,7 +130,6 @@ exports.postCommand = async function (caller, command, eventName, notification, }; async function executeCommand(caller, command, eventName, notification, data) { - const api = require('.'); const result = await posts[command](data.pid, caller.uid); if (result && eventName) { websockets.in(`uid_${caller.uid}`).emit(`posts.${command}`, result); @@ -137,12 +137,12 @@ async function executeCommand(caller, command, eventName, notification, data) { } if (result && command === 'upvote') { socketHelpers.upvote(result, notification); - await api.activitypub.like.note(caller, { pid: data.pid }); + await activitypub.out.like.note(caller.uid, data.pid); } else if (result && notification) { socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, command, notification); } else if (result && command === 'unvote') { socketHelpers.rescindUpvoteNotification(data.pid, caller.uid); - await api.activitypub.undo.like(caller, { pid: data.pid }); + await activitypub.out.undo.like(caller.uid, data.pid); } return result; } diff --git a/src/api/index.js b/src/api/index.js index 18cd8678f1..c454de93a5 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -11,7 +11,6 @@ module.exports = { categories: require('./categories'), search: require('./search'), flags: require('./flags'), - activitypub: require('./activitypub'), files: require('./files'), utils: require('./utils'), }; diff --git a/src/api/posts.js b/src/api/posts.js index 54454e6e52..74c02a210e 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -151,7 +151,7 @@ postsAPI.edit = async function (caller, data) { if (!editResult.post.deleted) { websockets.in(`topic_${editResult.topic.tid}`).emit('event:post_edited', editResult); setTimeout(() => { - require('.').activitypub.update.note(caller, { post: postObj[0] }); + activitypub.out.update.note(caller.uid, postObj[0]); }, 5000); return returnData; @@ -208,7 +208,7 @@ async function deleteOrRestore(caller, data, params) { // Explicitly non-awaited posts.getPostSummaryByPids([data.pid], caller.uid, { extraFields: ['edited'] }).then(([post]) => { - require('.').activitypub.update.note(caller, { post }); + activitypub.out.update.note(caller.uid, post); }); } @@ -254,7 +254,7 @@ postsAPI.purge = async function (caller, data) { posts.clearCachedPost(data.pid); await Promise.all([ posts.purge(data.pid, caller.uid), - require('.').activitypub.delete.note(caller, { pid: data.pid }), + activitypub.out.delete.note(caller.uid, data.pid), ]); websockets.in(`topic_${postData.tid}`).emit('event:post_purged', postData); diff --git a/src/api/topics.js b/src/api/topics.js index 964c49e2ec..9d3c149397 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -8,9 +8,9 @@ const meta = require('../meta'); const privileges = require('../privileges'); const events = require('../events'); const batch = require('../batch'); +const activitypub = require('../activitypub'); const utils = require('../utils'); -const activitypubApi = require('./activitypub'); const apiHelpers = require('./helpers'); const { doTopicAction } = apiHelpers; @@ -80,7 +80,7 @@ topicsAPI.create = async function (caller, data) { socketHelpers.notifyNew(caller.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); if (!isScheduling) { - await activitypubApi.create.note(caller, { pid: result.postData.pid }); + await activitypub.out.create.note(caller.uid, result.postData.pid); } return result.topicData; @@ -116,7 +116,7 @@ topicsAPI.reply = async function (caller, data) { } socketHelpers.notifyNew(caller.uid, 'newPost', result); - await activitypubApi.create.note(caller, { post: postData }); + await activitypub.out.create.note(caller.uid, postData); return postData; }; @@ -323,11 +323,11 @@ topicsAPI.move = async (caller, { tid, cid }) => { socketHelpers.sendNotificationToTopicOwner(tid, caller.uid, 'move', 'notifications:moved-your-topic'); if (utils.isNumber(cid) && parseInt(cid, 10) === -1) { - activitypubApi.remove.context(caller, { tid }); + activitypub.out.remove.context(caller.uid, tid); // tbd: activitypubApi.undo.announce? } else { // tbd: activitypubApi.move - activitypubApi.announce.category(caller, { tid }); + activitypub.out.announce.category(tid); } } diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 8a2a002713..996a9d386a 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -2,6 +2,7 @@ const categories = require('../../categories'); const meta = require('../../meta'); +const activitypub = require('../../activitypub'); const api = require('../../api'); const helpers = require('../helpers'); @@ -107,6 +108,7 @@ Categories.setModerator = async (req, res) => { }; Categories.follow = async (req, res, next) => { + // Priv check done in route middleware const { actor } = req.body; const id = parseInt(req.params.cid, 10); @@ -114,11 +116,7 @@ Categories.follow = async (req, res, next) => { return next(); } - await api.activitypub.follow(req, { - type: 'cid', - id, - actor, - }); + await activitypub.out.follow('cid', id, actor); helpers.formatApiResponse(200, res, {}); }; @@ -131,11 +129,6 @@ Categories.unfollow = async (req, res, next) => { return next(); } - await api.activitypub.unfollow(req, { - type: 'cid', - id, - actor, - }); - + await activitypub.out.undo.follow('cid', id, actor); helpers.formatApiResponse(200, res, {}); }; diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index b884ef93fb..a5cde6dad2 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -5,6 +5,7 @@ const path = require('path'); const crypto = require('crypto'); const api = require('../../api'); +const activitypub = require('../../activitypub'); const user = require('../../user'); const helpers = require('../helpers'); @@ -94,11 +95,7 @@ Users.changePassword = async (req, res) => { Users.follow = async (req, res) => { const remote = String(req.params.uid).includes('@'); if (remote) { - await api.activitypub.follow(req, { - type: 'uid', - id: req.uid, - actor: req.params.uid, - }); + await activitypub.out.follow('uid', req.uid, req.params.uid); } else { await api.users.follow(req, req.params); } @@ -109,11 +106,7 @@ Users.follow = async (req, res) => { Users.unfollow = async (req, res) => { const remote = String(req.params.uid).includes('@'); if (remote) { - await api.activitypub.unfollow(req, { - type: 'uid', - id: req.uid, - actor: req.params.uid, - }); + await activitypub.out.undo.follow('uid', req.uid, req.params.uid); } else { await api.users.unfollow(req, req.params); } diff --git a/src/flags.js b/src/flags.js index 8ae9a6c595..435bc10c42 100644 --- a/src/flags.js +++ b/src/flags.js @@ -5,7 +5,6 @@ const winston = require('winston'); const validator = require('validator'); const activitypub = require('./activitypub'); -const activitypubApi = require('./api/activitypub'); const db = require('./database'); const user = require('./user'); const groups = require('./groups'); @@ -477,8 +476,7 @@ Flags.create = async function (type, id, uid, reason, timestamp, forceFlag = fal const flagObj = await Flags.get(flagId); if (notifyRemote && activitypub.helpers.isUri(id)) { - const caller = await user.getUserData(uid); - activitypubApi.flag(caller, { ...flagObj, reason }); + activitypub.out.flag(uid, { ...flagObj, reason }); } plugins.hooks.fire('action:flags.create', { flag: flagObj }); @@ -531,7 +529,7 @@ Flags.purge = async function (flagIds) { flagData.flatMap( async (flagObj, i) => allReporterUids[i].map(async (uid) => { if (await db.isSortedSetMember(`flag:${flagObj.flagId}:remote`, uid)) { - await activitypubApi.undo.flag({ uid }, flagObj); + await activitypub.out.undo.flag(uid, flagObj); } }) ), @@ -569,7 +567,7 @@ Flags.addReport = async function (flagId, type, id, uid, reason, timestamp, targ ]); if (notifyRemote && activitypub.helpers.isUri(id)) { - await activitypubApi.flag({ uid }, { flagId, type, targetId: id, targetUid, uid, reason, timestamp }); + await activitypub.out.flag(uid, { flagId, type, targetId: id, targetUid, uid, reason, timestamp }); } plugins.hooks.fire('action:flags.addReport', { flagId, type, id, uid, reason, timestamp }); @@ -599,7 +597,7 @@ Flags.rescindReport = async (type, id, uid) => { if (await db.isSortedSetMember(`flag:${flagId}:remote`, uid)) { const flag = await Flags.get(flagId); - await activitypubApi.undo.flag({ uid }, flag); + await activitypub.out.undo.flag(uid, flag); } await db.sortedSetRemoveBulk([ diff --git a/src/messaging/delete.js b/src/messaging/delete.js index 9e0f23c797..c754e4fc9f 100644 --- a/src/messaging/delete.js +++ b/src/messaging/delete.js @@ -2,7 +2,7 @@ const sockets = require('../socket.io'); const plugins = require('../plugins'); -const api = require('../api'); +const activitypub = require('../activitypub'); module.exports = function (Messaging) { Messaging.deleteMessage = async (mid, uid) => await doDeleteRestore(mid, 1, uid); @@ -30,6 +30,6 @@ module.exports = function (Messaging) { plugins.hooks.fire('action:messaging.restore', { message: msgData }); } - api.activitypub.update.privateNote({ uid }, { messageObj: msgData }); + activitypub.out.update.privateNote(uid, msgData); } }; diff --git a/src/messaging/edit.js b/src/messaging/edit.js index 358e73f4be..86d8b07b6c 100644 --- a/src/messaging/edit.js +++ b/src/messaging/edit.js @@ -4,7 +4,7 @@ const db = require('../database'); const meta = require('../meta'); const user = require('../user'); const plugins = require('../plugins'); -const api = require('../api'); +const activitypub = require('../activitypub'); const privileges = require('../privileges'); const utils = require('../utils'); @@ -39,7 +39,7 @@ module.exports = function (Messaging) { }); if (!isPublic && utils.isNumber(messages[0].fromuid)) { - api.activitypub.update.privateNote({ uid: messages[0].fromuid }, { messageObj: messages[0] }); + activitypub.out.update.privateNote(messages[0].fromuid, messages[0]); } } diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 58611b6bcf..1c9475608d 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -8,7 +8,7 @@ const db = require('../database'); const notifications = require('../notifications'); const user = require('../user'); const io = require('../socket.io'); -const api = require('../api'); +const activitypub = require('../activitypub'); const plugins = require('../plugins'); const utils = require('../utils'); @@ -83,7 +83,7 @@ module.exports = function (Messaging) { await Promise.all([ sendNotification(fromUid, roomId, messageObj), !isPublic && utils.isNumber(fromUid) ? - api.activitypub.create.privateNote({ uid: fromUid }, { messageObj }) : null, + activitypub.out.create.privateNote(messageObj) : null, ]); } catch (err) { winston.error(`[messaging/notifications] Unabled to send notification\n${err.stack}`); diff --git a/src/topics/delete.js b/src/topics/delete.js index ee9d39e107..466d25a0dd 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -8,7 +8,7 @@ const categories = require('../categories'); const flags = require('../flags'); const plugins = require('../plugins'); const batch = require('../batch'); -const api = require('../api'); +const activitypub = require('../activitypub'); const utils = require('../utils'); module.exports = function (Topics) { @@ -25,7 +25,7 @@ module.exports = function (Topics) { deleterUid: uid, deletedTimestamp: Date.now(), }), - api.activitypub.remove.context({ uid }, { tid }), + activitypub.out.remove.context(uid, tid), ]); await categories.updateRecentTidForCid(cid); diff --git a/src/topics/scheduled.js b/src/topics/scheduled.js index 1134ae1dfa..a20109a50d 100644 --- a/src/topics/scheduled.js +++ b/src/topics/scheduled.js @@ -11,7 +11,7 @@ const topics = require('./index'); const categories = require('../categories'); const groups = require('../groups'); const user = require('../user'); -const api = require('../api'); +const activitypub = require('../activitypub'); const plugins = require('../plugins'); const Scheduled = module.exports; @@ -175,7 +175,7 @@ function federatePosts(uids, topicData) { topicData.forEach(({ mainPid: pid }, idx) => { const uid = uids[idx]; - api.activitypub.create.note({ uid }, { pid }); + activitypub.out.create.note(uid, pid); }); } diff --git a/src/user/categories.js b/src/user/categories.js index 09356eac5b..4248def8bd 100644 --- a/src/user/categories.js +++ b/src/user/categories.js @@ -5,8 +5,8 @@ const _ = require('lodash'); const db = require('../database'); const meta = require('../meta'); const categories = require('../categories'); +const activitypub = require('../activitypub'); const plugins = require('../plugins'); -const api = require('../api'); const utils = require('../utils'); module.exports = function (User) { @@ -30,12 +30,8 @@ module.exports = function (User) { throw new Error('[[error:no-category]]'); } - const apiMethod = state >= categories.watchStates.tracking ? 'follow' : 'unfollow'; - const follows = cids.filter(cid => !utils.isNumber(cid)).map(cid => api.activitypub[apiMethod]({ uid }, { - type: 'uid', - id: uid, - actor: cid, - })); // returns promises + const apiMethod = state >= categories.watchStates.tracking ? activitypub.out.follow : activitypub.out.undo.follow; + const follows = cids.filter(cid => !utils.isNumber(cid)).map(cid => apiMethod('uid', uid, cid)); // returns promises await Promise.all([ db.sortedSetsAdd(cids.map(cid => `cid:${cid}:uid:watch:state`), state, uid), diff --git a/src/user/profile.js b/src/user/profile.js index 3009d0a3d5..ec4a23d3a3 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -11,7 +11,7 @@ const meta = require('../meta'); const db = require('../database'); const groups = require('../groups'); const plugins = require('../plugins'); -const api = require('../api'); +const activitypub = require('../activitypub'); const tx = require('../translator'); module.exports = function (User) { @@ -68,7 +68,7 @@ module.exports = function (User) { fields: fields, oldData: oldData, }); - api.activitypub.update.profile({ uid }, { uid: updateUid }); + activitypub.out.update.profile(updateUid, uid); return await User.getUserFields(updateUid, [ 'email', 'username', 'userslug', From d02e188a5f7d6fb271d28653a82159ea9802d18c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 22 Oct 2025 15:04:47 -0400 Subject: [PATCH 058/141] feat: update Remove(Context) to use target instead of origin, federate out Move(Context) on topic move between local cids --- src/activitypub/inbox.js | 2 -- src/activitypub/index.js | 17 ++++++++++------- src/activitypub/out.js | 41 +++++++++++++++++++++++++++++++++++++--- src/api/topics.js | 2 +- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 9a9381f43f..23ac85c844 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -86,14 +86,12 @@ inbox.remove = async (req) => { if (!isContext) { return; // don't know how to handle other types } - console.log('isContext?', isContext); const mainPid = await activitypub.contexts.getItems(0, object.id, { returnRootId: true }); const exists = await posts.exists(mainPid); if (!exists) { return; // post not cached; do nothing. } - console.log('mainPid is', mainPid); // Ensure that cid is same-origin as the actor const tid = await posts.getPostField(mainPid, 'tid'); diff --git a/src/activitypub/index.js b/src/activitypub/index.js index e215d68c5d..767c88ce23 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -533,7 +533,7 @@ ActivityPub.buildRecipients = async function (object, { pid, uid, cid }) { * - Builds a list of targets for activitypub.send to consume * - Extends to and cc since the activity can be addressed more widely * - Optional parameters: - * - `cid`: includes followers of the passed-in cid (local only) + * - `cid`: includes followers of the passed-in cid (local only, can also be an array) * - `uid`: includes followers of the passed-in uid (local only) * - `pid`: includes post announcers and all topic participants */ @@ -551,12 +551,15 @@ ActivityPub.buildRecipients = async function (object, { pid, uid, cid }) { } if (cid) { - const cidFollowers = await ActivityPub.notes.getCategoryFollowers(cid); - followers = followers.concat(cidFollowers); - const followersUrl = `${nconf.get('url')}/category/${cid}/followers`; - if (!to.has(followersUrl)) { - cc.add(followersUrl); - } + cid = Array.isArray(cid) ? cid : [cid]; + await Promise.all(cid.map(async (cid) => { + const cidFollowers = await ActivityPub.notes.getCategoryFollowers(cid); + followers = followers.concat(cidFollowers); + const followersUrl = `${nconf.get('url')}/category/${cid}/followers`; + if (!to.has(followersUrl)) { + cc.add(followersUrl); + } + })); } const targets = new Set([...followers, ...to, ...cc]); diff --git a/src/activitypub/out.js b/src/activitypub/out.js index da6c62f361..7fb77c9140 100644 --- a/src/activitypub/out.js +++ b/src/activitypub/out.js @@ -351,10 +351,9 @@ Out.remove.context = enabledCheck(async (uid, tid) => { const { to, cc, targets } = await activitypub.buildRecipients({ to: [activitypub._constants.publicAddress], - cc: [`${nconf.get('url')}/category/${cid}/followers`], + cc: [], }, { cid }); - // Remove(Context) await activitypub.send('uid', uid, Array.from(targets), { id: `${nconf.get('url')}/topic/${tid}#activity/remove/${now.getTime()}`, type: 'Remove', @@ -362,7 +361,43 @@ Out.remove.context = enabledCheck(async (uid, tid) => { to, cc, object: `${nconf.get('url')}/topic/${tid}`, - origin: `${nconf.get('url')}/category/${cid}`, + target: `${nconf.get('url')}/category/${cid}`, + }); +}); + +Out.move = {}; + +Out.move.context = enabledCheck(async (uid, tid) => { + // Federates Move(Context); where Context is the tid + const now = new Date(); + const { cid, oldCid } = await topics.getTopicFields(tid, ['cid', 'oldCid']); + + // This check may be revised if inter-community moderation becomes real. + const isNotLocal = id => !utils.isNumber(cid) || parseInt(cid, 10) < 1; + if ([cid, oldCid].some(isNotLocal)) { + return; + } + + const allowed = await privileges.categories.can('topics:read', cid, activitypub._constants.uid); + if (!allowed) { + activitypub.helpers.log(`[activitypub/api] Not federating move of tid ${tid} to the fediverse due to privileges.`); + return; + } + + const { to, cc, targets } = await activitypub.buildRecipients({ + to: [activitypub._constants.publicAddress], + cc: [], + }, { cid: [cid, oldCid] }); + + await activitypub.send('uid', uid, Array.from(targets), { + id: `${nconf.get('url')}/topic/${tid}#activity/move/${now.getTime()}`, + type: 'Move', + actor: `${nconf.get('url')}/uid/${uid}`, + to, + cc, + object: `${nconf.get('url')}/topic/${tid}`, + origin: `${nconf.get('url')}/category/${oldCid}`, + target: `${nconf.get('url')}/category/${cid}`, }); }); diff --git a/src/api/topics.js b/src/api/topics.js index 9d3c149397..d7f2f4836f 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -326,7 +326,7 @@ topicsAPI.move = async (caller, { tid, cid }) => { activitypub.out.remove.context(caller.uid, tid); // tbd: activitypubApi.undo.announce? } else { - // tbd: activitypubApi.move + activitypub.out.move.context(caller.uid, tid); activitypub.out.announce.category(tid); } } From 22868d3f97be1807a53872d3cade8f735f83064d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 22 Oct 2025 15:05:06 -0400 Subject: [PATCH 059/141] fix: bad var --- src/activitypub/out.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activitypub/out.js b/src/activitypub/out.js index 7fb77c9140..014531c310 100644 --- a/src/activitypub/out.js +++ b/src/activitypub/out.js @@ -373,7 +373,7 @@ Out.move.context = enabledCheck(async (uid, tid) => { const { cid, oldCid } = await topics.getTopicFields(tid, ['cid', 'oldCid']); // This check may be revised if inter-community moderation becomes real. - const isNotLocal = id => !utils.isNumber(cid) || parseInt(cid, 10) < 1; + const isNotLocal = id => !utils.isNumber(id) || parseInt(id, 10) < 1; if ([cid, oldCid].some(isNotLocal)) { return; } From 4f2f872bf993313694701981e7d01b3d22fe671b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 22 Oct 2025 15:15:19 -0400 Subject: [PATCH 060/141] fix: update logic re: federating out topic moves --- src/activitypub/out.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/activitypub/out.js b/src/activitypub/out.js index 014531c310..40d597db51 100644 --- a/src/activitypub/out.js +++ b/src/activitypub/out.js @@ -373,8 +373,10 @@ Out.move.context = enabledCheck(async (uid, tid) => { const { cid, oldCid } = await topics.getTopicFields(tid, ['cid', 'oldCid']); // This check may be revised if inter-community moderation becomes real. - const isNotLocal = id => !utils.isNumber(id) || parseInt(id, 10) < 1; - if ([cid, oldCid].some(isNotLocal)) { + const isLocal = id => utils.isNumber(id) && parseInt(id, 10) > 0; + if (isLocal(oldCid) && !isLocal(cid)) { // moving to remote/uncategorized + return Out.remove.context(uid, tid); + } else if ((isLocal(cid) && !isLocal(oldCid)) || [cid, oldCid].every(!isLocal)) { // stealing or remote-to-remote return; } From c1f6e52ba5c6fd16dff1b55afb0c214d8d3f4690 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:36:33 -0400 Subject: [PATCH 061/141] fix(deps): update dependency nodemailer to v7.0.10 (#13726) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index c0b6bc44f8..f66a79f512 100644 --- a/install/package.json +++ b/install/package.json @@ -112,7 +112,7 @@ "nodebb-theme-peace": "2.2.48", "nodebb-theme-persona": "14.1.16", "nodebb-widget-essentials": "7.0.40", - "nodemailer": "7.0.9", + "nodemailer": "7.0.10", "nprogress": "0.2.0", "passport": "0.7.0", "passport-http-bearer": "1.0.1", From e3c55f76c1b6aacf31d1c9807ce79ad04f40907c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:36:55 -0400 Subject: [PATCH 062/141] chore(deps): update dependency lint-staged to v16.2.6 (#13725) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index f66a79f512..8249e4e99a 100644 --- a/install/package.json +++ b/install/package.json @@ -172,7 +172,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "27.0.1", - "lint-staged": "16.2.5", + "lint-staged": "16.2.6", "mocha": "11.7.4", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From 194cedb4d7e637b017c6c414a2f76e7dc795ab04 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 23 Oct 2025 12:02:59 -0400 Subject: [PATCH 063/141] fix: cross-check remove(context) target prop against cid --- src/activitypub/inbox.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 23ac85c844..8a9e5c7e93 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -80,7 +80,7 @@ inbox.add = async (req) => { }; inbox.remove = async (req) => { - const { actor, object } = req.body; + const { actor, object, target } = req.body; const isContext = activitypub._constants.acceptable.contextTypes.has(object.type); if (!isContext) { @@ -88,16 +88,17 @@ inbox.remove = async (req) => { } const mainPid = await activitypub.contexts.getItems(0, object.id, { returnRootId: true }); + const fromCid = target || object.audience; const exists = await posts.exists(mainPid); - if (!exists) { + if (!exists || !fromCid) { return; // post not cached; do nothing. } // Ensure that cid is same-origin as the actor const tid = await posts.getPostField(mainPid, 'tid'); const cid = await topics.getTopicField(tid, 'cid'); - if (utils.isNumber(cid)) { - // remote removal of topic in local cid; what?? + if (utils.isNumber(cid) || cid !== fromCid) { + // remote removal of topic in local cid, or resolved cid does not match return; } const actorHostname = new URL(actor).hostname; From 8ca52c7e78e9eaa314e2fe84b4f073b3cd7ceb39 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 23 Oct 2025 12:15:36 -0400 Subject: [PATCH 064/141] feat: handle Move(Context) activity --- src/activitypub/inbox.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 8a9e5c7e93..d7dbcf6834 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -114,6 +114,43 @@ inbox.remove = async (req) => { }); }; +inbox.move = async (req) => { + const { actor, object, origin, target } = req.body; + + const isContext = activitypub._constants.acceptable.contextTypes.has(object.type); + if (!isContext) { + return; // don't know how to handle other types + } + + const mainPid = await activitypub.contexts.getItems(0, object.id, { returnRootId: true }); + const fromCid = origin; + const toCid = target || object.audience; + const exists = await posts.exists(mainPid); + if (!exists || !toCid) { + return; // post not cached; do nothing. + } + + // Ensure that cid is same-origin as the actor + const tid = await posts.getPostField(mainPid, 'tid'); + const cid = await topics.getTopicField(tid, 'cid'); + if (utils.isNumber(cid)) { + // remote removal of topic in local cid, or resolved cid does not match + return; + } + const actorHostname = new URL(actor).hostname; + const toCidHostname = new URL(toCid).hostname; + const fromCidHostname = new URL(fromCid).hostname; + if (actorHostname !== toCidHostname || actorHostname !== fromCidHostname) { + throw new Error('[[error:activitypub.origin-mismatch]]'); + } + + activitypub.helpers.log(`[activitypub/inbox/remove] Moving topic ${tid} from ${fromCid} to ${toCid}`); + await topics.tools.move(tid, { + cid: toCid, + uid: 'system', + }); +}; + inbox.update = async (req) => { const { actor, object } = req.body; const isPublic = publiclyAddressed([...(object.to || []), ...(object.cc || [])]); From 25c088b228c12aa233764e2bc96edde602736b68 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 24 Oct 2025 09:21:02 +0000 Subject: [PATCH 065/141] Latest translations and fallbacks --- public/language/pl/admin/settings/uploads.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/pl/admin/settings/uploads.json b/public/language/pl/admin/settings/uploads.json index 561a29afc0..87d598de8b 100644 --- a/public/language/pl/admin/settings/uploads.json +++ b/public/language/pl/admin/settings/uploads.json @@ -22,7 +22,7 @@ "reject-image-height": "Maksymalna wysokość obrazu (w pikselach)", "reject-image-height-help": "Obrazy o wysokości przekraczającej tę wartość zostaną odrzucone.", "allow-topic-thumbnails": "Zezwalaj użytkownikom na ustawianie miniaturek tematów", - "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", + "show-post-uploads-as-thumbnails": "Pokaż załączniki do wpisów w formie miniaturek", "topic-thumb-size": "Rozmiar miniatury tematu", "allowed-file-extensions": "Dozwolone typy plików", "allowed-file-extensions-help": "Wprowadź rozdzielone przecinkami rozszerzenia plików (np. pdf,xls,doc). Pusta lista oznacza, że wszystkie rozszerzenia są dozwolone.", From 418717fdff7606651f9bc66ad9a597bd024a823d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:16:38 -0400 Subject: [PATCH 066/141] fix(deps): update dependency redis to v5.9.0 (#13727) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 8249e4e99a..7f46a6804a 100644 --- a/install/package.json +++ b/install/package.json @@ -124,7 +124,7 @@ "pretty": "^2.0.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", - "redis": "5.8.3", + "redis": "5.9.0", "rimraf": "6.0.1", "rss": "1.2.2", "rtlcss": "4.3.0", From ab9154aa49a132b28b2a938b19c143e1a638b041 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 24 Oct 2025 13:32:04 -0400 Subject: [PATCH 067/141] fix: logic error in out.remove.context --- src/activitypub/out.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/activitypub/out.js b/src/activitypub/out.js index 40d597db51..9c9bdb5dba 100644 --- a/src/activitypub/out.js +++ b/src/activitypub/out.js @@ -376,7 +376,10 @@ Out.move.context = enabledCheck(async (uid, tid) => { const isLocal = id => utils.isNumber(id) && parseInt(id, 10) > 0; if (isLocal(oldCid) && !isLocal(cid)) { // moving to remote/uncategorized return Out.remove.context(uid, tid); - } else if ((isLocal(cid) && !isLocal(oldCid)) || [cid, oldCid].every(!isLocal)) { // stealing or remote-to-remote + } else if ( + (isLocal(cid) && !isLocal(oldCid)) || // stealing, or + [cid, oldCid].every(id => !isLocal(id)) // remote-to-remote + ) { return; } From ff5f65bfa1cbdcab9738c8d488fb2ff601af2a07 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 27 Oct 2025 09:21:45 +0000 Subject: [PATCH 068/141] Latest translations and fallbacks --- .../zh-CN/admin/settings/activitypub.json | 6 ++-- .../language/zh-CN/admin/settings/chat.json | 12 +++---- .../zh-CN/admin/settings/uploads.json | 8 ++--- public/language/zh-CN/aria.json | 12 +++---- public/language/zh-CN/category.json | 18 +++++----- public/language/zh-CN/email.json | 22 ++++++------ public/language/zh-CN/error.json | 10 +++--- public/language/zh-CN/global.json | 16 ++++----- public/language/zh-CN/groups.json | 2 +- public/language/zh-CN/modules.json | 36 +++++++++---------- public/language/zh-CN/notifications.json | 2 +- public/language/zh-CN/register.json | 2 +- public/language/zh-CN/themes/persona.json | 2 +- 13 files changed, 74 insertions(+), 74 deletions(-) diff --git a/public/language/zh-CN/admin/settings/activitypub.json b/public/language/zh-CN/admin/settings/activitypub.json index 5a0ef40eca..4effab4baf 100644 --- a/public/language/zh-CN/admin/settings/activitypub.json +++ b/public/language/zh-CN/admin/settings/activitypub.json @@ -30,9 +30,9 @@ "rules.cid": "版块", "relays": "中继服务", - "relays.intro": "中继服务能提升您NodeBB论坛内容的发现效率。订阅中继服务意味着:中继服务接收的内容将转发至此处,而本论坛发布的内容则由中继服务向外同步传播。", - "relays.warning": "注:中继服务可能接收大量流量,并可能增加存储和处理成本。", - "relays.litepub": "NodeBB 遵循 LitePub 风格的中继标准。您在此处输入的URL应以 /actor 结尾。", + "relays.intro": "中继服务能提升您 NodeBB 论坛内容的发现效率。订阅中继服务意味着:中继服务接收的内容将转发至此处,而本论坛发布的内容则由中继服务向外同步传播。", + "relays.warning": "注意:中继服务可能接收大量传入流量,并可能增加存储和处理成本。", + "relays.litepub": "NodeBB 遵循 LitePub 风格的中继标准。您在此处输入的 URL 应以 /actor 结尾。", "relays.add": "添加新中继服务", "relays.relay": "中继服务", "relays.state": "状态", diff --git a/public/language/zh-CN/admin/settings/chat.json b/public/language/zh-CN/admin/settings/chat.json index d7c8f595a3..ac92fca1e4 100644 --- a/public/language/zh-CN/admin/settings/chat.json +++ b/public/language/zh-CN/admin/settings/chat.json @@ -5,13 +5,13 @@ "disable-editing": "禁止编辑/删除聊天消息", "disable-editing-help": "管理员和超级管理员不受此限制", "max-length": "聊天信息的最大长度", - "max-length-remote": "远程聊天信息的最长长度", - "max-length-remote-help": "对于本地用户,该值通常设置为高于聊天消息最大值,因为远程消息往往较长(包含 @ 提及等)。", + "max-length-remote": "远程聊天消息的最大长度", + "max-length-remote-help": "该值通常设置得高于本地用户的聊天消息上限,因为远程消息往往更长(包含@提及等内容)", "max-chat-room-name-length": "聊天室名称最大长度", "max-room-size": "聊天室的最多用户数", "delay": "发言频率(毫秒)", - "notification-delay": "聊天信息的通知延迟", - "notification-delay-help": "服务器可能无法承受即时通讯所带来的资源占用,因此在这段时间内发送的其他信息将被整理,并在每个延迟期通知用户一次。设置为 0 则禁用延迟。", - "restrictions.seconds-edit-after": "聊天信息保持可编辑状态的秒数。", - "restrictions.seconds-delete-after": "聊天信息保持可撤回状态的秒数。" + "notification-delay": "聊天消息通知延迟", + "notification-delay-help": "在此期间发送的额外消息将被汇总,并在每个延迟周期内向用户发送一次通知。设置为0可禁用延迟功能。", + "restrictions.seconds-edit-after": "聊天消息保持可编辑状态的秒数。", + "restrictions.seconds-delete-after": "聊天消息保持可撤回状态的秒数。" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/uploads.json b/public/language/zh-CN/admin/settings/uploads.json index 0c3cee593d..c2cc7c587e 100644 --- a/public/language/zh-CN/admin/settings/uploads.json +++ b/public/language/zh-CN/admin/settings/uploads.json @@ -5,14 +5,14 @@ "strip-exif-data": "去除 EXIF 数据", "preserve-orphaned-uploads": "当一个帖子被清除后保留上传的文件", "orphanExpiryDays": "保存未使用文件的天数", - "orphanExpiryDays-help": "超过此天数后,无主的上传文件将从文件系统中删除。
设置为 0 或留空表示禁用。", + "orphanExpiryDays-help": "超过此天数后,未使用的文件将从文件系统中删除。
设置为 0 或留空表示禁用。", "private-extensions": "自定义文件扩展名", "private-uploads-extensions-help": "在此处输入以逗号分隔的文件扩展名列表 (例如 pdf,xls,doc )并将其用于自定义。为空则表示允许所有扩展名。", "resize-image-width-threshold": "如果图像宽度超过指定大小,则对图像进行缩放", - "resize-image-width-threshold-help": "(单位:像素,默认值:2000 px,设为 0 则禁用)", + "resize-image-width-threshold-help": "(单位:像素,默认值:2000 px,设置为 0 则禁用)", "resize-image-width": "缩小图片到指定宽度", "resize-image-width-help": "(像素单位,默认 760 px,设置为0以禁用)", - "resize-image-keep-original": "调整大小后保留原始图片", + "resize-image-keep-original": "缩放后保留原始图片", "resize-image-quality": "调整图像大小时使用的质量", "resize-image-quality-help": "使用较低质量的设置来减小调整过大小的图像的文件大小", "max-file-size": "最大文件尺寸(单位 KiB)", @@ -22,7 +22,7 @@ "reject-image-height": "图片最大高度值(单位:像素)", "reject-image-height-help": "高于此数值大小的图片将会被拒绝", "allow-topic-thumbnails": "允许用户上传主题缩略图", - "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", + "show-post-uploads-as-thumbnails": "将帖子上传内容显示为缩略图", "topic-thumb-size": "主题缩略图大小", "allowed-file-extensions": "允许的文件扩展名", "allowed-file-extensions-help": "在此处输入以逗号分隔的文件扩展名列表 (例如 pdf,xls,doc )。 为空则表示允许所有扩展名。", diff --git a/public/language/zh-CN/aria.json b/public/language/zh-CN/aria.json index 9df98227c1..2467cf769b 100644 --- a/public/language/zh-CN/aria.json +++ b/public/language/zh-CN/aria.json @@ -1,9 +1,9 @@ { - "post-sort-option": "帖子分类选项,%1", - "topic-sort-option": "主题分类选项,%1", - "user-avatar-for": "用户头像%1", - "profile-page-for": "用户 %1 的资料页面", - "user-watched-tags": "用户关注标签", + "post-sort-option": "帖子排序选项,%1", + "topic-sort-option": "主题排序选项,%1", + "user-avatar-for": "%1 的用户头像", + "profile-page-for": "用户 %1 的个人资料页面", + "user-watched-tags": "用户关注的标签", "delete-upload-button": "删除上传按钮", - "group-page-link-for": "群组页面链接%1" + "group-page-link-for": "%1 的群组页面链接" } \ No newline at end of file diff --git a/public/language/zh-CN/category.json b/public/language/zh-CN/category.json index 27a3f239b0..a03dcf42c6 100644 --- a/public/language/zh-CN/category.json +++ b/public/language/zh-CN/category.json @@ -2,12 +2,12 @@ "category": "版块", "subcategories": "子版块", "uncategorized": "未分类的", - "uncategorized.description": "不完全符合任何现有类别的主题", - "handle.description": "您可以通过 %1 标签在开放的社交网络上关注这一版块。", + "uncategorized.description": "不严格符合任何现有版块的主题", + "handle.description": "此版块可通过社交网络公开平台使用用户名 %1 进行关注", "new-topic-button": "发表主题", "guest-login-post": "登录以发布", - "no-topics": "此版块还没有任何内容。
赶紧来发帖吧!", - "no-followers": "本网站无人跟踪或关注此版块。跟踪或关注此版块,以便开始接收更新。", + "no-topics": "此版块下尚无主题。
何不尝试发布一个?", + "no-followers": "本网站上没有用户正在关注或订阅此版块。请关注或订阅此版块,以便开始接收更新。", "browsing": "正在浏览", "no-replies": "尚无回复", "no-new-posts": "没有新主题", @@ -17,14 +17,14 @@ "tracking": "跟踪", "not-watching": "未关注", "ignoring": "已忽略", - "watching.description": "有新主题时通知我。
在未读/最近主题中显示。", + "watching.description": "有新主题时通知我。
在未读/最近主题中显示", "tracking.description": "显示未读和最近的主题", "not-watching.description": "不显示未读主题,显示最近主题", "ignoring.description": "不在未读和最近主题显示", - "watching.message": "您关注了此版块和全部子版块的动态。", - "tracking.message": "您关注了此版块和全部子版块的动态。", - "notwatching.message": "您未关注了此版块和全部子版块的动态。", - "ignoring.message": "您未关注此版块和全部子版块的动态。", + "watching.message": "您正在关注此版块及其所有子版块的更新", + "tracking.message": "您正在订阅此版块及其所有子版块的更新", + "notwatching.message": "您未订阅此版块及其所有子版块的更新", + "ignoring.message": "您正在忽略此版块及其所有子版块的更新", "watched-categories": "已关注的版块", "x-more-categories": "还有 %1 个版块" } \ No newline at end of file diff --git a/public/language/zh-CN/email.json b/public/language/zh-CN/email.json index da067df94d..137ea80a8e 100644 --- a/public/language/zh-CN/email.json +++ b/public/language/zh-CN/email.json @@ -6,26 +6,26 @@ "greeting-no-name": "您好", "greeting-with-name": "%1,您好", "email.verify-your-email.subject": "请验证你的电子邮箱", - "email.verify.text1": "您已要求我们更改或确认您的邮件地址", - "email.verify.text2": "为了安全起见,我们只会在通过邮件验证邮件地址所有权以后才会更改存档的邮件地址。假如您没有提出过此请求,您不用进行任何操作。", - "email.verify.text3": "一旦您验证了此电子邮箱地址,我们将会把您当前的电子邮箱地址替换为此电子邮箱地址(%1)。", + "email.verify.text1": "您已要求我们更改或确认您的电子邮件地址", + "email.verify.text2": "出于安全考虑,我们仅在通过邮件确认邮箱所有权后,才会修改或确认系统中登记的邮箱地址。若您未主动提出此请求,则无需采取任何行动。", + "email.verify.text3": "一旦您确认此电子邮件地址,我们将用此地址(%1)替换您当前的电子邮件地址。", "welcome.text1": "感谢您注册 %1 帐户!", "welcome.text2": "在您验证您绑定的邮箱地址之后,您的账户才能激活。", - "welcome.text3": "管理员批准了您的注册申请,您现在可以使用您的用户名和密码进行登录了。", + "welcome.text3": "管理员已批准您的注册申请。您现在可以使用用户名/密码登录。", "welcome.cta": "点击这里确认您的电子邮箱地址", "invitation.text1": "%1 邀请您加入 %2", "invitation.text2": "您的邀请将在 %1 天后过期。", "invitation.cta": "点击这里新建账号", - "reset.text1": "很可能是您忘记了密码,我们收到了重置您帐户密码的请求。 如果不是这个情况,请忽略此邮件。", + "reset.text1": "我们收到重置您密码的请求,可能是您忘记了密码。若非如此,请忽略此邮件。", "reset.text2": "如需继续重置密码,请点击下面的链接:", "reset.cta": "点击这里重置您的密码", "reset.notify.subject": "更改密码成功", "reset.notify.text1": "您在 %1 上的密码已经成功修改。", "reset.notify.text2": "如果您没有授权此操作,请立即联系管理员。", - "digest.unread-rooms": "未读房间", + "digest.unread-rooms": "未读聊天室", "digest.room-name-unreadcount": "%1 (%2 未读)", "digest.latest-topics": "来自 %1 的最新主题", - "digest.top-topics": "来自 %1 的关注主题", + "digest.top-topics": "来自 %1 的热门主题", "digest.popular-topics": "来自 %1 的热门主题", "digest.cta": "点击这里访问 %1", "digest.unsub.info": "根据您的订阅设置,为您发送此摘要。", @@ -37,22 +37,22 @@ "digest.title.week": "您的每周摘要", "digest.title.month": "您的每月摘要", "notif.chat.new-message-from-user": "来自 \"%1\" 的新消息", - "notif.chat.new-message-from-user-in-room": "来自 %1 在房间 %2 中的新消息", + "notif.chat.new-message-from-user-in-room": "来自 %1 的新消息,在聊天室 %2 ", "notif.chat.cta": "点击这里继续会话", "notif.chat.unsub.info": "根据您的订阅设置,为您发送此聊天提醒。", "notif.post.unsub.info": "根据您的订阅设置,为您发送此回帖提醒。", - "notif.post.unsub.one-click": "或者通过点击来取消订阅邮件", + "notif.post.unsub.one-click": "或者,您可以点击此处取消订阅此类邮件", "notif.cta": "点击这里前往论坛", "notif.cta-new-reply": "查看帖子", "notif.cta-new-chat": "查看聊天", "notif.test.short": "测试通知", - "notif.test.long": "这是一个测试的通知邮件。", + "notif.test.long": "这是对通知邮件功能的测试。快来帮忙!", "test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。", "unsub.cta": "点击这里修改这些设置", "unsubscribe": "退订", "unsub.success": "您将不再收到来自%1邮寄名单的邮件", "unsub.failure.title": "无法取消订阅", - "unsub.failure.message": "很不幸,我们不能将您从邮件列表里取消订阅,因为这个链接有问题。不过,您可以到您的用户设置里修改邮件偏好。

(错误:%1)", + "unsub.failure.message": "很遗憾,由于链接出现问题,我们未能成功为您取消邮件订阅。不过您可通过 用户设置 页面调整邮件偏好选项。

(错误:%1)", "banned.subject": "您在 %1 的账户已被封禁", "banned.text1": "您在 %2 的用户 %1 已被封禁。", "banned.text2": "本次封禁将在 %1 结束。", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index e91848b78d..39fabf9c1b 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -39,7 +39,7 @@ "email-not-confirmed": "您需要验证您的邮箱后才能在版块或主题中发布帖子,请点击此处以发送验证邮件。", "email-not-confirmed-chat": "您的电子邮箱尚未确认,无法聊天,请点击这里确认您的电子邮箱。", "email-not-confirmed-email-sent": "您的邮箱尚未验证,请检查您的收件箱以找到验证邮件。在您的邮箱被验证前,您可能不能在某些版块发布帖子或进行聊天。", - "no-email-to-confirm": "您的账号未设置电子邮箱。对于找回账号、聊天以及在版块中发布帖子这几项操作,电子邮箱是必需的。请点击此处输入电子邮箱。", + "no-email-to-confirm": "您的账号尚未设置电子邮箱。电子邮箱是账号找回的必要条件,在某些分类中进行聊天和发帖时也可能需要。请点击此处输入电子邮箱。", "user-doesnt-have-email": "用户“%1”还没有设置邮箱。", "email-confirm-failed": "我们无法确认您的电子邮箱,请重试", "confirm-email-already-sent": "确认邮件已发出,如需重新发送请等待 %1 分钟后再试。", @@ -105,7 +105,7 @@ "still-uploading": "请等待上传完成", "file-too-big": "上传文件的大小限制为 %1 KB - 请缩减文件大小", "guest-upload-disabled": "未登录用户不允许上传", - "cors-error": "由于CORS配置错误,无法上传图片。", + "cors-error": "由于CORS配置错误,无法上传图片", "upload-ratelimit-reached": "您在短时间内上传了过多的文件,请稍后再试", "upload-error-fallback": "无法上传图片 — %1", "scheduling-to-past": "请选择一个未来的日期。", @@ -120,7 +120,7 @@ "cant-mute-other-admins": "您不能禁言其他管理员!", "user-muted-for-hours": "您已被禁言,您在 %1 小时后才能发布内容", "user-muted-for-minutes": "您已被禁言,您在 %1 分钟后才能发布内容", - "cant-make-banned-users-admin": "您不能让被禁止的用户成为管理员。", + "cant-make-banned-users-admin": "您无法将被封禁的用户设为管理员。", "cant-remove-last-admin": "您是唯一的管理员。在删除您的管理员权限前,请添加另一个管理员。", "account-deletion-disabled": "账号删除功能已禁用", "cant-delete-admin": "在删除此账号之前,请先移除其管理权限。", @@ -222,7 +222,7 @@ "no-groups-selected": "没有用户组被选中", "invalid-home-page-route": "无效的首页路径", "invalid-session": "无效的会话", - "invalid-session-text": "您的登录会话似乎不再处于活动状态。请刷新此页面。", + "invalid-session-text": "您的登录会话似乎已失效。请刷新此页面。", "session-mismatch": "会话不匹配", "session-mismatch-text": "您的登录会话似乎与服务器不再匹配。请刷新此页面。", "no-topics-selected": "没有主题被选中!", @@ -247,7 +247,7 @@ "cant-set-self-as-parent": "无法将自身设置为父版块", "api.master-token-no-uid": "收到一个在请求体中没有对应 `_uid` 的主令牌", "api.400": "您传入的请求某些地方出错了。", - "api.401": "找不到有效的登录会话。请登录后再试。", + "api.401": "未找到有效的登录会话。请登录后重试。", "api.403": "您没有权限使用此调用", "api.404": "无效 API 调用", "api.426": "Write API 的请求需要 HTTPS,请用 HTTPS 重新发送您的请求", diff --git a/public/language/zh-CN/global.json b/public/language/zh-CN/global.json index a98d700a71..0a9fbbfff8 100644 --- a/public/language/zh-CN/global.json +++ b/public/language/zh-CN/global.json @@ -3,14 +3,14 @@ "search": "搜索", "buttons.close": "关闭", "403.title": "禁止访问", - "403.message": "您似乎碰到了一个您没有访问权限的页面。", - "403.login": "请您尝试登录后再试", + "403.message": "您似乎意外访问了一个您无权访问的页面。", + "403.login": "或许您应该先尝试登录?", "404.title": "未找到", - "404.message": "你似乎偶然发现了一个不存在的页面。
回到主页
", + "404.message": "您似乎偶然访问了一个不存在的页面。
请返回主页
", "500.title": "内部错误", "500.message": "哎呀!看来是哪里出错了!", "400.title": "错误的请求", - "400.message": "看起来这个链接是畸形的,请仔细检查并重新尝试。
回到主页
", + "400.message": "该链接格式似乎有误,请重新检查后再次尝试。
返回主页
", "register": "注册", "login": "登录", "please-log-in": "请登录", @@ -46,7 +46,7 @@ "header.notifications": "通知", "header.search": "搜索", "header.profile": "设置", - "header.account": "账户", + "header.account": "账号", "header.navigation": "导航", "header.manage": "管理", "header.drafts": "草稿", @@ -60,7 +60,7 @@ "alert.warning": "警告", "alert.info": "信息", "alert.banned": "已封禁", - "alert.banned.message": "您已被禁止,您当前的访问受到限制。", + "alert.banned.message": "您已被封禁,您当前的访问受到限制。", "alert.unbanned": "已解封", "alert.unbanned.message": "你的封禁已被解除。", "alert.unfollow": "您已取消关注 %1!", @@ -68,8 +68,8 @@ "users": "用户", "topics": "主题", "posts": "帖子", - "x-posts": "%1 个帖子", - "x-topics": "%1 个主题", + "x-posts": "%1 个帖子", + "x-topics": "%1 个主题", "x-reputation": "%1声望", "best": "最佳", "controversial": "有争议的", diff --git a/public/language/zh-CN/groups.json b/public/language/zh-CN/groups.json index 74b4fbb097..3f9ba5a0f7 100644 --- a/public/language/zh-CN/groups.json +++ b/public/language/zh-CN/groups.json @@ -23,7 +23,7 @@ "details.members": "成员列表", "details.pending": "待加入成员", "details.invited": "已邀请成员", - "details.has-no-posts": "此用户组的成员尚未发表任何帖子。", + "details.has-no-posts": "此群组的成员尚未发表任何帖子。", "details.latest-posts": "最新帖子", "details.private": "私有", "details.disableJoinRequests": "禁用申请加入群组", diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json index d9ae48b598..21683139a5 100644 --- a/public/language/zh-CN/modules.json +++ b/public/language/zh-CN/modules.json @@ -1,15 +1,15 @@ { "chat.room-id": "房间 %1", - "chat.chatting-with": "与聊天", + "chat.chatting-with": "与...聊天", "chat.placeholder": "在此处输入聊天信息,拖放图片", "chat.placeholder.mobile": "输入聊天信息", "chat.placeholder.message-room": "消息 #%1", - "chat.scroll-up-alert": "转到最近的信息", + "chat.scroll-up-alert": "转到最近的消息", "chat.usernames-and-x-others": "%1 和 %2 其他人", - "chat.chat-with-usernames": "与聊天", + "chat.chat-with-usernames": "与 %1 聊天", "chat.chat-with-usernames-and-x-others": "与%1 & %2 和其他人聊天", "chat.send": "发送", - "chat.no-active": "暂无聊天", + "chat.no-active": "您当前没有活跃的聊天。", "chat.user-typing-1": "%1 正在输入...", "chat.user-typing-2": "%1%2 正在输入...", "chat.user-typing-3": "%1%2%3 正在输入...", @@ -57,26 +57,26 @@ "chat.add-user-help": "在这里查找更多用户。被选中的用户会被添加到聊天中。新用户不能他们被加入对话前的聊天消息。只有聊天室所有者()可以从聊天室中移除用户。", "chat.confirm-chat-with-dnd-user": "该用户已将其状态设置为 DnD(请勿打扰)。 您仍希望与其聊天吗?", "chat.room-name-optional": "房间名称(可选)", - "chat.rename-room": "重命名房间", - "chat.rename-placeholder": "在这里输入房间名字", - "chat.rename-help": "这里设置的房间名字能够被房间内所有人都看到。", + "chat.rename-room": "重命名聊天室", + "chat.rename-placeholder": "在这里输入聊天室名字", + "chat.rename-help": "这里设置的聊天室名字能够被聊天室内所有人都看到。", "chat.leave": "离开", "chat.leave-room": "离开房间", "chat.leave-prompt": "您确定您要离开聊天室?", "chat.leave-help": "离开此聊天会切断您和此聊天以后的联系。如果您未来重新加入了,您将不能看到您重新加入之前的聊天记录。", "chat.delete": "删除", "chat.delete-room": "删除房间", - "chat.delete-prompt": "您确定要删除此聊天室?", - "chat.in-room": "在此房间", + "chat.delete-prompt": "您确定您要删除此聊天室?", + "chat.in-room": "在此聊天室", "chat.kick": "踢出", "chat.show-ip": "显示 IP", "chat.copy-text": "复制文本", "chat.copy-link": "复制链接", - "chat.owner": "房间所有者", + "chat.owner": "聊天室所有者", "chat.grant-rescind-ownership": "给予/撤销所有权", - "chat.system.user-join": "%1 加入了房间", - "chat.system.user-leave": "%1 离开了房间", - "chat.system.room-rename": "%2 已将房间重命名为 \"%1\"", + "chat.system.user-join": "%1 加入了聊天室", + "chat.system.user-leave": "%1 离开了聊天室", + "chat.system.room-rename": "%2 已将此聊天室重命名为“%1” ", "composer.compose": "编写帮助", "composer.show-preview": "显示预览", "composer.hide-preview": "隐藏预览", @@ -85,7 +85,7 @@ "composer.user-said": "%1 说:", "composer.discard": "确定想要取消此帖?", "composer.submit-and-lock": "提交并锁定", - "composer.toggle-dropdown": "标为 Dropdown", + "composer.toggle-dropdown": "切换下拉菜单", "composer.uploading": "正在上传 %1", "composer.formatting.bold": "加粗", "composer.formatting.italic": "倾斜", @@ -106,7 +106,7 @@ "composer.zen-mode": "无干扰模式", "composer.select-category": "选择一个版块", "composer.textarea.placeholder": "在此处输入您的帖子内容,拖放图像", - "composer.post-queue-alert": "Hello👋!
This forum uses a post queue system, since you are a new user your post will be hidden until it is approved by our moderation team.", + "composer.post-queue-alert": "你好👋!
本论坛采用发帖队列系统,由于你是新用户,您的帖子在审核团队批准前将处于隐藏状态。", "composer.schedule-for": "定时主题到", "composer.schedule-date": "日期", "composer.schedule-time": "时间", @@ -114,8 +114,8 @@ "composer.change-schedule-date": "更改日期", "composer.set-schedule-date": "设置日期", "composer.discard-all-drafts": "丢弃所有的草稿", - "composer.no-drafts": "你没有草稿", - "composer.discard-draft-confirm": "你想丢弃这个草案吗?", + "composer.no-drafts": "您没有草稿", + "composer.discard-draft-confirm": "您想丢弃这个草稿吗?", "composer.remote-pid-editing": "编辑远程帖子", "composer.remote-pid-content-immutable": "远程帖子的内容不可编辑。不过,您可以更改主题标题和标签。", "bootbox.ok": "确认", @@ -128,7 +128,7 @@ "cover.saved": "封面照片和位置已保存", "thumbs.modal.title": "管理主题缩略图", "thumbs.modal.no-thumbs": "没有找到缩略图。", - "thumbs.modal.resize-note": "注意:此论坛被配置为缩放主题缩略图到最大值为 %1", + "thumbs.modal.resize-note": "注意:此论坛被配置为缩放主题缩略图到最大值为 %1px", "thumbs.modal.add": "添加缩略图", "thumbs.modal.remove": "移除缩略图", "thumbs.modal.confirm-remove": "您确定您要移除此缩略图吗?" diff --git a/public/language/zh-CN/notifications.json b/public/language/zh-CN/notifications.json index 7df8ab7fd0..533651060d 100644 --- a/public/language/zh-CN/notifications.json +++ b/public/language/zh-CN/notifications.json @@ -71,7 +71,7 @@ "users-csv-exported": "用户列表 CSV 已导出,点击以下载", "post-queue-accepted": "您先前提交的帖子已通过查验,点击这里查看您的帖子。", "post-queue-rejected": "您先前提交的帖子已被拒绝", - "post-queue-notify": "您先前提交的帖子收到了通知:“%1”", + "post-queue-notify": "您先前提交的帖子收到了通知:
“%1”", "email-confirmed": "电子邮箱已确认", "email-confirmed-message": "感谢您验证您的电子邮箱。您的帐户现已完全激活。", "email-confirm-error-message": "验证的您电子邮箱地址时出现了问题。可能是因为验证码无效或已过期。", diff --git a/public/language/zh-CN/register.json b/public/language/zh-CN/register.json index 9d03e65295..7edf5c6077 100644 --- a/public/language/zh-CN/register.json +++ b/public/language/zh-CN/register.json @@ -20,7 +20,7 @@ "terms-of-use-error": "您必须同意使用条款", "registration-added-to-queue": "您的注册正在等待批准。一旦通过,管理员会发送邮件通知您。", "registration-queue-average-time": "我们通常的注册批准时间为 %1 小时 %2 分钟。", - "registration-queue-auto-approve-time": "您在此论坛的帐号将会在最迟 %1  小时后被完全激活。", + "registration-queue-auto-approve-time": "您在此论坛的帐号将会在最迟 %1 小时后被完全激活。", "interstitial.intro": "我们需要一些额外信息以更新您的账号。", "interstitial.intro-new": "我们需要一些额外信息以创建您的账号。", "interstitial.errors-found": "请检查输入的信息:", diff --git a/public/language/zh-CN/themes/persona.json b/public/language/zh-CN/themes/persona.json index 18a9f2080f..fe468bb4a6 100644 --- a/public/language/zh-CN/themes/persona.json +++ b/public/language/zh-CN/themes/persona.json @@ -1,6 +1,6 @@ { "settings.title": "主题设置", - "settings.intro": "你可以在这里定制你的主题设置。设置是以每个设备为基础存储的,所以你能够在不同的设备上有不同的设置(手机、平板电脑、桌面等)。", + "settings.intro": "您可以在这里自定义主题设置。设置将按设备单独存储,因此您可以在不同设备(手机、平板、台式机等)上使用不同的设置。", "settings.mobile-menu-side": "移动端导航菜单切换到另一侧", "settings.autoHidingNavbar": "滚动时自动隐藏导航条", "settings.autoHidingNavbar-xs": "非常小的屏幕(如纵向模式的手机)。", From a49efe49ea7dbfb724e26a10ff66720893a2f328 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:57:57 -0400 Subject: [PATCH 069/141] fix(deps): update dependency commander to v14.0.2 (#13731) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 7f46a6804a..3a6155f6d7 100644 --- a/install/package.json +++ b/install/package.json @@ -53,7 +53,7 @@ "chart.js": "4.5.1", "cli-graph": "3.2.2", "clipboard": "2.0.11", - "commander": "14.0.1", + "commander": "14.0.2", "compare-versions": "6.1.1", "compression": "1.8.1", "connect-flash": "0.1.1", From 964a5388b72195381d0017ca15b1eca1a63926cc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 28 Oct 2025 13:40:35 -0400 Subject: [PATCH 070/141] fix(deps): bump mentions to 4.8.0 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 3a6155f6d7..d5883420fa 100644 --- a/install/package.json +++ b/install/package.json @@ -103,7 +103,7 @@ "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-link-preview": "2.1.5", "nodebb-plugin-markdown": "13.2.1", - "nodebb-plugin-mentions": "4.7.6", + "nodebb-plugin-mentions": "4.8.0", "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.5", "nodebb-rewards-essentials": "1.0.2", From 6f448ce2f6d433ad22dd9d8158da1c3793333039 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 11:47:10 -0400 Subject: [PATCH 071/141] fix(deps): update dependency validator to v13.15.20 (#13733) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d5883420fa..d225704739 100644 --- a/install/package.json +++ b/install/package.json @@ -149,7 +149,7 @@ "toobusy-js": "0.5.1", "tough-cookie": "6.0.0", "undici": "^7.10.0", - "validator": "13.15.15", + "validator": "13.15.20", "webpack": "5.102.1", "webpack-merge": "6.0.1", "winston": "3.18.3", From 524df6e5485aa833fc7ec55333908e99e1a7a0b0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 29 Oct 2025 12:32:21 -0400 Subject: [PATCH 072/141] fix: update category mock to save full handle --- src/activitypub/mocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 81db4b7a01..d83df6e1e1 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -320,7 +320,7 @@ Mocks.category = async (actors) => { const payload = { cid, name, - handle: preferredUsername, + handle: `${preferredUsername}@${hostname}`, slug: `${preferredUsername}@${hostname}`, description: summary, descriptionParsed: posts.sanitize(summary), From 5c3b126166e839c289453b9b6cad1dfb63356a8a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 29 Oct 2025 12:32:36 -0400 Subject: [PATCH 073/141] fix(deps): update mentions --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d225704739..197f6c2513 100644 --- a/install/package.json +++ b/install/package.json @@ -103,7 +103,7 @@ "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-link-preview": "2.1.5", "nodebb-plugin-markdown": "13.2.1", - "nodebb-plugin-mentions": "4.8.0", + "nodebb-plugin-mentions": "4.8.1", "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.5", "nodebb-rewards-essentials": "1.0.2", From 07d169d29e2665fa26141661a09f04a8c7624070 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:07:34 -0400 Subject: [PATCH 074/141] chore(deps): update dependency smtp-server to v3.16.0 (#13737) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 197f6c2513..6d2c10594d 100644 --- a/install/package.json +++ b/install/package.json @@ -177,7 +177,7 @@ "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", - "smtp-server": "3.15.0" + "smtp-server": "3.16.0" }, "optionalDependencies": { "sass-embedded": "1.93.2" From b5c1e8e7f624aa5dc4991de64c722b115771ba66 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:07:55 -0400 Subject: [PATCH 075/141] fix(deps): update dependency sitemap to v8.0.2 (#13736) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 6d2c10594d..4425799caf 100644 --- a/install/package.json +++ b/install/package.json @@ -135,7 +135,7 @@ "semver": "7.7.3", "serve-favicon": "2.5.1", "sharp": "0.34.4", - "sitemap": "8.0.1", + "sitemap": "8.0.2", "socket.io": "4.8.1", "socket.io-client": "4.8.1", "@socket.io/redis-adapter": "8.3.0", From 97e5aa1d1823ea1fae4cb8cd126687e654fcdefe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:08:05 -0400 Subject: [PATCH 076/141] chore(deps): update mongo docker tag to v8.2 (#13738) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 03dbbf8f3c..6304098eca 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -75,7 +75,7 @@ jobs: - 6379:6379 mongo: - image: 'mongo:8.0' + image: 'mongo:8.2' ports: # Maps port 27017 on service container to the host - 27017:27017 From a0a10c8b5cd667e699b1e1aef15e6a67d3f2b6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 29 Oct 2025 13:16:34 -0400 Subject: [PATCH 077/141] chore: up ttlcache to 2.x --- install/package.json | 2 +- src/cache/ttl.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 3a6155f6d7..ff5d94af5e 100644 --- a/install/package.json +++ b/install/package.json @@ -33,7 +33,7 @@ "@fontsource/inter": "5.2.8", "@fontsource/poppins": "5.2.7", "@fortawesome/fontawesome-free": "6.7.2", - "@isaacs/ttlcache": "1.4.1", + "@isaacs/ttlcache": "2.0.1", "@nodebb/spider-detector": "2.0.3", "@popperjs/core": "2.11.8", "@textcomplete/contenteditable": "0.1.13", diff --git a/src/cache/ttl.js b/src/cache/ttl.js index c8ed90af57..61cd4c07f4 100644 --- a/src/cache/ttl.js +++ b/src/cache/ttl.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = function (opts) { - const TTLCache = require('@isaacs/ttlcache'); + const { TTLCache } = require('@isaacs/ttlcache'); const os = require('os'); const winston = require('winston'); const chalk = require('chalk'); From f6219d0026bd977f96dc021c8bf35707ce7059e8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 29 Oct 2025 14:49:53 -0400 Subject: [PATCH 078/141] fix: update logic so that purging a post does not remove toPid fields from children, updated addParentPosts so that post existence is checked --- src/posts/delete.js | 15 +++++---------- src/topics/posts.js | 3 +++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/posts/delete.js b/src/posts/delete.js index dddb33e6a9..ba637ccff5 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -197,20 +197,15 @@ module.exports = function (Posts) { } async function deleteFromReplies(postData) { - const arrayOfReplyPids = await db.getSortedSetsMembers(postData.map(p => `pid:${p.pid}:replies`)); - const allReplyPids = _.flatten(arrayOfReplyPids); - const promises = [ - db.deleteObjectFields( - allReplyPids.map(pid => `post:${pid}`), ['toPid'] - ), - db.deleteAll(postData.map(p => `pid:${p.pid}:replies`)), - ]; + // Any replies to deleted posts will retain toPid reference (gh#13527) + await db.deleteAll(postData.map(p => `pid:${p.pid}:replies`)); + // Remove post(s) from parents' replies zsets const postsWithParents = postData.filter(p => parseInt(p.toPid, 10)); const bulkRemove = postsWithParents.map(p => [`pid:${p.toPid}:replies`, p.pid]); - promises.push(db.sortedSetRemoveBulk(bulkRemove)); - await Promise.all(promises); + await db.sortedSetRemoveBulk(bulkRemove); + // Recalculate reply count const parentPids = _.uniq(postsWithParents.map(p => p.toPid)); const counts = await db.sortedSetsCard(parentPids.map(pid => `pid:${pid}:replies`)); await db.setObjectBulk(parentPids.map((pid, index) => [`post:${pid}`, { replies: counts[index] }])); diff --git a/src/topics/posts.js b/src/topics/posts.js index 8201bcad02..41b819d219 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -184,6 +184,9 @@ module.exports = function (Topics) { .filter(p => p && p.hasOwnProperty('toPid') && (activitypub.helpers.isUri(p.toPid) || utils.isNumber(p.toPid))) .map(postObj => postObj.toPid); + const exists = await posts.exists(parentPids); + parentPids = parentPids.filter((_, idx) => exists[idx]); + if (!parentPids.length) { return; } From 30b1212a0ae0e40e6c18cd7cf6af7ce37e9a80c5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 29 Oct 2025 14:52:59 -0400 Subject: [PATCH 079/141] fix: relax toPid assertion checks so that it only checks that it is a number or uri --- src/posts/create.js | 20 ++------------------ test/topics.js | 45 --------------------------------------------- 2 files changed, 2 insertions(+), 63 deletions(-) diff --git a/src/posts/create.js b/src/posts/create.js index fa7ca1d071..064e337a94 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -7,7 +7,6 @@ const user = require('../user'); const topics = require('../topics'); const categories = require('../categories'); const groups = require('../groups'); -const privileges = require('../privileges'); const activitypub = require('../activitypub'); const utils = require('../utils'); @@ -24,8 +23,8 @@ module.exports = function (Posts) { throw new Error('[[error:invalid-uid]]'); } - if (data.toPid) { - await checkToPid(data.toPid, uid); + if (data.toPid && !utils.isNumber(data.toPid) && !activitypub.helpers.isUri(data.toPid)) { + throw new Error('[[error:invalid-pid]]'); } const pid = data.pid || await db.incrObjectField('global', 'nextPid'); @@ -101,19 +100,4 @@ module.exports = function (Posts) { db.incrObjectField(`post:${postData.toPid}`, 'replies'), ]); } - - async function checkToPid(toPid, uid) { - if (!utils.isNumber(toPid) && !activitypub.helpers.isUri(toPid)) { - throw new Error('[[error:invalid-pid]]'); - } - - const [toPost, canViewToPid] = await Promise.all([ - Posts.getPostFields(toPid, ['pid', 'deleted']), - privileges.posts.can('posts:view_deleted', toPid, uid), - ]); - const toPidExists = !!toPost.pid; - if (!toPidExists || (toPost.deleted && !canViewToPid)) { - throw new Error('[[error:invalid-pid]]'); - } - } }; diff --git a/test/topics.js b/test/topics.js index 7136280339..9f6b2c719b 100644 --- a/test/topics.js +++ b/test/topics.js @@ -314,51 +314,6 @@ describe('Topic\'s', () => { }); }); - it('should fail to create new reply with toPid that has been purged', async () => { - const { postData } = await topics.post({ - uid: topic.userId, - cid: topic.categoryId, - title: utils.generateUUID(), - content: utils.generateUUID(), - }); - await posts.purge(postData.pid, topic.userId); - - await assert.rejects( - topics.reply({ uid: topic.userId, content: 'test post', tid: postData.topic.tid, toPid: postData.pid }), - { message: '[[error:invalid-pid]]' } - ); - }); - - it('should fail to create a new reply with toPid that has been deleted (user cannot view_deleted)', async () => { - const { postData } = await topics.post({ - uid: topic.userId, - cid: topic.categoryId, - title: utils.generateUUID(), - content: utils.generateUUID(), - }); - await posts.delete(postData.pid, topic.userId); - const uid = await User.create({ username: utils.generateUUID().slice(0, 10) }); - - await assert.rejects( - topics.reply({ uid, content: 'test post', tid: postData.topic.tid, toPid: postData.pid }), - { message: '[[error:invalid-pid]]' } - ); - }); - - it('should properly create a new reply with toPid that has been deleted (user\'s own deleted post)', async () => { - const { postData } = await topics.post({ - uid: topic.userId, - cid: topic.categoryId, - title: utils.generateUUID(), - content: utils.generateUUID(), - }); - await posts.delete(postData.pid, topic.userId); - const uid = await User.create({ username: utils.generateUUID().slice(0, 10) }); - - const { pid } = await topics.reply({ uid: topic.userId, content: 'test post', tid: postData.topic.tid, toPid: postData.pid }); - assert(pid); - }); - it('should delete nested relies properly', async () => { const result = await topics.post({ uid: fooUid, title: 'nested test', content: 'main post', cid: topic.categoryId }); const reply1 = await topics.reply({ uid: fooUid, content: 'reply post 1', tid: result.topicData.tid }); From 748cc5eecda87f2f0c4335f2318c00689cf52a04 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 29 Oct 2025 15:15:01 -0400 Subject: [PATCH 080/141] fix: logic error in context generation --- src/controllers/activitypub/actors.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/activitypub/actors.js b/src/controllers/activitypub/actors.js index 850585e379..b4f4ddd055 100644 --- a/src/controllers/activitypub/actors.js +++ b/src/controllers/activitypub/actors.js @@ -178,7 +178,9 @@ Actors.topic = async function (req, res, next) { } // Convert pids to urls - collection.orderedItems = collection.orderedItems.map(pid => (utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid)); + if (collection.orderedItems) { + collection.orderedItems = collection.orderedItems.map(pid => (utils.isNumber(pid) ? `${nconf.get('url')}/post/${pid}` : pid)); + } const object = { '@context': 'https://www.w3.org/ns/activitystreams', From 4858abe1498a6c75d3f3b7108484d32f7e7439c6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 29 Oct 2025 15:18:13 -0400 Subject: [PATCH 081/141] fix: add replies in parallel during note assertion --- src/activitypub/notes.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index b0efa76e08..0673a99b0d 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -248,18 +248,16 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { } } - for (const post of unprocessed) { + await Promise.all(unprocessed.map(async (post) => { const { to, cc } = post._activitypub; try { - // eslint-disable-next-line no-await-in-loop await topics.reply(post); - // eslint-disable-next-line no-await-in-loop await Notes.updateLocalRecipients(post.pid, { to, cc }); } catch (e) { activitypub.helpers.log(`[activitypub/notes.assert] Could not add reply (${post.pid}): ${e.message}`); } - } + })); await Notes.syncUserInboxes(tid, uid); From 425d2eb2954f6c32aec6cb8c3d58712965ab49e9 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 30 Oct 2025 09:20:53 +0000 Subject: [PATCH 082/141] Latest translations and fallbacks --- public/language/ro/admin/admin.json | 26 +++--- .../language/ro/admin/advanced/database.json | 2 +- public/language/ro/admin/dashboard.json | 6 +- .../language/ro/admin/development/info.json | 4 +- .../language/ro/admin/manage/categories.json | 64 +++++++-------- .../language/ro/admin/manage/privileges.json | 2 +- .../ro/admin/manage/user-custom-fields.json | 52 ++++++------ public/language/ro/admin/menu.json | 2 +- .../ro/admin/settings/activitypub.json | 82 +++++++++---------- public/language/ro/admin/settings/chat.json | 4 +- public/language/ro/admin/settings/email.json | 4 +- .../language/ro/admin/settings/general.json | 4 +- .../ro/admin/settings/navigation.json | 2 +- public/language/ro/admin/settings/post.json | 6 +- .../language/ro/admin/settings/uploads.json | 6 +- public/language/ro/admin/settings/user.json | 2 +- public/language/ro/pages.json | 6 +- public/language/ro/post-queue.json | 8 +- public/language/ro/recent.json | 4 +- public/language/ro/search.json | 2 +- public/language/ro/social.json | 8 +- public/language/ro/tags.json | 2 +- public/language/ro/topic.json | 36 ++++---- public/language/ro/unread.json | 2 +- public/language/ro/users.json | 2 +- public/language/ro/world.json | 32 ++++---- 26 files changed, 185 insertions(+), 185 deletions(-) diff --git a/public/language/ro/admin/admin.json b/public/language/ro/admin/admin.json index 96c58b1733..7be102ffdc 100644 --- a/public/language/ro/admin/admin.json +++ b/public/language/ro/admin/admin.json @@ -1,18 +1,18 @@ { - "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", - "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + "alert.confirm-rebuild-and-restart": "Sigur dorești să reconstruiești și să repornești NodeBB?", + "alert.confirm-restart": "Sigur dorești să repornești NodeBB?", - "acp-title": "%1 | NodeBB Admin Control Panel", - "settings-header-contents": "Contents", - "changes-saved": "Changes Saved", - "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", - "changes-not-saved": "Changes Not Saved", - "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)", - "save-changes": "Save changes", + "acp-title": "%1 | Panou de control NodeBB", + "settings-header-contents": "Conținut", + "changes-saved": "Modificări Salvate", + "changes-saved-message": "Modificările aduse configurației NodeBB au fost salvate.", + "changes-not-saved": "Modificări Nesalvate", + "changes-not-saved-message": "NodeBB a întâmpinat o problemă la salvarea modificărilor. (%1)", + "save-changes": "Salvați modificările", "min": "Min:", "max": "Max:", - "view": "View", - "edit": "Edit", - "add": "Add", - "select-icon": "Select Icon" + "view": "Vizualizare", + "edit": "Modifică", + "add": "Adaugă", + "select-icon": "Selectați Icon" } \ No newline at end of file diff --git a/public/language/ro/admin/advanced/database.json b/public/language/ro/admin/advanced/database.json index 55eea6c023..f67e83859a 100644 --- a/public/language/ro/admin/advanced/database.json +++ b/public/language/ro/admin/advanced/database.json @@ -17,7 +17,7 @@ "mongo.file-size": "File Size", "mongo.resident-memory": "Resident Memory", "mongo.virtual-memory": "Virtual Memory", - "mongo.mapped-memory": "Mapped Memory", + "mongo.mapped-memory": "Memorie mapată", "mongo.bytes-in": "Bytes In", "mongo.bytes-out": "Bytes Out", "mongo.num-requests": "Number of Requests", diff --git a/public/language/ro/admin/dashboard.json b/public/language/ro/admin/dashboard.json index 0be6d5866c..9e4b880154 100644 --- a/public/language/ro/admin/dashboard.json +++ b/public/language/ro/admin/dashboard.json @@ -75,7 +75,7 @@ "graphs.page-views-registered": "Page Views Registered", "graphs.page-views-guest": "Page Views Guest", "graphs.page-views-bot": "Page Views Bot", - "graphs.page-views-ap": "ActivityPub Page Views", + "graphs.page-views-ap": "Vizualizări Pagină ActivityPub", "graphs.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", @@ -96,7 +96,7 @@ "expand-analytics": "Expand analytics", "clear-search-history": "Clear Search History", "clear-search-history-confirm": "Are you sure you want to clear entire search history?", - "search-term": "Term", + "search-term": "Termen", "search-count": "Count", - "view-all": "View all" + "view-all": "Arată Tot" } diff --git a/public/language/ro/admin/development/info.json b/public/language/ro/admin/development/info.json index 9834719daf..782b9133e0 100644 --- a/public/language/ro/admin/development/info.json +++ b/public/language/ro/admin/development/info.json @@ -3,7 +3,7 @@ "ip": "IP %1", "nodes-responded": "%1 nodes responded within %2ms!", "host": "host", - "primary": "primary / jobs", + "primary": "principal / job-uri", "pid": "pid", "nodejs": "nodejs", "online": "online", @@ -19,7 +19,7 @@ "registered": "Registered", "sockets": "Sockets", - "connection-count": "Connection Count", + "connection-count": "Număr Conexiuni", "guests": "Guests", "info": "Info" diff --git a/public/language/ro/admin/manage/categories.json b/public/language/ro/admin/manage/categories.json index 38037f7206..11e06b840f 100644 --- a/public/language/ro/admin/manage/categories.json +++ b/public/language/ro/admin/manage/categories.json @@ -1,25 +1,25 @@ { "manage-categories": "Manage Categories", "add-category": "Add category", - "add-local-category": "Add Local category", - "add-remote-category": "Add Remote category", - "remove": "Remove", - "rename": "Rename", + "add-local-category": "Adăugați Categorie Locală", + "add-remote-category": "Adăugă Categorie de la Distanță", + "remove": "Elimină", + "rename": "Redenumește", "jump-to": "Jump to...", "settings": "Category Settings", "edit-category": "Edit Category", "privileges": "Privileges", "back-to-categories": "Back to categories", - "id": "Category ID", + "id": "ID Categorie", "name": "Category Name", - "handle": "Category Handle", - "handle.help": "Your category handle is used as a representation of this category across other networks, similar to a username. A category handle must not match an existing username or user group.", + "handle": "Identificator Categorie", + "handle.help": "Identificatorul categoriei este folosit ca o reprezentare a acestei categorii în alte rețele, similar unui nume de utilizator. Un identificator de categorie nu trebuie să corespundă unui nume de utilizator sau unui grup de utilizatori existent.", "description": "Category Description", - "federatedDescription": "Federated Description", - "federatedDescription.help": "This text will be appended to the category description when queried by other websites/apps.", - "federatedDescription.default": "This is a forum category containing topical discussion. You can start new discussions by mentioning this category.", - "topic-template": "Topic Template", - "topic-template.help": "Define a template for new topics created in this category.", + "federatedDescription": "Descriere Federată", + "federatedDescription.help": "Acest text va fi adăugat la descrierea categoriei atunci când va fi interogat de alte site-uri web/aplicații.", + "federatedDescription.default": "Aceasta este o categorie de forum care conține discuții pe teme. Puteți începe discuții noi menționând această categorie.", + "topic-template": "Șablon Subiect", + "topic-template.help": "Definiți un șablon pentru subiectele noi create în această categorie.", "bg-color": "Background Colour", "text-color": "Text Colour", "bg-image-size": "Background Image Size", @@ -49,7 +49,7 @@ "disable": "Disable", "edit": "Edit", "analytics": "Analytics", - "federation": "Federation", + "federation": "Federație", "view-category": "View category", "set-order": "Set order", @@ -89,32 +89,32 @@ "analytics.topics-daily": "Figure 3 – Daily topics created in this category", "analytics.posts-daily": "Figure 4 – Daily posts made in this category", - "federation.title": "Federation settings for \"%1\" category", - "federation.disabled": "Federation is disabled site-wide, so category federation settings are currently unavailable.", - "federation.disabled-cta": "Federation Settings →", - "federation.syncing-header": "Synchronization", - "federation.syncing-intro": "A category can follow a \"Group Actor\" via the ActivityPub protocol. If content is received from one of the actors listed below, it will be automatically added to this category.", - "federation.syncing-caveat": "N.B. Setting up syncing here establishes a one-way synchronization. NodeBB attempts to subscribe/follow the actor, but the reverse cannot be assumed.", - "federation.syncing-none": "This category is not currently following anybody.", - "federation.syncing-add": "Synchronize with...", + "federation.title": "Setări Federație pentru categoria \"%1\"", + "federation.disabled": "Federația este dezactivată la nivel de site, așadar setările de federare a categoriilor nu sunt disponibile în prezent.", + "federation.disabled-cta": "Setări Federație →", + "federation.syncing-header": "Sincronizare", + "federation.syncing-intro": "O categorie poate urma un „Actor de grup” prin protocolul ActivityPub. Dacă se primește conținut de la unul dintre actorii enumerați mai jos, acesta va fi adăugat automat în această categorie.", + "federation.syncing-caveat": "Notă: Configurarea sincronizării aici stabilește o sincronizare unidirecțională. NodeBB încearcă să se aboneze/să urmărească actorul, dar nu se poate presupune inversul.", + "federation.syncing-none": "Această categorie nu urmărește pe nimeni în prezent.", + "federation.syncing-add": "Sincronizează cu...", "federation.syncing-actorUri": "Actor", - "federation.syncing-follow": "Follow", - "federation.syncing-unfollow": "Unfollow", - "federation.followers": "Remote users following this category", - "federation.followers-handle": "Handle", + "federation.syncing-follow": "Urmărește", + "federation.syncing-unfollow": "Nu urmări", + "federation.followers": "Utilizatori la distanță care urmăresc această categorie", + "federation.followers-handle": "Identificator", "federation.followers-id": "ID", - "federation.followers-none": "No followers.", - "federation.followers-autofill": "Autofill", + "federation.followers-none": "Niciun urmăritor.", + "federation.followers-autofill": "Completare automată", "alert.created": "Created", "alert.create-success": "Category successfully created!", "alert.none-active": "You have no active categories.", "alert.create": "Create a Category", - "alert.add": "Add a Category", - "alert.add-help": "Remote categories can be added to the categories listing by specifying their handle.

Note — The remote category may not reflect all topics published unless at least one local user tracks/watches it.", - "alert.rename": "Rename a Remote Category", - "alert.rename-help": "Please enter a new name for this category. Leave blank to restore original name.", - "alert.confirm-remove": "Do you really want to remove this category? You can add it back at any time.", + "alert.add": "Adăugă Categorie", + "alert.add-help": "Categoriile la distanță pot fi adăugate la lista de categorii specificând identificatorul/identificatorul acestora.

Notă — Categoria la distanță poate să nu reflecte toate subiectele publicate, cu excepția cazului în care cel puțin un utilizator local o urmărește/o urmărește.", + "alert.rename": "Redenumiți o categorie de la distanță", + "alert.rename-help": "Vă rugăm să introduceți un nume nou pentru această categorie. Lăsați câmpul necompletat pentru a restaura numele original.", + "alert.confirm-remove": "Sigur doriți să eliminați această categorie? O puteți adăuga din nou oricând.", "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", "alert.purge-success": "Category purged!", "alert.copy-success": "Settings Copied!", diff --git a/public/language/ro/admin/manage/privileges.json b/public/language/ro/admin/manage/privileges.json index 240cff6aa5..1b0a547853 100644 --- a/public/language/ro/admin/manage/privileges.json +++ b/public/language/ro/admin/manage/privileges.json @@ -8,7 +8,7 @@ "edit-privileges": "Edit Privileges", "select-clear-all": "Select/Clear All", "chat": "Chat", - "chat-with-privileged": "Chat with Privileged", + "chat-with-privileged": "Conversează cu cineva cu drepturi", "upload-images": "Upload Images", "upload-files": "Upload Files", "signature": "Signature", diff --git a/public/language/ro/admin/manage/user-custom-fields.json b/public/language/ro/admin/manage/user-custom-fields.json index dab10670d2..a2c2da240d 100644 --- a/public/language/ro/admin/manage/user-custom-fields.json +++ b/public/language/ro/admin/manage/user-custom-fields.json @@ -1,28 +1,28 @@ { - "title": "Manage Custom User Fields", - "create-field": "Create Field", - "edit-field": "Edit Field", - "manage-custom-fields": "Manage Custom Fields", - "type-of-input": "Type of input", - "key": "Key", - "name": "Name", - "icon": "Icon", - "type": "Type", - "min-rep": "Minimum Reputation", - "input-type-text": "Input (Text)", - "input-type-link": "Input (Link)", - "input-type-number": "Input (Number)", - "input-type-date": "Input (Date)", - "input-type-select": "Select", - "input-type-select-multi": "Select Multiple", - "select-options": "Options", - "select-options-help": "Add one option per line for the select element", - "minimum-reputation": "Minimum reputation", - "minimum-reputation-help": "If a user has less than this value they won't be able to use this field", - "delete-field-confirm-x": "Do you really want to delete custom field \"%1\"?", - "custom-fields-saved": "Custom fields saved", - "visibility": "Visibility", - "visibility-all": "Everyone can see the field", - "visibility-loggedin": "Only logged in users can see the field", - "visibility-privileged": "Only privileged users like admins & moderators can see the field" + "title": "Gestionarea câmpurilor personalizate ale utilizatorilor", + "create-field": "Creare Câmp", + "edit-field": "Modificare Câmp", + "manage-custom-fields": "Administrare Câmpuri Personalizate", + "type-of-input": "Tipul editorului", + "key": "Cheie", + "name": "Nume", + "icon": "Iconîță", + "type": "Tip", + "min-rep": "Reputație Minimă", + "input-type-text": "Editor (Text)", + "input-type-link": "Editor (Link)", + "input-type-number": "Editor (Număr)", + "input-type-date": "Editor (Dată)", + "input-type-select": "Selecție", + "input-type-select-multi": "Selecție Multiplă", + "select-options": "Opțiuni", + "select-options-help": "Adăugați pe linie câte o opțiune pentru elementul select", + "minimum-reputation": "Reputație Minimă", + "minimum-reputation-help": "Dacă un utilizator are o valoare mai mică decât această, nu va putea folosi acest câmp.", + "delete-field-confirm-x": "Sigur doriți să ștergeți câmpul personalizat „%1”?", + "custom-fields-saved": "Câmpuri personalizate salvate", + "visibility": "Vizibilitate", + "visibility-all": "Oricine poate vedea câmpul", + "visibility-loggedin": "Doar utilizatorii autentificați pot vedea câmpul", + "visibility-privileged": "Doar utilizatorii privilegiați ca administratori sau moderatori pot vedea câmpul" } \ No newline at end of file diff --git a/public/language/ro/admin/menu.json b/public/language/ro/admin/menu.json index 913c74f475..8114756e80 100644 --- a/public/language/ro/admin/menu.json +++ b/public/language/ro/admin/menu.json @@ -38,7 +38,7 @@ "settings/tags": "Tags", "settings/notifications": "Notifications", "settings/api": "API Access", - "settings/activitypub": "Federation (ActivityPub)", + "settings/activitypub": "Federație (ActivityPub)", "settings/sounds": "Sounds", "settings/social": "Social", "settings/cookies": "Cookies", diff --git a/public/language/ro/admin/settings/activitypub.json b/public/language/ro/admin/settings/activitypub.json index 5ab4fa43e8..e94dfe6b6d 100644 --- a/public/language/ro/admin/settings/activitypub.json +++ b/public/language/ro/admin/settings/activitypub.json @@ -1,48 +1,48 @@ { - "intro-lead": "What is Federation?", - "intro-body": "NodeBB is able to communicate with other NodeBB instances that support it. This is achieved through a protocol called ActivityPub. If enabled, NodeBB will also be able to communicate with other apps and websites that use ActivityPub (e.g. Mastodon, Peertube, etc.)", + "intro-lead": "Ce este Federația?", + "intro-body": "NodeBB poate comunica cu alte instanțe NodeBB care îl suportă. Acest lucru se realizează printr-un protocol numit ActivityPub. Dacă este activat, NodeBB va putea comunica și cu alte aplicații și site-uri web care utilizează ActivityPub (de exemplu, Mastodon, Peertube etc.).", "general": "General", - "pruning": "Content Pruning", - "content-pruning": "Days to keep remote content", - "content-pruning-help": "Note that remote content that has received engagement (a reply or a upvote/downvote) will be preserved. (0 for disabled)", - "user-pruning": "Days to cache remote user accounts", - "user-pruning-help": "Remote user accounts will only be pruned if they have no posts. Otherwise they will be re-retrieved. (0 for disabled)", - "enabled": "Enable Federation", - "enabled-help": "If enabled, will allow this NodeBB will be able to communicate with all Activitypub-enabled clients on the wider fediverse.", - "allowLoopback": "Allow loopback processing", - "allowLoopback-help": "Useful for debugging purposes only. You should probably leave this disabled.", + "pruning": "Eliminarea Conținutului", + "content-pruning": "Zile pentru păstrarea conținutului de la distanță", + "content-pruning-help": "Rețineți că va fi păstrat conținutul de la distanță cu care s-a interacționat (un răspuns sau un vot pozitiv/negativ). (0 pentru dezactivat)", + "user-pruning": "Zile pentru a păstra în memoria cache conturile de utilizatori de la distanță", + "user-pruning-help": "Conturile de utilizatori de la distanță vor fi eliminate doar dacă nu au postări. În caz contrar, vor fi recuperate. (0 pentru dezactivat)", + "enabled": "Activează Federația", + "enabled-help": "Dacă este activat, acest lucru va permite ca NodeBB să poată comunica cu toți clienții compatibili cu Activitypub de pe fediverse.", + "allowLoopback": "Permite procesarea loopback", + "allowLoopback-help": "Util doar pentru depanare. Probabil ar trebui să lași această opțiune dezactivată.", - "probe": "Open in App", - "probe-enabled": "Try to open ActivityPub-enabled resources in NodeBB", - "probe-enabled-help": "If enabled, NodeBB will check every external link for an ActivityPub equivalent, and load it in NodeBB instead.", - "probe-timeout": "Lookup Timeout (milliseconds)", - "probe-timeout-help": "(Default: 2000) If the lookup query does not receive a response within the set timeframe, will send the user to the link directly instead. Adjust this number higher if sites are responding slowly and you wish to give extra time.", + "probe": "Deschide în Aplicație", + "probe-enabled": "Încercă să deschidă resurse compatibile cu ActivityPub în NodeBB", + "probe-enabled-help": "Dacă este activat, NodeBB va verifica fiecare link extern pentru un echivalent ActivityPub și îl va încărca în NodeBB.", + "probe-timeout": "Timp de așteptare (milisecunde)", + "probe-timeout-help": "(Implicit: 2000) Dacă interogarea de căutare nu primește un răspuns în intervalul de timp setat, utilizatorul va fi direcționat direct către link. Ajustați acest număr mai mare dacă site-urile răspund lent și doriți să acordați timp suplimentar.", - "rules": "Categorization", - "rules-intro": "Content discovered via ActivityPub can be automatically categorized based on certain rules (e.g. hashtag)", - "rules.modal.title": "How it works", - "rules.modal.instructions": "Any incoming content is checked against these categorization rules, and matching content is automatically moved into the category of choice.

N.B. Content that is already categorized (i.e. in a remote category) will not pass through these rules.", - "rules.add": "Add New Rule", - "rules.help-hashtag": "Topics containing this case-insensitive hashtag will match. Do not enter the # symbol", - "rules.help-user": "Topics created by the entered user will match. Enter a handle or full ID (e.g. bob@example.org or https://example.org/users/bob.", - "rules.type": "Type", - "rules.value": "Value", - "rules.cid": "Category", + "rules": "Clasificare", + "rules-intro": "Conținutul descoperit prin ActivityPub poate fi clasificat automat pe baza anumitor reguli (de exemplu, hashtag)", + "rules.modal.title": "Cum funcționează", + "rules.modal.instructions": "Orice conținut primit este verificat în funcție de aceste reguli de clasificare, iar conținutul corespunzător este mutat automat în categoria aleasă.

N.B. Conținutul care este deja clasificat (adică într-o categorie de la distanță) nu va trece prin aceste reguli.", + "rules.add": "Adăugă Regulă Nouă", + "rules.help-hashtag": "Subiectele care conțin acest hashtag fără a ține cont de majuscule/minuscule se vor potrivi. Nu introduceți simbolul #", + "rules.help-user": "Subiectele create de utilizatorul introdus se vor potrivi. Introduceți un nume de utilizator sau un ID complet (e.g. bob@example.org sau https://example.org/users/bob.", + "rules.type": "Tip", + "rules.value": "Valoare", + "rules.cid": "Categorie", - "relays": "Relays", - "relays.intro": "A relay improves discovery of content to and from your NodeBB. Subscribing to a relay means content received by the relay is forwarded here, and content posted here is syndicated outward by the relay.", - "relays.warning": "Note: Relays can send larges amounts of traffic in, and may increase storage and processing costs.", - "relays.litepub": "NodeBB follows the LitePub-style relay standard. The URL you enter here should end with /actor.", - "relays.add": "Add New Relay", - "relays.relay": "Relay", - "relays.state": "State", - "relays.state-0": "Pending", - "relays.state-1": "Receiving only", - "relays.state-2": "Active", + "relays": "Retransmițători", + "relays.intro": "O funcție de retransmitere îmbunătățește descoperirea conținutului către și de la NodeBB-ul dvs. Abonarea la o funcție de retransmitere înseamnă că respectivul conținut primit de către retransmitere este redirecționat aici, iar conținutul postat aici este sindicalizat către exterior de către retransmitere.", + "relays.warning": "Notă: Releele pot trimite volume mari de trafic și pot crește costurile de stocare și procesare.", + "relays.litepub": "NodeBB respectă standardul de retransmisie în stil LitePub. URL-ul pe care îl introduceți aici ar trebui să se termine cu /actor.", + "relays.add": "Adaugă Retransmițător Nou", + "relays.relay": "Retransmițător", + "relays.state": "Stare", + "relays.state-0": "În Așteptare", + "relays.state-1": "Doar Primește", + "relays.state-2": "Activ", - "server-filtering": "Filtering", - "count": "This NodeBB is currently aware of %1 server(s)", - "server.filter-help": "Specify servers you would like to bar from federating with your NodeBB. Alternatively, you may opt to selectively allow federation with specific servers, instead. Both options are supported, although they are mutually exclusive.", - "server.filter-help-hostname": "Enter just the instance hostname below (e.g. example.org), separated by line breaks.", - "server.filter-allow-list": "Use this as an Allow List instead" + "server-filtering": "Filtrează", + "count": "NodeBB cunoaște acum %1 server(e)", + "server.filter-help": "Specificați serverele interzise a se conecta cu NodeBB-ul dvs. Alternativ, puteți opta să permiteți selectiv conectarea cu anumite servere. Ambele opțiuni sunt acceptate, deși se exclud reciproc.", + "server.filter-help-hostname": "Introduceți mai jos doar numele instanței (de exemplu, example.org), câte unul pe rând.", + "server.filter-allow-list": "Folosește ca Poziții Permise" } \ No newline at end of file diff --git a/public/language/ro/admin/settings/chat.json b/public/language/ro/admin/settings/chat.json index 6d6cad284b..0ffa932af3 100644 --- a/public/language/ro/admin/settings/chat.json +++ b/public/language/ro/admin/settings/chat.json @@ -5,8 +5,8 @@ "disable-editing": "Disable chat message editing/deletion", "disable-editing-help": "Administrators and global moderators are exempt from this restriction", "max-length": "Maximum length of chat messages", - "max-length-remote": "Maximum length of remote chat messages", - "max-length-remote-help": "This value is usually set higher than the chat message maximum for local users as remote messages tend to be longer (with @ mentions, etc.)", + "max-length-remote": "Lungimea maximă a mesajelor de chat la distanță", + "max-length-remote-help": "Această valoare este de obicei setată la o valoare mai mare decât numărul maxim de mesaje de chat pentru utilizatorii locali, deoarece mesajele la distanță tind să fie mai lungi (cu mențiuni @ etc.).", "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", diff --git a/public/language/ro/admin/settings/email.json b/public/language/ro/admin/settings/email.json index 0310939cb3..ac3d5eb0f9 100644 --- a/public/language/ro/admin/settings/email.json +++ b/public/language/ro/admin/settings/email.json @@ -28,8 +28,8 @@ "smtp-transport.password": "Password", "smtp-transport.pool": "Enable pooled connections", "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", - "smtp-transport.allow-self-signed": "Allow self-signed certificates", - "smtp-transport.allow-self-signed-help": "Enabling this setting will allow you to use self-signed or invalid TLS certificates.", + "smtp-transport.allow-self-signed": "Permite certificate self-signed", + "smtp-transport.allow-self-signed-help": "Activarea acestei setări vă va permite să utilizați certificate TLS self-signed sau invalide.", "template": "Edit Email Template", "template.select": "Select Email Template", diff --git a/public/language/ro/admin/settings/general.json b/public/language/ro/admin/settings/general.json index d56c819745..ebebb10fa5 100644 --- a/public/language/ro/admin/settings/general.json +++ b/public/language/ro/admin/settings/general.json @@ -15,7 +15,7 @@ "title-layout": "Title Layout", "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", "description.placeholder": "A short description about your community", - "description": "Site Description", + "description": "Descrierea site-ului", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", "logo-and-icons": "Site Logo & Icons", @@ -51,7 +51,7 @@ "topic-tools": "Topic Tools", "home-page": "Home Page", "home-page-route": "Home Page Route", - "home-page-description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-description": "Alegeți ce pagină este afișată când utilizatorii navighează la adresa URL rădăcină a forumului dvs.", "custom-route": "Custom Route", "allow-user-home-pages": "Allow User Home Pages", "home-page-title": "Title of the home page (default \"Home\")", diff --git a/public/language/ro/admin/settings/navigation.json b/public/language/ro/admin/settings/navigation.json index 3a71061ecf..130a14c17a 100644 --- a/public/language/ro/admin/settings/navigation.json +++ b/public/language/ro/admin/settings/navigation.json @@ -10,7 +10,7 @@ "id": "ID: optional", "properties": "Properties:", - "show-to-groups": "Show to Groups:", + "show-to-groups": "Afișare în Grupurile:", "open-new-window": "Open in a new window", "dropdown": "Dropdown", "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a class="dropdown-item" href="https://myforum.com">Link 1</a></li>", diff --git a/public/language/ro/admin/settings/post.json b/public/language/ro/admin/settings/post.json index e000f6b10b..5c11545151 100644 --- a/public/language/ro/admin/settings/post.json +++ b/public/language/ro/admin/settings/post.json @@ -4,11 +4,11 @@ "sorting.post-default": "Default Post Sorting", "sorting.oldest-to-newest": "Oldest to Newest", "sorting.newest-to-oldest": "Newest to Oldest", - "sorting.recently-replied": "Recently Replied", - "sorting.recently-created": "Recently Created", + "sorting.recently-replied": "Răspunse Recent", + "sorting.recently-created": "Create Recent", "sorting.most-votes": "Most Votes", "sorting.most-posts": "Most Posts", - "sorting.most-views": "Most Views", + "sorting.most-views": "Cele Mai Văzute", "sorting.topic-default": "Default Topic Sorting", "length": "Post Length", "post-queue": "Post Queue", diff --git a/public/language/ro/admin/settings/uploads.json b/public/language/ro/admin/settings/uploads.json index e91a7bee36..da12502884 100644 --- a/public/language/ro/admin/settings/uploads.json +++ b/public/language/ro/admin/settings/uploads.json @@ -9,10 +9,10 @@ "private-extensions": "File extensions to make private", "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", "resize-image-width-threshold": "Resize images if they are wider than specified width", - "resize-image-width-threshold-help": "(in pixels, default: 2000 pixels, set to 0 to disable)", + "resize-image-width-threshold-help": "(în pixeli, implicit: 2000 pixeli, setați la 0 pentru dezactivare)", "resize-image-width": "Resize images down to specified width", "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", - "resize-image-keep-original": "Keep original image after resize", + "resize-image-keep-original": "Păstrează imaginea originală după redimensionare", "resize-image-quality": "Quality to use when resizing images", "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", "max-file-size": "Maximum File Size (in KiB)", @@ -22,7 +22,7 @@ "reject-image-height": "Maximum Image Height (in pixels)", "reject-image-height-help": "Images taller than this value will be rejected.", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", - "show-post-uploads-as-thumbnails": "Show post uploads as thumbnails", + "show-post-uploads-as-thumbnails": "Afișează încărcările de postări ca miniaturi", "topic-thumb-size": "Topic Thumb Size", "allowed-file-extensions": "Allowed File Extensions", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", diff --git a/public/language/ro/admin/settings/user.json b/public/language/ro/admin/settings/user.json index c8cc3c9c34..7f87942d2a 100644 --- a/public/language/ro/admin/settings/user.json +++ b/public/language/ro/admin/settings/user.json @@ -64,7 +64,7 @@ "show-email": "Show email", "show-fullname": "Show fullname", "restrict-chat": "Only allow chat messages from users I follow", - "disable-incoming-chats": "Disable incoming chat messages", + "disable-incoming-chats": "Dezactivați primirea de mesaje", "outgoing-new-tab": "Open outgoing links in new tab", "topic-search": "Enable In-Topic Searching", "update-url-with-post-index": "Update url with post index while browsing topics", diff --git a/public/language/ro/pages.json b/public/language/ro/pages.json index 87ab32ddb6..41f0901058 100644 --- a/public/language/ro/pages.json +++ b/public/language/ro/pages.json @@ -36,7 +36,7 @@ "chat": "Chatting with %1", "flags": "Flags", "flag-details": "Flag %1 Details", - "world": "World", + "world": "Lumea", "account/edit": "Editing \"%1\"", "account/edit/password": "Editing password of \"%1\"", "account/edit/username": "Editing username of \"%1\"", @@ -55,7 +55,7 @@ "account/settings-of": "Changing settings of %1", "account/watched": "Topics watched by %1", "account/ignored": "Topics ignored by %1", - "account/read": "Topics read by %1", + "account/read": "Subiecte citite de %1", "account/upvoted": "Posts upvoted by %1", "account/downvoted": "Posts downvoted by %1", "account/best": "Best posts made by %1", @@ -63,7 +63,7 @@ "account/blocks": "Blocked users for %1", "account/uploads": "Uploads by %1", "account/sessions": "Login Sessions", - "account/shares": "Topics shared by %1", + "account/shares": "Subiecte partajate de %1", "confirm": "Email Confirmed", "maintenance.text": "%1 is currently undergoing maintenance.
Please come back another time.", "maintenance.messageIntro": "Additionally, the administrator has left this message:", diff --git a/public/language/ro/post-queue.json b/public/language/ro/post-queue.json index 24b33da2e6..c9d806c61b 100644 --- a/public/language/ro/post-queue.json +++ b/public/language/ro/post-queue.json @@ -3,10 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "The post queue is currently disabled. To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "enabling-help": "Coada de publicare este dezactivată . Pentru a o activa, mergeți la Setări → Post → Post Queue și activați Post Queue.", "back-to-list": "Back to Post Queue", - "public-intro": "If you have any queued posts, they will be shown here.", - "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
If you have queued posts awaiting approval, you will be able to see them here.", + "public-intro": "Dacă aveți postări în coadă, acestea vor fi afișate aici.", + "public-description": "Acest forum este configurat să adauge automat în coadă postările de la conturile noi, în așteptarea aprobării moderatorului.
Dacă aveți postări în coadă care așteaptă aprobarea, le veți putea vedea aici.", "user": "User", "when": "When", "category": "Category", @@ -39,5 +39,5 @@ "remove-selected-confirm": "Do you want to remove %1 selected posts?", "bulk-accept-success": "%1 posts accepted", "bulk-reject-success": "%1 posts rejected", - "links-in-this-post": "Links in this post" + "links-in-this-post": "Linkuri în această postare" } \ No newline at end of file diff --git a/public/language/ro/recent.json b/public/language/ro/recent.json index 825ef74692..9e878b96c7 100644 --- a/public/language/ro/recent.json +++ b/public/language/ro/recent.json @@ -8,6 +8,6 @@ "no-recent-topics": "Nu există subiecte recente.", "no-popular-topics": "Nu sunt subiecte populare.", "load-new-posts": "Load new posts", - "uncategorized.title": "All known topics", - "uncategorized.intro": "This page shows a chronological listing of every topic that this forum has received.
The views and opinions expressed in the topics below are not moderated and may not represent the views and opinions of this website." + "uncategorized.title": "Toate subiectele cunoscute", + "uncategorized.intro": "Această pagină prezintă o listă cronologică a fiecărui subiect primit de acest forum. Părerile și opiniile exprimate în subiectele de mai jos nu sunt moderate și este posibil să nu reprezinte opiniile și opiniile acestui site web." } \ No newline at end of file diff --git a/public/language/ro/search.json b/public/language/ro/search.json index f994b924cb..0d544d3545 100644 --- a/public/language/ro/search.json +++ b/public/language/ro/search.json @@ -7,7 +7,7 @@ "in-titles": "In titles", "in-titles-posts": "In titles and posts", "in-posts": "In posts", - "in-bookmarks": "In bookmarks", + "in-bookmarks": "În marcaje", "in-categories": "In categories", "in-users": "In users", "in-tags": "In tags", diff --git a/public/language/ro/social.json b/public/language/ro/social.json index 5b8dd99a46..40dc8ec5ec 100644 --- a/public/language/ro/social.json +++ b/public/language/ro/social.json @@ -7,8 +7,8 @@ "sign-up-with-google": "Sign up with Google", "log-in-with-facebook": "Log in with Facebook", "continue-with-facebook": "Continue with Facebook", - "sign-in-with-linkedin": "Sign in with LinkedIn", - "sign-up-with-linkedin": "Sign up with LinkedIn", - "sign-in-with-wordpress": "Sign in with WordPress", - "sign-up-with-wordpress": "Sign up with WordPress" + "sign-in-with-linkedin": "Conectează-te cu LinkedIn", + "sign-up-with-linkedin": "Înregistrează-te cu LinkedIn", + "sign-in-with-wordpress": "Conectează-te cu WordPress", + "sign-up-with-wordpress": "Înregistrează-te cu WordPress" } \ No newline at end of file diff --git a/public/language/ro/tags.json b/public/language/ro/tags.json index b412a8c85d..cdf91bf1e0 100644 --- a/public/language/ro/tags.json +++ b/public/language/ro/tags.json @@ -3,7 +3,7 @@ "no-tag-topics": "Nu există nici un subiect cu acest tag.", "no-tags-found": "No tags found", "tags": "Taguri", - "enter-tags-here": "Enter tags, %1 - %2 characters.", + "enter-tags-here": "Introduceți etichete, %1 - %2 caractere.", "enter-tags-here-short": "Introdu taguri...", "no-tags": "În acest moment nu există nici un tag.", "select-tags": "Select Tags", diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index 5d44fe17d7..30691b771b 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -15,7 +15,7 @@ "replies-to-this-post": "%1 Replies", "one-reply-to-this-post": "1 Reply", "last-reply-time": "Last reply", - "reply-options": "Reply options", + "reply-options": "Opțiuni răspuns", "reply-as-topic": "Răspunde ca subiect", "guest-login-reply": "Login pentru a răspunde", "login-to-view": "🔒 Log in to view", @@ -27,7 +27,7 @@ "restore": "Restaurează", "move": "Mută", "change-owner": "Change Owner", - "manage-editors": "Manage Editors", + "manage-editors": "Gestionați Editorii", "fork": "Bifurcă", "link": "Link", "share": "Distribuie", @@ -36,7 +36,7 @@ "pinned": "Pinned", "pinned-with-expiry": "Pinned until %1", "scheduled": "Scheduled", - "deleted": "Deleted", + "deleted": "Șters", "moved": "Moved", "moved-from": "Moved from %1", "copy-code": "Copy Code", @@ -61,8 +61,8 @@ "user-restored-topic-on": "%1 restored this topic on %2", "user-moved-topic-from-ago": "%1 moved this topic from %2 %3", "user-moved-topic-from-on": "%1 moved this topic from %2 on %3", - "user-shared-topic-ago": "%1 shared this topic %2", - "user-shared-topic-on": "%1 shared this topic on %2", + "user-shared-topic-ago": "%1 a distribuit acest subiect %2", + "user-shared-topic-on": "%1 a distribuit acest subiect pe %2", "user-queued-post-ago": "%1 queued post for approval %3", "user-queued-post-on": "%1 queued post for approval on %3", "user-referenced-topic-ago": "%1 referenced this topic %3", @@ -106,7 +106,7 @@ "thread-tools.move-posts": "Move Posts", "thread-tools.move-all": "Mută-le pe toate", "thread-tools.change-owner": "Change Owner", - "thread-tools.manage-editors": "Manage Editors", + "thread-tools.manage-editors": "Gestionați Editorii", "thread-tools.select-category": "Select Category", "thread-tools.fork": "Bifurcă Subiect", "thread-tools.tag": "Tag Topic", @@ -137,7 +137,7 @@ "bookmarks": "Bookmarks", "bookmarks.has-no-bookmarks": "You haven't bookmarked any posts yet.", "copy-permalink": "Copy Permalink", - "go-to-original": "View Original Post", + "go-to-original": "Vizualizați Postarea Originală", "loading-more-posts": "Se încarcă mai multe mesaje", "move-topic": "Mută Subiect", "move-topics": "Mută Subiecte", @@ -162,7 +162,7 @@ "move-posts-instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", "move-topic-instruction": "Select the target category and then click move", "change-owner-instruction": "Click the posts you want to assign to another user", - "manage-editors-instruction": "Manage the users who can edit this post below.", + "manage-editors-instruction": "Gestionați mai jos utilizatorii care pot edita această postare.", "composer.title-placeholder": "Introdu numele subiectului aici ...", "composer.handle-placeholder": "Enter your name/handle here", "composer.hide": "Hide", @@ -188,8 +188,8 @@ "sort-by": "Sortează de la", "oldest-to-newest": "Vechi la Noi", "newest-to-oldest": "Noi la Vechi", - "recently-replied": "Recently Replied", - "recently-created": "Recently Created", + "recently-replied": "Răspunse Recent", + "recently-created": "Create Recent", "most-votes": "Most Votes", "most-posts": "Most Posts", "most-views": "Most Views", @@ -214,15 +214,15 @@ "last-post": "Last post", "go-to-my-next-post": "Go to my next post", "no-more-next-post": "You don't have more posts in this topic", - "open-composer": "Open composer", + "open-composer": "Deschide composer-ul", "post-quick-reply": "Quick reply", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", - "upvote-post": "Upvote post", - "downvote-post": "Downvote post", - "post-tools": "Post tools", - "unread-posts-link": "Unread posts link", - "thumb-image": "Topic thumbnail image", - "announcers": "Shares", - "announcers-x": "Shares (%1)" + "upvote-post": "Votează pentru postare", + "downvote-post": "Votează împotriva postării", + "post-tools": "Unelte de postare", + "unread-posts-link": "Link pentru postări necitite", + "thumb-image": "Imagine miniatură subiect", + "announcers": "Partajări", + "announcers-x": "Partajări (%1)" } \ No newline at end of file diff --git a/public/language/ro/unread.json b/public/language/ro/unread.json index bccada87c3..f80d56c432 100644 --- a/public/language/ro/unread.json +++ b/public/language/ro/unread.json @@ -3,7 +3,7 @@ "no-unread-topics": "Nu există nici un subiect necitit.", "load-more": "Încarcă mai multe", "mark-as-read": "Marchează ca citit", - "mark-as-unread": "Mark as Unread", + "mark-as-unread": "Marchează ca Necitit", "selected": "Selectate", "all": "Toate", "all-categories": "Toate categoriile", diff --git a/public/language/ro/users.json b/public/language/ro/users.json index fd6c6a6c58..ba124c9d31 100644 --- a/public/language/ro/users.json +++ b/public/language/ro/users.json @@ -1,6 +1,6 @@ { "all-users": "All Users", - "followed-users": "Followed Users", + "followed-users": "Utilizatori Urmăriți", "latest-users": "Ultimii Utilizatori", "top-posters": "Top Utilizatori", "most-reputation": "Cei mai apreciați utilizatori", diff --git a/public/language/ro/world.json b/public/language/ro/world.json index 7fdb1569f2..33a94dd925 100644 --- a/public/language/ro/world.json +++ b/public/language/ro/world.json @@ -1,21 +1,21 @@ { - "name": "World", - "popular": "Popular topics", - "recent": "All topics", - "help": "Help", + "name": "Lumea", + "popular": "Subiecte Populare", + "recent": "Toate subiectele", + "help": "Ajutor", - "help.title": "What is this page?", - "help.intro": "Welcome to your corner of the fediverse.", - "help.fediverse": "The \"fediverse\" is a network of interconnected applications and websites that all talk to one another and whose users can see each other. This forum is federated, and can interact with that social web (or \"fediverse\"). This page is your corner of the fediverse. It consists solely of topics created by — and shared from — users you follow.", - "help.build": "There might not be a lot of topics here to start; that's normal. You will start to see more content here over time when you start following other users.", - "help.federating": "Likewise, if users from outside of this forum start following you, then your posts will start appearing on those apps and websites as well.", - "help.next-generation": "This is the next generation of social media, start contributing today!", + "help.title": "Ce este în pagina curentă", + "help.intro": "Bine ai venit în colțul tău din universul fediverse", + "help.fediverse": "„Fediversul” este o rețea de aplicații și site-uri web interconectate care comunică între ele și ai căror utilizatori se pot vedea reciproc. Acest forum este federat și poate interacționa cu acea rețea socială (sau „fediversul”). Această pagină este colțul tău din fedivers. Constă exclusiv din subiecte create de — și partajate de — utilizatori pe care îi urmărești.", + "help.build": "S-ar putea să nu fie multe subiecte de început aici; este normal. Vei începe să vezi mai mult conținut aici în timp, când vei începe să urmărești alți utilizatori.", + "help.federating": "De asemenea, dacă utilizatori din afara acestui forum încep să te urmărească, atunci postările tale vor începe să apară și pe acele aplicații și site-uri web.", + "help.next-generation": "Aceasta este următoarea generație de social media, începe să contribui chiar azi!", - "onboard.title": "Your window to the fediverse...", - "onboard.what": "This is your personalized category made up of only content found outside of this forum. Whether something shows up in this page depends on whether you follow them, or whether that post was shared by someone you follow.", - "onboard.why": "There's a lot that goes on outside of this forum, and not all of it is relevant to your interests. That's why following people is the best way to signal that you want to see more from someone.", - "onboard.how": "In the meantime, you can click on the shortcut buttons at the top to see what else this forum knows about, and start discovering some new content!", + "onboard.title": "Fereastra ta către fedivers...", + "onboard.what": "Aceasta este categoria ta personalizată, formată doar din conținut găsit în afara acestui forum. Afișarea unui element pe această pagină depinde de dacă îl urmărești sau dacă postarea respectivă a fost distribuită de cineva pe care îl urmărești.", + "onboard.why": "Se întâmplă multe lucruri în afara acestui forum și nu toate sunt relevante pentru interesele tale. De aceea, urmărirea oamenilor este cea mai bună modalitate de a semnala că vrei să vezi mai multe de la cineva.", + "onboard.how": "Între timp, puteți da clic pe butoanele de comandă rapidă din partea de sus pentru a vedea ce mai știe acest forum și pentru a începe să descoperiți conținut nou!", - "show-categories": "Show categories", - "hide-categories": "Hide categories" + "show-categories": "Afișează categoriile", + "hide-categories": "Ascunde categoriile" } \ No newline at end of file From b5ea20898e40c93f4dc125a948ba2d016af755b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 30 Oct 2025 20:32:24 -0400 Subject: [PATCH 083/141] chore: up express-useragent --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 71545ac52f..76abd3e20c 100644 --- a/install/package.json +++ b/install/package.json @@ -69,7 +69,7 @@ "esbuild": "0.25.11", "express": "4.21.2", "express-session": "1.18.2", - "express-useragent": "1.0.15", + "express-useragent": "2.0.1", "fetch-cookie": "3.1.0", "file-loader": "6.2.0", "fs-extra": "11.3.2", From 179440372aef747f824b91e5fa023d1e269f22eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 30 Oct 2025 20:34:01 -0400 Subject: [PATCH 084/141] refactor: get rid of post.exists check, if post doesnt exist content is falsy --- src/topics/posts.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/topics/posts.js b/src/topics/posts.js index 41b819d219..a8535939e9 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -184,9 +184,6 @@ module.exports = function (Topics) { .filter(p => p && p.hasOwnProperty('toPid') && (activitypub.helpers.isUri(p.toPid) || utils.isNumber(p.toPid))) .map(postObj => postObj.toPid); - const exists = await posts.exists(parentPids); - parentPids = parentPids.filter((_, idx) => exists[idx]); - if (!parentPids.length) { return; } @@ -212,7 +209,7 @@ module.exports = function (Topics) { parentPost.content = foundPost.content; return; } - parentPost = await posts.parsePost(parentPost); + await posts.parsePost(parentPost); })); const parents = {}; @@ -230,7 +227,7 @@ module.exports = function (Topics) { }); postData.forEach((post) => { - if (parents[post.toPid]) { + if (parents[post.toPid] && parents[post.toPid].content) { post.parent = parents[post.toPid]; } }); From 9d3e8179600482ce7cb175466e14042378908ec3 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 31 Oct 2025 09:40:59 -0400 Subject: [PATCH 085/141] fix: bump themes for cross-post support, #13396 --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 76abd3e20c..8d1f84abc2 100644 --- a/install/package.json +++ b/install/package.json @@ -107,10 +107,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.5", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.22", + "nodebb-theme-harmony": "2.1.23", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.48", - "nodebb-theme-persona": "14.1.16", + "nodebb-theme-persona": "14.1.17", "nodebb-widget-essentials": "7.0.40", "nodemailer": "7.0.10", "nprogress": "0.2.0", From 98a1101d40a52c778cc6e3d111d447b2552c89d2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 31 Oct 2025 09:44:06 -0400 Subject: [PATCH 086/141] test: update test for toPid logic to reflect that toPid stays even if parent is purged --- test/topics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/topics.js b/test/topics.js index 9f6b2c719b..fb3f9cb835 100644 --- a/test/topics.js +++ b/test/topics.js @@ -327,7 +327,7 @@ describe('Topic\'s', () => { replies = await apiPosts.getReplies({ uid: fooUid }, { pid: reply1.pid }); assert.strictEqual(replies, null); toPid = await posts.getPostField(reply2.pid, 'toPid'); - assert.strictEqual(toPid, null); + assert.strictEqual(parseInt(toPid, 10), parseInt(reply1.pid, 10)); }); }); From 4ce4e773cb4c23dbacc6b5c26f9cad8f74799162 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:17:35 -0400 Subject: [PATCH 087/141] chore(deps): update dependency jsdom to v27.1.0 (#13743) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 8d1f84abc2..6ad10567e6 100644 --- a/install/package.json +++ b/install/package.json @@ -171,7 +171,7 @@ "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", - "jsdom": "27.0.1", + "jsdom": "27.1.0", "lint-staged": "16.2.6", "mocha": "11.7.4", "mocha-lcov-reporter": "1.3.0", From cb96701b470bb6bc95062b4e76a526629008841d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:27:01 -0400 Subject: [PATCH 088/141] chore(deps): update dependency sass-embedded to v1.93.3 (#13745) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 6ad10567e6..1dd6ecaf15 100644 --- a/install/package.json +++ b/install/package.json @@ -180,7 +180,7 @@ "smtp-server": "3.16.0" }, "optionalDependencies": { - "sass-embedded": "1.93.2" + "sass-embedded": "1.93.3" }, "resolutions": { "*/jquery": "3.7.1" From ba1230735f7d23cb930a53b5e243bd9ba6b33b3b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:27:10 -0400 Subject: [PATCH 089/141] fix(deps): update dependency sass to v1.93.3 (#13746) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 1dd6ecaf15..bfdcdaadf8 100644 --- a/install/package.json +++ b/install/package.json @@ -129,7 +129,7 @@ "rss": "1.2.2", "rtlcss": "4.3.0", "sanitize-html": "2.17.0", - "sass": "1.93.2", + "sass": "1.93.3", "satori": "0.18.3", "sbd": "^1.0.19", "semver": "7.7.3", From a36d89fcdaa0759e31ee2a38fbbe2f04784ac6a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:27:53 -0400 Subject: [PATCH 090/141] fix(deps): update dependency rimraf to v6.1.0 (#13744) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index bfdcdaadf8..c5352230ed 100644 --- a/install/package.json +++ b/install/package.json @@ -125,7 +125,7 @@ "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", "redis": "5.9.0", - "rimraf": "6.0.1", + "rimraf": "6.1.0", "rss": "1.2.2", "rtlcss": "4.3.0", "sanitize-html": "2.17.0", From 85d2667215dd944405b0054ef39907416ad00b64 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 1 Nov 2025 09:20:27 +0000 Subject: [PATCH 091/141] Latest translations and fallbacks --- public/language/zh-CN/modules.json | 2 +- public/language/zh-CN/search.json | 18 +++++++++--------- public/language/zh-CN/topic.json | 6 +++--- public/language/zh-CN/user.json | 12 ++++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json index 21683139a5..6d29de3341 100644 --- a/public/language/zh-CN/modules.json +++ b/public/language/zh-CN/modules.json @@ -1,5 +1,5 @@ { - "chat.room-id": "房间 %1", + "chat.room-id": "聊天室 %1", "chat.chatting-with": "与...聊天", "chat.placeholder": "在此处输入聊天信息,拖放图片", "chat.placeholder.mobile": "输入聊天信息", diff --git a/public/language/zh-CN/search.json b/public/language/zh-CN/search.json index 8cd7bc355b..ebe9d2db6a 100644 --- a/public/language/zh-CN/search.json +++ b/public/language/zh-CN/search.json @@ -25,7 +25,7 @@ "all": "所有", "any": "任何", "posted-by": "发表", - "posted-by-usernames": "被发布:%1", + "posted-by-usernames": "发布者:%1", "type-a-username": "输入用户名", "search-child-categories": "搜索子版块", "has-tags": "有标签", @@ -49,20 +49,20 @@ "three-months": "三个月", "six-months": "六个月", "one-year": "一年", - "time-newer-than-86400": "时间:比昨天更早", - "time-older-than-86400": "时间:比昨天更晚", + "time-newer-than-86400": "时间:比昨天更新", + "time-older-than-86400": "时间:比昨天更久远", "time-newer-than-604800": "时间:一周以内", - "time-older-than-604800": "时间:一周前", + "time-older-than-604800": "时间:超过一周", "time-newer-than-1209600": "时间:两周以内", - "time-older-than-1209600": "时间:两周前", + "time-older-than-1209600": "时间:超过两周", "time-newer-than-2592000": "时间:一个月以内", - "time-older-than-2592000": "时间:一个月前", + "time-older-than-2592000": "时间:超过一个月", "time-newer-than-7776000": "时间:三个月以内", - "time-older-than-7776000": "时间:三个月前", + "time-older-than-7776000": "时间:超过三个月", "time-newer-than-15552000": "时间:六个月以内", - "time-older-than-15552000": "时间:六个月前", + "time-older-than-15552000": "时间:超过六个月", "time-newer-than-31104000": "时间:一年以内", - "time-older-than-31104000": "时间:一年前", + "time-older-than-31104000": "时间:超过一年", "sort-by": "排序", "sort": "排序", "last-reply-time": "最后回复时间", diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json index a9830d1883..4a68353e43 100644 --- a/public/language/zh-CN/topic.json +++ b/public/language/zh-CN/topic.json @@ -66,16 +66,16 @@ "user-queued-post-ago": "%1 篇 已排队 待审批的帖子 %3", "user-queued-post-on": "在 %3 中 已排队 %1 篇待审批的帖子", "user-referenced-topic-ago": "%1 被引用 于这个主题 %3", - "user-referenced-topic-on": "%1 在 %3 中 引用了 这个主题", + "user-referenced-topic-on": "%1 在 %3 引用了 此主题", "user-forked-topic-ago": "%1 分支于 这个主题 %3", - "user-forked-topic-on": "%1 这个主题的分支在 %3", + "user-forked-topic-on": "%1 在 %3 上 分支了 这个主题", "bookmark-instructions": "点击阅读本主题帖中的最新回复", "flag-post": "举报这个帖子", "flag-user": "举报此用户", "already-flagged": "已举报", "view-flag-report": "查看举报报告", "resolve-flag": "解决举报", - "merged-message": "此主题已合并到%2", + "merged-message": "此主题已合并到 %2", "forked-message": "此主题由 %2 分支而来", "deleted-message": "此主题已被删除。只有拥有主题管理权限的用户可以查看。", "following-topic.message": "当有人回复此主题时,您会收到通知。", diff --git a/public/language/zh-CN/user.json b/public/language/zh-CN/user.json index e8eafaad63..80358d9096 100644 --- a/public/language/zh-CN/user.json +++ b/public/language/zh-CN/user.json @@ -20,8 +20,8 @@ "unmute-account": "解除账号禁言", "delete-account": "删除帐号", "delete-account-as-admin": "删除账号", - "delete-content": "删除账号内容", - "delete-all": "删除账号和内容", + "delete-content": "删除账号 内容", + "delete-all": "删除 账号内容", "delete-account-confirm": "您确定要匿名化您的所有帖子并删除账号吗?
此操作不可撤销,您将无法恢复您的任何数据

请输入您的密码,以确认您要删除这个账号。", "delete-this-account-confirm": "您确定您要删除此账号同时保留其发布的内容吗?
此操作不可逆,帖子将被匿名化,而且您将无法恢复帖子和被删除账号的联系

", "delete-account-content-confirm": "您确定要删除账户内容(帖子/主题/上传)吗?
此操作不可逆,而且您无法恢复任何数据

", @@ -204,10 +204,10 @@ "browser-version-on-platform": "%1 %2 在 %3", "consent.title": "您的权利与许可", "consent.lead": "本论坛将会收集与处理您的个人信息。", - "consent.intro": "我们收集这些信息将仅用于个性化您于本社区的体验,和关联您的用户账号与您所发表的帖子。在注册过程中您需要提供一个用户名和邮箱地址,您也可以选择是否提供额外的个人信息,以完善您的用户资料。

在您的用户账号有效期内,我们将保留您的信息。您可以在任何时候通过删除您的账号,以撤回您的许可。您可以在任何时候通过您的权力与许可页面,获取一份您对本论坛的贡献的副本。

如果您有任何疑问,我们鼓励您与本论坛管理团队联系。", - "consent.email-intro": "我们有时可能会向您的注册邮件地址发送电子邮件,以向您提供有关于您的新动态和/或新活动。您可以通过您的用户设置页面自定义(包括直接禁用)社区摘要的发送频率,以及选择性地接收哪些类型的通知。", - "consent.digest-frequency": "本社区默认每 %1 发送一封摘要邮件,除非您在用户设置中明确更改了此项。", - "consent.digest-off": "本社区默认不发送摘要邮件,除非您在用户设置中明确更改了此项。", + "consent.intro": "我们严格使用这些信息来个性化您在本社区的体验,并将您发布的帖子关联至您的用户账号。注册时您需提供用户名和电子邮箱地址,也可选择性提供其他信息以完善本网站的用户资料。

我们将保存这些信息直至您的用户账号终止,您可随时通过注销账号撤回授权。您可随时通过“权利与授权”页面申请获取您在本网站的贡献内容副本。

如有任何疑问或顾虑,欢迎联系本论坛管理团队。", + "consent.email-intro": "我们可能会不定期向您注册的电子邮件地址发送邮件,以便提供更新信息和/或通知您与您相关的新动态。您可通过用户设置页面自定义社区摘要的接收频率(包括完全停用该功能),并选择希望通过邮件接收的通知类型。", + "consent.digest-frequency": "除非在您的用户设置中明确更改,否则本社区默认每 %1 发送一次邮件摘要。", + "consent.digest-off": "除非您在用户设置中明确更改,否则本社区不会发送任何邮件摘要。", "consent.received": "您已许可本网站收集与处理您的个人数据。无需其他额外操作。", "consent.not-received": "您未许可本网站收集与处理您的个人数据。本网站的管理团队可能于任何时候删除您的账号,以符合通用数据保护条例的要求。", "consent.give": "授予许可", From 090eb0884527b7a36a84985b04dc3aea40d3bd4b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:55:48 -0500 Subject: [PATCH 092/141] fix(deps): update dependency esbuild to v0.25.12 (#13748) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index c5352230ed..e28e0d5fd8 100644 --- a/install/package.json +++ b/install/package.json @@ -66,7 +66,7 @@ "csrf-sync": "4.2.1", "daemon": "1.1.0", "diff": "8.0.2", - "esbuild": "0.25.11", + "esbuild": "0.25.12", "express": "4.21.2", "express-session": "1.18.2", "express-useragent": "2.0.1", From 4e7867a95d7cca8b244773c897d3e94ffc08b1d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:56:01 -0500 Subject: [PATCH 093/141] chore(deps): update dependency @eslint/js to v9.39.1 (#13747) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index e28e0d5fd8..b0308a82f2 100644 --- a/install/package.json +++ b/install/package.json @@ -164,7 +164,7 @@ "@commitlint/cli": "20.1.0", "@commitlint/config-angular": "20.0.0", "coveralls": "3.1.1", - "@eslint/js": "9.38.0", + "@eslint/js": "9.39.1", "@stylistic/eslint-plugin": "5.5.0", "eslint-config-nodebb": "1.1.11", "eslint-plugin-import": "2.32.0", From 13c23fddd7508388a54216f4bc63203bdf5625c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:56:41 -0500 Subject: [PATCH 094/141] chore(deps): update github artifact actions (#13730) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index fb2bd3ccce..e74e58c07b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -80,7 +80,7 @@ jobs: touch "${{ runner.temp }}/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: digests-${{ env.PLATFORM_PAIR }} path: ${{ runner.temp }}/digests/* @@ -96,7 +96,7 @@ jobs: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}" >> $GITHUB_ENV echo "CURRENT_DATE_NST=$(date +'%Y%m%d-%H%M%S' -d '-3 hours -30 minutes')" >> $GITHUB_ENV - name: Download digests - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: path: ${{ runner.temp }}/digests pattern: digests-* From 4e33c1dfd3a59e15f194659b8f810c258839a6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 4 Nov 2025 12:42:08 -0500 Subject: [PATCH 095/141] chore: up harmony, closes #13753 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index b0308a82f2..f385cd7b64 100644 --- a/install/package.json +++ b/install/package.json @@ -107,7 +107,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.5", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.23", + "nodebb-theme-harmony": "2.1.24", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.48", "nodebb-theme-persona": "14.1.17", From 1921ccaa101baff4b777d995b3a686e56a9ab619 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:43:34 -0500 Subject: [PATCH 096/141] fix(deps): update dependency sitemap to v9 (#13752) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index f385cd7b64..38e7a2b3a7 100644 --- a/install/package.json +++ b/install/package.json @@ -135,7 +135,7 @@ "semver": "7.7.3", "serve-favicon": "2.5.1", "sharp": "0.34.4", - "sitemap": "8.0.2", + "sitemap": "9.0.0", "socket.io": "4.8.1", "socket.io-client": "4.8.1", "@socket.io/redis-adapter": "8.3.0", From a34284df834b2fc5ea50655b528e05d80ca22cf0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:44:03 -0500 Subject: [PATCH 097/141] fix(deps): update dependency bcryptjs to v3.0.3 (#13751) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 38e7a2b3a7..875f46d00c 100644 --- a/install/package.json +++ b/install/package.json @@ -43,7 +43,7 @@ "archiver": "7.0.1", "async": "3.2.6", "autoprefixer": "10.4.21", - "bcryptjs": "3.0.2", + "bcryptjs": "3.0.3", "benchpressjs": "2.5.5", "body-parser": "2.2.0", "bootbox": "6.0.4", From 4c5f7f6060c87a6528545a26f95cd05a79357638 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:54:57 -0500 Subject: [PATCH 098/141] chore(deps): update redis docker tag to v8.2.3 (#13750) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- docker-compose-pgsql.yml | 2 +- docker-compose-redis.yml | 2 +- docker-compose.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6304098eca..7be9ff42da 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: - 5432:5432 redis: - image: 'redis:8.2.2' + image: 'redis:8.2.3' # Set health checks to wait until redis has started options: >- --health-cmd "redis-cli ping" diff --git a/docker-compose-pgsql.yml b/docker-compose-pgsql.yml index 3c55eb6c3b..27ab3cdbb8 100644 --- a/docker-compose-pgsql.yml +++ b/docker-compose-pgsql.yml @@ -24,7 +24,7 @@ services: - postgres-data:/var/lib/postgresql/data redis: - image: redis:8.2.2-alpine + image: redis:8.2.3-alpine restart: unless-stopped command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning'] # command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # uncomment if you want to use snapshotting instead of AOF diff --git a/docker-compose-redis.yml b/docker-compose-redis.yml index 2cd7197231..9dcc03ac29 100644 --- a/docker-compose-redis.yml +++ b/docker-compose-redis.yml @@ -14,7 +14,7 @@ services: - ./install/docker/setup.json:/usr/src/app/setup.json redis: - image: redis:8.2.2-alpine + image: redis:8.2.3-alpine restart: unless-stopped command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning'] # command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # uncomment if you want to use snapshotting instead of AOF diff --git a/docker-compose.yml b/docker-compose.yml index ee7a18ceb0..37decabf23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,7 @@ services: - mongo-data:/data/db - ./install/docker/mongodb-user-init.js:/docker-entrypoint-initdb.d/user-init.js redis: - image: redis:8.2.2-alpine + image: redis:8.2.3-alpine restart: unless-stopped command: ['redis-server', '--appendonly', 'yes', '--loglevel', 'warning'] # command: ['redis-server', '--save', '60', '1', '--loglevel', 'warning'] # uncomment if you want to use snapshotting instead of AOF From a8e45587bc8015974d80a76d37e60ff026726827 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 5 Nov 2025 09:22:28 +0000 Subject: [PATCH 099/141] Latest translations and fallbacks --- public/language/ar/error.json | 1 + public/language/az/error.json | 1 + public/language/bg/error.json | 1 + public/language/bn/error.json | 1 + public/language/cs/error.json | 1 + public/language/da/error.json | 1 + public/language/de/error.json | 1 + public/language/el/error.json | 1 + public/language/en-US/error.json | 1 + public/language/en-x-pirate/error.json | 1 + public/language/es/error.json | 1 + public/language/et/error.json | 1 + public/language/fa-IR/error.json | 1 + public/language/fi/error.json | 1 + public/language/fr/error.json | 1 + public/language/gl/error.json | 1 + public/language/he/error.json | 1 + public/language/hr/error.json | 1 + public/language/hu/error.json | 1 + public/language/hy/error.json | 1 + public/language/id/error.json | 1 + public/language/it/error.json | 1 + public/language/ja/error.json | 1 + public/language/ko/error.json | 1 + public/language/lt/error.json | 1 + public/language/lv/error.json | 1 + public/language/ms/error.json | 1 + public/language/nb/error.json | 1 + public/language/nl/error.json | 1 + public/language/nn-NO/error.json | 1 + public/language/pl/error.json | 1 + public/language/pt-BR/error.json | 1 + public/language/pt-PT/error.json | 1 + public/language/ro/error.json | 1 + public/language/ru/error.json | 1 + public/language/rw/error.json | 1 + public/language/sc/error.json | 1 + public/language/sk/error.json | 1 + public/language/sl/error.json | 1 + public/language/sq-AL/error.json | 1 + public/language/sr/error.json | 1 + public/language/sv/error.json | 1 + public/language/th/error.json | 1 + public/language/tr/error.json | 1 + public/language/uk/error.json | 1 + public/language/ur/error.json | 1 + public/language/vi/error.json | 1 + public/language/zh-CN/error.json | 1 + public/language/zh-TW/error.json | 1 + 49 files changed, 49 insertions(+) diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 4d2ea94cba..8e52b61425 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -147,6 +147,7 @@ "post-already-restored": "سبق وتم إلغاء حذف هذا الرد", "topic-already-deleted": "سبق وتم حذف هذا الموضوع", "topic-already-restored": "سبق وتم إلغاء حذف هذا الرد", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "لا يمكنك محو المشاركة الأساسية، يرجى حذف الموضوع بدلاً عن ذلك", "topic-thumbnails-are-disabled": "الصور المصغرة غير مفعلة.", "invalid-file": "ملف غير مقبول", diff --git a/public/language/az/error.json b/public/language/az/error.json index 62ca9934ff..5cbd2b5af8 100644 --- a/public/language/az/error.json +++ b/public/language/az/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Bu yazı artıq bərpa olunub", "topic-already-deleted": "Bu mövzu artıq silinib", "topic-already-restored": "Bu mövzu artıq bərpa olunub", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Siz əsas yazını silə bilməzsiniz, lütfən, əvəzinə mövzunu silin", "topic-thumbnails-are-disabled": "Mövzu kiçik şəkilləri deaktiv edilib.", "invalid-file": "Etibarsız fayl", diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 94d12a36b1..7d8d8dd869 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Тази публикация вече е възстановена", "topic-already-deleted": "Тази тема вече е изтрита", "topic-already-restored": "Тази тема вече е възстановена", + "topic-already-crossposted": "Тази тема вече е била публикувана там.", "cant-purge-main-post": "Не можете да изчистите първоначалната публикация. Моля, вместо това изтрийте темата.", "topic-thumbnails-are-disabled": "Иконките на темите са изключени.", "invalid-file": "Грешен файл", diff --git a/public/language/bn/error.json b/public/language/bn/error.json index 3dfb852227..c5ccfa7b0f 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -147,6 +147,7 @@ "post-already-restored": "এই পোষ্টটি ইতিমধ্যে পুনরোদ্ধার করা হয়েছে", "topic-already-deleted": "এই টপিকটি ইতিমধ্যে ডিলিট করা হয়েছে", "topic-already-restored": "এই টপিকটি ইতিমধ্যে পুনরোদ্ধার করা হয়েছে", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "টপিক থাম্বনেল নিষ্ক্রিয় করা।", "invalid-file": "ভুল ফাইল", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 4295b7d97a..6c4c0c2833 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Tento příspěvek byl již obnoven", "topic-already-deleted": "Toto téma bylo již odstraněno", "topic-already-restored": "Toto téma bylo již obnoveno", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Nemůžete vymazat hlavní příspěvek, místo toho odstraňte téma", "topic-thumbnails-are-disabled": "Miniatury témat jsou zakázány.", "invalid-file": "Neplatný soubor", diff --git a/public/language/da/error.json b/public/language/da/error.json index 9418e8663c..d421ec2c4e 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Dette indlæg er allerede blevet genskabt", "topic-already-deleted": "Denne tråd er allerede blevet slettet", "topic-already-restored": "Denne tråd er allerede blevet genskabt", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Du kan ikke udradere hoved indlægget, fjern venligt tråden istedet", "topic-thumbnails-are-disabled": "Tråd miniaturebilleder er slået fra.", "invalid-file": "Ugyldig fil", diff --git a/public/language/de/error.json b/public/language/de/error.json index b292fdf3f1..c1af918b65 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Dieser Beitrag ist bereits wiederhergestellt worden", "topic-already-deleted": "Dieses Thema ist bereits gelöscht worden", "topic-already-restored": "Dieses Thema ist bereits wiederhergestellt worden", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Du kannst den Hauptbeitrag nicht löschen, bitte lösche stattdessen das Thema", "topic-thumbnails-are-disabled": "Vorschaubilder für Themen sind deaktiviert", "invalid-file": "Ungültige Datei", diff --git a/public/language/el/error.json b/public/language/el/error.json index 70bedd30c8..b09940f7f8 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -147,6 +147,7 @@ "post-already-restored": "This post has already been restored", "topic-already-deleted": "This topic has already been deleted", "topic-already-restored": "This topic has already been restored", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "Οι εικόνες θεμάτων είναι απενεργοποιημένες", "invalid-file": "Άκυρο Αρχείο", diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index c3bb2dc892..eb42797f04 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -147,6 +147,7 @@ "post-already-restored": "This post has already been restored", "topic-already-deleted": "This topic has already been deleted", "topic-already-restored": "This topic has already been restored", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", "invalid-file": "Invalid File", diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json index c3bb2dc892..eb42797f04 100644 --- a/public/language/en-x-pirate/error.json +++ b/public/language/en-x-pirate/error.json @@ -147,6 +147,7 @@ "post-already-restored": "This post has already been restored", "topic-already-deleted": "This topic has already been deleted", "topic-already-restored": "This topic has already been restored", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", "invalid-file": "Invalid File", diff --git a/public/language/es/error.json b/public/language/es/error.json index 27a8776929..5e3b3c58b3 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Esta publicación ya ha sido restaurada", "topic-already-deleted": "Este tema ya ha sido borrado", "topic-already-restored": "Este tema ya ha sido restaurado", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "No puedes purgar el mensaje principal, por favor utiliza borrar tema", "topic-thumbnails-are-disabled": "Las miniaturas de los temas están deshabilitadas.", "invalid-file": "Archivo no válido", diff --git a/public/language/et/error.json b/public/language/et/error.json index 7fd4027cec..37508f05f0 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Postitus on juba taastatud", "topic-already-deleted": "Teema on juba kustutatud", "topic-already-restored": "Teema on juba taastatud", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Te ei saa eemaldada peamist postitust, pigem kustutage teema ära.", "topic-thumbnails-are-disabled": "Teema thumbnailid on keelatud.", "invalid-file": "Vigane fail", diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json index 7660c66770..34498fc1f0 100644 --- a/public/language/fa-IR/error.json +++ b/public/language/fa-IR/error.json @@ -147,6 +147,7 @@ "post-already-restored": "پست قبلا بازگردانی شده است.", "topic-already-deleted": "موضوع قبلا حذف شده است", "topic-already-restored": "موضوع قبلا بازگردانی شده است", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "شما نمی‌توانید پست اصلی را پاک کنید، لطفا موضوع را به جای آن پاک کنید.", "topic-thumbnails-are-disabled": "چهرک‌های موضوع غیرفعال شده است.", "invalid-file": "فایل نامعتبر است.", diff --git a/public/language/fi/error.json b/public/language/fi/error.json index d7d1af89ea..771da7a5c1 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Tämä viesti on jo palautettu", "topic-already-deleted": "Tämä aihe on jo poistettu", "topic-already-restored": "Tämä aihe on jo palautettu", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "Aiheiden kuvakkeet eivät ole käytössä", "invalid-file": "Virheellinen tiedosto", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index d22c59af08..1c1329390a 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Message déjà restauré", "topic-already-deleted": "Sujet déjà supprimé", "topic-already-restored": "Sujet déjà restauré", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Il n'est pas possible d'effacer le message principal, veuillez supprimer le sujet entier à la place.", "topic-thumbnails-are-disabled": "Les miniatures de sujet sont désactivés", "invalid-file": "Fichier invalide", diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 7669c41b8f..e2c0414ede 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -147,6 +147,7 @@ "post-already-restored": "A publicación foi restaurada", "topic-already-deleted": "O tema foi borrado", "topic-already-restored": "O tema foi restaurado", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Non podes purgar a publicación principal, por favor, elimínaa no seu canto.", "topic-thumbnails-are-disabled": "Miniaturas do tema deshabilitadas.", "invalid-file": "Arquivo Inválido", diff --git a/public/language/he/error.json b/public/language/he/error.json index a492795106..013df27b96 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -147,6 +147,7 @@ "post-already-restored": "פוסט זה כבר שוחזר", "topic-already-deleted": "נושא זה כבר נמחק", "topic-already-restored": "נושא זה כבר שוחזר", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "לא ניתן למחוק את הפוסט הראשי, ניתן למחוק את הנושא במקום זה", "topic-thumbnails-are-disabled": "תמונות ממוזערות לנושא אינן מאופשרות.", "invalid-file": "קובץ לא תקין", diff --git a/public/language/hr/error.json b/public/language/hr/error.json index 4e2d576d5c..fbf67edcc9 100644 --- a/public/language/hr/error.json +++ b/public/language/hr/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Ova objava je povraćena", "topic-already-deleted": "Ova tema je već obrisana", "topic-already-restored": "Ova tema je povraćena", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Nemožete odbaciti glavnu objavu, obrišite temu za brisanje", "topic-thumbnails-are-disabled": "Slike tema su onemogućene", "invalid-file": "Pogrešna datoteka", diff --git a/public/language/hu/error.json b/public/language/hu/error.json index e337533e57..2483c26e37 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Ez a bejegyzés már visszaállításra került", "topic-already-deleted": "Ezt a témakör már törlésre került", "topic-already-restored": "Ez a témakör már helyreállításra került", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Nem tisztíthatod ki ezt a témakört, inkább töröld", "topic-thumbnails-are-disabled": "Témakör bélyegképek tíltásra kerültek.", "invalid-file": "Érvénytelen fájl", diff --git a/public/language/hy/error.json b/public/language/hy/error.json index e6c4a3c18b..7db353d01a 100644 --- a/public/language/hy/error.json +++ b/public/language/hy/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Այս գրառումն արդեն վերականգնվել է", "topic-already-deleted": "Այս թեման արդեն ջնջված է", "topic-already-restored": "Այս թեման արդեն վերականգնվել է", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Դուք չեք կարող մաքրել հիմնական գրառումը, փոխարենը ջնջեք թեման", "topic-thumbnails-are-disabled": "Թեմայի մանրապատկերներն անջատված են:", "invalid-file": "Անվավեր ֆայլ", diff --git a/public/language/id/error.json b/public/language/id/error.json index 21d344ce72..5bade5dae2 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Postingan ini sudah direstore", "topic-already-deleted": "Topik ini sudah dihapus", "topic-already-restored": "Topik ini sudah direstore", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "Thumbnail di topik ditiadakan", "invalid-file": "File Salah", diff --git a/public/language/it/error.json b/public/language/it/error.json index 1c4a16adf9..50bfa7a26b 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Questo post è già stato ripristinato", "topic-already-deleted": "Questa discussione è già stata eliminata", "topic-already-restored": "Questa discussione è già stata ripristinata", + "topic-already-crossposted": "Questa discussione è già stata pubblicata lì.", "cant-purge-main-post": "Non puoi eliminare definitivamente il post principale, per favore elimina invece la discussione", "topic-thumbnails-are-disabled": "Le miniature della Discussione sono disabilitate.", "invalid-file": "File non valido", diff --git a/public/language/ja/error.json b/public/language/ja/error.json index be0b4396d1..3941c3d08c 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -147,6 +147,7 @@ "post-already-restored": "この投稿が既に復元されました", "topic-already-deleted": "このスレッドは既に削除されました", "topic-already-restored": "このスレッドは既に復元されました", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "メインの投稿を削除することはできません。代わりにスレッドを削除してください", "topic-thumbnails-are-disabled": "スレッドのサムネイルが無効された", "invalid-file": "無効なファイル", diff --git a/public/language/ko/error.json b/public/language/ko/error.json index 80ee9f08e5..5a651299c2 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -147,6 +147,7 @@ "post-already-restored": "이 게시물은 복원되었습니다", "topic-already-deleted": "이 토픽은 삭제되었습니다", "topic-already-restored": "이 토픽은 복원되었습니다", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "주요 게시물을 정리할 수 없습니다. 대신 토픽을 삭제하세요", "topic-thumbnails-are-disabled": "토픽 썸네일이 비활성화되었습니다.", "invalid-file": "잘못된 파일", diff --git a/public/language/lt/error.json b/public/language/lt/error.json index 5fbf1188c4..787374e85c 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Šis įrašas jau atstatytas", "topic-already-deleted": "Ši tema jau ištrinta", "topic-already-restored": "Ši tema jau atkurta", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Jūs negalite išvalyti pagrindinio pranešimo, prašome ištrinkite temą nedelsiant", "topic-thumbnails-are-disabled": "Temos paveikslėliai neleidžiami.", "invalid-file": "Klaidingas failas", diff --git a/public/language/lv/error.json b/public/language/lv/error.json index 6195541875..e75b8a9019 100644 --- a/public/language/lv/error.json +++ b/public/language/lv/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Raksts jau ir atjaunots", "topic-already-deleted": "Temats jau ir izdzēsts", "topic-already-restored": "Temats jau ir atjaunots", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Nevar iztīrīt galveno rakstu, lūdzu, tā vietā izdzēsi tematu", "topic-thumbnails-are-disabled": "Tematu sīktēli ir atspējoti.", "invalid-file": "Nederīgs fails", diff --git a/public/language/ms/error.json b/public/language/ms/error.json index 26e1b5a310..b0a08abe48 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Kiriman ini telah dipulihkan", "topic-already-deleted": "Topik ini telah dipadam", "topic-already-restored": "Kiriman ini telah dipulihkan", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Anda tidak boleh memadam, kiriman utama, sebaliknya sila pada topik", "topic-thumbnails-are-disabled": "Topik kecil dilumpuhkan.", "invalid-file": "Fail tak sah", diff --git a/public/language/nb/error.json b/public/language/nb/error.json index a497b4f446..295fc7a7f9 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Dette innlegget har allerede blitt gjenopprettet", "topic-already-deleted": "Dette emnet har allerede blitt slettet", "topic-already-restored": "Dette emnet har allerede blitt gjenopprettet", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Du kan ikke slette hovedinnlegget. Vennligst slett emnet i stedet.", "topic-thumbnails-are-disabled": "Emne-minatyrbilder har blitt deaktivert", "invalid-file": "Ugyldig fil", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 662a161197..586c81e656 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Dit bericht is al hersteld", "topic-already-deleted": "Dit onderwerp is al verwijderd", "topic-already-restored": "Dit onderwerp is al hersteld", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Het is niet mogelijk het eerste bericht te verwijderen. Hiervoor dient het gehele onderwerp verwijderd te worden.", "topic-thumbnails-are-disabled": "Miniatuurweergaven bij onderwerpen uitgeschakeld.", "invalid-file": "Ongeldig bestand", diff --git a/public/language/nn-NO/error.json b/public/language/nn-NO/error.json index c25a7f8cb2..2a1c581a74 100644 --- a/public/language/nn-NO/error.json +++ b/public/language/nn-NO/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Dette innlegget har allereie blitt gjenoppretta", "topic-already-deleted": "Dette emnet har allereie blitt sletta", "topic-already-restored": "Dette emnet har allereie blitt gjenoppretta", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Du kan ikkje rense hovudinnlegget, ver venleg å slette emnet i staden", "topic-thumbnails-are-disabled": "Miniatyrbilete for emne er deaktivert.", "invalid-file": "Ugyldig fil", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 27cb6c9d42..02721106dd 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Ten post został już przywrócony", "topic-already-deleted": "Ten temat został już skasowany", "topic-already-restored": "Ten temat został już przywrócony", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Nie możesz wymazać głównego posta, zamiast tego usuń temat", "topic-thumbnails-are-disabled": "Miniatury tematów są wyłączone.", "invalid-file": "Błędny plik", diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json index 2fc1039862..d02827700d 100644 --- a/public/language/pt-BR/error.json +++ b/public/language/pt-BR/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Este post já foi restaurado", "topic-already-deleted": "Esté tópico já foi deletado", "topic-already-restored": "Este tópico já foi restaurado", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Você não pode remover o post principal, ao invés disso, apague o tópico por favor.", "topic-thumbnails-are-disabled": "Thumbnails para tópico estão desativados.", "invalid-file": "Arquivo Inválido", diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json index fa0418c41c..68eacd4407 100644 --- a/public/language/pt-PT/error.json +++ b/public/language/pt-PT/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Esta publicação já foi restaurada", "topic-already-deleted": "Este tópico já foi eliminado", "topic-already-restored": "Este tópico já foi restaurado", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Não podes eliminar a publicação principal, em vez disso, por favor apaga o tópico", "topic-thumbnails-are-disabled": "Miniaturas para os tópicos estão desativadas.", "invalid-file": "Ficheiro inválido", diff --git a/public/language/ro/error.json b/public/language/ro/error.json index abc63f3b06..5ffde39996 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -147,6 +147,7 @@ "post-already-restored": "This post has already been restored", "topic-already-deleted": "This topic has already been deleted", "topic-already-restored": "This topic has already been restored", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "Pictogramele pentru subiect sunt interzise.", "invalid-file": "Fișier invalid", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index d741a098a1..4a553e390d 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Это сообщение уже восстановлено", "topic-already-deleted": "Тема уже удалена", "topic-already-restored": "Тема уже восстановлена", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Вы не можете стереть первое сообщение в теме. Пожалуйста, удалите саму тему.", "topic-thumbnails-are-disabled": "Иконки тем отключены.", "invalid-file": "Некорректный файл", diff --git a/public/language/rw/error.json b/public/language/rw/error.json index 0452b4fc11..1756938782 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Ibi byari byaragaruwe", "topic-already-deleted": "Iki kiganiro cyari cyarakuweho", "topic-already-restored": "Iki kiganiro cyari cyaragaruwe", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Ntabwo ushobora gusibanganya icyashyizweho kandi ibindi bigishamikiyeho. Ahubwo wakuraho ikiganiro cyose", "topic-thumbnails-are-disabled": "Ishushondanga ntiyemerewe.", "invalid-file": "Ifayilo Ntiyemewe", diff --git a/public/language/sc/error.json b/public/language/sc/error.json index c3bb2dc892..eb42797f04 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -147,6 +147,7 @@ "post-already-restored": "This post has already been restored", "topic-already-deleted": "This topic has already been deleted", "topic-already-restored": "This topic has already been restored", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", "invalid-file": "Invalid File", diff --git a/public/language/sk/error.json b/public/language/sk/error.json index cd21ec05a3..d567ee3556 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Tento príspevok bol obnovený", "topic-already-deleted": "Táto téma bola odstránená", "topic-already-restored": "Táto téma bola obnovená", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Nemôžete očistiť hlavný príspevok, namiesto toho prosíme odstráňte tému", "topic-thumbnails-are-disabled": "Náhľady tém sú zablokované.", "invalid-file": "Neplatný súbor", diff --git a/public/language/sl/error.json b/public/language/sl/error.json index ac9775d30b..31a71083ec 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Ta objava je že bila obnovljena.", "topic-already-deleted": "Ta tema je že bila izbrisana.", "topic-already-restored": "Ta tema je že bila obnovljena.", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Ne morete odstraniti prve objave, prosimo, izbrišite temo.", "topic-thumbnails-are-disabled": "Sličice teme so onemogočene.", "invalid-file": "Nedovoljena datoteka", diff --git a/public/language/sq-AL/error.json b/public/language/sq-AL/error.json index b1aab456a0..5597b14d32 100644 --- a/public/language/sq-AL/error.json +++ b/public/language/sq-AL/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Ky postim tashmë është rikthyer", "topic-already-deleted": "Kjo temë tashmë është fshirë", "topic-already-restored": "Kjo temë tashmë është rikthyer", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Ju nuk mund të fshini postimin kryesor, ju lutemi fshini temën në vend të saj", "topic-thumbnails-are-disabled": "Miniaturat e temës janë çaktivizuar.", "invalid-file": "Dokument i pavlefshëm", diff --git a/public/language/sr/error.json b/public/language/sr/error.json index ec8ef67b20..3c011d4b3a 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Ова порука је већ обновљена", "topic-already-deleted": "Ова тема је већ избрисана", "topic-already-restored": "Ова тема је већ обновљена", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Не можете очистити насловну поруку, избришите тему уместо тога", "topic-thumbnails-are-disabled": "Сличице тема су онемогућене.", "invalid-file": "Неисправна датотека", diff --git a/public/language/sv/error.json b/public/language/sv/error.json index 19a3580440..2d91974915 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Inlägget är redan återställt", "topic-already-deleted": "Ämnet är redan raderat", "topic-already-restored": "Ämnet är redan återställt", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Huvudinlägg kan ej rensas bort, ta bort ämnet istället", "topic-thumbnails-are-disabled": "Miniatyrbilder för ämnen är inaktiverat", "invalid-file": "Ogiltig fil", diff --git a/public/language/th/error.json b/public/language/th/error.json index 529a74d404..e6cf42d581 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -147,6 +147,7 @@ "post-already-restored": "โพสต์นี้ถูกกู้คืนเรียบร้อยแล้ว", "topic-already-deleted": "กระทู้นี้ถูกลบไปแล้ว", "topic-already-restored": "กระทู้นี้ถูกกู้คืนเรียบร้อยแล้ว", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "คุณไม่สามารถลบล้างโพสต์หลักได้ กรุณาลบกระทู้แทน", "topic-thumbnails-are-disabled": "ภาพตัวอย่างของกระทู้ถูกปิดใช้งาน", "invalid-file": "ไฟล์ไม่ถูกต้อง", diff --git a/public/language/tr/error.json b/public/language/tr/error.json index ea93e9abe6..b16b2d15ba 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -147,6 +147,7 @@ "post-already-restored": "İleti zaten geri getirilmiş", "topic-already-deleted": "Başlık zaten silinmiş", "topic-already-restored": "Başlık zaten geri getirilmiş", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "İlk iletiyi silemezsiniz, bunun yerine konuyu silin", "topic-thumbnails-are-disabled": "Başlık resimleri kapalı.", "invalid-file": "Geçersiz Dosya", diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 002b6a8471..9d871644e7 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Цей пост вже відновлено", "topic-already-deleted": "Ця тема вже була видалена", "topic-already-restored": "Ця тема вже була відновлена", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Ви не можете видалити головний пост, натомість видаліть тему.", "topic-thumbnails-are-disabled": "Мініатюри теми вимкнено.", "invalid-file": "Невірний файл", diff --git a/public/language/ur/error.json b/public/language/ur/error.json index 21e92491e6..279e5d997c 100644 --- a/public/language/ur/error.json +++ b/public/language/ur/error.json @@ -147,6 +147,7 @@ "post-already-restored": "یہ پوسٹ پہلے سے بحال ہو چکی ہے", "topic-already-deleted": "یہ موضوع پہلے سے حذف ہو چکا ہے", "topic-already-restored": "یہ موضوع پہلے سے بحال ہو چکا ہے", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "آپ ابتدائی پوسٹ کو صاف نہیں کر سکتے۔ براہ کرم اس کے بجائے موضوع کو حذف کریں۔", "topic-thumbnails-are-disabled": "موضوعات کے تھمب نیلز غیر فعال ہیں۔", "invalid-file": "غلط فائل", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index ec481020a1..2293f2de02 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -147,6 +147,7 @@ "post-already-restored": "Bài viết này đã được khôi phục", "topic-already-deleted": "Chủ đề này đã bị xóa", "topic-already-restored": "Chủ đề này đã được khôi phục", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "Bạn không thể xoá bài viết chính, thay vào đó vui lòng xóa chủ đề", "topic-thumbnails-are-disabled": "Ảnh Thumbnails chủ đề đã bị tắt", "invalid-file": "Tệp Không Hợp Lệ", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index 39fabf9c1b..ee9f6b6a21 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -147,6 +147,7 @@ "post-already-restored": "此帖已经恢复", "topic-already-deleted": "此主题已被删除", "topic-already-restored": "此主题已恢复", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "无法清除主贴,请直接删除主题", "topic-thumbnails-are-disabled": "主题缩略图已禁用", "invalid-file": "无效文件", diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json index 14a9bcdbbb..4cbbafcffd 100644 --- a/public/language/zh-TW/error.json +++ b/public/language/zh-TW/error.json @@ -147,6 +147,7 @@ "post-already-restored": "此貼文已經恢復", "topic-already-deleted": "此主題已被刪除", "topic-already-restored": "此主題已恢復", + "topic-already-crossposted": "This topic has already been cross-posted there.", "cant-purge-main-post": "無法清除主貼文,請直接刪除主題", "topic-thumbnails-are-disabled": "主題縮圖已停用", "invalid-file": "無效檔案", From ed83bc5b83f243e68faa56f1c1852b613aa08c2b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 5 Nov 2025 12:55:03 -0500 Subject: [PATCH 100/141] revert: remove `federatedDescription` category field, closes #13757 --- public/language/en-GB/admin/manage/categories.json | 3 --- src/activitypub/mocks.js | 10 +++------- src/categories/data.js | 2 +- src/views/admin/manage/category.tpl | 10 ---------- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/public/language/en-GB/admin/manage/categories.json b/public/language/en-GB/admin/manage/categories.json index 38037f7206..cdb3e1f356 100644 --- a/public/language/en-GB/admin/manage/categories.json +++ b/public/language/en-GB/admin/manage/categories.json @@ -15,9 +15,6 @@ "handle": "Category Handle", "handle.help": "Your category handle is used as a representation of this category across other networks, similar to a username. A category handle must not match an existing username or user group.", "description": "Category Description", - "federatedDescription": "Federated Description", - "federatedDescription.help": "This text will be appended to the category description when queried by other websites/apps.", - "federatedDescription.default": "This is a forum category containing topical discussion. You can start new discussions by mentioning this category.", "topic-template": "Topic Template", "topic-template.help": "Define a template for new topics created in this category.", "bg-color": "Background Colour", diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index d83df6e1e1..3fe103f3b1 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -521,11 +521,11 @@ Mocks.actors.user = async (uid) => { }; Mocks.actors.category = async (cid) => { - let { + const { name, handle: preferredUsername, slug, - descriptionParsed: summary, federatedDescription, backgroundImage, + descriptionParsed: summary, backgroundImage, } = await categories.getCategoryFields(cid, - ['name', 'handle', 'slug', 'description', 'descriptionParsed', 'federatedDescription', 'backgroundImage']); + ['name', 'handle', 'slug', 'description', 'descriptionParsed', 'backgroundImage']); const publicKey = await activitypub.getPublicKey('cid', cid); let icon; @@ -546,10 +546,6 @@ Mocks.actors.category = async (cid) => { }; } - // Append federated desc. - const fallback = await translator.translate('[[admin/manage/categories:federatedDescription.default]]'); - summary += `

${federatedDescription || fallback}

\n`; - return { '@context': [ 'https://www.w3.org/ns/activitystreams', diff --git a/src/categories/data.js b/src/categories/data.js index dc4467ffa3..bf2ddac25f 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -117,7 +117,7 @@ function modifyCategory(category, fields) { db.parseIntFields(category, intFields, fields); - const escapeFields = ['name', 'nickname', 'description', 'federatedDescription', 'color', 'bgColor', 'backgroundImage', 'imageClass', 'class', 'link']; + const escapeFields = ['name', 'nickname', 'description', 'color', 'bgColor', 'backgroundImage', 'imageClass', 'class', 'link']; escapeFields.forEach((field) => { if (category.hasOwnProperty(field)) { category[field] = validator.escape(String(category[field] || '')); diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index b935638ac4..19b0efa116 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -36,16 +36,6 @@
-
- - -

- [[admin/manage/categories:federatedDescription.help]] -

-
-