From a0d9f999456c80465eb9c105cb9a69d3b1298066 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 17 Feb 2025 04:45:48 +0000 Subject: [PATCH 01/27] chore: incrementing version number - v4.0.4 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index ee8ba84e8a..94b5101af9 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.0.3", + "version": "4.0.4", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From d3b69a397f937645dc8aa9f18f1163170d8999df Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 17 Feb 2025 04:45:48 +0000 Subject: [PATCH 02/27] chore: update changelog for v4.0.4 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 783b4dd082..d8de9449cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +#### v4.0.4 (2025-02-17) + +##### Chores + +* up harmony (0fed9a76) +* up harmony (ef2c606d) +* up harmony (f1da510f) +* up deps (fa366095) +* up harmony (df07fcfa) +* up harmony (de5caf8f) +* up harmony (d1f78295) +* incrementing version number - v4.0.3 (2b65c735) +* update changelog for v4.0.3 (123e1635) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) +* **i18n:** fallback strings for new resources: nodebb.themes-harmony (99210918) + +##### Bug Fixes + +* clear parsed post cache when updating a post's attachments, #13164 (33d7b9b3) +* logic failure causing remote posts with image to not parse properly, #13164 (d936d5c0) +* change the passed-in notificatiom id for `notifyTagFollowers` to contain the list of matched tags (04f51cc6) +* actor.prune, dont try deleting same users (ffbe4b7b) +* getLocalFollowCounts, show non existing deletes (cfbb8ff8) +* return null if field isn't in hash (70a9f6d3) +* getUserField so that it always returns null (e85662a5) +* isArray check (224910b1) +* sanity-check the id when mocking a post (5cbf3dd7) +* missing actor on some local activities when federating out (040584f0) + +##### Performance Improvements + +* closes #13145, reduce calls in actors.prune (d590c2af) + +##### Refactors + +* single remove (77dd6dd0) +* cleanup ip:recent (d8724708) +* hooks button (c4b01330) + #### v4.0.3 (2025-02-09) ##### Chores From 4134a075c832db8b7f18bbf262c81233a1c097d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 17 Feb 2025 14:26:49 -0500 Subject: [PATCH 03/27] fix: remove handle on category purge closes #13171 --- src/categories/delete.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/categories/delete.js b/src/categories/delete.js index a03d96ee37..6581098c10 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -31,6 +31,9 @@ module.exports = function (Categories) { if (categoryData && categoryData.name) { bulkRemove.push(['categories:name', `${categoryData.name.slice(0, 200).toLowerCase()}:${cid}`]); } + if (categoryData && categoryData.handle) { + bulkRemove.push(['categoryhandle:cid', categoryData.handle]); + } await db.sortedSetRemoveBulk(bulkRemove); await removeFromParent(cid); From b0e8058f580532b5344043f2e58f73aa68a5595c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 17 Feb 2025 21:03:49 -0500 Subject: [PATCH 04/27] fix: page index for single page, closes #13173 add test case --- src/pagination.js | 10 +++++----- test/pagination.js | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/pagination.js b/src/pagination.js index bed225560a..c9f082a9a4 100644 --- a/src/pagination.js +++ b/src/pagination.js @@ -29,8 +29,7 @@ pagination.create = function (currentPage, pageCount, queryObj) { if (startPage > pageCount - 5) { startPage -= 2 - (pageCount - currentPage); } - let i; - for (i = 0; i < 5; i += 1) { + for (let i = 0; i < 5; i += 1) { pagesToShow.push(startPage + i); } @@ -45,10 +44,11 @@ pagination.create = function (currentPage, pageCount, queryObj) { return { page: page, active: page === currentPage, qs: qs.stringify(queryObj) }; }); - for (i = pages.length - 1; i > 0; i -= 1) { + for (let i = pages.length - 1; i > 0; i -= 1) { + const prevPage = pages[i].page - 1; if (pages[i].page - 2 === pages[i - 1].page) { - pages.splice(i, 0, { page: pages[i].page - 1, active: false, qs: qs.stringify(queryObj) }); - } else if (pages[i].page - 1 !== pages[i - 1].page) { + pages.splice(i, 0, { page: prevPage, active: false, qs: qs.stringify({ ...queryObj, page: prevPage }) }); + } else if (prevPage !== pages[i - 1].page) { pages.splice(i, 0, { separator: true }); } } diff --git a/test/pagination.js b/test/pagination.js index 3073728d8d..126208b276 100644 --- a/test/pagination.js +++ b/test/pagination.js @@ -26,6 +26,18 @@ describe('Pagination', () => { done(); }); + it('should create pagination for 18 pages and should not turn page 3 into separator', (done) => { + const data = pagination.create(6, 18); + // [1, 2, 3, 4, 5, (6), 7, 8, seperator, 17, 18] + assert.equal(data.pages.length, 11); + assert.equal(data.rel.length, 2); + assert.strictEqual(data.pages[2].qs, 'page=3'); + assert.equal(data.pageCount, 18); + assert.equal(data.prev.page, 5); + assert.equal(data.next.page, 7); + done(); + }); + it('should create pagination for 3 pages with query params', (done) => { const data = pagination.create(1, 3, { key: 'value' }); assert.equal(data.pages.length, 3); From 5e71d597a47c6b16859a8be4c49d855e821008f1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 18 Feb 2025 10:12:22 -0500 Subject: [PATCH 05/27] fix: notes.assertPrivate sanity checks --- src/activitypub/notes.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 03cca7d9d8..f40d4bdb68 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -229,6 +229,10 @@ Notes.assertPrivate = async (object) => { // Given an object, adds it to an existing chat or creates a new chat otherwise // todo: context stuff + if (!object || !object.id || !activitypub.helpers.isUri(object.id)) { + return null; + } + const localUids = []; const recipients = new Set([...object.to, ...object.cc]); await Promise.all(Array.from(recipients).map(async (value) => { From 80cc1d34b08357ec089366883e5d7dcdac6ebb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 10:42:52 -0500 Subject: [PATCH 06/27] fix: closes #13176, check if uid is number when creating tokens --- src/api/utils.js | 8 +++++++- src/views/admin/partials/edit-token-modal.tpl | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/api/utils.js b/src/api/utils.js index 67e496a5f5..06d6ce741d 100644 --- a/src/api/utils.js +++ b/src/api/utils.js @@ -52,6 +52,9 @@ utils.tokens.get = async (tokens) => { }; utils.tokens.generate = async ({ uid, description }) => { + if (!srcUtils.isNumber(uid)) { + throw new Error('[[error:invalid-uid]]'); + } if (parseInt(uid, 10) !== 0) { const uidExists = await user.exists(uid); if (!uidExists) { @@ -66,7 +69,7 @@ utils.tokens.generate = async ({ uid, description }) => { }; utils.tokens.add = async ({ token, uid, description = '', timestamp = Date.now() }) => { - if (!token || uid === undefined) { + if (!token || uid === undefined || !srcUtils.isNumber(uid)) { throw new Error('[[error:invalid-data]]'); } @@ -80,6 +83,9 @@ utils.tokens.add = async ({ token, uid, description = '', timestamp = Date.now() }; utils.tokens.update = async (token, { uid, description }) => { + if (!srcUtils.isNumber(uid)) { + throw new Error('[[error:invalid-uid]]'); + } await Promise.all([ db.setObject(`token:${token}`, { uid, description }), db.sortedSetAdd(`tokens:uid`, uid, token), diff --git a/src/views/admin/partials/edit-token-modal.tpl b/src/views/admin/partials/edit-token-modal.tpl index 87644f6dcd..22c100ccab 100644 --- a/src/views/admin/partials/edit-token-modal.tpl +++ b/src/views/admin/partials/edit-token-modal.tpl @@ -1,13 +1,13 @@
- +

[[admin/settings/api:uid-help-text]]

- +
\ No newline at end of file From 6431824216c1871ea112064575575261254404d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 10:53:25 -0500 Subject: [PATCH 07/27] test: wait after post request --- test/activitypub/analytics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/activitypub/analytics.js b/test/activitypub/analytics.js index 0f6cd7af36..9b97980cd0 100644 --- a/test/activitypub/analytics.js +++ b/test/activitypub/analytics.js @@ -128,7 +128,7 @@ describe('Analytics', () => { let counters; ({ counters } = analytics.peek()); const before = { ...counters }; - + const { setTimeout } = require('timers/promises'); const id = `https://example.org/activity/${utils.generateUUID()}`; await controllers.activitypub.postInbox({ body: { @@ -141,7 +141,7 @@ describe('Analytics', () => { }, }, }, { sendStatus: () => {} }); - + await setTimeout(2000); ({ counters } = analytics.peek()); const after = { ...counters }; From 92708d2f6b275e3afa41b228f4f91c87caf6e938 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 18 Feb 2025 10:56:56 -0500 Subject: [PATCH 08/27] fix: #13170, remove mime-type and regex test for "Emoji" attachment, wrap tag name in colons if not provided --- src/posts/create.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/posts/create.js b/src/posts/create.js index 869fdf3a05..656ae68ab0 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -11,8 +11,6 @@ const privileges = require('../privileges'); const activitypub = require('../activitypub'); const utils = require('../utils'); -const isEmojiShortcode = /^:[\w]+:$/; - module.exports = function (Posts) { Posts.create = async function (data) { // This is an internal method, consider using Topics.reply instead @@ -54,9 +52,15 @@ module.exports = function (Posts) { if (_activitypub && _activitypub.tag && Array.isArray(_activitypub.tag)) { _activitypub.tag .filter(tag => tag.type === 'Emoji' && - isEmojiShortcode.test(tag.name) && - tag.icon && tag.icon.mediaType && tag.icon.mediaType.startsWith('image/')) + 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'), ``); }); } From f28240732265f28251157d9f41695446daf194a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 11:43:34 -0500 Subject: [PATCH 09/27] test: show objects on fail --- test/activitypub/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/activitypub/analytics.js b/test/activitypub/analytics.js index 9b97980cd0..af836be14f 100644 --- a/test/activitypub/analytics.js +++ b/test/activitypub/analytics.js @@ -147,7 +147,7 @@ describe('Analytics', () => { const metrics = ['activities', 'activities:byType:Like', 'activities:byHost:example.org']; metrics.forEach((metric) => { - assert(before[metric] && after[metric]); + assert(before[metric] && after[metric], JSON.stringify({ before, after }, null, 2)); assert(before[metric] < after[metric]); }); }); From 669755d1e9f73dfe733bc192715a80cf8fe8adeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 11:57:28 -0500 Subject: [PATCH 10/27] test: dont clear local when testing --- src/analytics.js | 3 +++ test/activitypub/analytics.js | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/analytics.js b/src/analytics.js index 45e8f698d9..b70aec7e5c 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -28,8 +28,11 @@ const total = _.cloneDeep(local); const runJobs = nconf.get('runJobs'); +Analytics.pause = false; + Analytics.init = async function () { new cronJob('*/10 * * * * *', (async () => { + if (Analytics.pause) return; publishLocalAnalytics(); if (runJobs) { await sleep(2000); diff --git a/test/activitypub/analytics.js b/test/activitypub/analytics.js index af836be14f..eda578b675 100644 --- a/test/activitypub/analytics.js +++ b/test/activitypub/analytics.js @@ -126,9 +126,9 @@ describe('Analytics', () => { it('should increment various metrics', async () => { let counters; + analytics.pause = true; ({ counters } = analytics.peek()); const before = { ...counters }; - const { setTimeout } = require('timers/promises'); const id = `https://example.org/activity/${utils.generateUUID()}`; await controllers.activitypub.postInbox({ body: { @@ -141,7 +141,7 @@ describe('Analytics', () => { }, }, }, { sendStatus: () => {} }); - await setTimeout(2000); + ({ counters } = analytics.peek()); const after = { ...counters }; @@ -150,5 +150,6 @@ describe('Analytics', () => { assert(before[metric] && after[metric], JSON.stringify({ before, after }, null, 2)); assert(before[metric] < after[metric]); }); + analytics.pause = false; }); }); From 9997189aeae75784e43507e0c54b2b7e017a05a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 12:06:02 -0500 Subject: [PATCH 11/27] feat: remove activities older than a week --- src/activitypub/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 3eec80eae1..1ab36c3773 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -61,6 +61,7 @@ ActivityPub.startJobs = () => { new CronJob('0 0 * * *', async () => { try { await ActivityPub.notes.prune(); + await db.sortedSetsRemoveRangeByScore(['activities:datetime'], '-inf', Date.now() - 604800000); } catch (err) { winston.error(err.stack); } From 4bc0031f587f62f810a5af0fdb241f413bd3821f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 18 Feb 2025 13:07:11 -0500 Subject: [PATCH 12/27] chore: add test helper to activitypub file --- src/activitypub/helpers.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index dd0b0c05ff..5a50e2e8ed 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -28,6 +28,16 @@ const sha256 = payload => crypto.createHash('sha256').update(payload).digest('he const Helpers = module.exports; +Helpers._test = (method, args) => { + // because I am lazy and I probably wrote some variant of this below code 1000 times already + setTimeout(async () => { + console.log(await method.apply(method, args)); + }, 2500); +}; +// process.nextTick(() => { +// Helpers._test(activitypub.notes.assert, [1, `https://`]); +// }); + let _lastLog; Helpers.log = (message) => { if (!message) { From de6e63bbd7e0be5459d8541faec280312b1532b9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 18 Feb 2025 13:33:11 -0500 Subject: [PATCH 13/27] fix: add back chronological sorting of asserted notes --- src/activitypub/notes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index f40d4bdb68..2b6a0fceaa 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -71,7 +71,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { } // Reorder chain items by timestamp - // chain = chain.sort((a, b) => a.timestamp - b.timestamp); + chain = chain.sort((a, b) => a.timestamp - b.timestamp); const mainPost = chain[0]; let { pid: mainPid, tid, uid: authorId, timestamp, name, content, sourceContent, _activitypub } = mainPost; From 6245e33d6e1702c740d3f3b835d5a8a03fbb073d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 18 Feb 2025 13:34:17 -0500 Subject: [PATCH 14/27] fix: #13179, fix context resolution failure bug with frequency --- src/activitypub/contexts.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/activitypub/contexts.js b/src/activitypub/contexts.js index 12dcae1059..b6cb1890e6 100644 --- a/src/activitypub/contexts.js +++ b/src/activitypub/contexts.js @@ -55,8 +55,20 @@ Contexts.getItems = async (uid, id, options) => { options.root = true; } - activitypub.helpers.log(`[activitypub/context] Retrieving context ${id}`); - let { type, items, orderedItems, first, next } = await activitypub.get('uid', uid, id); + // Page object instead of id + let object; + if (!id && options.object) { + object = options.object; + } else { + activitypub.helpers.log(`[activitypub/context] Retrieving context/page ${id}`); + try { + object = await activitypub.get('uid', uid, id); + } catch (e) { + return false; + } + } + let { type, items, orderedItems, first, next } = object; + if (!acceptableTypes.includes(type)) { return false; } @@ -84,14 +96,18 @@ Contexts.getItems = async (uid, id, options) => { if (next) { activitypub.helpers.log('[activitypub/context] Fetching next page...'); + const isUrl = activitypub.helpers.isUri(next); Array - .from(await Contexts.getItems(uid, next, { + .from(await Contexts.getItems(uid, isUrl && next, { ...options, root: false, + object: !isUrl && next, })) .forEach((item) => { chain.add(item); }); + + return chain; } // Handle special case where originating object is not actually part of the context collection From f67a0a124ad74b7cf6c79d00c1da27483fa9cc6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 17:04:54 -0500 Subject: [PATCH 15/27] feat: add upload button to quickreply --- install/package.json | 6 +++--- public/src/modules/quickreply.js | 1 + public/src/modules/uploadHelpers.js | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/install/package.json b/install/package.json index 94b5101af9..fc8a11f427 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.1", "nodebb-plugin-web-push": "0.7.2", "nodebb-rewards-essentials": "1.0.1", - "nodebb-theme-harmony": "2.0.25", + "nodebb-theme-harmony": "2.0.26", "nodebb-theme-lavender": "7.1.17", - "nodebb-theme-peace": "2.2.38", - "nodebb-theme-persona": "14.0.14", + "nodebb-theme-peace": "2.2.39", + "nodebb-theme-persona": "14.0.15", "nodebb-widget-essentials": "7.0.32", "nodemailer": "6.9.16", "nprogress": "0.2.0", diff --git a/public/src/modules/quickreply.js b/public/src/modules/quickreply.js index ee12097eff..b0d5b1a672 100644 --- a/public/src/modules/quickreply.js +++ b/public/src/modules/quickreply.js @@ -38,6 +38,7 @@ define('quickreply', [ }); uploadHelpers.init({ + uploadBtnEl: $('[component="topic/quickreply/upload/button"]'), dragDropAreaEl: $('[component="topic/quickreply/container"] .quickreply-message'), pasteEl: element, uploadFormEl: $('[component="topic/quickreply/upload"]'), diff --git a/public/src/modules/uploadHelpers.js b/public/src/modules/uploadHelpers.js index ce6cb08476..49ed9fd9fb 100644 --- a/public/src/modules/uploadHelpers.js +++ b/public/src/modules/uploadHelpers.js @@ -41,6 +41,7 @@ define('uploadHelpers', ['alerts'], function (alerts) { const fileInput = formEl.find('input[name="files[]"]'); options.uploadBtnEl.on('click', function () { fileInput.trigger('click'); + return false; }); fileInput.on('change', function (e) { const files = (e.target || {}).files || From 310fab65596dfcc015e478e7b65d4ac29f49375c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 22:05:17 -0500 Subject: [PATCH 16/27] chore: up dbsearch --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index fc8a11f427..4294da25f9 100644 --- a/install/package.json +++ b/install/package.json @@ -100,7 +100,7 @@ "nconf": "0.12.1", "nodebb-plugin-2factor": "7.5.9", "nodebb-plugin-composer-default": "10.2.45", - "nodebb-plugin-dbsearch": "6.2.9", + "nodebb-plugin-dbsearch": "6.2.10", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.1.0", From d0a9ddeaab7bbeb6cbbba4e4bc9ee7e747ff092e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 18 Feb 2025 22:28:08 -0500 Subject: [PATCH 17/27] chore: up dbsearch --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 4294da25f9..3b366b25a2 100644 --- a/install/package.json +++ b/install/package.json @@ -100,7 +100,7 @@ "nconf": "0.12.1", "nodebb-plugin-2factor": "7.5.9", "nodebb-plugin-composer-default": "10.2.45", - "nodebb-plugin-dbsearch": "6.2.10", + "nodebb-plugin-dbsearch": "6.2.12", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.1.0", From bb9687bd897445d0df8de4ae872d896a2eb19f5b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 19 Feb 2025 12:02:11 -0500 Subject: [PATCH 18/27] fix: #13172, Topics.addParentPosts not sending sourceContent in calling parsePosts --- src/topics/posts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/topics/posts.js b/src/topics/posts.js index bbbeb9c636..e32c18e727 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -192,7 +192,7 @@ module.exports = function (Topics) { const pidToPrivs = _.zipObject(parentPids, postPrivileges); parentPids = parentPids.filter(p => pidToPrivs[p]['topics:read']); - const parentPosts = await posts.getPostsFields(parentPids, ['uid', 'pid', 'timestamp', 'content', 'deleted']); + const parentPosts = await posts.getPostsFields(parentPids, ['uid', 'pid', 'timestamp', 'content', 'sourceContent', 'deleted']); const parentUids = _.uniq(parentPosts.map(postObj => postObj && postObj.uid)); const userData = await user.getUsersFields(parentUids, ['username', 'userslug', 'picture']); From c0996a80896408b1aa88ecabef4171e8dfcb7d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 19 Feb 2025 18:01:57 -0500 Subject: [PATCH 19/27] chore: up harmony --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 3b366b25a2..cd46fb47a3 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-plugin-spam-be-gone": "2.3.1", "nodebb-plugin-web-push": "0.7.2", "nodebb-rewards-essentials": "1.0.1", - "nodebb-theme-harmony": "2.0.26", + "nodebb-theme-harmony": "2.0.27", "nodebb-theme-lavender": "7.1.17", "nodebb-theme-peace": "2.2.39", "nodebb-theme-persona": "14.0.15", From e23a14c1758c57ce89cf97bda863c9bf9f9ce97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 19 Feb 2025 19:21:59 -0500 Subject: [PATCH 20/27] chore: up widgets --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index cd46fb47a3..84141c499c 100644 --- a/install/package.json +++ b/install/package.json @@ -112,7 +112,7 @@ "nodebb-theme-lavender": "7.1.17", "nodebb-theme-peace": "2.2.39", "nodebb-theme-persona": "14.0.15", - "nodebb-widget-essentials": "7.0.32", + "nodebb-widget-essentials": "7.0.34", "nodemailer": "6.9.16", "nprogress": "0.2.0", "passport": "0.7.0", From a410587ce1ee00e09f051577124854a9eaa341b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 19 Feb 2025 20:09:31 -0500 Subject: [PATCH 21/27] fix: closes #13180, don't execute cron jobs if ap disabled --- src/activitypub/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 1ab36c3773..eb60b7a1a6 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -59,6 +59,9 @@ ActivityPub.instances = require('./instances'); ActivityPub.startJobs = () => { ActivityPub.helpers.log('[activitypub/jobs] Registering jobs.'); new CronJob('0 0 * * *', async () => { + if (!meta.config.activitypubEnabled) { + return; + } try { await ActivityPub.notes.prune(); await db.sortedSetsRemoveRangeByScore(['activities:datetime'], '-inf', Date.now() - 604800000); @@ -68,6 +71,9 @@ ActivityPub.startJobs = () => { }, null, true, null, null, false); // change last argument to true for debugging new CronJob('*/30 * * * *', async () => { + if (!meta.config.activitypubEnabled) { + return; + } try { await ActivityPub.actors.prune(); } catch (err) { From f82f00e5f5a8cd06a814f46416d8569f83b19a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 19 Feb 2025 23:25:36 -0500 Subject: [PATCH 22/27] chore: up harmony --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 84141c499c..a1a3f5b729 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-plugin-spam-be-gone": "2.3.1", "nodebb-plugin-web-push": "0.7.2", "nodebb-rewards-essentials": "1.0.1", - "nodebb-theme-harmony": "2.0.27", + "nodebb-theme-harmony": "2.0.28", "nodebb-theme-lavender": "7.1.17", "nodebb-theme-peace": "2.2.39", "nodebb-theme-persona": "14.0.15", From 625f47514f6271df09d0678f0e343c60fedf15dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 20 Feb 2025 09:18:03 -0500 Subject: [PATCH 23/27] fix: escape ip blacklist rules --- src/controllers/globalmods.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/globalmods.js b/src/controllers/globalmods.js index 2ad0d54b4f..5b6b1ee607 100644 --- a/src/controllers/globalmods.js +++ b/src/controllers/globalmods.js @@ -1,5 +1,7 @@ 'use strict'; +const validator = require('validator'); + const user = require('../user'); const meta = require('../meta'); const analytics = require('../analytics'); @@ -20,7 +22,7 @@ globalModsController.ipBlacklist = async function (req, res, next) { ]); res.render('ip-blacklist', { title: '[[pages:ip-blacklist]]', - rules: rules, + rules: validator.escape(String(rules)), analytics: analyticsData, breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:ip-blacklist]]' }]), }); From b8200095c011ac363e287742033d941ef57f3d8a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 11:38:47 -0500 Subject: [PATCH 24/27] fix: #13129, serve category backgroundImage as actor `icon`, not `image` + tests for category actor --- src/activitypub/mocks.js | 21 ++++++------ test/activitypub.js | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 003d392333..223d1a8cbd 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -341,23 +341,24 @@ Mocks.actors.category = async (cid) => { } = await categories.getCategoryData(cid); const publicKey = await activitypub.getPublicKey('cid', cid); - let image; + let icon; if (backgroundImage) { const filename = path.basename(utils.decodeHTMLEntities(backgroundImage)); - image = { + icon = { type: 'Image', mediaType: mime.getType(filename), url: `${nconf.get('url')}${utils.decodeHTMLEntities(backgroundImage)}`, }; + } else { + icon = await categories.icons.get(cid); + icon = icon.get('png'); + icon = { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}${icon}`, + }; } - let icon = await categories.icons.get(cid); - icon = icon.get('png'); - icon = { - type: 'Image', - mediaType: 'image/png', - url: `${nconf.get('url')}${icon}`, - }; return { '@context': [ @@ -375,7 +376,7 @@ Mocks.actors.category = async (cid) => { name, preferredUsername, summary, - image, + // image, // todo once categories have cover photos icon, publicKey: { diff --git a/test/activitypub.js b/test/activitypub.js index aee20d8037..d2c2334140 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -350,6 +350,79 @@ describe('ActivityPub integration', () => { }); }); + describe.only('Category Actor endpoint', () => { + let cid; + let slug; + let description; + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + description = utils.generateUUID(); + ({ cid } = await categories.create({ + name: slug, + description, + })); + }); + + it('should return a valid ActivityPub Actor JSON-LD payload', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(body.hasOwnProperty('@context')); + assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); + + ['id', 'url', /* 'followers', 'following', */ 'inbox', 'outbox'].forEach((prop) => { + assert(body.hasOwnProperty(prop)); + assert(body[prop]); + }); + + assert.strictEqual(body.id, `${nconf.get('url')}/category/${cid}`); + assert.strictEqual(body.type, 'Group'); + assert.strictEqual(body.summary, description); + assert.deepStrictEqual(body.icon, { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}/assets/uploads/category/category-${cid}-icon.png`, + }); + }); + + it('should contain a `publicKey` property with a public key', async () => { + const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(body.hasOwnProperty('publicKey')); + assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); + }); + + it('should serve the the backgroundImage in `icon` if set', async () => { + const payload = {}; + payload[cid] = { + backgroundImage: `/assets/uploads/files/test.png`, + }; + await categories.update(payload); + + const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert.deepStrictEqual(body.icon, { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}/assets/uploads/files/test.png`, + }); + }); + }); + describe('Instance Actor endpoint', () => { let response; let body; From 7520e4f64de58ab195dd9a78b15dc4945d6fb052 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 11:54:24 -0500 Subject: [PATCH 25/27] chore: bump composer to 10.2.46 for #13132 --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index a1a3f5b729..715b472581 100644 --- a/install/package.json +++ b/install/package.json @@ -99,7 +99,7 @@ "multiparty": "4.2.3", "nconf": "0.12.1", "nodebb-plugin-2factor": "7.5.9", - "nodebb-plugin-composer-default": "10.2.45", + "nodebb-plugin-composer-default": "10.2.46", "nodebb-plugin-dbsearch": "6.2.12", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", @@ -200,4 +200,4 @@ "url": "https://github.com/barisusakli" } ] -} \ No newline at end of file +} From 93f48409c5aa297b7ce75ea901f07013ad90b57c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 12:24:17 -0500 Subject: [PATCH 26/27] fix: #13136, do not log 404s for AP requests --- src/activitypub/helpers.js | 5 +++++ src/controllers/404.js | 7 +++++++ src/middleware/activitypub.js | 6 ++---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 5a50e2e8ed..79aebc25dc 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -64,6 +64,11 @@ Helpers.isUri = (value) => { }); }; +Helpers.assertAccept = accept => (accept && accept.split(',').some((value) => { + const parts = value.split(';').map(v => v.trim()); + return activitypub._constants.acceptableTypes.includes(value || parts[0]); +})); + Helpers.isWebfinger = (value) => { // N.B. returns normalized handle, so truthy check! if (webfingerRegex.test(value) && !Helpers.isUri(value)) { diff --git a/src/controllers/404.js b/src/controllers/404.js index becc206e76..bed1a085e3 100644 --- a/src/controllers/404.js +++ b/src/controllers/404.js @@ -6,6 +6,7 @@ const validator = require('validator'); const meta = require('../meta'); const plugins = require('../plugins'); +const activitypub = require('../activitypub'); const middleware = require('../middleware'); const helpers = require('../middleware/helpers'); const { secureRandom } = require('../utils'); @@ -24,6 +25,12 @@ exports.handle404 = helpers.try(async (req, res) => { if (isClientScript.test(req.url)) { res.type('text/javascript').status(404).send('Not Found'); + } else if ( + activitypub.helpers.assertAccept(req.headers.accept) || + (req.headers['Content-Type'] && activitypub._constants.acceptableTypes.includes(req.headers['Content-Type'])) + ) { + // todo: separate logging of AP 404s + res.sendStatus(404); } else if ( !res.locals.isAPI && ( req.path.startsWith(`${relativePath}/assets/uploads`) || diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index f9b8dcd009..ee7d8a2460 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -16,10 +16,8 @@ middleware.assertS2S = async function (req, res, next) { return next('route'); } - const pass = (accept && accept.split(',').some((value) => { - const parts = value.split(';').map(v => v.trim()); - return activitypub._constants.acceptableTypes.includes(value || parts[0]); - })) || (contentType && activitypub._constants.acceptableTypes.includes(contentType)); + const pass = activitypub.helpers.assertAccepts(accept) || + (contentType && activitypub._constants.acceptableTypes.includes(contentType)); if (!pass) { return next('route'); From e63f1234a7fa4c5d9d9be7dafd07be498dcbe1ee Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 12:50:05 -0500 Subject: [PATCH 27/27] fix: typo --- src/middleware/activitypub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index ee7d8a2460..05f67d3338 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -16,7 +16,7 @@ middleware.assertS2S = async function (req, res, next) { return next('route'); } - const pass = activitypub.helpers.assertAccepts(accept) || + const pass = activitypub.helpers.assertAccept(accept) || (contentType && activitypub._constants.acceptableTypes.includes(contentType)); if (!pass) {