From f4faa0b7d19fb84924495cfea8d1082fab87b7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 20 Nov 2023 18:33:02 -0500 Subject: [PATCH 0001/4744] feat: better layout for manage chat room modal --- public/src/client/chats/manage.js | 1 + src/views/modals/manage-room.tpl | 54 +++++++++++++++++-------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/public/src/client/chats/manage.js b/public/src/client/chats/manage.js index fce6db3505..72fbf2ab1f 100644 --- a/public/src/client/chats/manage.js +++ b/public/src/client/chats/manage.js @@ -32,6 +32,7 @@ define('forum/chats/manage', [ }); modal = bootbox.dialog({ title: '[[modules:chat.manage-room]]', + size: 'large', message: html, onEscape: true, }); diff --git a/src/views/modals/manage-room.tpl b/src/views/modals/manage-room.tpl index 0469897464..08c96ccb0b 100644 --- a/src/views/modals/manage-room.tpl +++ b/src/views/modals/manage-room.tpl @@ -1,19 +1,5 @@
- - -

-

[[modules:chat.add-user-help]]

- -
- - - - - {{{ if user.isAdmin }}} -
- - {{{ if room.public }}} - - - - +
{{{ end }}} + + + +

+

[[modules:chat.add-user-help]]

+ +
+ +
+
+ + +
    +
  • [[modules:chat.retrieving-users]]
  • +
+
+ {{{ if (user.isAdmin && room.public) }}} +
+ + +
+ {{{ end }}} +
+ {{{ if user.isAdmin }}} +
From c404ef73cf42c616279b4eea93c0d7a1bb8fc732 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:22:09 -0500 Subject: [PATCH 0002/4744] fix(deps): update dependency lru-cache to v10.0.3 (#12175) 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 fcab369b69..691e1be128 100644 --- a/install/package.json +++ b/install/package.json @@ -84,7 +84,7 @@ "jsonwebtoken": "9.0.2", "lodash": "4.17.21", "logrotate-stream": "0.2.9", - "lru-cache": "10.0.2", + "lru-cache": "10.0.3", "mime": "3.0.0", "mkdirp": "3.0.1", "mongodb": "6.3.0", From 00cb5839b5bc0f23410be15e263210ea7ba41fed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:22:25 -0500 Subject: [PATCH 0003/4744] fix(deps): update dependency esbuild to v0.19.7 (#12176) 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 691e1be128..00d5a83b69 100644 --- a/install/package.json +++ b/install/package.json @@ -63,7 +63,7 @@ "csrf-sync": "4.0.1", "daemon": "1.1.0", "diff": "5.1.0", - "esbuild": "0.19.5", + "esbuild": "0.19.7", "express": "4.18.2", "express-session": "1.17.3", "express-useragent": "1.0.15", From fd5d7b651f3ef0a68cd1981e5a666cd9badf03bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:22:35 -0500 Subject: [PATCH 0004/4744] chore(deps): update commitlint monorepo to v18.4.3 (#12177) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 00d5a83b69..4927638679 100644 --- a/install/package.json +++ b/install/package.json @@ -154,8 +154,8 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@commitlint/cli": "18.4.2", - "@commitlint/config-angular": "18.4.2", + "@commitlint/cli": "18.4.3", + "@commitlint/config-angular": "18.4.3", "coveralls": "3.1.1", "eslint": "8.54.0", "eslint-config-nodebb": "0.2.1", From 22932bdb40f6fcedae56af5e6353923f0a5fc136 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:00:50 -0500 Subject: [PATCH 0005/4744] fix(deps): update dependency lru-cache to v10.1.0 (#12181) 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 4927638679..d00e9c5264 100644 --- a/install/package.json +++ b/install/package.json @@ -84,7 +84,7 @@ "jsonwebtoken": "9.0.2", "lodash": "4.17.21", "logrotate-stream": "0.2.9", - "lru-cache": "10.0.3", + "lru-cache": "10.1.0", "mime": "3.0.0", "mkdirp": "3.0.1", "mongodb": "6.3.0", From 0a4f3c8a569969fda39cf8a348fd244d41d01ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 24 Nov 2023 17:05:36 -0500 Subject: [PATCH 0006/4744] fix: #12183, remove ensureLoggedIn middleware from category routes add privilege check to getTopicCount --- src/api/categories.js | 4 ++++ src/routes/write/categories.js | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/api/categories.js b/src/api/categories.js index 774091fd61..892c8e3d6a 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -80,6 +80,10 @@ categoriesAPI.delete = async function (caller, { cid }) { }; categoriesAPI.getTopicCount = async (caller, { cid }) => { + const allowed = await privileges.categories.can('find', cid, caller.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } const count = await categories.getCategoryField(cid, 'topic_count'); return { count }; }; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index ca149a54da..a6d464af6f 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -16,10 +16,10 @@ module.exports = function () { setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete); - setupApiRoute(router, 'get', '/:cid/count', [...middlewares, middleware.assert.category], controllers.write.categories.getTopicCount); - setupApiRoute(router, 'get', '/:cid/posts', [...middlewares, middleware.assert.category], controllers.write.categories.getPosts); - setupApiRoute(router, 'get', '/:cid/children', [...middlewares, middleware.assert.category], controllers.write.categories.getChildren); - setupApiRoute(router, 'get', '/:cid/topics', [...middlewares, middleware.assert.category], controllers.write.categories.getTopics); + setupApiRoute(router, 'get', '/:cid/count', [middleware.assert.category], controllers.write.categories.getTopicCount); + setupApiRoute(router, 'get', '/:cid/posts', [middleware.assert.category], controllers.write.categories.getPosts); + setupApiRoute(router, 'get', '/:cid/children', [middleware.assert.category], controllers.write.categories.getChildren); + setupApiRoute(router, 'get', '/:cid/topics', [middleware.assert.category], controllers.write.categories.getTopics); setupApiRoute(router, 'put', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); setupApiRoute(router, 'delete', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); From 1ca986e62eb2f8d5dcd426068bcde8c6c32c6fa3 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 25 Nov 2023 09:18:23 +0000 Subject: [PATCH 0007/4744] Latest translations and fallbacks --- public/language/fr/admin/extend/widgets.json | 4 ++-- public/language/fr/admin/settings/chat.json | 2 +- public/language/fr/admin/settings/user.json | 16 ++++++++-------- public/language/fr/category.json | 10 +++++----- public/language/fr/error.json | 2 +- public/language/fr/modules.json | 4 ++-- public/language/fr/notifications.json | 6 +++--- public/language/fr/post-queue.json | 6 +++--- public/language/fr/social.json | 4 ++-- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/public/language/fr/admin/extend/widgets.json b/public/language/fr/admin/extend/widgets.json index 351ca4632a..fae5f066c8 100644 --- a/public/language/fr/admin/extend/widgets.json +++ b/public/language/fr/admin/extend/widgets.json @@ -30,6 +30,6 @@ "start-date": "Date de début", "end-date": "Date de fin", "hide-on-mobile": "Masquer sur mobile", - "hide-drafts": "Hide drafts", - "show-drafts": "Show drafts" + "hide-drafts": "Masquer les brouillons", + "show-drafts": "Afficher les brouillons" } \ No newline at end of file diff --git a/public/language/fr/admin/settings/chat.json b/public/language/fr/admin/settings/chat.json index 3634d2118c..f57876fb80 100644 --- a/public/language/fr/admin/settings/chat.json +++ b/public/language/fr/admin/settings/chat.json @@ -7,7 +7,7 @@ "max-length": "Longueur maximale des messages de discussion", "max-chat-room-name-length": "Longueur maximale des noms de salons", "max-room-size": "Nombre maximum d'utilisateurs dans une même discussion", - "delay": "Temps entre les messages de chat (ms)", + "delay": "Temps entre chaque message de discussion (en millisecondes)", "notification-delay": "Délai de notification pour les messages de chat", "notification-delay-help": "Les messages supplémentaires envoyés pendant cette période sont regroupés et l’utilisateur est averti pendant ce délai. Définissez cette valeur sur 0 pour désactiver le délai.", "restrictions.seconds-edit-after": "Nombre de secondes pendant lesquelles un message de discussion restera modifiable.", diff --git a/public/language/fr/admin/settings/user.json b/public/language/fr/admin/settings/user.json index df7c33e54f..0440614549 100644 --- a/public/language/fr/admin/settings/user.json +++ b/public/language/fr/admin/settings/user.json @@ -79,14 +79,14 @@ "follow-replied-topics": "S'abonner aux sujets auxquels vous répondez", "default-notification-settings": "Paramètres des notifications par défaut", "categoryWatchState": "Abonnement par défaut", - "categoryWatchState.tracking": "Tracking", + "categoryWatchState.tracking": "Suivi", "categoryWatchState.notwatching": "Non abonné", "categoryWatchState.ignoring": "Ignoré", - "restrictions-new": "New User Restrictions", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-between-new": "Seconds between posts for new users", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", - "restrictions.seconds-edit-after-new": "Number of seconds a post remains editable for new users (set to 0 to disable)", - "restrictions.milliseconds-between-messages": "Time between chat messages for new users (ms)", - "restrictions.groups-exempt-from-new-user-restrictions": "Select groups that should be exempt from the new user restrictions" + "restrictions-new": "Restrictions des nouveaux utilisateurs", + "restrictions.rep-threshold": "Seuil de réputation avant que ces restrictions ne soient levées", + "restrictions.seconds-between-new": "Secondes entre les messages pour les nouveaux utilisateurs", + "restrictions.seconds-before-new": "Quelques secondes avant qu'un nouvel utilisateur puisse publier son premier message", + "restrictions.seconds-edit-after-new": "Nombre de secondes pendant lesquelles une publication reste modifiable (définissez la valeur sur 0 pour la désactiver)", + "restrictions.milliseconds-between-messages": "Temps entre chaque message de discussion (en millisecondes)", + "restrictions.groups-exempt-from-new-user-restrictions": "Sélectionnez les groupes qui devraient être exemptés des nouvelles restrictions d'utilisateur" } diff --git a/public/language/fr/category.json b/public/language/fr/category.json index 679de3c00d..f005ed6d19 100644 --- a/public/language/fr/category.json +++ b/public/language/fr/category.json @@ -10,15 +10,15 @@ "watch": "S'abonner", "ignore": "Ne plus surveiller", "watching": "Suivi", - "tracking": "Tracking", + "tracking": "Suivi", "not-watching": "Ne plus suivre", "ignoring": "Ignoré", - "watching.description": "Notify me of new topics.
Show topics in unread & recent", - "tracking.description": "Shows topics in unread & recent", + "watching.description": "Me notifier les nouvelles réponses.
Afficher le sujet dans l'onglet \"Non lu\" et \"récent\"", + "tracking.description": "Afficher les sujets non lus et récents", "not-watching.description": "Ne pas afficher les sujets non lus, afficher les récents", - "ignoring.description": "Do not show topics in unread & recent", + "ignoring.description": "Ne pas afficher les sujets non lus et récents", "watching.message": "Vous suivez maintenant les mises à jour de cette catégorie et de ses sous-catégories", - "tracking.message": "You are now tracking updates from this category and all subcategories", + "tracking.message": "Vous suivez maintenant les mises à jour de cette catégorie et de ses sous-catégories", "notwatching.message": "Vous ne suivez aucune mise à jour de cette catégorie et de ses sous-catégories.", "ignoring.message": "Vous ignorez maintenant les mises à jour de cette catégorie et de ses sous-catégories.", "watched-categories": "Catégories surveillées", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index dc4f714c1b..9c3ba9dee9 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -91,7 +91,7 @@ "category-not-selected": "Aucune catégorie sélectionnée", "too-many-posts": "Vous ne pouvez poster que toutes les %1 seconde(s) - merci de patienter avant de publier à nouveau.", "too-many-posts-newbie": "En tant que nouvel utilisateur, vous ne pouvez poster que toutes les %1 seconde(s) jusqu'à ce que vous obteniez une réputation de %2 - patientez avant de publier de nouveau.", - "too-many-posts-newbie-minutes": "As a new user, you can only post once every %1 minute(s) until you have earned %2 reputation - please wait before posting again", + "too-many-posts-newbie-minutes": "En tant que nouvel utilisateur, vous ne pouvez poster que toutes les %1 seconde(s) jusqu'à ce que vous obteniez une réputation de %2 - patientez avant de publier de nouveau.", "already-posting": "Vous pouvez poster", "tag-too-short": "Veuillez entrer un mot-clé plus long. Les mots-clés doivent contenir au moins %1 caractère(s).", "tag-too-long": "Veuillez entrer un mot-clé plus court. Les mot-clés ne peuvent excéder %1 caractère(s).", diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json index 1004b3cfc2..ad46215cfb 100644 --- a/public/language/fr/modules.json +++ b/public/language/fr/modules.json @@ -68,8 +68,8 @@ "chat.in-room": "Dans cet espace de discussion", "chat.kick": "Exclure", "chat.show-ip": "Voir IP", - "chat.copy-text": "Copy Text", - "chat.copy-link": "Copy Link", + "chat.copy-text": "Copier le texte", + "chat.copy-link": "Copier le lien", "chat.owner": "Espace Admin", "chat.grant-rescind-ownership": "Promouvoir/rétrograder comme propriétaire", "chat.system.user-join": "%1 a rejoint la discussion ", diff --git a/public/language/fr/notifications.json b/public/language/fr/notifications.json index 7eca6d7a16..a165b91046 100644 --- a/public/language/fr/notifications.json +++ b/public/language/fr/notifications.json @@ -13,7 +13,7 @@ "all": "Tout", "topics": "Sujets", "tags": "Mots-clés", - "categories": "Categories", + "categories": "Catégories", "replies": "Réponses", "chat": "Discussions", "group-chat": "Groupe de discussions", @@ -56,7 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 a posté un nouveau sujet avec le mot-clé %2 et %3", "user-posted-topic-with-tag-triple": "%1 a posté un nouveau sujet avec les mot-clés %2, %3 et %4", "user-posted-topic-with-tag-multiple": "%1 a posté un nouveau sujet avec les mot-clés %2", - "user-posted-topic-in-category": "%1 has posted a new topic in %2", + "user-posted-topic-in-category": "%1 a posté un nouveau sujet: %2", "user-started-following-you": "%1 vous suit.", "user-started-following-you-dual": "%1 et %2 se sont abonnés à votre compte.", "user-started-following-you-triple": "%1, %2 et %3 ont commencé à vous suivre.", @@ -83,7 +83,7 @@ "notificationType-upvote": "Lorsque quelqu'un a voté pour un de vos messages", "notificationType-new-topic": "Lorsque quelqu'un que vous suivez publie un sujet", "notificationType-new-topic-with-tag": "Lorsqu'un sujet est publié avec un mot-clé que vous suivez", - "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", + "notificationType-new-topic-in-category": "Lorsqu'un sujet est publié dans une catégorie que vous regardez", "notificationType-new-reply": "Lorsqu'une nouvelle réponse est ajoutée dans un sujet que vous suivez", "notificationType-post-edit": "Lorsqu'un article est modifié dans un sujet que vous regardez", "notificationType-follow": "Lorsque quelqu'un commence à vous suivre", diff --git a/public/language/fr/post-queue.json b/public/language/fr/post-queue.json index f82f350223..04758af10b 100644 --- a/public/language/fr/post-queue.json +++ b/public/language/fr/post-queue.json @@ -3,10 +3,10 @@ "post-queue": "File d’attente des messages", "no-queued-posts": "Il n'y a pas de messages dans la file d'attente des messages.", "no-single-post": "Le sujet ou le message que vous recherchez n'est plus dans la file d'attente. Il a probablement déjà été approuvé ou supprimé.", - "enabling-help": "The post queue is currently disabled. To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "enabling-help": "La file d'attente des publications est actuellement désactivée. Pour activer cette fonctionnalité, accédez à Paramètres → Publication → File d'attente et activez la File d'attente de publication.", "back-to-list": "Retour à la file d'attente", - "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": "Si vous avez des publications en file d'attente, elles seront affichées ici.", + "public-description": "Ce forum est configuré pour mettre automatiquement en file d'attente les publications des nouveaux comptes, en attente de l'approbation du modérateur.
Si vous avez mis en file d'attente des publications en attente d'approbation, vous pourrez les voir ici.", "user": "Utilisateur", "when": "Quand", "category": "Catégorie", diff --git a/public/language/fr/social.json b/public/language/fr/social.json index 062bb9c029..c0a67052a8 100644 --- a/public/language/fr/social.json +++ b/public/language/fr/social.json @@ -7,6 +7,6 @@ "sign-up-with-google": "Inscrivez-vous avec Google", "log-in-with-facebook": "Connectez-vous avec Facebook", "continue-with-facebook": "Continuer avec Facebook", - "sign-in-with-linkedin": "Sign in with LinkedIn", - "sign-up-with-linkedin": "Sign up with LinkedIn" + "sign-in-with-linkedin": "Connectez-vous avec LinkedIn", + "sign-up-with-linkedin": "Inscrivez-vous avec LinkedIn" } \ No newline at end of file From 56950547501e75b09b566cc1a16335bad6c39d0b Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 26 Nov 2023 09:18:22 +0000 Subject: [PATCH 0008/4744] Latest translations and fallbacks --- public/language/he/admin/settings/user.json | 16 ++++++++-------- public/language/he/category.json | 10 +++++----- public/language/he/modules.json | 4 ++-- public/language/he/notifications.json | 6 +++--- public/language/he/post-queue.json | 4 ++-- public/language/he/social.json | 4 ++-- public/language/he/topic.json | 4 ++-- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/public/language/he/admin/settings/user.json b/public/language/he/admin/settings/user.json index 79fac8cbb5..b442c9cb71 100644 --- a/public/language/he/admin/settings/user.json +++ b/public/language/he/admin/settings/user.json @@ -79,14 +79,14 @@ "follow-replied-topics": "עקוב אחר נושאים שהגבת עליהם", "default-notification-settings": "הגדרות התראות ברירת מחדל", "categoryWatchState": "מצב מעקב על קטגוריה בברירת מחדל", - "categoryWatchState.tracking": "Tracking", + "categoryWatchState.tracking": "מעקב", "categoryWatchState.notwatching": "לא עוקב", "categoryWatchState.ignoring": "מתעלם", - "restrictions-new": "New User Restrictions", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-between-new": "Seconds between posts for new users", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", - "restrictions.seconds-edit-after-new": "Number of seconds a post remains editable for new users (set to 0 to disable)", - "restrictions.milliseconds-between-messages": "Time between chat messages for new users (ms)", - "restrictions.groups-exempt-from-new-user-restrictions": "Select groups that should be exempt from the new user restrictions" + "restrictions-new": "הגבלות משתמש חדש", + "restrictions.rep-threshold": "סף מוניטין לפני הסרת הגבלות אלו", + "restrictions.seconds-between-new": "שניות בין פוסטים למשתמשים חדשים", + "restrictions.seconds-before-new": "שניות לפני שמשתמש חדש יכול לפרסם את הפוסט הראשון שלו", + "restrictions.seconds-edit-after-new": "מספר השניות שפוסט נשאר ניתן לעריכה עבור משתמשים חדשים (הגדר ל-0 כדי להשבית)", + "restrictions.milliseconds-between-messages": "זמן בין הודעות צ'אט למשתמשים חדשים (ms)", + "restrictions.groups-exempt-from-new-user-restrictions": "בחר קבוצות שיהיו פטורות מהגבלות משתמש חדש" } diff --git a/public/language/he/category.json b/public/language/he/category.json index 30108704ea..17abd2c13f 100644 --- a/public/language/he/category.json +++ b/public/language/he/category.json @@ -10,15 +10,15 @@ "watch": "עקוב", "ignore": "התעלם", "watching": "עוקב", - "tracking": "Tracking", + "tracking": "מעקב", "not-watching": "לא עוקב", "ignoring": "מתעלם", - "watching.description": "Notify me of new topics.
Show topics in unread & recent", - "tracking.description": "Shows topics in unread & recent", + "watching.description": "הודע לי על נושאים חדשים.
הצג נושאים שלא נקראו ואחרונים", + "tracking.description": "מציג נושאים שלא נקראו ואחרונים", "not-watching.description": "הסתר בנושאים שלא נקראו, הצג בנושאים אחרונים", - "ignoring.description": "Do not show topics in unread & recent", + "ignoring.description": "אל תציג נושאים שלא נקראו ואחרונים", "watching.message": "בחרת לעקוב אחר עדכונים בקטגוריה זו וכל תת-הקטגוריות", - "tracking.message": "You are now tracking updates from this category and all subcategories", + "tracking.message": "כעת אתה עוקב אחר עדכונים מקטגוריה זו ומכל קטגוריות המשנה", "notwatching.message": "בחרת לא לעקוב אחר עדכונים בקטגוריה זו וכל תת-הקטגוריות", "ignoring.message": "בחרת להתעלם מעדכונים בקטגוריה זו וכל תת-הקטגוריות", "watched-categories": "קטגוריות במעקב", diff --git a/public/language/he/modules.json b/public/language/he/modules.json index 977c3fd015..4b425dcaf8 100644 --- a/public/language/he/modules.json +++ b/public/language/he/modules.json @@ -68,8 +68,8 @@ "chat.in-room": "בתוך חדר זה", "chat.kick": "הוצא", "chat.show-ip": "הצג IP", - "chat.copy-text": "Copy Text", - "chat.copy-link": "Copy Link", + "chat.copy-text": "העתק טקסט", + "chat.copy-link": "העתק קישור", "chat.owner": "מנהלי החדר", "chat.grant-rescind-ownership": "הענק/בטל בעלות", "chat.system.user-join": "%1 הצטרף לחדר ", diff --git a/public/language/he/notifications.json b/public/language/he/notifications.json index bc99828297..ba5f2a41d3 100644 --- a/public/language/he/notifications.json +++ b/public/language/he/notifications.json @@ -13,7 +13,7 @@ "all": "הכל", "topics": "נושאים", "tags": "תגיות", - "categories": "Categories", + "categories": "קטגוריות", "replies": "תגובות", "chat": "צ'אטים", "group-chat": "צ'אט קבוצתי", @@ -56,7 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 פרסם נושא חדש עם התגיות %1 ו-%3", "user-posted-topic-with-tag-triple": "%1 פרסם נושא חדש עם התגיות %2, %3 ו-%4", "user-posted-topic-with-tag-multiple": "%1 פרסם נושא חדש עם התגיות %2", - "user-posted-topic-in-category": "%1 has posted a new topic in %2", + "user-posted-topic-in-category": "%1 פרסם נושא חדש ב%2", "user-started-following-you": "%1 התחיל לעקוב אחריך.", "user-started-following-you-dual": "%1 ו-%2 התחילו לעקוב אחריך.", "user-started-following-you-triple": "%1, %2 ו3% התחילו לעקוב אחריך.", @@ -83,7 +83,7 @@ "notificationType-upvote": "כאשר מישהו מצביע בעד הפוסט שלך", "notificationType-new-topic": "כשמישהו שאתה עוקב אחריו פרסם נושא", "notificationType-new-topic-with-tag": "כאשר נושא מתפרסם עם תג שאתה עוקב אחריו", - "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", + "notificationType-new-topic-in-category": "כאשר נושא מתפרסם בקטגוריה שאתה עוקב אחריה", "notificationType-new-reply": "כשתגובה חדשה מפורסמת בנושא שאתה עוקב אחריו", "notificationType-post-edit": "כשפוסט נערך בנושא שאתה עוקב אחריו", "notificationType-follow": "כשמישהו מתחיל לעקוב אחריך", diff --git a/public/language/he/post-queue.json b/public/language/he/post-queue.json index 80fc182303..6a7530a1ed 100644 --- a/public/language/he/post-queue.json +++ b/public/language/he/post-queue.json @@ -5,8 +5,8 @@ "no-single-post": "הנושא או הפוסט שאתה מחפש כבר לא בתור. כנראה שהוא כבר אושר או נמחק.", "enabling-help": "The post queue is currently disabled. To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", "back-to-list": "חזרה לתור פוסטים", - "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": "אם יש לך פוסטים בתור, הם יוצגו כאן.", + "public-description": "פורום זה מוגדר לדרוש אישור ידני של פוסטים מחשבונות חדשים על ידי מנהל.
אם יש לך פוסטים בתור הממתינים לאישור, תוכל לראות אותם כאן.", "user": "משתמש", "when": "כאשר", "category": "קטגוריה", diff --git a/public/language/he/social.json b/public/language/he/social.json index 83b0971cd5..31633e056a 100644 --- a/public/language/he/social.json +++ b/public/language/he/social.json @@ -7,6 +7,6 @@ "sign-up-with-google": "הירשם באמצעות Google", "log-in-with-facebook": "היכנס באמצעות Facebook", "continue-with-facebook": "המשך בFacebook", - "sign-in-with-linkedin": "Sign in with LinkedIn", - "sign-up-with-linkedin": "Sign up with LinkedIn" + "sign-in-with-linkedin": "היכנס באמצעות LinkedIn", + "sign-up-with-linkedin": "הירשם באמצעות LinkedIn" } \ No newline at end of file diff --git a/public/language/he/topic.json b/public/language/he/topic.json index be477b77e4..4d51a6088a 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -167,8 +167,8 @@ "composer.new-topic": "נושא חדש", "composer.editing-in": "עריכת פוסט ב-%1", "composer.uploading": "מעלה...", - "composer.thumb-url-label": "הדביקו את כתובת ה-URL לתמונה מוקטנת עבור הנושא", - "composer.thumb-title": "הוסיפו תמונה מוקטנת לנושא זה", + "composer.thumb-url-label": "הדביקו את כתובת ה-URL לתמונה הממוזערת עבור הנושא", + "composer.thumb-title": "הוספת תמונה ממוזערת לנושא", "composer.thumb-url-placeholder": "http://example.com/thumb.png", "composer.thumb-file-label": "או העלו קובץ", "composer.thumb-remove": "ניקוי שדות", From 50a90f8e03c520c0f1a94f93c188cb3e4e507fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 27 Nov 2023 09:11:26 -0500 Subject: [PATCH 0009/4744] fix: don't require login for listing categories --- src/routes/write/categories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index a6d464af6f..0f7aa1c473 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -10,7 +10,7 @@ const { setupApiRoute } = routeHelpers; module.exports = function () { const middlewares = [middleware.ensureLoggedIn]; - setupApiRoute(router, 'get', '/', [...middlewares], controllers.write.categories.list); + setupApiRoute(router, 'get', '/', controllers.write.categories.list); setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.categories.create); setupApiRoute(router, 'get', '/:cid', [], controllers.write.categories.get); setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); From 8c0472a08561d78b62ce4a8dd9a072dc779d304a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:57:23 -0500 Subject: [PATCH 0010/4744] chore(deps): update dependency jsdom to v23 (#12186) 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 d00e9c5264..bedca0ec84 100644 --- a/install/package.json +++ b/install/package.json @@ -163,7 +163,7 @@ "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", - "jsdom": "22.1.0", + "jsdom": "23.0.0", "lint-staged": "15.1.0", "mocha": "10.2.0", "mocha-lcov-reporter": "1.3.0", From bc59856e55a5127ac8d197417400f38d7d80a0ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:57:44 -0500 Subject: [PATCH 0011/4744] fix(deps): update dependency esbuild to v0.19.8 (#12187) 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 bedca0ec84..9b19065645 100644 --- a/install/package.json +++ b/install/package.json @@ -63,7 +63,7 @@ "csrf-sync": "4.0.1", "daemon": "1.1.0", "diff": "5.1.0", - "esbuild": "0.19.7", + "esbuild": "0.19.8", "express": "4.18.2", "express-session": "1.17.3", "express-useragent": "1.0.15", From bbf7c5e19289c9b11edd285744369f05ea6c4759 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:38:57 -0500 Subject: [PATCH 0012/4744] fix(deps): update dependency passport to v0.7.0 (#12190) 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 9b19065645..aa03f05d83 100644 --- a/install/package.json +++ b/install/package.json @@ -109,7 +109,7 @@ "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", - "passport": "0.6.0", + "passport": "0.7.0", "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", "pg": "8.11.3", From 4eaf2320d669ee6dc6e9d8e99dbc29d2ddda33cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 12:39:12 -0500 Subject: [PATCH 0013/4744] fix(deps): update dependency fs-extra to v11.2.0 (#12191) 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 aa03f05d83..5618b57e52 100644 --- a/install/package.json +++ b/install/package.json @@ -68,7 +68,7 @@ "express-session": "1.17.3", "express-useragent": "1.0.15", "file-loader": "6.2.0", - "fs-extra": "11.1.1", + "fs-extra": "11.2.0", "graceful-fs": "4.2.11", "helmet": "7.1.0", "html-to-text": "9.0.5", From b9050139507a139c5b147abe5ce833a9dbe2b963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 28 Nov 2023 20:58:07 -0500 Subject: [PATCH 0014/4744] fix: closes #12185, fix cli user password reset refactor session get/destroy --- src/api/users.js | 7 +------ src/cli/user.js | 1 + src/database/index.js | 22 ++++++++++++++++++++++ src/socket.io/index.js | 8 ++------ src/user/auth.js | 18 +++++------------- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/api/users.js b/src/api/users.js index ea0ce2f6b2..eda2b15d62 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -1,6 +1,5 @@ 'use strict'; -const util = require('util'); const path = require('path'); const fs = require('fs').promises; @@ -345,10 +344,6 @@ usersAPI.deleteToken = async (caller, { uid, token }) => { return true; }; -const getSessionAsync = util.promisify((sid, callback) => { - db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)); -}); - usersAPI.revokeSession = async (caller, { uid, uuid }) => { // Only admins or global mods (besides the user themselves) can revoke sessions if (parseInt(uid, 10) !== caller.uid && !await user.isAdminOrGlobalMod(caller.uid)) { @@ -359,7 +354,7 @@ usersAPI.revokeSession = async (caller, { uid, uuid }) => { let _id; for (const sid of sids) { /* eslint-disable no-await-in-loop */ - const sessionObj = await getSessionAsync(sid); + const sessionObj = await db.sessionStoreGet(sid); if (sessionObj && sessionObj.meta && sessionObj.meta.uuid === uuid) { _id = sid; break; diff --git a/src/cli/user.js b/src/cli/user.js index bbd747865f..f2db7e4a58 100644 --- a/src/cli/user.js +++ b/src/cli/user.js @@ -77,6 +77,7 @@ let winston; async function init() { db = require('../database'); await db.init(); + await db.initSessionStore(); user = require('../user'); groups = require('../groups'); diff --git a/src/database/index.js b/src/database/index.js index 51febea19d..2366ae3671 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -34,4 +34,26 @@ primaryDB.initSessionStore = async function () { primaryDB.sessionStore = await sessionStoreDB.createSessionStore(sessionStoreConfig); }; +function promisifySessionStoreMethod(method, sid) { + return new Promise((resolve, reject) => { + if (!primaryDB.sessionStore) { + resolve(method === 'get' ? null : undefined); + return; + } + + primaryDB.sessionStore[method](sid, (err, result) => { + if (err) reject(err); + else resolve(method === 'get' ? result || null : undefined); + }); + }); +} + +primaryDB.sessionStoreGet = function (sid) { + return promisifySessionStoreMethod('get', sid); +}; + +primaryDB.sessionStoreDestroy = function (sid) { + return promisifySessionStoreMethod('destroy', sid); +}; + module.exports = primaryDB; diff --git a/src/socket.io/index.js b/src/socket.io/index.js index c10f271585..d0ba0b4b19 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -241,10 +241,6 @@ async function checkMaintenance(socket) { throw new Error(`[[pages:maintenance.text, ${validator.escape(String(meta.config.title || 'NodeBB'))}]]`); } -const getSessionAsync = util.promisify( - (sid, callback) => db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)) -); - async function validateSession(socket, errorMsg) { const req = socket.request; const { sessionId } = await plugins.hooks.fire('filter:sockets.sessionId', { @@ -256,7 +252,7 @@ async function validateSession(socket, errorMsg) { return; } - const sessionData = await getSessionAsync(sessionId); + const sessionData = await db.sessionStoreGet(sessionId); if (!sessionData) { throw new Error(errorMsg); } @@ -282,7 +278,7 @@ async function authorize(request, callback) { request: request, }); - const sessionData = await getSessionAsync(sessionId); + const sessionData = await db.sessionStoreGet(sessionId); request.session = sessionData; let uid = 0; if (sessionData && sessionData.passport && sessionData.passport.user) { diff --git a/src/user/auth.js b/src/user/auth.js index 5330903a15..954d00a0c5 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -2,7 +2,6 @@ const winston = require('winston'); const validator = require('validator'); -const util = require('util'); const _ = require('lodash'); const db = require('../database'); const meta = require('../meta'); @@ -62,17 +61,10 @@ module.exports = function (User) { ]); }; - const getSessionFromStore = util.promisify( - (sid, callback) => db.sessionStore.get(sid, (err, sessObj) => callback(err, sessObj || null)) - ); - const sessionStoreDestroy = util.promisify( - (sid, callback) => db.sessionStore.destroy(sid, err => callback(err)) - ); - User.auth.getSessions = async function (uid, curSessionId) { await cleanExpiredSessions(uid); const sids = await db.getSortedSetRevRange(`uid:${uid}:sessions`, 0, 19); - let sessions = await Promise.all(sids.map(sid => getSessionFromStore(sid))); + let sessions = await Promise.all(sids.map(sid => db.sessionStoreGet(sid))); sessions = sessions.map((sessObj, idx) => { if (sessObj && sessObj.meta) { sessObj.meta.current = curSessionId === sids[idx]; @@ -93,7 +85,7 @@ module.exports = function (User) { const expiredSids = []; await Promise.all(Object.keys(uuidMapping).map(async (uuid) => { const sid = uuidMapping[uuid]; - const sessionObj = await getSessionFromStore(sid); + const sessionObj = await db.sessionStoreGet(sid); const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || !sessionObj.passport.hasOwnProperty('user') || parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); @@ -128,13 +120,13 @@ module.exports = function (User) { User.auth.revokeSession = async function (sessionId, uid) { winston.verbose(`[user.auth] Revoking session ${sessionId} for user ${uid}`); - const sessionObj = await getSessionFromStore(sessionId); + const sessionObj = await db.sessionStoreGet(sessionId); if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) { await db.deleteObjectField(`uid:${uid}:sessionUUID:sessionId`, sessionObj.meta.uuid); } await Promise.all([ db.sortedSetRemove(`uid:${uid}:sessions`, sessionId), - sessionStoreDestroy(sessionId), + db.sessionStoreDestroy(sessionId), ]); }; @@ -159,7 +151,7 @@ module.exports = function (User) { await Promise.all([ db.deleteAll(sessionKeys.concat(sessionUUIDKeys)), - ...sids.map(sid => sessionStoreDestroy(sid)), + ...sids.map(sid => db.sessionStoreDestroy(sid)), ]); }, { batch: 1000 }); }; From 0ec9d4c393653e2bebc91d110d5f4464afaab0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 28 Nov 2023 20:59:26 -0500 Subject: [PATCH 0015/4744] chore: up themes --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 5618b57e52..da4106fd95 100644 --- a/install/package.json +++ b/install/package.json @@ -102,10 +102,10 @@ "nodebb-plugin-ntfy": "1.7.3", "nodebb-plugin-spam-be-gone": "2.2.0", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.1.99", + "nodebb-theme-harmony": "1.1.100", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", - "nodebb-theme-persona": "13.2.47", + "nodebb-theme-persona": "13.2.48", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", From 6790000d1aec8a6babfe96aebb8ac57dafbe719e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 28 Nov 2023 20:58:07 -0500 Subject: [PATCH 0016/4744] fix: closes #12185, fix cli user password reset refactor session get/destroy --- src/api/users.js | 7 +------ src/cli/user.js | 1 + src/database/index.js | 22 ++++++++++++++++++++++ src/socket.io/index.js | 8 ++------ src/user/auth.js | 18 +++++------------- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/api/users.js b/src/api/users.js index f7c4edc2d2..f09aa782c8 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -1,6 +1,5 @@ 'use strict'; -const util = require('util'); const path = require('path'); const fs = require('fs').promises; @@ -330,10 +329,6 @@ usersAPI.deleteToken = async (caller, { uid, token }) => { return true; }; -const getSessionAsync = util.promisify((sid, callback) => { - db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)); -}); - usersAPI.revokeSession = async (caller, { uid, uuid }) => { // Only admins or global mods (besides the user themselves) can revoke sessions if (parseInt(uid, 10) !== caller.uid && !await user.isAdminOrGlobalMod(caller.uid)) { @@ -344,7 +339,7 @@ usersAPI.revokeSession = async (caller, { uid, uuid }) => { let _id; for (const sid of sids) { /* eslint-disable no-await-in-loop */ - const sessionObj = await getSessionAsync(sid); + const sessionObj = await db.sessionStoreGet(sid); if (sessionObj && sessionObj.meta && sessionObj.meta.uuid === uuid) { _id = sid; break; diff --git a/src/cli/user.js b/src/cli/user.js index bbd747865f..f2db7e4a58 100644 --- a/src/cli/user.js +++ b/src/cli/user.js @@ -77,6 +77,7 @@ let winston; async function init() { db = require('../database'); await db.init(); + await db.initSessionStore(); user = require('../user'); groups = require('../groups'); diff --git a/src/database/index.js b/src/database/index.js index 51febea19d..2366ae3671 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -34,4 +34,26 @@ primaryDB.initSessionStore = async function () { primaryDB.sessionStore = await sessionStoreDB.createSessionStore(sessionStoreConfig); }; +function promisifySessionStoreMethod(method, sid) { + return new Promise((resolve, reject) => { + if (!primaryDB.sessionStore) { + resolve(method === 'get' ? null : undefined); + return; + } + + primaryDB.sessionStore[method](sid, (err, result) => { + if (err) reject(err); + else resolve(method === 'get' ? result || null : undefined); + }); + }); +} + +primaryDB.sessionStoreGet = function (sid) { + return promisifySessionStoreMethod('get', sid); +}; + +primaryDB.sessionStoreDestroy = function (sid) { + return promisifySessionStoreMethod('destroy', sid); +}; + module.exports = primaryDB; diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 8f03eb2a9d..2348caadf7 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -241,10 +241,6 @@ async function checkMaintenance(socket) { throw new Error(`[[pages:maintenance.text, ${validator.escape(String(meta.config.title || 'NodeBB'))}]]`); } -const getSessionAsync = util.promisify( - (sid, callback) => db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)) -); - async function validateSession(socket, errorMsg) { const req = socket.request; const { sessionId } = await plugins.hooks.fire('filter:sockets.sessionId', { @@ -256,7 +252,7 @@ async function validateSession(socket, errorMsg) { return; } - const sessionData = await getSessionAsync(sessionId); + const sessionData = await db.sessionStoreGet(sessionId); if (!sessionData) { throw new Error(errorMsg); } @@ -282,7 +278,7 @@ async function authorize(request, callback) { request: request, }); - const sessionData = await getSessionAsync(sessionId); + const sessionData = await db.sessionStoreGet(sessionId); request.session = sessionData; let uid = 0; if (sessionData && sessionData.passport && sessionData.passport.user) { diff --git a/src/user/auth.js b/src/user/auth.js index 5330903a15..954d00a0c5 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -2,7 +2,6 @@ const winston = require('winston'); const validator = require('validator'); -const util = require('util'); const _ = require('lodash'); const db = require('../database'); const meta = require('../meta'); @@ -62,17 +61,10 @@ module.exports = function (User) { ]); }; - const getSessionFromStore = util.promisify( - (sid, callback) => db.sessionStore.get(sid, (err, sessObj) => callback(err, sessObj || null)) - ); - const sessionStoreDestroy = util.promisify( - (sid, callback) => db.sessionStore.destroy(sid, err => callback(err)) - ); - User.auth.getSessions = async function (uid, curSessionId) { await cleanExpiredSessions(uid); const sids = await db.getSortedSetRevRange(`uid:${uid}:sessions`, 0, 19); - let sessions = await Promise.all(sids.map(sid => getSessionFromStore(sid))); + let sessions = await Promise.all(sids.map(sid => db.sessionStoreGet(sid))); sessions = sessions.map((sessObj, idx) => { if (sessObj && sessObj.meta) { sessObj.meta.current = curSessionId === sids[idx]; @@ -93,7 +85,7 @@ module.exports = function (User) { const expiredSids = []; await Promise.all(Object.keys(uuidMapping).map(async (uuid) => { const sid = uuidMapping[uuid]; - const sessionObj = await getSessionFromStore(sid); + const sessionObj = await db.sessionStoreGet(sid); const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || !sessionObj.passport.hasOwnProperty('user') || parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); @@ -128,13 +120,13 @@ module.exports = function (User) { User.auth.revokeSession = async function (sessionId, uid) { winston.verbose(`[user.auth] Revoking session ${sessionId} for user ${uid}`); - const sessionObj = await getSessionFromStore(sessionId); + const sessionObj = await db.sessionStoreGet(sessionId); if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) { await db.deleteObjectField(`uid:${uid}:sessionUUID:sessionId`, sessionObj.meta.uuid); } await Promise.all([ db.sortedSetRemove(`uid:${uid}:sessions`, sessionId), - sessionStoreDestroy(sessionId), + db.sessionStoreDestroy(sessionId), ]); }; @@ -159,7 +151,7 @@ module.exports = function (User) { await Promise.all([ db.deleteAll(sessionKeys.concat(sessionUUIDKeys)), - ...sids.map(sid => sessionStoreDestroy(sid)), + ...sids.map(sid => db.sessionStoreDestroy(sid)), ]); }, { batch: 1000 }); }; From 97f6c539145ad67e59176614a654791959e8d0c7 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 29 Nov 2023 09:18:46 +0000 Subject: [PATCH 0017/4744] Latest translations and fallbacks --- public/language/hy/admin/extend/widgets.json | 4 ++-- public/language/hy/admin/settings/user.json | 16 ++++++++-------- public/language/hy/category.json | 8 ++++---- public/language/hy/error.json | 2 +- public/language/hy/flags.json | 6 +++--- public/language/hy/modules.json | 4 ++-- public/language/hy/notifications.json | 10 +++++----- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/public/language/hy/admin/extend/widgets.json b/public/language/hy/admin/extend/widgets.json index b3d7f053f5..2c477816ae 100644 --- a/public/language/hy/admin/extend/widgets.json +++ b/public/language/hy/admin/extend/widgets.json @@ -30,6 +30,6 @@ "start-date": "Սկիզբ", "end-date": "Ավարտ", "hide-on-mobile": "Թաքցնել բջջայինի վրա", - "hide-drafts": "Hide drafts", - "show-drafts": "Show drafts" + "hide-drafts": "Թաքցնել սևագրերը", + "show-drafts": "Ցույց տալ սևագրերը" } \ No newline at end of file diff --git a/public/language/hy/admin/settings/user.json b/public/language/hy/admin/settings/user.json index ab653e838e..84ba34fd34 100644 --- a/public/language/hy/admin/settings/user.json +++ b/public/language/hy/admin/settings/user.json @@ -79,14 +79,14 @@ "follow-replied-topics": "Հետևեք այն թեմաներին, որոնց պատասխանում եք", "default-notification-settings": "Հիմնական ծանուցման կարգավորումներ", "categoryWatchState": "Հիմնական կատեգորիայի դիտման վիճակը", - "categoryWatchState.tracking": "Tracking", + "categoryWatchState.tracking": "Հետևել", "categoryWatchState.notwatching": "Չեն դիտում ", "categoryWatchState.ignoring": "Անտեսել ", - "restrictions-new": "New User Restrictions", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-between-new": "Seconds between posts for new users", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", - "restrictions.seconds-edit-after-new": "Number of seconds a post remains editable for new users (set to 0 to disable)", - "restrictions.milliseconds-between-messages": "Time between chat messages for new users (ms)", - "restrictions.groups-exempt-from-new-user-restrictions": "Select groups that should be exempt from the new user restrictions" + "restrictions-new": "Նոր Օգտատիրոջ Սահմանափակումներ\n ", + "restrictions.rep-threshold": "Վարկանիշի շեմը՝ մինչև այս սահմանափակումների վերացումը\n ", + "restrictions.seconds-between-new": "Նոր օգտատերերի համար գրառումների միջև ընկած վայրկյաններ\n ", + "restrictions.seconds-before-new": "Վայրկյաններ առաջ, երբ նոր օգտատերը կարող է կատարել իր առաջին գրառումը", + "restrictions.seconds-edit-after-new": "Գրառման վայրկյանների քանակը մնում է խմբագրելի (անջատելու համար դնել 0)", + "restrictions.milliseconds-between-messages": "Նոր օգտատերերի համար հաղորդագրությունների միջև ընկած ժամանակը (մվ)", + "restrictions.groups-exempt-from-new-user-restrictions": "Ընտրեք խմբեր, որոնք պետք է ազատվեն նոր օգտատերերի սահմանափակումնեից" } diff --git a/public/language/hy/category.json b/public/language/hy/category.json index e63f73702e..30bd778208 100644 --- a/public/language/hy/category.json +++ b/public/language/hy/category.json @@ -13,12 +13,12 @@ "tracking": "Tracking", "not-watching": "Չեն դիտում", "ignoring": "Անտեսել", - "watching.description": "Notify me of new topics.
Show topics in unread & recent", - "tracking.description": "Shows topics in unread & recent", + "watching.description": "Տեղեկացնել նոր թեմաների մասին.
Ցույց տալ չընթերցված և վերջին թեմաները.", + "tracking.description": "Ցույց տալ չընթերցված և վերջին թեմաները.", "not-watching.description": "Չընթերցված թեմաները չցուցադրել, ցուցադրել վերջինները", - "ignoring.description": "Do not show topics in unread & recent", + "ignoring.description": "Ցույց չտալ չընթերցված և վերջին թեմաները.", "watching.message": "Դուք այժմ դիտում եք թարմացումներ այս կատեգորիայից և բոլոր ենթակատեգորիաներից", - "tracking.message": "You are now tracking updates from this category and all subcategories", + "tracking.message": "Դուք այժմ հետևում եք այս կատեգորիայի և բոլոր ենթակատեգորիաների թարմացումներին.", "notwatching.message": "Դուք չեք դիտում այս կատեգորիայի և բոլոր ենթակատեգորիաների թարմացումները", "ignoring.message": "Դուք այժմ անտեսում եք այս կատեգորիայի և բոլոր ենթակատեգորիաների թարմացումները", "watched-categories": "Դիտված կատեգորիաներ", diff --git a/public/language/hy/error.json b/public/language/hy/error.json index a50b469cd5..81d84ced2e 100644 --- a/public/language/hy/error.json +++ b/public/language/hy/error.json @@ -91,7 +91,7 @@ "category-not-selected": "Կատեգորիան ընտրված չէ:", "too-many-posts": "Դուք կարող եք գրառում անել միայն յուրաքանչյուր %1 վայրկյան(եր) մեկ անգամ. խնդրում ենք սպասել նորից գրառում անելուց առաջ", "too-many-posts-newbie": "Որպես նոր օգտատեր, դուք կարող եք հրապարակել միայն յուրաքանչյուր %1 վայրկյան(եր) մեկ անգամ, քանի դեռ չեք վաստակել %2 վարկանիշ, խնդրում ենք սպասել՝ նորից գրառում կատարելուց առաջ:", - "too-many-posts-newbie-minutes": "As a new user, you can only post once every %1 minute(s) until you have earned %2 reputation - please wait before posting again", + "too-many-posts-newbie-minutes": "Որպես նոր օգտատեր, դուք կարող եք հրապարակել միայն %1 րոպեն մեկ անգամ քանի դեռ չեք վաստակել %2 հեղինակություն. Խնդրում ենք սպասել՝ կրկին գրառում կատարելուց առաջ. ", "already-posting": "Դուք արդեն հրապարակում եք", "tag-too-short": "Խնդրում ենք մուտքագրել ավելի երկար թեգ: Թեգերը պետք է պարունակեն առնվազն %1 նիշ(ներ)", "tag-too-long": "Խնդրում ենք մուտքագրել ավելի կարճ թեգ: Թեգերը չեն կարող ավելի երկար լինել, քան %1 նիշ(ներ)", diff --git a/public/language/hy/flags.json b/public/language/hy/flags.json index edca973e17..68ea85b320 100644 --- a/public/language/hy/flags.json +++ b/public/language/hy/flags.json @@ -1,6 +1,6 @@ { "state": "Փուլ", - "report": "Report", + "report": "Հաշվետվություն", "reports": "Զեկույցներ", "first-reported": "Առաջին զեկույցը", "no-flags": "Դրոշներ չեն գտնվել:", @@ -9,8 +9,8 @@ "update": "Թարմացում ", "updated": "Updated", "resolved": "Լուծվել է", - "report-added": "Added", - "report-rescinded": "Rescinded", + "report-added": "Ավելացված է", + "report-rescinded": "Չեղարկված է", "target-purged": "Բովանդակությունը, որին անդրադարձել է այս դրոշը, մաքրվել է և այլևս հասանելի չէ:", "target-aboutme-empty": "Այս օգտատերը չունի "About Me" set.", diff --git a/public/language/hy/modules.json b/public/language/hy/modules.json index 4ffec84f27..e121ea6bf1 100644 --- a/public/language/hy/modules.json +++ b/public/language/hy/modules.json @@ -68,8 +68,8 @@ "chat.in-room": "Այս սենյակում", "chat.kick": "Kick", "chat.show-ip": "Ցույց տալ IP", - "chat.copy-text": "Copy Text", - "chat.copy-link": "Copy Link", + "chat.copy-text": "Պատճենել տեքստը", + "chat.copy-link": "Պատճենել հղումը", "chat.owner": "Սենյակի սեփականատեր", "chat.grant-rescind-ownership": "Տրամադրել/վերացնել սեփականության իրավունքը", "chat.system.user-join": "%1-ը միացել է սենյակին ", diff --git a/public/language/hy/notifications.json b/public/language/hy/notifications.json index 104e391cfc..96a515cbd4 100644 --- a/public/language/hy/notifications.json +++ b/public/language/hy/notifications.json @@ -13,14 +13,14 @@ "all": "Բոլորը", "topics": "Թեմաներ", "tags": "Պիտակներ", - "categories": "Categories", + "categories": "Կատեգորիաներ", "replies": "Պատասխաններ", "chat": "Զրույցներ", "group-chat": "Խմբային զրույցներ", "public-chat": "Հանրային նամակներ", "follows": "Հետևորդներ", "upvote": "Կողմ ձայներ", - "awards": "Awards", + "awards": "Մրցանակներ", "new-flags": "Նոր դրոշներ", "my-flags": "Ինձ հանձնարարված դրոշներ", "bans": "Արգելքներ", @@ -56,7 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 պիտակով նոր թեմա է տեղադրել %2 և %3 - ում։", "user-posted-topic-with-tag-triple": "%1 պիտակով նոր թեմա է տեղադրել %2, %3 և %4 - ում։", "user-posted-topic-with-tag-multiple": "%1 պիտակով նոր թեմա է տեղադրել %2 - ում։", - "user-posted-topic-in-category": "%1 has posted a new topic in %2", + "user-posted-topic-in-category": "%1 նոր թեմա է տեղադրել %2", "user-started-following-you": "%1 սկսեց հետևել ձեզ", "user-started-following-you-dual": "%1 և %2 սկսեցին հետևել ձեզ:", "user-started-following-you-triple": "%1, %2 և %3 սկսել են հետևել Ձեզ։", @@ -83,7 +83,7 @@ "notificationType-upvote": "Երբ ինչ-որ մեկը կողմ է քվեարկում ձեր գրառմանը", "notificationType-new-topic": "Երբ մեկը, ում հետևում եք, թեմա է հրապարակում", "notificationType-new-topic-with-tag": "Երբ թեման տեղադրվում է պիտակով, որը դուք հետևում եք", - "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", + "notificationType-new-topic-in-category": "Երբ թեման տեղադրված է այն կատերգորիայում, որը դուք հետևում եք.", "notificationType-new-reply": "Երբ ձեր դիտած թեմայում տեղադրվում է նոր պատասխան", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "Երբ ինչ-որ մեկը սկսում է հետևել քեզ", @@ -97,5 +97,5 @@ "notificationType-post-queue": "Երբ նոր գրառումը հերթագրվում է", "notificationType-new-post-flag": "Երբ գրառումը դրոշակված է", "notificationType-new-user-flag": "Երբ օգտվողը դրոշակված է", - "notificationType-new-reward": "When you earn a new reward" + "notificationType-new-reward": "Երբ դուք ստանում եք մրցանակ." } \ No newline at end of file From f31faa457d1f11aa97d4f2b6276b615313eda47b Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 29 Nov 2023 17:21:39 +0000 Subject: [PATCH 0018/4744] chore: incrementing version number - v3.5.2 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 93af7416b7..5ef77abd22 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "3.5.1", + "version": "3.5.2", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From e2e85053a6bb2988aa9f4f1f10b36e0c4a0f1025 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 29 Nov 2023 17:21:40 +0000 Subject: [PATCH 0019/4744] chore: update changelog for v3.5.2 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84078cf370..0a0993f03d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +#### v3.5.2 (2023-11-29) + +##### Chores + +* up composer (49013f81) +* incrementing version number - v3.5.1 (4c543488) +* update changelog for v3.5.1 (48f7ae99) +* incrementing version number - v3.5.0 (d06fb4f0) +* incrementing version number - v3.4.3 (5c984250) +* incrementing version number - v3.4.2 (3f0dac38) +* incrementing version number - v3.4.1 (01e69574) +* incrementing version number - v3.4.0 (fd9247c5) +* incrementing version number - v3.3.9 (5805e770) +* incrementing version number - v3.3.8 (a5603565) +* incrementing version number - v3.3.7 (b26f1744) +* incrementing version number - v3.3.6 (7fb38792) +* incrementing version number - v3.3.4 (a67f84ea) +* incrementing version number - v3.3.3 (f94d239b) +* incrementing version number - v3.3.2 (ec9dac97) +* incrementing version number - v3.3.1 (151cc68f) +* incrementing version number - v3.3.0 (fc1ad70f) +* incrementing version number - v3.2.3 (b06d3e63) +* incrementing version number - v3.2.2 (758ecfcd) +* incrementing version number - v3.2.1 (20145074) +* incrementing version number - v3.2.0 (9ecac38e) +* incrementing version number - v3.1.7 (0b4e81ab) +* incrementing version number - v3.1.6 (b3a3b130) +* incrementing version number - v3.1.5 (ec19343a) +* incrementing version number - v3.1.4 (2452783c) +* incrementing version number - v3.1.3 (3b4e9d3f) +* incrementing version number - v3.1.2 (40fa3489) +* incrementing version number - v3.1.1 (40250733) +* incrementing version number - v3.1.0 (0cb386bd) +* incrementing version number - v3.0.1 (26f6ea49) +* incrementing version number - v3.0.0 (224e08cd) + +##### Bug Fixes + +* closes #12185, fix cli user password reset (6790000d) +* thumb width (a9ef58a5) + #### v3.5.1 (2023-11-14) ##### Chores From 17cd19c701d8a4959c02091dc50d445bf1982418 Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:55:57 +0800 Subject: [PATCH 0020/4744] types: add types for database abstration layer (#10762) * types: add types for database abstration layer Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * types: fix more type dependent return value cases Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * types: make INodeBBDatabaseBackend implement the five major interface set Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * update types * update type names * add reverse for options in processSortedSet * add getSortedSetMembersWithScores and getSortedSetsMembersWithScores --------- Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> --- types/database/hash.d.ts | 56 +++++++++ types/database/index.d.ts | 54 ++++++++ types/database/list.d.ts | 15 +++ types/database/set.d.ts | 25 ++++ types/database/string.d.ts | 35 ++++++ types/database/zset.d.ts | 245 +++++++++++++++++++++++++++++++++++++ 6 files changed, 430 insertions(+) create mode 100644 types/database/hash.d.ts create mode 100644 types/database/index.d.ts create mode 100644 types/database/list.d.ts create mode 100644 types/database/set.d.ts create mode 100644 types/database/string.d.ts create mode 100644 types/database/zset.d.ts diff --git a/types/database/hash.d.ts b/types/database/hash.d.ts new file mode 100644 index 0000000000..3bc7a93834 --- /dev/null +++ b/types/database/hash.d.ts @@ -0,0 +1,56 @@ +export interface Hash { + decrObjectField( + key: string | string[], + field: string, + ): Promise + + deleteObjectField(key: string, field: string): Promise + + deleteObjectFields(key: string, fields: string[]): Promise + + getObject(key: string, fields: string[]): Promise + + getObjectField(key: string, field: string): Promise + + getObjectFields(key: string, fields: string[]): Promise> + + getObjectKeys(key: string): Promise + + getObjectValues(key: string): Promise + + getObjects(keys: string[], fields: string[]): Promise + + getObjectsFields( + keys: string[], + fields: string[], + ): Promise[]> + + incrObjectField( + key: string | string[], + field: string, + ): Promise + + incrObjectFieldBy( + key: string | string[], + field: string, + value: number, + ): Promise + + incrObjectFieldByBulk( + data: [key: string, batch: Record][], + ): Promise + + isObjectField(key: string, field: string): Promise + + isObjectFields(key: string, fields: string[]): Promise + + setObject(key: string | string[], data: Record): Promise + + setObjectBulk(args: [key: string, data: Record][]): Promise + + setObjectField( + key: string | string[], + field: string, + value: any, + ): Promise +} diff --git a/types/database/index.d.ts b/types/database/index.d.ts new file mode 100644 index 0000000000..8ec2e7d865 --- /dev/null +++ b/types/database/index.d.ts @@ -0,0 +1,54 @@ +import { Store } from 'express-session' + +export { Hash } from './hash' +export { List } from './list' +export { Set } from './set' +export { Item } from './string' +export { + SortedSet, + SortedSetTheoryOperation, + SortedSetScanBaseParameters, +} from './zset' + +export interface Database { + checkCompatibility(callback: () => void): Promise + + checkCompatibilityVersion( + version: string, + callback: () => void, + ): Promise + + close(): Promise + + createIndices(callback: () => void): Promise + + createSessionStore(options: any): Promise + + emptydb(): Promise + + flushdb(): Promise + + info(db: any): Promise + + init(): Promise +} + +export type RedisStyleMatchString = + | string + | `*${string}` + | `${string}*` + | `*${string}*` +export type RedisStyleRangeString = `${'(' | '['}${string}` | `${string}` + +export enum ObjectType { + HASH = 'hash', + LIST = 'list', + SET = 'set', + STRING = 'string', + SORTED_SET = 'zset', +} + +export type ValueAndScore = { value: string; score: number } +export type RedisStyleAggregate = 'SUM' | 'MIN' | 'MAX' +export type NumberTowardsMinima = number | '-inf' +export type NumberTowardsMaxima = number | '+inf' diff --git a/types/database/list.d.ts b/types/database/list.d.ts new file mode 100644 index 0000000000..aa43cfa738 --- /dev/null +++ b/types/database/list.d.ts @@ -0,0 +1,15 @@ +export interface List { + listPrepend(key: string, value: string): Promise + + listAppend(key: string, value: string): Promise + + listRemoveLast(key: string): Promise + + listRemoveAll(key: string, value: string | string[]): Promise + + listTrim(key: string, start: number, stop: number): Promise + + getListRange(key: string, start: number, stop: number): Promise + + listLength(key: string): Promise +} diff --git a/types/database/set.d.ts b/types/database/set.d.ts new file mode 100644 index 0000000000..5dcfcc5ab2 --- /dev/null +++ b/types/database/set.d.ts @@ -0,0 +1,25 @@ +export interface Set { + getSetMembers(key: string): Promise + + getSetsMembers(keys: string[]): Promise + + isMemberOfSets(sets: string[], value: string): Promise + + isSetMember(key: string, value: string): Promise + + isSetMembers(key: string, values: string[]): Promise + + setAdd(key: string, value: string | string[]): Promise + + setCount(key: string): Promise + + setRemove(key: string | string[], value: string | string[]): Promise + + setRemoveRandom(key: string): Promise + + setsAdd(keys: string[], value: string | string[]): Promise + + setsCount(keys: string[]): Promise + + setsRemove(keys: string[], value: string): Promise +} diff --git a/types/database/string.d.ts b/types/database/string.d.ts new file mode 100644 index 0000000000..090a5cf252 --- /dev/null +++ b/types/database/string.d.ts @@ -0,0 +1,35 @@ +import { ObjectType, RedisStyleMatchString } from './index' + +export interface Item { + delete(key: string): Promise + + deleteAll(keys: string[]): Promise + + exists(key: string): Promise + + exists(key: string[]): Promise + + expire(key: string, seconds: number): Promise + + expireAt(key: string, timestampInSeconds: number): Promise + + get(key: string): Promise + + increment(key: string): Promise + + pexpire(key: string, ms: number): Promise + + pexpireAt(key: string, timestampInMs: number): Promise + + pttl(key: string): Promise + + rename(oldkey: string, newkey: string): Promise + + scan(params: { match: RedisStyleMatchString }): Promise + + set(key: string, value: string): Promise + + ttl(key: string): Promise + + type(key: string): Promise +} diff --git a/types/database/zset.d.ts b/types/database/zset.d.ts new file mode 100644 index 0000000000..b3b5e71bff --- /dev/null +++ b/types/database/zset.d.ts @@ -0,0 +1,245 @@ +import { + NumberTowardsMaxima, + NumberTowardsMinima, + RedisStyleAggregate, + RedisStyleMatchString, + RedisStyleRangeString, + ValueAndScore, +} from './index' + +export type SortedSetTheoryOperation = { + sets: string[] + sort?: 'ASC' | 'DESC' + start?: number + stop?: number + weights?: number[] + aggregate?: RedisStyleAggregate +} + +export type SortedSetScanBaseParameters = { + key: string + match: RedisStyleMatchString + limit?: number +} + +export interface SortedSet { + getSortedSetIntersect( + params: SortedSetTheoryOperation & { withScores: true }, + ): Promise + + getSortedSetIntersect( + params: SortedSetTheoryOperation & { withScores?: false }, + ): Promise + + getSortedSetMembers(key: string): Promise + + getSortedSetMembersWithScores(key: string): Promise + + getSortedSetRange( + key: string | string[], + start: number, + stop: number, + ): Promise + + getSortedSetRangeByLex( + key: string | string[], + min: RedisStyleRangeString | '-', + max: RedisStyleRangeString | '+', + start?: number, + count?: number, + ): Promise + + getSortedSetRangeByScore( + key: string | string[], + start: number, + count: number, + min: NumberTowardsMinima, + max: NumberTowardsMaxima, + ): Promise + + getSortedSetRangeByScoreWithScores( + key: string | string[], + start: number, + count: number, + min: NumberTowardsMinima, + max: NumberTowardsMaxima, + ): Promise + + getSortedSetRangeWithScores( + key: string | string[], + start: number, + stop: number, + ): Promise + + getSortedSetRevIntersect( + params: SortedSetTheoryOperation & { withScores: true }, + ): Promise + + getSortedSetRevIntersect( + params: SortedSetTheoryOperation & { withScores?: false }, + ): Promise + + getSortedSetRevRange( + key: string | string[], + start: number, + stop: number, + ): Promise + + getSortedSetRevRangeByLex( + key: string, + max: RedisStyleRangeString | '+', + min: RedisStyleRangeString | '-', + start?: number, + count?: number, + ): Promise + + getSortedSetRevRangeByScore( + key: string, + start: number, + count: number, + max: NumberTowardsMaxima | '+', + min: NumberTowardsMinima | '-', + ): Promise + + getSortedSetRevRangeByScoreWithScores( + key: string, + start: number, + count: number, + max: NumberTowardsMaxima, + min: NumberTowardsMinima, + ): Promise + + getSortedSetRevRangeWithScores( + key: string, + start: number, + stop: number, + ): Promise + + getSortedSetRevUnion( + params: SortedSetTheoryOperation & { withScores?: false }, + ): Promise + + getSortedSetRevUnion( + params: SortedSetTheoryOperation & { withScores: true }, + ): Promise + + getSortedSetScan( + params: SortedSetScanBaseParameters & { withScores: true }, + ): Promise + + getSortedSetScan( + params: SortedSetScanBaseParameters & { withScores?: false }, + ): Promise + + getSortedSetUnion( + params: SortedSetTheoryOperation & { withScores: true }, + ): Promise + + getSortedSetUnion( + params: SortedSetTheoryOperation & { withScores?: false }, + ): Promise + + getSortedSetsMembers(keys: string[]): Promise + + getSortedSetsMembersWithScores(keys: string[]): Promise + + isMemberOfSortedSets(keys: string[], value: string): Promise + + isSortedSetMember(key: string, value: string): Promise + + isSortedSetMembers(key: string, values: string[]): Promise + + processSortedSet( + setKey: string, + processFn: (ids: number[]) => Promise | void, + options: { withScores?: boolean; batch?: number; interval?: number, reverse?: boolean; }, + ): Promise + + sortedSetAdd(key: string, score: number, value: string): Promise + + sortedSetAdd(key: string, score: number[], value: string[]): Promise + + sortedSetAddBulk( + args: [key: string, score: number[], value: string[]][], + ): Promise + + sortedSetCard(key: string): Promise + + sortedSetCount( + key: string, + min: NumberTowardsMinima, + max: NumberTowardsMaxima, + ): Promise + + sortedSetIncrBy( + key: string, + increment: number, + value: string, + ): Promise + + sortedSetIncrByBulk( + data: [key: string, increment: number, value: string][], + ): Promise + + sortedSetIntersectCard(keys: string[]): Promise + + sortedSetLexCount( + key: string, + min: RedisStyleRangeString, + max: RedisStyleRangeString, + ): Promise + + sortedSetRank(key: string, value: string): Promise + + sortedSetRanks(key: string, values: string[]): Promise<(number | null)[]> + + sortedSetRemove( + key: string | string[], + value: string | string[], + ): Promise + + sortedSetRemoveBulk(data: [key: string, member: string][]): Promise + + sortedSetRemoveRangeByLex( + key: string, + min: RedisStyleRangeString | '-', + max: RedisStyleRangeString | '+', + ): Promise + + sortedSetRevRank(key: string, value: string): Promise + + sortedSetRevRanks(key: string, values: string[]): Promise + + sortedSetScore(key: string, value: string): Promise + + sortedSetScores(key: string, values: string[]): Promise + + sortedSetUnionCard(keys: string[]): Promise + + sortedSetsAdd( + keys: string[], + scores: number | number[], + value: string, + ): Promise + + sortedSetsCard(keys: string[]): Promise + + sortedSetsCardSum(keys: string[]): Promise + + sortedSetsRanks( + keys: T, + values: { [K in keyof T]: string }, + ): Promise + + sortedSetsRemove(keys: string[], value: string): Promise + + sortedSetsRemoveRangeByScore( + keys: string[], + min: NumberTowardsMinima, + max: NumberTowardsMaxima, + ): Promise + + sortedSetsRevRanks(keys: string[], values: string[]): Promise + + sortedSetsScore(keys: string[], value: string): Promise +} From 1f287c74c8095ecdda762fc8bee2f1b0cf099644 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 09:56:10 -0500 Subject: [PATCH 0021/4744] fix(deps): update dependency sharp to v0.33.0 (#12194) 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 ce8a38b7b0..76ff51e39e 100644 --- a/install/package.json +++ b/install/package.json @@ -128,7 +128,7 @@ "sass": "1.69.5", "semver": "7.5.4", "serve-favicon": "2.5.0", - "sharp": "0.32.6", + "sharp": "0.33.0", "sitemap": "7.1.1", "socket.io": "4.7.2", "socket.io-client": "4.7.2", From a50b141f6d6b757a09e314b93dd462b3d6159d09 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 09:56:31 -0500 Subject: [PATCH 0022/4744] chore(deps): update dependency jsdom to v23.0.1 (#12196) 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 76ff51e39e..3ac8451cde 100644 --- a/install/package.json +++ b/install/package.json @@ -163,7 +163,7 @@ "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", - "jsdom": "23.0.0", + "jsdom": "23.0.1", "lint-staged": "15.1.0", "mocha": "10.2.0", "mocha-lcov-reporter": "1.3.0", From a94f4a482da90218f50b94605b6aa12eb70a2d3c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 09:56:42 -0500 Subject: [PATCH 0023/4744] fix(deps): update dependency @fortawesome/fontawesome-free to v6.5.0 (#12193) 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 3ac8451cde..8d19820af2 100644 --- a/install/package.json +++ b/install/package.json @@ -31,7 +31,7 @@ "@adactive/bootstrap-tagsinput": "0.8.2", "@fontsource/inter": "5.0.15", "@fontsource/poppins": "5.0.8", - "@fortawesome/fontawesome-free": "6.4.2", + "@fortawesome/fontawesome-free": "6.5.0", "@isaacs/ttlcache": "1.4.1", "@popperjs/core": "2.11.8", "ace-builds": "1.31.2", From a8ea2340f4530ffab38e3f24919da3e1d7ba8fb2 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 1 Dec 2023 09:18:48 +0000 Subject: [PATCH 0024/4744] Latest translations and fallbacks --- public/language/hy/category.json | 2 +- public/language/hy/post-queue.json | 4 ++-- public/language/hy/social.json | 4 ++-- public/language/hy/themes/harmony.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/language/hy/category.json b/public/language/hy/category.json index 30bd778208..5a35ddd815 100644 --- a/public/language/hy/category.json +++ b/public/language/hy/category.json @@ -10,7 +10,7 @@ "watch": "Դիտել", "ignore": "Անտեսել", "watching": "Դիտում", - "tracking": "Tracking", + "tracking": "Հետևել", "not-watching": "Չեն դիտում", "ignoring": "Անտեսել", "watching.description": "Տեղեկացնել նոր թեմաների մասին.
Ցույց տալ չընթերցված և վերջին թեմաները.", diff --git a/public/language/hy/post-queue.json b/public/language/hy/post-queue.json index 9e707cd3f5..bd7b786c5e 100644 --- a/public/language/hy/post-queue.json +++ b/public/language/hy/post-queue.json @@ -5,8 +5,8 @@ "no-single-post": "Ձեր փնտրած թեման կամ գրառումն այլևս հերթում չէ: Այն հավանաբար արդեն հաստատված կամ ջնջված է:", "enabling-help": "The post queue is currently disabled. To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", "back-to-list": "Վերադառնալ Գրառումների հերթին", - "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": "Եթե ունեք գրառումներ հերթում, դրանք կցուցադրվեն այստեղ", + "public-description": "Այս ֆորումը կազմված է այնպես, որ ավտոմատ կերպով հերթագրի նոր հաշիվներից գրառումները՝ սպասելով մոդերատորի հաստատմանը.
Եթե դուք ունեք հերթագրված գրառումներ, որոնք սպասում են հաստատման, դուք կկարողանաք տեսնել դրանք այստեղ. ", "user": "Օգտատեր", "when": "Երբ", "category": "Կատեգորիա", diff --git a/public/language/hy/social.json b/public/language/hy/social.json index cadb9ba632..1f88d125ba 100644 --- a/public/language/hy/social.json +++ b/public/language/hy/social.json @@ -7,6 +7,6 @@ "sign-up-with-google": "Գրանցվեք Google-ով", "log-in-with-facebook": "Մուտք գործեք Facebook-ով", "continue-with-facebook": "Շարունակեք Facebook-ով", - "sign-in-with-linkedin": "Sign in with LinkedIn", - "sign-up-with-linkedin": "Sign up with LinkedIn" + "sign-in-with-linkedin": "Մուտք գործեք LinkedIn-ով", + "sign-up-with-linkedin": "Գրանցվեք LinkedIn-ով" } \ No newline at end of file diff --git a/public/language/hy/themes/harmony.json b/public/language/hy/themes/harmony.json index 6ae7c24cf5..2bc4146683 100644 --- a/public/language/hy/themes/harmony.json +++ b/public/language/hy/themes/harmony.json @@ -12,6 +12,6 @@ "settings.stickyToolbar.help": "Թեմայի և կատեգորիայի էջերի գործիքագոտին կմնա էջի վերևում", "settings.autohideBottombar": "Ավտոմատ թաքցնել ներքևի բարը", "settings.autohideBottombar.help": "Բջջային դիտման ներքևի տողը կթաքցվի, երբ էջը ներքև իջացնեք", - "settings.openSidebars": "Open sidebars", + "settings.openSidebars": "Բացել կողքի տողերը", "settings.chatModals": "Միացնել զրույցի ռեժիմները" } \ No newline at end of file From 75f063ba60324cc78b73fd6df557e095ae34cb01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:07:21 -0500 Subject: [PATCH 0025/4744] fix(deps): update dependency ace-builds to v1.32.0 (#12197) 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 8d19820af2..e6fe209d5c 100644 --- a/install/package.json +++ b/install/package.json @@ -34,7 +34,7 @@ "@fortawesome/fontawesome-free": "6.5.0", "@isaacs/ttlcache": "1.4.1", "@popperjs/core": "2.11.8", - "ace-builds": "1.31.2", + "ace-builds": "1.32.0", "archiver": "6.0.1", "async": "3.2.5", "autoprefixer": "10.4.16", From cd625705a0da0163dfacb0d301b62bc22eb750db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:07:44 -0500 Subject: [PATCH 0026/4744] fix(deps): update dependency sortablejs to v1.15.1 (#12200) 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 e6fe209d5c..275acbd896 100644 --- a/install/package.json +++ b/install/package.json @@ -133,7 +133,7 @@ "socket.io": "4.7.2", "socket.io-client": "4.7.2", "@socket.io/redis-adapter": "8.2.1", - "sortablejs": "1.15.0", + "sortablejs": "1.15.1", "spdx-license-list": "6.8.0", "spider-detector": "2.0.1", "terser-webpack-plugin": "5.3.9", From 72d6a4b16d7e3f42124c14422151784d628d1800 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:08:17 -0500 Subject: [PATCH 0027/4744] fix(deps): update dependency nodebb-theme-harmony to v1.1.101 (#12199) 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 275acbd896..c70635ffbc 100644 --- a/install/package.json +++ b/install/package.json @@ -102,7 +102,7 @@ "nodebb-plugin-ntfy": "1.7.3", "nodebb-plugin-spam-be-gone": "2.2.0", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.1.100", + "nodebb-theme-harmony": "1.1.101", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", "nodebb-theme-persona": "13.2.48", From b41c7f2a8a2c9b7b3f80cdc87a16a69ab9d7359a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:08:25 -0500 Subject: [PATCH 0028/4744] fix(deps): update dependency @fortawesome/fontawesome-free to v6.5.1 (#12198) 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 c70635ffbc..36132f5726 100644 --- a/install/package.json +++ b/install/package.json @@ -31,7 +31,7 @@ "@adactive/bootstrap-tagsinput": "0.8.2", "@fontsource/inter": "5.0.15", "@fontsource/poppins": "5.0.8", - "@fortawesome/fontawesome-free": "6.5.0", + "@fortawesome/fontawesome-free": "6.5.1", "@isaacs/ttlcache": "1.4.1", "@popperjs/core": "2.11.8", "ace-builds": "1.32.0", From 2d8026ebb75cc9a9ebeb3de38d82247a723b36ca Mon Sep 17 00:00:00 2001 From: Opliko Date: Fri, 1 Dec 2023 15:08:50 +0100 Subject: [PATCH 0029/4744] Add basic author information to topic data (#12202) * feat: add author metadata to topics * docs: add author object to OpenAPI definition * docs: add remaining author properties to openapi definition * docs: mark optional properties optional * docs: properly set required properties --- public/openapi/read/topic/topic_id.yaml | 14 ++++++++++++++ src/controllers/topics.js | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/public/openapi/read/topic/topic_id.yaml b/public/openapi/read/topic/topic_id.yaml index cee628cce2..8b6ffdbd90 100644 --- a/public/openapi/read/topic/topic_id.yaml +++ b/public/openapi/read/topic/topic_id.yaml @@ -408,6 +408,20 @@ get: type: string postIndex: type: number + author: + type: object + required: [username, uid] + properties: + username: + type: string + userslug: + type: string + uid: + type: number + fullname: + type: string + displayname: + type: string - type: object description: Optional properties that may or may not be present (except for `tid`, which is always present, and is only here as a hack to pass validation) properties: diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 518882d826..16e1cad3a7 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -114,7 +114,8 @@ topicsController.get = async function getTopic(req, res, next) { topicData.postIndex = postIndex; - await Promise.all([ + const [author] = await Promise.all([ + user.getUserFields(topicData.uid, ['username', 'userslug']), buildBreadcrumbs(topicData), addOldCategory(topicData, userPrivileges), addTags(topicData, req, res, currentPage), @@ -123,12 +124,12 @@ topicsController.get = async function getTopic(req, res, next) { analytics.increment([`pageviews:byCid:${topicData.category.cid}`]), ]); + topicData.author = author; topicData.pagination = pagination.create(currentPage, pageCount, req.query); topicData.pagination.rel.forEach((rel) => { rel.href = `${url}/topic/${topicData.slug}${rel.href}`; res.locals.linkTags.push(rel); }); - res.render('topic', topicData); }; From e4656bd41c03f2556eaceec77fcd88d33f9566b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 1 Dec 2023 09:13:55 -0500 Subject: [PATCH 0030/4744] 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 36132f5726..66aac0b56c 100644 --- a/install/package.json +++ b/install/package.json @@ -102,7 +102,7 @@ "nodebb-plugin-ntfy": "1.7.3", "nodebb-plugin-spam-be-gone": "2.2.0", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.1.101", + "nodebb-theme-harmony": "1.1.102", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", "nodebb-theme-persona": "13.2.48", From dbbf3a2c6f5fd75283fb567f8f0f824f23d9d6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 1 Dec 2023 09:32:48 -0500 Subject: [PATCH 0031/4744] 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 66aac0b56c..a336d0fdcf 100644 --- a/install/package.json +++ b/install/package.json @@ -102,7 +102,7 @@ "nodebb-plugin-ntfy": "1.7.3", "nodebb-plugin-spam-be-gone": "2.2.0", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.1.102", + "nodebb-theme-harmony": "1.1.103", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", "nodebb-theme-persona": "13.2.48", From 78835ebbe90215a2aff9f87333bb32e67536fdcb Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 1 Dec 2023 12:24:59 -0500 Subject: [PATCH 0032/4744] fix: incorrect call to load additional group members --- public/src/client/groups/memberlist.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/groups/memberlist.js b/public/src/client/groups/memberlist.js index 3a2839cab3..38a3680b86 100644 --- a/public/src/client/groups/memberlist.js +++ b/public/src/client/groups/memberlist.js @@ -122,7 +122,7 @@ define('forum/groups/memberlist', ['api', 'bootbox', 'alerts'], function (api, b } members.attr('loading', 1); - api.get(`/groups/${groupName}/members`, { + api.get(`/groups/${ajaxify.data.group.slug}/members`, { after: members.attr('data-nextstart'), }, function (err, data) { if (err) { From 2c6024e07f81d8aa181a3bf4ec666c2f99a1dc92 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 1 Dec 2023 13:04:32 -0500 Subject: [PATCH 0033/4744] feat: update groups.leave to allow global mods to kick users out of groups --- src/api/groups.js | 7 +++---- test/groups.js | 13 +++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/api/groups.js b/src/api/groups.js index 5372f386be..5aa9c04f64 100644 --- a/src/api/groups.js +++ b/src/api/groups.js @@ -201,10 +201,9 @@ groupsAPI.leave = async function (caller, data) { throw new Error('[[error:cant-remove-self-as-admin]]'); } - const [groupData, isCallerAdmin, isCallerOwner, userExists, isMember] = await Promise.all([ + const [groupData, isCallerOwner, userExists, isMember] = await Promise.all([ groups.getGroupData(groupName), - user.isAdministrator(caller.uid), - groups.ownership.isOwner(caller.uid, groupName), + isOwner(caller, groupName, false), user.exists(data.uid), groups.isMember(data.uid, groupName), ]); @@ -221,7 +220,7 @@ groupsAPI.leave = async function (caller, data) { throw new Error('[[error:group-leave-disabled]]'); } - if (isSelf || isCallerAdmin || isCallerOwner) { + if (isSelf || isCallerOwner) { await groups.leave(groupName, data.uid); } else { throw new Error('[[error:no-privileges]]'); diff --git a/test/groups.js b/test/groups.js index 10ff138639..00bd044e85 100644 --- a/test/groups.js +++ b/test/groups.js @@ -759,7 +759,7 @@ describe('Groups', () => { }); }); - describe('socket methods', () => { + describe('socket/api methods', () => { it('should error if data is null', (done) => { socketGroups.before({ uid: 0 }, 'groups.join', null, (err) => { assert.equal(err.message, '[[error:invalid-data]]'); @@ -1166,12 +1166,21 @@ describe('Groups', () => { ); }); - it('should remove user from group', async () => { + it('should remove user from group if caller is admin', async () => { await apiGroups.leave({ uid: adminUid }, { uid: testUid, slug: 'newgroup' }); const isMember = await Groups.isMember(testUid, 'newgroup'); assert(!isMember); }); + it('should remove user from group if caller is a global moderator', async () => { + const globalModUid = await User.getUidByUsername('glomod'); + await apiGroups.join({ uid: adminUid }, { uid: testUid, slug: 'newgroup' }); + + await apiGroups.leave({ uid: globalModUid }, { uid: testUid, slug: 'newgroup' }); + const isMember = await Groups.isMember(testUid, 'newgroup'); + assert(!isMember); + }); + it('should fail with invalid data', async () => { let err; try { From 9cd4dac729785790555da31dc65028f4473e5f76 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 4 Dec 2023 09:18:51 +0000 Subject: [PATCH 0034/4744] Latest translations and fallbacks --- public/language/hy/rewards.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/language/hy/rewards.json b/public/language/hy/rewards.json index f923cf1500..c7105edb01 100644 --- a/public/language/hy/rewards.json +++ b/public/language/hy/rewards.json @@ -1,10 +1,10 @@ { - "awarded-x-reputation": "You have been awarded %1 reputation", - "awarded-group-membership": "You have been added to the group %1", + "awarded-x-reputation": "Դուք արժանացել եք %1 հեղինակությանը", + "awarded-group-membership": "Ձեզ ավելացրել են խմբում %1", - "essentials/user.reputation-conditional-value": "(Reputation %1 %2)", - "essentials/user.postcount-conditional-value": "(Post Count %1 %2)", - "essentials/user.lastonline-conditional-value": "(Last Online %1 %2)", - "essentials/user.joindate-conditional-value": "(Join Date %1 %2)", - "essentials/user.daysregistered-conditional-value": "(Days Registered %1 %2)" + "essentials/user.reputation-conditional-value": "Հեղինակություն ( %1 %2)", + "essentials/user.postcount-conditional-value": "(Գրառումների քանակ %1 %2)", + "essentials/user.lastonline-conditional-value": "(Վերջին անգամ առցանց %1 %2)", + "essentials/user.joindate-conditional-value": "(Միանալու ամսաթիվ %1 %2)", + "essentials/user.daysregistered-conditional-value": "(Գրանցված օրեր %1 %2)" } \ No newline at end of file From 0b3eb6c02d96de4a3121e7e8603385d489be2853 Mon Sep 17 00:00:00 2001 From: Opliko Date: Sun, 3 Dec 2023 23:05:20 +0100 Subject: [PATCH 0035/4744] fix: extract all pages when stripping metadata fixes #12207 --- src/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image.js b/src/image.js index 2374693aa0..62f84d365e 100644 --- a/src/image.js +++ b/src/image.js @@ -115,7 +115,7 @@ image.stripEXIF = async function (path) { } const buffer = await fs.promises.readFile(path); const sharp = requireSharp(); - await sharp(buffer, { failOnError: true }).rotate().toFile(path); + await sharp(buffer, { failOnError: true, pages: -1 }).rotate().toFile(path); } catch (err) { winston.error(err.stack); } From f8219aa6cd2589fdc7c1c7558e491c7a1cbd8bd9 Mon Sep 17 00:00:00 2001 From: Opliko Date: Sun, 3 Dec 2023 23:17:23 +0100 Subject: [PATCH 0036/4744] feat: remove gif exif stripping exception --- src/image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/image.js b/src/image.js index 62f84d365e..4f07267f16 100644 --- a/src/image.js +++ b/src/image.js @@ -103,7 +103,7 @@ image.size = async function (path) { }; image.stripEXIF = async function (path) { - if (!meta.config.stripEXIFData || path.endsWith('.gif') || path.endsWith('.svg')) { + if (!meta.config.stripEXIFData || path.endsWith('.svg')) { return; } try { From 9763e97f5a1b3bd85edc101f88a5ab3012843d9b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:38:42 -0500 Subject: [PATCH 0037/4744] chore(deps): update dependency lint-staged to v15.2.0 (#12210) 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 a336d0fdcf..9b3454fbd5 100644 --- a/install/package.json +++ b/install/package.json @@ -164,7 +164,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "23.0.1", - "lint-staged": "15.1.0", + "lint-staged": "15.2.0", "mocha": "10.2.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From da87970475c0ef0103dd050d85e27de2c4db9484 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:38:53 -0500 Subject: [PATCH 0038/4744] fix(deps): update dependency postcss to v8.4.32 (#12204) 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 9b3454fbd5..c249dbce10 100644 --- a/install/package.json +++ b/install/package.json @@ -114,7 +114,7 @@ "passport-local": "1.0.0", "pg": "8.11.3", "pg-cursor": "2.10.3", - "postcss": "8.4.31", + "postcss": "8.4.32", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", From 32a403b2bdcfd226d09afd1796cefbebaf709281 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:39:06 -0500 Subject: [PATCH 0039/4744] chore(deps): update dependency eslint to v8.55.0 (#12203) 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 c249dbce10..01b17cdee7 100644 --- a/install/package.json +++ b/install/package.json @@ -157,7 +157,7 @@ "@commitlint/cli": "18.4.3", "@commitlint/config-angular": "18.4.3", "coveralls": "3.1.1", - "eslint": "8.54.0", + "eslint": "8.55.0", "eslint-config-nodebb": "0.2.1", "eslint-plugin-import": "2.29.0", "grunt": "1.6.1", From 6dab99fd7ba2afcfc245f3b8b08539cbe7f45364 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:45:13 -0500 Subject: [PATCH 0040/4744] fix(deps): update dependency nodebb-theme-persona to v13.2.49 (#12218) 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 01b17cdee7..34ad3b5795 100644 --- a/install/package.json +++ b/install/package.json @@ -105,7 +105,7 @@ "nodebb-theme-harmony": "1.1.103", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", - "nodebb-theme-persona": "13.2.48", + "nodebb-theme-persona": "13.2.49", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", From b6b569c0a85b1e017c27a4427fa903372653a2da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:45:33 -0500 Subject: [PATCH 0041/4744] fix(deps): update dependency chart.js to v4.4.1 (#12217) 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 34ad3b5795..003a896f9f 100644 --- a/install/package.json +++ b/install/package.json @@ -45,7 +45,7 @@ "bootstrap": "5.3.2", "bootswatch": "5.3.2", "chalk": "4.1.2", - "chart.js": "4.4.0", + "chart.js": "4.4.1", "cli-graph": "3.2.2", "clipboard": "2.0.11", "colors": "1.4.0", From 2c1c4dfe414925842025cfccd74ffff9513e9557 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 5 Dec 2023 10:41:14 -0500 Subject: [PATCH 0042/4744] test: migrate socket.io groups tests to use api v3 --- src/api/groups.js | 2 +- test/groups.js | 63 +++++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/api/groups.js b/src/api/groups.js index 5aa9c04f64..95074c4b6a 100644 --- a/src/api/groups.js +++ b/src/api/groups.js @@ -85,7 +85,7 @@ groupsAPI.listMembers = async (caller, data) => { const { query } = data; const after = parseInt(data.after || 0, 10); let response; - if (query) { + if (query && query.length) { response = await groups.searchMembers({ uid: caller.uid, query, diff --git a/test/groups.js b/test/groups.js index 00bd044e85..3fc70f009a 100644 --- a/test/groups.js +++ b/test/groups.js @@ -9,6 +9,7 @@ const db = require('./mocks/databasemock'); const helpers = require('./helpers'); const Groups = require('../src/groups'); const User = require('../src/user'); +const plugins = require('../src/plugins'); const utils = require('../src/utils'); const socketGroups = require('../src/socket.io/groups'); const apiGroups = require('../src/api/groups'); @@ -20,6 +21,12 @@ describe('Groups', () => { let adminUid; let testUid; before(async () => { + // Attach an emailer hook so related requests do not error + plugins.hooks.register('emailer-test', { + hook: 'static:email.send', + method: dummyEmailerHook, + }); + const navData = require('../install/data/navigation.json'); await navigation.save(navData); @@ -76,6 +83,14 @@ describe('Groups', () => { await Groups.join('administrators', adminUid); }); + async function dummyEmailerHook(data) { + // pretend to handle sending emails + } + + after(async () => { + plugins.hooks.unregister('emailer-test', 'static:email.send'); + }); + describe('.list()', () => { it('should list the groups present', (done) => { Groups.getGroupsFromSet('groups:visible:createtime', 0, -1, (err, groups) => { @@ -167,16 +182,13 @@ describe('Groups', () => { } await createAndJoinGroup('newuser', 'newuser@b.com'); await createAndJoinGroup('bob', 'bob@b.com'); - const data = await socketGroups.searchMembers({ uid: adminUid }, { groupName: 'Test', query: '' }); - assert.equal(data.users.length, 3); + const { users } = await apiGroups.listMembers({ uid: adminUid }, { slug: 'test', query: '' }); + assert.equal(users.length, 3); }); - it('should search group members', (done) => { - socketGroups.searchMembers({ uid: adminUid }, { groupName: 'Test', query: 'test' }, (err, data) => { - assert.ifError(err); - assert.strictEqual('testuser', data.users[0].username); - done(); - }); + it('should search group members', async () => { + const { users } = await apiGroups.listMembers({ uid: adminUid }, { slug: 'test', query: 'test' }); + assert.strictEqual('testuser', users[0].username); }); it('should not return hidden groups', async () => { @@ -1050,34 +1062,25 @@ describe('Groups', () => { } }); - it('should fail to load more groups with invalid data', (done) => { - socketGroups.loadMore({ uid: adminUid }, {}, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - done(); - }); + it('should load initial set of groups when passed no arguments', async () => { + const { groups } = await apiGroups.list({ uid: adminUid }, {}); + assert(Array.isArray(groups)); }); - it('should load more groups', (done) => { - socketGroups.loadMore({ uid: adminUid }, { after: 0, sort: 'count' }, (err, data) => { - assert.ifError(err); - assert(Array.isArray(data.groups)); - done(); - }); + it('should load more groups', async () => { + const { groups } = await apiGroups.list({ uid: adminUid }, { after: 0, sort: 'count' }); + assert(Array.isArray(groups)); }); - it('should fail to load more members with invalid data', (done) => { - socketGroups.loadMoreMembers({ uid: adminUid }, {}, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - done(); - }); + it('should load initial set of group members when passed no arguments', async () => { + const { users } = await apiGroups.listMembers({ uid: adminUid }, {}); + assert(users); + assert(Array.isArray(users)); }); - it('should load more members', (done) => { - socketGroups.loadMoreMembers({ uid: adminUid }, { after: 0, groupName: 'PrivateCanJoin' }, (err, data) => { - assert.ifError(err); - assert(Array.isArray(data.users)); - done(); - }); + it('should load more members', async () => { + const { users } = await apiGroups.listMembers({ uid: adminUid }, { after: 0, groupName: 'PrivateCanJoin' }); + assert(Array.isArray(users)); }); }); From 565ca3cc3b78d6e5177b7834d496927b96b6c88c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 5 Dec 2023 10:41:23 -0500 Subject: [PATCH 0043/4744] fix: deprecated emailer hook --- test/api.js | 4 ++-- test/authentication.js | 4 ++-- test/controllers.js | 4 ++-- test/emailer.js | 16 ++++++++-------- test/flags.js | 4 ++-- test/socket.io.js | 4 ++-- test/user.js | 4 ++-- test/user/emails.js | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/test/api.js b/test/api.js index de94973086..e3d420c83a 100644 --- a/test/api.js +++ b/test/api.js @@ -175,7 +175,7 @@ describe('API', async () => { after(async () => { plugins.hooks.unregister('core', 'filter:search.query', dummySearchHook); - plugins.hooks.unregister('emailer-test', 'filter:email.send'); + plugins.hooks.unregister('emailer-test', 'static:email.send'); }); async function setupData() { @@ -306,7 +306,7 @@ describe('API', async () => { }); // Attach an emailer hook so related requests do not error plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method: dummyEmailerHook, }); diff --git a/test/authentication.js b/test/authentication.js index 12afc00c15..8f0ba9389c 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -26,7 +26,7 @@ describe('authentication', () => { before((done) => { // Attach an emailer hook so related requests do not error plugins.hooks.register('authentication-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method: dummyEmailerHook, }); @@ -39,7 +39,7 @@ describe('authentication', () => { }); after(() => { - plugins.hooks.unregister('authentication-test', 'filter:email.send'); + plugins.hooks.unregister('authentication-test', 'static:email.send'); }); it('should allow login with email for uid 1', async () => { diff --git a/test/controllers.js b/test/controllers.js index 4c73fa8211..82e517640a 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -346,7 +346,7 @@ describe('Controllers', () => { before(async () => { // Attach an emailer hook so related requests do not error plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method: dummyEmailerHook, }); @@ -361,7 +361,7 @@ describe('Controllers', () => { after(() => { meta.config.requireEmailAddress = 0; - plugins.hooks.unregister('emailer-test', 'filter:email.send'); + plugins.hooks.unregister('emailer-test', 'static:email.send'); }); it('email interstitial should still apply if empty email entered and requireEmailAddress is enabled', async () => { diff --git a/test/emailer.js b/test/emailer.js index 28daa896af..35f82b0ecc 100644 --- a/test/emailer.js +++ b/test/emailer.js @@ -58,14 +58,14 @@ describe('emailer', () => { }; Plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method, }); Emailer.sendToEmail(template, email, language, params, (err) => { assert.equal(err, error); - Plugins.hooks.unregister('emailer-test', 'filter:email.send', method); + Plugins.hooks.unregister('emailer-test', 'static:email.send', method); done(); }); }); @@ -157,14 +157,14 @@ describe('emailer', () => { assert(false); // if thrown, email was sent }; Plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method, }); await user.bans.ban(recipientUid); await Emailer.send('test', recipientUid, {}); - Plugins.hooks.unregister('emailer-test', 'filter:email.send', method); + Plugins.hooks.unregister('emailer-test', 'static:email.send', method); }); it('should return true if the template is "banned"', async () => { @@ -172,12 +172,12 @@ describe('emailer', () => { assert(true); // if thrown, email was sent }; Plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method, }); await Emailer.send('banned', recipientUid, {}); - Plugins.hooks.unregister('emailer-test', 'filter:email.send', method); + Plugins.hooks.unregister('emailer-test', 'static:email.send', method); }); it('should return true if system settings allow sending to banned users', async () => { @@ -185,7 +185,7 @@ describe('emailer', () => { assert(true); // if thrown, email was sent }; Plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method, }); @@ -194,7 +194,7 @@ describe('emailer', () => { meta.config.sendEmailToBanned = 0; await user.bans.unban(recipientUid); - Plugins.hooks.unregister('emailer-test', 'filter:email.send', method); + Plugins.hooks.unregister('emailer-test', 'static:email.send', method); }); }); }); diff --git a/test/flags.js b/test/flags.js index 914e9ae72a..65dd23ef66 100644 --- a/test/flags.js +++ b/test/flags.js @@ -35,7 +35,7 @@ describe('Flags', () => { const dummyEmailerHook = async (data) => {}; // Attach an emailer hook so related requests do not error plugins.hooks.register('flags-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method: dummyEmailerHook, }); @@ -70,7 +70,7 @@ describe('Flags', () => { }); after(() => { - plugins.hooks.unregister('flags-test', 'filter:email.send'); + plugins.hooks.unregister('flags-test', 'static:email.send'); }); describe('.create()', () => { diff --git a/test/socket.io.js b/test/socket.io.js index 54568b47e0..f9b8b677df 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -236,12 +236,12 @@ describe('socket.io', () => { before(() => { // Attach an emailer hook so related requests do not error plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method: dummyEmailerHook, }); }); after(() => { - plugins.hooks.unregister('emailer-test', 'filter:email.send'); + plugins.hooks.unregister('emailer-test', 'static:email.send'); }); it('should validate emails', (done) => { diff --git a/test/user.js b/test/user.js index 6133f14f82..907a43f388 100644 --- a/test/user.js +++ b/test/user.js @@ -38,7 +38,7 @@ describe('User', () => { before((done) => { // Attach an emailer hook so related requests do not error plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method: dummyEmailerHook, }); @@ -56,7 +56,7 @@ describe('User', () => { }); }); after(() => { - plugins.hooks.unregister('emailer-test', 'filter:email.send'); + plugins.hooks.unregister('emailer-test', 'static:email.send'); }); beforeEach(() => { diff --git a/test/user/emails.js b/test/user/emails.js index 9ea19e3a01..6afbfc8023 100644 --- a/test/user/emails.js +++ b/test/user/emails.js @@ -23,7 +23,7 @@ describe('email confirmation (library methods)', () => { before(() => { // Attach an emailer hook so related requests do not error plugins.hooks.register('emailer-test', { - hook: 'filter:email.send', + hook: 'static:email.send', method: dummyEmailerHook, }); }); @@ -36,7 +36,7 @@ describe('email confirmation (library methods)', () => { }); after(async () => { - plugins.hooks.unregister('emailer-test', 'filter:email.send'); + plugins.hooks.unregister('emailer-test', 'static:email.send'); }); describe('isValidationPending', () => { From 445b70deda20201b7d9a68f7224da751b3db728c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 5 Dec 2023 11:47:55 -0500 Subject: [PATCH 0044/4744] test: migrate socket modules tests to v3 api --- src/api/chats.js | 24 ++++++-- src/api/users.js | 6 +- test/messaging.js | 136 +++++++++++++++++++++------------------------- 3 files changed, 88 insertions(+), 78 deletions(-) diff --git a/src/api/chats.js b/src/api/chats.js index 964bfdc071..db07ac32f0 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -36,7 +36,11 @@ async function rateLimitExceeded(caller, field) { return false; } -chatsAPI.list = async (caller, { uid, start, stop, page, perPage }) => { +chatsAPI.list = async (caller, { uid = caller.uid, start, stop, page, perPage } = {}) => { + if (!start && !stop && !page) { + throw new Error('[[error:invalid-data]]'); + } + if (!start && !stop && page) { winston.warn('[api/chats] Sending `page` and `perPage` to .list() is deprecated in favour of `start` and `stop`. The deprecated parameters will be removed in v4.'); start = Math.max(0, page - 1) * perPage; @@ -315,7 +319,11 @@ chatsAPI.toggleOwner = async (caller, { roomId, uid, state }) => { return await messaging.toggleOwner(uid, roomId, state); }; -chatsAPI.listMessages = async (caller, { uid, roomId, start, direction = null }) => { +chatsAPI.listMessages = async (caller, { uid = caller.uid, roomId, start = 0, direction = null } = {}) => { + if (!roomId) { + throw new Error('[[error:invalid-data]]'); + } + const count = 50; let stop = start + count - 1; if (direction === 1 || direction === -1) { @@ -353,12 +361,20 @@ chatsAPI.getPinnedMessages = async (caller, { start, roomId }) => { return { messages }; }; -chatsAPI.getMessage = async (caller, { mid, roomId }) => { +chatsAPI.getMessage = async (caller, { mid, roomId } = {}) => { + if (!mid || !roomId) { + throw new Error('[[error:invalid-data]]'); + } + const messages = await messaging.getMessagesData([mid], caller.uid, roomId, false); return messages.pop(); }; -chatsAPI.getRawMessage = async (caller, { mid, roomId }) => { +chatsAPI.getRawMessage = async (caller, { mid, roomId } = {}) => { + if (!mid || !roomId) { + throw new Error('[[error:invalid-data]]'); + } + const [isAdmin, canViewMessage, inRoom] = await Promise.all([ user.isAdministrator(caller.uid), messaging.canViewMessage(mid, roomId, caller.uid), diff --git a/src/api/users.js b/src/api/users.js index eda2b15d62..febcf290e6 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -147,7 +147,11 @@ usersAPI.getStatus = async (caller, { uid }) => { return { status }; }; -usersAPI.getPrivateRoomId = async (caller, { uid }) => { +usersAPI.getPrivateRoomId = async (caller, { uid } = {}) => { + if (!uid) { + throw new Error('[[error:invalid-data]]'); + } + let roomId = await messaging.hasPrivateChat(caller.uid, uid); roomId = parseInt(roomId, 10); diff --git a/test/messaging.js b/test/messaging.js index 9ea17cf402..4709aff351 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -374,8 +374,9 @@ describe('Messaging Library', () => { assert.equal(messageData.content, 'first chat message'); assert(messageData.fromUser); assert(messageData.roomId, roomId); - const raw = - await util.promisify(socketModules.chats.getRaw)({ uid: mocks.users.foo.uid }, { mid: messageData.messageId }); + const { content: raw } = await api.chats.getRawMessage( + { uid: mocks.users.foo.uid }, { mid: messageData.messageId, roomId } + ); assert.equal(raw, 'first chat message'); }); @@ -390,33 +391,38 @@ describe('Messaging Library', () => { meta.config.chatMessageDelay = oldValue; }); - it('should return invalid-data error', (done) => { - socketModules.chats.getRaw({ uid: mocks.users.foo.uid }, null, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - socketModules.chats.getRaw({ uid: mocks.users.foo.uid }, {}, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - done(); - }); - }); + it('should return invalid-data error', async () => { + await assert.rejects( + api.chats.getRawMessage({ uid: mocks.users.foo.uid }, undefined), + { message: '[[error:invalid-data]]' } + ); + + + await assert.rejects( + api.chats.getRawMessage({ uid: mocks.users.foo.uid }, {}), + { message: '[[error:invalid-data]]' } + ); }); - it('should return not allowed error if mid is not in room', async () => { + it('should return not allowed error if user is not in room', async () => { const uids = await User.create({ username: 'dummy' }); let { body } = await callv3API('post', '/chats', { uids: [uids] }, 'baz'); const myRoomId = body.response.roomId; assert(myRoomId); try { - await socketModules.chats.getRaw({ uid: mocks.users.baz.uid }, { mid: 200 }); + await api.chats.getRawMessage({ uid: mocks.users.baz.uid }, { mid: 200 }); } catch (err) { assert(err); - assert.equal(err.message, '[[error:not-allowed]]'); + assert.equal(err.message, '[[error:invalid-data]]'); } ({ body } = await callv3API('post', `/chats/${myRoomId}`, { roomId: myRoomId, message: 'admin will see this' }, 'baz')); const message = body.response; - const raw = await socketModules.chats.getRaw({ uid: mocks.users.foo.uid }, { mid: message.messageId }); - assert.equal(raw, 'admin will see this'); + const { content } = await api.chats.getRawMessage( + { uid: mocks.users.foo.uid }, { mid: message.messageId, roomId: myRoomId } + ); + assert.equal(content, 'admin will see this'); }); @@ -476,13 +482,6 @@ describe('Messaging Library', () => { await api.chats.mark({ uid: mocks.users.foo.uid }, { state: 0, roomId: roomId }); }); - it('should mark all rooms read', (done) => { - socketModules.chats.markAllRead({ uid: mocks.users.foo.uid }, {}, (err) => { - assert.ifError(err); - done(); - }); - }); - it('should fail to rename room with invalid data', async () => { const { body } = await callv3API('put', `/chats/${roomId}`, { name: null }, 'foo'); assert.strictEqual(body.status.message, await translator.translate('[[error:invalid-data]]')); @@ -517,68 +516,59 @@ describe('Messaging Library', () => { assert.strictEqual(body.response.roomName, 'new room name'); }); - it('should return true if user is dnd', (done) => { - db.setObjectField(`user:${mocks.users.herp.uid}`, 'status', 'dnd', (err) => { - assert.ifError(err); - socketModules.chats.isDnD({ uid: mocks.users.foo.uid }, mocks.users.herp.uid, (err, isDnD) => { - assert.ifError(err); - assert(isDnD); - done(); - }); - }); + it('should return true if user is dnd', async () => { + await db.setObjectField(`user:${mocks.users.herp.uid}`, 'status', 'dnd'); + const { status } = await api.users.getStatus({ uid: mocks.users.foo.uid }, { uid: mocks.users.herp.uid }); + assert.strictEqual(status, 'dnd'); }); - it('should fail to load recent chats with invalid data', (done) => { - socketModules.chats.getRecentChats({ uid: mocks.users.foo.uid }, null, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - socketModules.chats.getRecentChats({ uid: mocks.users.foo.uid }, { after: null }, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - socketModules.chats.getRecentChats({ uid: mocks.users.foo.uid }, { after: 0, uid: null }, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - done(); - }); - }); - }); - }); - - it('should load recent chats of user', (done) => { - socketModules.chats.getRecentChats( - { uid: mocks.users.foo.uid }, - { after: 0, uid: mocks.users.foo.uid }, - (err, data) => { - assert.ifError(err); - assert(Array.isArray(data.rooms)); - done(); - } + it('should fail to load recent chats with invalid data', async () => { + await assert.rejects( + api.chats.list({ uid: mocks.users.foo.uid }, undefined), + { message: '[[error:invalid-data]]' } ); + + await assert.rejects( + api.chats.list({ uid: mocks.users.foo.uid }, { start: null }), + { message: '[[error:invalid-data]]' } + ); + + await assert.rejects( + api.chats.list({ uid: mocks.users.foo.uid }, { start: 0, uid: null }), + { message: '[[error:invalid-data]]' } + ); + }); + + it('should load recent chats of user', async () => { + const { rooms } = await api.chats.list( + { uid: mocks.users.foo.uid }, { start: 0, stop: 9, uid: mocks.users.foo.uid } + ); + assert(Array.isArray(rooms)); }); it('should escape teaser', async () => { await callv3API('post', `/chats/${roomId}`, { roomId: roomId, message: ' { + await assert.rejects( + api.users.getPrivateRoomId({ uid: null }, undefined), + { message: '[[error:invalid-data]]' } ); - assert.equal(data.rooms[0].teaser.content, '<svg/onload=alert(document.location);'); + await assert.rejects( + api.users.getPrivateRoomId({ uid: mocks.users.foo.uid }, undefined), + { message: '[[error:invalid-data]]' } + ); }); - it('should fail to check if user has private chat with invalid data', (done) => { - socketModules.chats.hasPrivateChat({ uid: null }, null, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - socketModules.chats.hasPrivateChat({ uid: mocks.users.foo.uid }, null, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - done(); - }); - }); - }); - - it('should check if user has private chat with another uid', (done) => { - socketModules.chats.hasPrivateChat({ uid: mocks.users.foo.uid }, mocks.users.herp.uid, (err, roomId) => { - assert.ifError(err); - assert(roomId); - done(); - }); + it('should check if user has private chat with another uid', async () => { + const { roomId } = await api.users.getPrivateRoomId({ uid: mocks.users.foo.uid }, { uid: mocks.users.herp.uid }); + assert(roomId); }); }); From da2441b9bd293d7188ee645be3322a7305a43a19 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 6 Dec 2023 09:19:56 +0000 Subject: [PATCH 0045/4744] Latest translations and fallbacks --- public/language/hy/post-queue.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/hy/post-queue.json b/public/language/hy/post-queue.json index bd7b786c5e..a5dc59fcf4 100644 --- a/public/language/hy/post-queue.json +++ b/public/language/hy/post-queue.json @@ -3,7 +3,7 @@ "post-queue": "Գրառումների հերթ", "no-queued-posts": "Գրառումների հերթում գրառումներ չկան:", "no-single-post": "Ձեր փնտրած թեման կամ գրառումն այլևս հերթում չէ: Այն հավանաբար արդեն հաստատված կամ ջնջված է:", - "enabling-help": "The post queue is currently disabled. To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "enabling-help": "Գրառումների հերթը այս պահին միացված չէ . Այս գործառույթը միացնելու համար, անցեք Կարգավորումներ → Գրառում → Գրառման հերթ և միացրեք Գրառման հերթը.", "back-to-list": "Վերադառնալ Գրառումների հերթին", "public-intro": "Եթե ունեք գրառումներ հերթում, դրանք կցուցադրվեն այստեղ", "public-description": "Այս ֆորումը կազմված է այնպես, որ ավտոմատ կերպով հերթագրի նոր հաշիվներից գրառումները՝ սպասելով մոդերատորի հաստատմանը.
Եթե դուք ունեք հերթագրված գրառումներ, որոնք սպասում են հաստատման, դուք կկարողանաք տեսնել դրանք այստեղ. ", From 51d8f3b195bddb13a13ddc0de110722774d9bb1b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sat, 6 May 2023 00:16:09 -0400 Subject: [PATCH 0046/4744] fix: moved .well-known assets to separate router file, added basic webfinger implementation added tests for webfinger controller --- src/controllers/index.js | 1 + src/controllers/well-known.js | 48 +++++++++++++++++++++++++ src/routes/index.js | 2 ++ src/routes/user.js | 3 -- src/routes/well-known.js | 9 +++++ test/controllers.js | 68 +++++++++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/controllers/well-known.js create mode 100644 src/routes/well-known.js diff --git a/src/controllers/index.js b/src/controllers/index.js index 253df71a67..b5dc1373e7 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -12,6 +12,7 @@ const helpers = require('./helpers'); const Controllers = module.exports; Controllers.ping = require('./ping'); +Controllers['well-known'] = require('./well-known'); Controllers.home = require('./home'); Controllers.topics = require('./topics'); Controllers.posts = require('./posts'); diff --git a/src/controllers/well-known.js b/src/controllers/well-known.js new file mode 100644 index 0000000000..c45c6ea8d3 --- /dev/null +++ b/src/controllers/well-known.js @@ -0,0 +1,48 @@ +'use strict'; + +const nconf = require('nconf'); + +const user = require('../user'); +const privileges = require('../privileges'); + +const Controller = module.exports; + +Controller.webfinger = async (req, res) => { + const { resource } = req.query; + const { hostname } = nconf.get('url_parsed'); + + if (!resource || !resource.startsWith('acct:') || !resource.endsWith(hostname)) { + return res.sendStatus(400); + } + + const canView = await privileges.global.can('view:users', req.uid); + console.log('canView', canView, req.uid); + if (!canView) { + return res.sendStatus(403); + } + + // Get the slug + const slug = resource.slice(5, resource.length - (hostname.length + 1)); + + const uid = await user.getUidByUserslug(slug); + if (!uid) { + return res.sendStatus(404); + } + + const response = { + subject: `acct:${slug}@${hostname}`, + aliases: [ + `${nconf.get('url')}/uid/${uid}`, + `${nconf.get('url')}/user/${slug}`, + ], + links: [ + { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${nconf.get('url')}/user/${slug}`, + }, + ], + }; + + res.status(200).json(response); +}; diff --git a/src/routes/index.js b/src/routes/index.js index 4008f1565a..8def527624 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -22,6 +22,7 @@ const _mounts = { api: require('./api'), admin: require('./admin'), feed: require('./feeds'), + 'well-known': require('./well-known'), }; _mounts.main = (app, middleware, controllers) => { @@ -157,6 +158,7 @@ function addCoreRoutes(app, router, middleware, mounts) { _mounts.main(router, middleware, controllers); _mounts.mod(router, middleware, controllers); _mounts.globalMod(router, middleware, controllers); + _mounts['well-known'](router, middleware, controllers); addRemountableRoutes(app, router, middleware, mounts); diff --git a/src/routes/user.js b/src/routes/user.js index 49f551dc59..131e7940bb 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -37,9 +37,6 @@ module.exports = function (app, name, middleware, controllers) { setupPageRoute(app, `/${name}/:userslug/edit/username`, accountMiddlewares, controllers.accounts.edit.username); setupPageRoute(app, `/${name}/:userslug/edit/email`, accountMiddlewares, controllers.accounts.edit.email); setupPageRoute(app, `/${name}/:userslug/edit/password`, accountMiddlewares, controllers.accounts.edit.password); - app.use('/.well-known/change-password', (req, res) => { - res.redirect('/me/edit/password'); - }); setupPageRoute(app, `/${name}/:userslug/info`, accountMiddlewares, controllers.accounts.info.get); setupPageRoute(app, `/${name}/:userslug/settings`, accountMiddlewares, controllers.accounts.settings.get); setupPageRoute(app, `/${name}/:userslug/uploads`, accountMiddlewares, controllers.accounts.uploads.get); diff --git a/src/routes/well-known.js b/src/routes/well-known.js new file mode 100644 index 0000000000..ac54a1c210 --- /dev/null +++ b/src/routes/well-known.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = function (app, middleware, controllers) { + app.use('/.well-known/change-password', (req, res) => { + res.redirect('/me/edit/password'); + }); + + app.get('/.well-known/webfinger', controllers['well-known'].webfinger); +}; diff --git a/test/controllers.js b/test/controllers.js index 82e517640a..bd53d7c312 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -2816,6 +2816,74 @@ describe('Controllers', () => { } }); + describe('.well-known', () => { + describe('webfinger', () => { + let uid; + let username; + + before(async () => { + username = utils.generateUUID().slice(0, 10); + uid = await user.create({ username }); + }); + + it('should error if resource parameter is missing', async () => { + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger`, { + json: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert.strictEqual(response.statusCode, 400); + }); + + it('should error if resource parameter is malformed', async () => { + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=foobar`, { + json: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert.strictEqual(response.statusCode, 400); + }); + + it('should deny access if view:users privilege is not enabled for guests', async () => { + await privileges.global.rescind(['groups:view:users'], 'guests'); + + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${username}@${nconf.get('url_parsed').hostname}`, { + json: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert.strictEqual(response.statusCode, 403); + + await privileges.global.give(['groups:view:users'], 'guests'); + }); + + it('should respond appropriately if the user requested does not exist locally', async () => { + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar@${nconf.get('url_parsed').hostname}`, { + json: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert.strictEqual(response.statusCode, 404); + }); + + it('should return a valid webfinger response if the user exists', async () => { + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${username}@${nconf.get('url_parsed').hostname}`, { + json: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert.strictEqual(response.statusCode, 200); + assert(['subject', 'aliases', 'links'].every(prop => response.body.hasOwnProperty(prop))); + assert(response.body.subject, `acct:${username}@${nconf.get('url_parsed').hostname}`); + }); + }); + }); + after((done) => { const analytics = require('../src/analytics'); analytics.writeData(done); From 2dec357aee6f0c7a554732d80c871b47def6ded1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 17 May 2023 13:13:30 -0400 Subject: [PATCH 0047/4744] feat: activitypub actor endpoint for user accounts --- src/activitypub.js | 42 ++++++++++++++++++++++++++++++++++ src/controllers/activitypub.js | 41 +++++++++++++++++++++++++++++++++ src/controllers/index.js | 1 + src/controllers/well-known.js | 6 ++++- src/messaging/uploads.js | 0 src/middleware/index.js | 24 +++++++++++++++++++ src/routes/activitypub.js | 7 ++++++ src/routes/index.js | 2 ++ 8 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/activitypub.js create mode 100644 src/controllers/activitypub.js create mode 100644 src/messaging/uploads.js create mode 100644 src/routes/activitypub.js diff --git a/src/activitypub.js b/src/activitypub.js new file mode 100644 index 0000000000..ec25ef600f --- /dev/null +++ b/src/activitypub.js @@ -0,0 +1,42 @@ +'use strict'; + +const { generateKeyPairSync } = require('crypto'); + +const winston = require('winston'); + +const db = require('./database'); + +const ActivityPub = module.exports; + +ActivityPub.getPublicKey = async (uid) => { + let publicKey; + + try { + ({ publicKey } = await db.getObject(`uid:${uid}:keys`)); + } catch (e) { + ({ publicKey } = await generateKeys(uid)); + } + + return publicKey; +}; + +async function generateKeys(uid) { + winston.info(`[activitypub] Generating RSA key-pair for uid ${uid}`); + const { + publicKey, + privateKey, + } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + + await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey }); + return { publicKey, privateKey }; +} diff --git a/src/controllers/activitypub.js b/src/controllers/activitypub.js new file mode 100644 index 0000000000..734897380f --- /dev/null +++ b/src/controllers/activitypub.js @@ -0,0 +1,41 @@ +'use strict'; + +const nconf = require('nconf'); + +const user = require('../user'); +const activitypub = require('../activitypub'); + +const Controller = module.exports; + +Controller.getActor = async (req, res) => { + // todo: view:users priv gate + const { userslug } = req.params; + const { uid } = res.locals; + const { username, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid); + const publicKey = await activitypub.getPublicKey(uid); + + res.status(200).json({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ], + id: `${nconf.get('url')}/user/${userslug}`, + url: `${nconf.get('url')}/user/${userslug}`, + followers: `${nconf.get('url')}/user/${userslug}/followers`, + following: `${nconf.get('url')}/user/${userslug}/following`, + inbox: `${nconf.get('url')}/user/${userslug}/inbox`, + outbox: `${nconf.get('url')}/user/${userslug}/outbox`, + + type: 'Person', + preferredUsername: username, + summary: aboutme, + icon: picture ? `${nconf.get('url')}${picture}` : null, + image: cover ? `${nconf.get('url')}${cover}` : null, + + publicKey: { + id: `${nconf.get('url')}/user/${userslug}`, + owner: `${nconf.get('url')}/user/${userslug}#key`, + publicKeyPem: publicKey, + }, + }); +}; diff --git a/src/controllers/index.js b/src/controllers/index.js index b5dc1373e7..51a1cf87bf 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -13,6 +13,7 @@ const Controllers = module.exports; Controllers.ping = require('./ping'); Controllers['well-known'] = require('./well-known'); +Controllers.activitypub = require('./activitypub'); Controllers.home = require('./home'); Controllers.topics = require('./topics'); Controllers.posts = require('./posts'); diff --git a/src/controllers/well-known.js b/src/controllers/well-known.js index c45c6ea8d3..86caae4173 100644 --- a/src/controllers/well-known.js +++ b/src/controllers/well-known.js @@ -16,7 +16,6 @@ Controller.webfinger = async (req, res) => { } const canView = await privileges.global.can('view:users', req.uid); - console.log('canView', canView, req.uid); if (!canView) { return res.sendStatus(403); } @@ -41,6 +40,11 @@ Controller.webfinger = async (req, res) => { type: 'text/html', href: `${nconf.get('url')}/user/${slug}`, }, + { + rel: 'self', + type: 'application/activity+json', + href: `${nconf.get('url')}/user/${slug}`, // actor + }, ], }; diff --git a/src/messaging/uploads.js b/src/messaging/uploads.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/middleware/index.js b/src/middleware/index.js index 80a5f568c6..4a9ebb44f8 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -297,3 +297,27 @@ middleware.handleMultipart = (req, res, next) => { multipartMiddleware(req, res, next); }; + +middleware.proceedOnActivityPub = (req, res, next) => { + // For whatever reason, express accepts does not recognize "profile" as a valid differentiator + // Therefore, manual header parsing is used here. + const { accept } = req.headers; + if (!accept) { + return next('route'); + } + + const acceptable = [ + 'application/activity+json', + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + ]; + const pass = accept.split(',').some((value) => { + const parts = value.split(';').map(v => v.trim()); + return acceptable.includes(value || parts[0]); + }); + + if (!pass) { + return next('route'); + } + + next(); +}; diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js new file mode 100644 index 0000000000..14e840d5b1 --- /dev/null +++ b/src/routes/activitypub.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function (app, middleware, controllers) { + const middlewares = [middleware.proceedOnActivityPub, middleware.exposeUid]; + + app.get('/user/:userslug', middlewares, controllers.activitypub.getActor); +}; diff --git a/src/routes/index.js b/src/routes/index.js index 8def527624..451d68a9fe 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -23,6 +23,7 @@ const _mounts = { admin: require('./admin'), feed: require('./feeds'), 'well-known': require('./well-known'), + activitypub: require('./activitypub'), }; _mounts.main = (app, middleware, controllers) => { @@ -155,6 +156,7 @@ function addCoreRoutes(app, router, middleware, mounts) { _mounts.api(router, middleware, controllers); _mounts.feed(router, middleware, controllers); + _mounts.activitypub(router, middleware, controllers); _mounts.main(router, middleware, controllers); _mounts.mod(router, middleware, controllers); _mounts.globalMod(router, middleware, controllers); From 4bd8d28a8b3a36e7e53ab6bd7bb724da6f4ebb0a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 22 May 2023 23:38:11 -0400 Subject: [PATCH 0048/4744] test: added test cases for activitypub integration, WIP --- test/activitypub.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/activitypub.js diff --git a/test/activitypub.js b/test/activitypub.js new file mode 100644 index 0000000000..5276d5e15c --- /dev/null +++ b/test/activitypub.js @@ -0,0 +1,27 @@ +'use strict'; + +const nconf = require('nconf'); +const request = require('request-promise-native'); + +const db = require('./mocks/databasemock'); + +describe('ActivityPub integration', () => { + describe('WebFinger endpoint', () => { + it('should return a 404 Not Found if no user exists by that username', async () => { + const response = await request(`${nconf.get('url')}/register/complete`, { + method: 'post', + jar, + json: true, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + headers: { + 'x-csrf-token': token, + }, + form: { + email: '', + }, + }); + }); + }); +}); From 1c8e13bb12eaadde23d14cfbdd5bc39a27b3bbfa Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 23 May 2023 16:13:16 -0400 Subject: [PATCH 0049/4744] test: updated activitypub test suite --- src/activitypub.js | 2 +- test/activitypub.js | 176 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 169 insertions(+), 9 deletions(-) diff --git a/src/activitypub.js b/src/activitypub.js index ec25ef600f..4f4fe7ae37 100644 --- a/src/activitypub.js +++ b/src/activitypub.js @@ -21,7 +21,7 @@ ActivityPub.getPublicKey = async (uid) => { }; async function generateKeys(uid) { - winston.info(`[activitypub] Generating RSA key-pair for uid ${uid}`); + winston.verbose(`[activitypub] Generating RSA key-pair for uid ${uid}`); const { publicKey, privateKey, diff --git a/test/activitypub.js b/test/activitypub.js index 5276d5e15c..d41ee508da 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -1,27 +1,187 @@ 'use strict'; +const assert = require('assert'); const nconf = require('nconf'); const request = require('request-promise-native'); const db = require('./mocks/databasemock'); +const slugify = require('../src/slugify'); +const utils = require('../src/utils'); + +const user = require('../src/user'); +const privileges = require('../src/privileges'); describe('ActivityPub integration', () => { describe('WebFinger endpoint', () => { + let uid; + let slug; + const { hostname } = nconf.get('url_parsed'); + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + uid = await user.create({ username: slug }); + }); + it('should return a 404 Not Found if no user exists by that username', async () => { - const response = await request(`${nconf.get('url')}/register/complete`, { - method: 'post', - jar, + const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar@${hostname}`, { + method: 'get', json: true, - followRedirect: false, + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert(response); + assert.strictEqual(response.statusCode, 404); + }); + + it('should return a 400 Bad Request if the request is malformed', async () => { + const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar`, { + method: 'get', + json: true, + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert(response); + assert.strictEqual(response.statusCode, 400); + }); + + it('should return 403 Forbidden if the calling user is not allowed to view the user list/profiles', async () => { + await privileges.global.rescind(['groups:view:users'], 'guests'); + const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${hostname}`, { + method: 'get', + json: true, + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert(response); + assert.strictEqual(response.statusCode, 403); + await privileges.global.give(['groups:view:users'], 'guests'); + }); + + it('should return a valid WebFinger response otherwise', async () => { + const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${hostname}`, { + method: 'get', + json: true, + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + + ['subject', 'aliases', 'links'].forEach((prop) => { + assert(response.body.hasOwnProperty(prop)); + assert(response.body[prop]); + }); + + assert.strictEqual(response.body.subject, `acct:${slug}@${hostname}`); + + assert(Array.isArray(response.body.aliases)); + assert([`${nconf.get('url')}/uid/${uid}`, `${nconf.get('url')}/user/${slug}`].every(url => response.body.aliases.includes(url))); + + assert(Array.isArray(response.body.links)); + }); + }); + + describe('ActivityPub screener middleware', () => { + let uid; + let slug; + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + uid = await user.create({ username: slug }); + }); + + it('should return regular user profile html if Accept header is not ActivityPub-related', async () => { + const response = await request(`${nconf.get('url')}/user/${slug}`, { + method: 'get', + followRedirect: true, simple: false, resolveWithFullResponse: true, headers: { - 'x-csrf-token': token, - }, - form: { - email: '', + Accept: 'text/html', }, }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(response.body.startsWith('')); + }); + + it('should return the ActivityPub Actor JSON-LD payload if the correct Accept header is provided', async () => { + const response = await request(`${nconf.get('url')}/user/${slug}`, { + method: 'get', + json: true, + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(response.body.hasOwnProperty('@context')); + assert(response.body['@context'].includes('https://www.w3.org/ns/activitystreams')); + }); + }); + + describe('Actor endpoint', () => { + let uid; + let slug; + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + uid = await user.create({ username: slug }); + }); + + it('should return a valid ActivityPub Actor JSON-LD payload', async () => { + const response = await request(`${nconf.get('url')}/user/${slug}`, { + method: 'get', + json: true, + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(response.body.hasOwnProperty('@context')); + assert(response.body['@context'].includes('https://www.w3.org/ns/activitystreams')); + + ['id', 'url', 'followers', 'following', 'inbox', 'outbox'].forEach((prop) => { + assert(response.body.hasOwnProperty(prop)); + assert(response.body[prop]); + }); + + assert.strictEqual(response.body.id, response.body.url); + assert.strictEqual(response.body.type, 'Person'); + }); + + it('should contain a `publicKey` property with a public key', async () => { + const response = await request(`${nconf.get('url')}/user/${slug}`, { + method: 'get', + json: true, + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(response.body.hasOwnProperty('publicKey')); + assert(['id', 'owner', 'publicKeyPem'].every(prop => response.body.publicKey.hasOwnProperty(prop))); }); }); }); From 099124c49ea806626c8a48655ec678653f0ea3ed Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 24 May 2023 14:00:41 -0400 Subject: [PATCH 0050/4744] feat: global switch for disabling federation, + test --- public/language/en-GB/admin/menu.json | 1 + .../en-GB/admin/settings/activitypub.json | 7 +++++ src/middleware/index.js | 2 +- src/views/admin/partials/navigation.tpl | 1 + src/views/admin/settings/activitypub.tpl | 20 +++++++++++++ test/activitypub.js | 29 +++++++++++++++++++ 6 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 public/language/en-GB/admin/settings/activitypub.json create mode 100644 src/views/admin/settings/activitypub.tpl diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 6e30be22b3..913c74f475 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -38,6 +38,7 @@ "settings/tags": "Tags", "settings/notifications": "Notifications", "settings/api": "API Access", + "settings/activitypub": "Federation (ActivityPub)", "settings/sounds": "Sounds", "settings/social": "Social", "settings/cookies": "Cookies", diff --git a/public/language/en-GB/admin/settings/activitypub.json b/public/language/en-GB/admin/settings/activitypub.json new file mode 100644 index 0000000000..989eeaf6f3 --- /dev/null +++ b/public/language/en-GB/admin/settings/activitypub.json @@ -0,0 +1,7 @@ +{ + "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.)", + + "general": "General", + "enabled": "Enable Federation" +} \ No newline at end of file diff --git a/src/middleware/index.js b/src/middleware/index.js index 4a9ebb44f8..f8ba614ab2 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -302,7 +302,7 @@ middleware.proceedOnActivityPub = (req, res, next) => { // For whatever reason, express accepts does not recognize "profile" as a valid differentiator // Therefore, manual header parsing is used here. const { accept } = req.headers; - if (!accept) { + if (!accept || !meta.config.activityPubEnabled) { return next('route'); } diff --git a/src/views/admin/partials/navigation.tpl b/src/views/admin/partials/navigation.tpl index 8c89f6a702..6c842c2abf 100644 --- a/src/views/admin/partials/navigation.tpl +++ b/src/views/admin/partials/navigation.tpl @@ -86,6 +86,7 @@ [[admin/menu:settings/pagination]] [[admin/menu:settings/notifications]] [[admin/menu:settings/api]] + [[admin/menu:settings/activitypub]] [[admin/menu:settings/cookies]] [[admin/menu:settings/web-crawler]] [[admin/menu:settings/advanced]] diff --git a/src/views/admin/settings/activitypub.tpl b/src/views/admin/settings/activitypub.tpl new file mode 100644 index 0000000000..a32920619a --- /dev/null +++ b/src/views/admin/settings/activitypub.tpl @@ -0,0 +1,20 @@ + + +

[[admin/settings/activitypub:intro-lead]]

+

[[admin/settings/activitypub:intro-body]]

+ +
+ +
+
[[admin/settings/activitypub:general]]
+
+
+
+ + +
+
+
+
+ + diff --git a/test/activitypub.js b/test/activitypub.js index d41ee508da..5193fdfbee 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -8,10 +8,19 @@ const db = require('./mocks/databasemock'); const slugify = require('../src/slugify'); const utils = require('../src/utils'); +const meta = require('../src/meta'); const user = require('../src/user'); const privileges = require('../src/privileges'); describe('ActivityPub integration', () => { + before(() => { + meta.config.activityPubEnabled = 1; + }); + + after(() => { + delete meta.config.activityPubEnabled; + }); + describe('WebFinger endpoint', () => { let uid; let slug; @@ -98,6 +107,26 @@ describe('ActivityPub integration', () => { uid = await user.create({ username: slug }); }); + it('should return regular user profile html if federation is disabled', async () => { + delete meta.config.activityPubEnabled; + + const response = await request(`${nconf.get('url')}/user/${slug}`, { + method: 'get', + followRedirect: true, + simple: false, + resolveWithFullResponse: true, + headers: { + Accept: 'text/html', + }, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(response.body.startsWith('')); + + meta.config.activityPubEnabled = 1; + }); + it('should return regular user profile html if Accept header is not ActivityPub-related', async () => { const response = await request(`${nconf.get('url')}/user/${slug}`, { method: 'get', From 81b6260f2eea930576c8a15b6fcf82f63e29e50e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 25 May 2023 14:29:06 -0400 Subject: [PATCH 0051/4744] feat: inbox and outbox routes, stub controllers --- src/controllers/activitypub.js | 25 +++++++++++++++++++++++++ src/routes/activitypub.js | 6 ++++++ 2 files changed, 31 insertions(+) diff --git a/src/controllers/activitypub.js b/src/controllers/activitypub.js index 734897380f..921e59cf2c 100644 --- a/src/controllers/activitypub.js +++ b/src/controllers/activitypub.js @@ -39,3 +39,28 @@ Controller.getActor = async (req, res) => { }, }); }; + +Controller.getOutbox = async (req, res) => { + // stub + res.status(200).json({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + totalItems: 0, + orderedItems: [], + }); +}; + +Controller.postOutbox = async (req, res) => { + // This is a client-to-server feature so it is deliberately not implemented at this time. + res.sendStatus(405); +}; + +Controller.getInbox = async (req, res) => { + // This is a client-to-server feature so it is deliberately not implemented at this time. + res.sendStatus(405); +}; + +Controller.postInbox = async (req, res) => { + // stub — other activity-pub services will push stuff here. + res.sendStatus(405); +}; diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 14e840d5b1..f96484a7b2 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -4,4 +4,10 @@ module.exports = function (app, middleware, controllers) { const middlewares = [middleware.proceedOnActivityPub, middleware.exposeUid]; app.get('/user/:userslug', middlewares, controllers.activitypub.getActor); + + app.get('/user/:userslug/outbox', middlewares, controllers.activitypub.getOutbox); + app.post('/user/:userslug/outbox', middlewares, controllers.activitypub.postOutbox); + + app.get('/user/:userslug/inbox', middlewares, controllers.activitypub.getInbox); + app.post('/user/:userslug/inbox', middlewares, controllers.activitypub.postInbox); }; From 7e1dac39eaaa0e86910359cf434492dcb41912e4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 28 May 2023 15:42:29 -0400 Subject: [PATCH 0052/4744] feat: followers and following endpoints --- src/controllers/activitypub.js | 36 ++++++++++++++++++++++++++++++++++ src/routes/activitypub.js | 3 +++ 2 files changed, 39 insertions(+) diff --git a/src/controllers/activitypub.js b/src/controllers/activitypub.js index 921e59cf2c..9e4527c7ac 100644 --- a/src/controllers/activitypub.js +++ b/src/controllers/activitypub.js @@ -40,6 +40,42 @@ Controller.getActor = async (req, res) => { }); }; +Controller.getFollowing = async (req, res) => { + const { followingCount: totalItems } = await user.getUserFields(res.locals.uid, ['followingCount']); + + const page = parseInt(req.query.page, 10) || 1; + const resultsPerPage = 50; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + + let orderedItems = await user.getFollowing(res.locals.uid, start, stop); + orderedItems = orderedItems.map(({ userslug }) => `${nconf.get('url')}/user/${userslug}`); + res.status(200).json({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + totalItems, + orderedItems, + }); +}; + +Controller.getFollowers = async (req, res) => { + const { followerCount: totalItems } = await user.getUserFields(res.locals.uid, ['followerCount']); + + const page = parseInt(req.query.page, 10) || 1; + const resultsPerPage = 50; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + + let orderedItems = await user.getFollowers(res.locals.uid, start, stop); + orderedItems = orderedItems.map(({ userslug }) => `${nconf.get('url')}/user/${userslug}`); + res.status(200).json({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + totalItems, + orderedItems, + }); +}; + Controller.getOutbox = async (req, res) => { // stub res.status(200).json({ diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index f96484a7b2..02fe9a2bf0 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -5,6 +5,9 @@ module.exports = function (app, middleware, controllers) { app.get('/user/:userslug', middlewares, controllers.activitypub.getActor); + app.get('/user/:userslug/following', middlewares, controllers.activitypub.getFollowing); + app.get('/user/:userslug/followers', middlewares, controllers.activitypub.getFollowers); + app.get('/user/:userslug/outbox', middlewares, controllers.activitypub.getOutbox); app.post('/user/:userslug/outbox', middlewares, controllers.activitypub.postOutbox); From a05b674e2732dfa9dcc7d46901c78cf624b7307b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 29 May 2023 17:42:44 -0400 Subject: [PATCH 0053/4744] feat: ability to view federated profiles via url manipulation --- src/activitypub/helpers.js | 32 ++++++++++++++++++++ src/{activitypub.js => activitypub/index.js} | 23 +++++++++++++- src/controllers/accounts/profile.js | 32 ++++++++++++++++++++ src/middleware/index.js | 10 +++++- 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/activitypub/helpers.js rename src/{activitypub.js => activitypub/index.js} (63%) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js new file mode 100644 index 0000000000..7c77462839 --- /dev/null +++ b/src/activitypub/helpers.js @@ -0,0 +1,32 @@ +'use strict'; + +const request = require('request-promise-native'); + +const Helpers = module.exports; + +Helpers.query = async (id) => { + const [username, hostname] = id.split('@'); + if (!username || !hostname) { + return false; + } + + // Make a webfinger query to retrieve routing information + const response = await request(`https://${hostname}/.well-known/webfinger?resource=acct:${id}`, { + simple: false, + resolveWithFullResponse: true, + json: true, + }); + + if (response.statusCode !== 200 || !response.body.hasOwnProperty('links')) { + return false; + } + + // Parse links to find actor endpoint + let actorUri = response.body.links.filter(link => link.type === 'application/activity+json' && link.rel === 'self'); + if (actorUri.length) { + actorUri = actorUri.pop(); + ({ href: actorUri } = actorUri); + } + + return { username, hostname, actorUri }; +}; diff --git a/src/activitypub.js b/src/activitypub/index.js similarity index 63% rename from src/activitypub.js rename to src/activitypub/index.js index 4f4fe7ae37..7bac91ff48 100644 --- a/src/activitypub.js +++ b/src/activitypub/index.js @@ -3,11 +3,32 @@ const { generateKeyPairSync } = require('crypto'); const winston = require('winston'); +const request = require('request-promise-native'); -const db = require('./database'); +const db = require('../database'); +const helpers = require('./helpers'); const ActivityPub = module.exports; +ActivityPub.getActor = async (id) => { + const { hostname, actorUri: uri } = await helpers.query(id); + if (!uri) { + return false; + } + + const actor = await request({ + uri, + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + json: true, + }); + + actor.hostname = hostname; + + return actor; +}; + ActivityPub.getPublicKey = async (uid) => { let publicKey; diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index 1ef9756784..909066da18 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -10,12 +10,18 @@ const categories = require('../../categories'); const plugins = require('../../plugins'); const privileges = require('../../privileges'); const accountHelpers = require('./helpers'); +const { getActor } = require('../../activitypub'); const helpers = require('../helpers'); +const slugify = require('../../slugify'); const utils = require('../../utils'); const profileController = module.exports; profileController.get = async function (req, res, next) { + if (res.locals.uid === -2) { + return profileController.getFederated(req, res, next); + } + const lowercaseSlug = req.params.userslug.toLowerCase(); if (req.params.userslug !== lowercaseSlug) { @@ -58,6 +64,32 @@ profileController.get = async function (req, res, next) { res.render('account/profile', userData); }; +profileController.getFederated = async function (req, res, next) { + const { userslug: uid } = req.params; + const actor = await getActor(uid); + if (!actor) { + return next(); + } + // console.log(actor); + const { preferredUsername, published, icon, image, name, summary, hostname } = actor; + + const payload = { + uid, + username: `${preferredUsername}@${hostname}`, + userslug: slugify(`${preferredUsername}@${hostname}`), + fullname: name, + joindate: new Date(published).getTime(), + picture: typeof icon === 'string' ? icon : icon.url, + uploadedpicture: typeof icon === 'string' ? icon : icon.url, + 'cover:url': typeof image === 'string' ? image : image.url, + 'cover:position': '50% 50%', + aboutme: summary, + aboutmeParsed: summary, + }; + + res.render('account/profile', payload); +}; + async function incrementProfileViews(req, userData) { if (req.uid >= 1) { req.session.uids_viewed = req.session.uids_viewed || {}; diff --git a/src/middleware/index.js b/src/middleware/index.js index f8ba614ab2..349178598d 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -172,7 +172,15 @@ async function expose(exposedField, method, field, req, res, next) { if (!req.params.hasOwnProperty(field)) { return next(); } - const value = await method(String(req.params[field]).toLowerCase()); + const param = String(req.params[field]).toLowerCase(); + + // potential hostname — ActivityPub + if (param.indexOf('@') !== -1) { + res.locals[exposedField] = -2; + return next(); + } + + const value = await method(param); if (!value) { next('route'); return; From 0cbbce8c16a00cce4c65ee8f5649ac03d9c457c5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 2 Jun 2023 14:22:43 -0400 Subject: [PATCH 0054/4744] chore: update AP helpers export, 404 logic reversal, no slugify in userslug in mock profile from remote instance --- src/activitypub/index.js | 5 +++-- src/controllers/accounts/profile.js | 2 +- src/middleware/assert.js | 10 +++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 7bac91ff48..b9b0a60279 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -6,12 +6,13 @@ const winston = require('winston'); const request = require('request-promise-native'); const db = require('../database'); -const helpers = require('./helpers'); const ActivityPub = module.exports; +ActivityPub.helpers = require('./helpers'); + ActivityPub.getActor = async (id) => { - const { hostname, actorUri: uri } = await helpers.query(id); + const { hostname, actorUri: uri } = await ActivityPub.helpers.query(id); if (!uri) { return false; } diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index 909066da18..d97261ee1e 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -76,7 +76,7 @@ profileController.getFederated = async function (req, res, next) { const payload = { uid, username: `${preferredUsername}@${hostname}`, - userslug: slugify(`${preferredUsername}@${hostname}`), + userslug: `${preferredUsername}@${hostname}`, fullname: name, joindate: new Date(published).getTime(), picture: typeof icon === 'string' ? icon : icon.url, diff --git a/src/middleware/assert.js b/src/middleware/assert.js index 6c0f5ef72f..ccbc007275 100644 --- a/src/middleware/assert.js +++ b/src/middleware/assert.js @@ -17,6 +17,7 @@ const posts = require('../posts'); const messaging = require('../messaging'); const flags = require('../flags'); const slugify = require('../slugify'); +const activitypub = require('../activitypub'); const helpers = require('./helpers'); const controllerHelpers = require('../controllers/helpers'); @@ -24,11 +25,14 @@ const controllerHelpers = require('../controllers/helpers'); const Assert = module.exports; Assert.user = helpers.try(async (req, res, next) => { - if (!await user.exists(req.params.uid)) { - return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-user]]')); + if ( + (isFinite(req.params.uid) && await user.exists(req.params.uid)) || + (req.params.uid.indexOf('@') !== -1 && await activitypub.helpers.query(req.params.uid)) + ) { + return next(); } - next(); + controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-user]]')); }); Assert.group = helpers.try(async (req, res, next) => { From ab3ff320b565958c4b4aa32144bdaafa58cdd184 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 16 Jun 2023 10:57:34 -0400 Subject: [PATCH 0055/4744] refactor: acp tpl + config option - Updated ACP template to match new format - changed global switch to `activitypubEnabled` (lowercase p) --- src/middleware/index.js | 2 +- src/views/admin/settings/activitypub.tpl | 30 ++++++++++++------------ test/activitypub.js | 8 +++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/middleware/index.js b/src/middleware/index.js index 349178598d..2d09dfcdf1 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -310,7 +310,7 @@ middleware.proceedOnActivityPub = (req, res, next) => { // For whatever reason, express accepts does not recognize "profile" as a valid differentiator // Therefore, manual header parsing is used here. const { accept } = req.headers; - if (!accept || !meta.config.activityPubEnabled) { + if (!accept || !meta.config.activitypubEnabled) { return next('route'); } diff --git a/src/views/admin/settings/activitypub.tpl b/src/views/admin/settings/activitypub.tpl index a32920619a..8002a4c5d5 100644 --- a/src/views/admin/settings/activitypub.tpl +++ b/src/views/admin/settings/activitypub.tpl @@ -1,20 +1,20 @@ - +
+ -

[[admin/settings/activitypub:intro-lead]]

-

[[admin/settings/activitypub:intro-body]]

+

[[admin/settings/activitypub:intro-lead]]

+

[[admin/settings/activitypub:intro-body]]

-
+
-
-
[[admin/settings/activitypub:general]]
-
-
-
- - -
-
+
+
[[admin/settings/activitypub:general]]
+
+
+
+ + +
+
+
- - diff --git a/test/activitypub.js b/test/activitypub.js index 5193fdfbee..ce1c51ab98 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -14,11 +14,11 @@ const privileges = require('../src/privileges'); describe('ActivityPub integration', () => { before(() => { - meta.config.activityPubEnabled = 1; + meta.config.activitypubEnabled = 1; }); after(() => { - delete meta.config.activityPubEnabled; + delete meta.config.activitypubEnabled; }); describe('WebFinger endpoint', () => { @@ -108,7 +108,7 @@ describe('ActivityPub integration', () => { }); it('should return regular user profile html if federation is disabled', async () => { - delete meta.config.activityPubEnabled; + delete meta.config.activitypubEnabled; const response = await request(`${nconf.get('url')}/user/${slug}`, { method: 'get', @@ -124,7 +124,7 @@ describe('ActivityPub integration', () => { assert.strictEqual(response.statusCode, 200); assert(response.body.startsWith('')); - meta.config.activityPubEnabled = 1; + meta.config.activitypubEnabled = 1; }); it('should return regular user profile html if Accept header is not ActivityPub-related', async () => { From 57895b72466522f70822873096afef3669d68d2c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 16 Jun 2023 11:26:12 -0400 Subject: [PATCH 0056/4744] feat: add .has() call to cache/ttl --- src/cache/ttl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cache/ttl.js b/src/cache/ttl.js index 292c76fdc7..5e1bc2d5cd 100644 --- a/src/cache/ttl.js +++ b/src/cache/ttl.js @@ -30,7 +30,7 @@ module.exports = function (opts) { }); }); - cache.has = (key) => { + cache.has = function (key) { if (!cache.enabled) { return false; } From 4f5f025d5776ce74b78a5a9f5a4cb64d7b7f35b6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 16 Jun 2023 11:26:25 -0400 Subject: [PATCH 0057/4744] feat: add webfinger ttl cache --- src/activitypub/helpers.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 7c77462839..d001cfb2ed 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -2,6 +2,10 @@ const request = require('request-promise-native'); +const ttl = require('../cache/ttl'); + +const webfingerCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours + const Helpers = module.exports; Helpers.query = async (id) => { @@ -10,6 +14,10 @@ Helpers.query = async (id) => { return false; } + if (webfingerCache.has(id)) { + return webfingerCache.get(id); + } + // Make a webfinger query to retrieve routing information const response = await request(`https://${hostname}/.well-known/webfinger?resource=acct:${id}`, { simple: false, @@ -28,5 +36,6 @@ Helpers.query = async (id) => { ({ href: actorUri } = actorUri); } + webfingerCache.set(id, { username, hostname, actorUri }); return { username, hostname, actorUri }; }; From e7184eb8cc518bc716244840ccb1f129e0cabeb7 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 19 Jun 2023 17:29:22 -0400 Subject: [PATCH 0058/4744] feat: http signatures support, .sign() and .verify() AP helper methods --- src/activitypub/helpers.js | 30 +++++- src/activitypub/index.js | 149 ++++++++++++++++++++++++---- src/controllers/accounts/profile.js | 3 +- src/controllers/activitypub.js | 9 +- src/middleware/index.js | 11 ++ src/routes/activitypub.js | 2 +- 6 files changed, 174 insertions(+), 30 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index d001cfb2ed..2be0f4fa5d 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -1,7 +1,10 @@ 'use strict'; const request = require('request-promise-native'); +const { generateKeyPairSync, sign } = require('crypto'); +const winston = require('winston'); +const db = require('../database'); const ttl = require('../cache/ttl'); const webfingerCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours @@ -36,6 +39,29 @@ Helpers.query = async (id) => { ({ href: actorUri } = actorUri); } - webfingerCache.set(id, { username, hostname, actorUri }); - return { username, hostname, actorUri }; + const { publicKey } = response.body; + + webfingerCache.set(id, { username, hostname, actorUri, publicKey }); + return { username, hostname, actorUri, publicKey }; +}; + +Helpers.generateKeys = async (uid) => { + winston.verbose(`[activitypub] Generating RSA key-pair for uid ${uid}`); + const { + publicKey, + privateKey, + } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + }, + }); + + await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey }); + return { publicKey, privateKey }; }; diff --git a/src/activitypub/index.js b/src/activitypub/index.js index b9b0a60279..20d7b38967 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -1,11 +1,12 @@ 'use strict'; -const { generateKeyPairSync } = require('crypto'); - -const winston = require('winston'); const request = require('request-promise-native'); +const url = require('url'); +const nconf = require('nconf'); +const { createHash, createSign, createVerify } = require('crypto'); const db = require('../database'); +const user = require('../user'); const ActivityPub = module.exports; @@ -36,29 +37,135 @@ ActivityPub.getPublicKey = async (uid) => { try { ({ publicKey } = await db.getObject(`uid:${uid}:keys`)); } catch (e) { - ({ publicKey } = await generateKeys(uid)); + ({ publicKey } = await ActivityPub.helpers.generateKeys(uid)); } return publicKey; }; -async function generateKeys(uid) { - winston.verbose(`[activitypub] Generating RSA key-pair for uid ${uid}`); - const { - publicKey, - privateKey, - } = generateKeyPairSync('rsa', { - modulusLength: 2048, - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', +ActivityPub.getPrivateKey = async (uid) => { + let privateKey; + + try { + ({ privateKey } = await db.getObject(`uid:${uid}:keys`)); + } catch (e) { + ({ privateKey } = await ActivityPub.helpers.generateKeys(uid)); + } + + return privateKey; +}; + +ActivityPub.fetchPublicKey = async (uri) => { + // Used for retrieving the public key from the passed-in keyId uri + const { publicKey } = await request({ + uri, + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', }, + json: true, }); - await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey }); - return { publicKey, privateKey }; -} + return publicKey; +}; + +ActivityPub.sign = async (uid, url, payload) => { + // Returns string for use in 'Signature' header + const { host, pathname } = new URL(url); + const date = new Date().toUTCString(); + const key = await ActivityPub.getPrivateKey(uid); + const userslug = await user.getUserField(uid, 'userslug'); + const keyId = `${nconf.get('url')}/user/${userslug}#key`; + let digest = null; + + let headers = '(request-target) host date'; + let signed_string = `(request-target): ${payload ? 'post' : 'get'} ${pathname}\nhost: ${host}\ndate: ${date}`; + + // Calculate payload hash if payload present + if (payload) { + const payloadHash = createHash('sha256'); + payloadHash.update(JSON.stringify(payload)); + digest = payloadHash.digest('hex'); + headers += ' digest'; + signed_string += `\ndigest: ${digest}`; + } + + // Sign string using private key + const signatureHash = createHash('sha256'); + signatureHash.update(signed_string); + const signatureDigest = signatureHash.digest('hex'); + let signature = createSign('sha256'); + signature.update(signatureDigest); + signature.end(); + signature = signature.sign(key, 'hex'); + signature = btoa(signature); + + // Construct signature header + return { + date, + digest, + signature: `keyId="${keyId}",headers="${headers}",signature="${signature}"`, + }; +}; + +ActivityPub.verify = async (req) => { + // Break the signature apart + const { keyId, headers, signature } = req.headers.signature.split(',').reduce((memo, cur) => { + const split = cur.split('="'); + const key = split.shift(); + const value = split.join('="'); + memo[key] = value.slice(0, -1); + return memo; + }, {}); + + // Retrieve public key from remote instance + const { publicKeyPem } = await ActivityPub.fetchPublicKey(keyId); + + // Re-construct signature string + const signed_string = headers.split(' ').reduce((memo, cur) => { + if (cur === '(request-target)') { + memo.push(`${cur}: ${String(req.method).toLowerCase()} ${req.path}`); + } else if (req.headers.hasOwnProperty(cur)) { + memo.push(`${cur}: ${req.headers[cur]}`); + } + + return memo; + }, []).join('\n'); + + // Verify the signature string via public key + try { + const signatureHash = createHash('sha256'); + signatureHash.update(signed_string); + const signatureDigest = signatureHash.digest('hex'); + const verify = createVerify('sha256'); + verify.update(signatureDigest); + verify.end(); + const verified = verify.verify(publicKeyPem, atob(signature), 'hex'); + return verified; + } catch (e) { + return false; + } +}; + +/** + * This is just some code to test signing and verification. This should really be in the test suite. + */ +// setTimeout(async () => { +// const payload = { +// foo: 'bar', +// }; +// const signature = await ActivityPub.sign(1, 'http://127.0.0.1:4567/user/julian/inbox', payload); + +// const res = await request({ +// uri: 'http://127.0.0.1:4567/user/julian/inbox', +// method: 'post', +// headers: { +// Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', +// ...signature, +// }, +// json: true, +// body: payload, +// simple: false, +// }); + +// console.log(res); +// }, 1000); diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index d97261ee1e..7932ad78fa 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -70,9 +70,8 @@ profileController.getFederated = async function (req, res, next) { if (!actor) { return next(); } - // console.log(actor); - const { preferredUsername, published, icon, image, name, summary, hostname } = actor; + const { preferredUsername, published, icon, image, name, summary, hostname } = actor; const payload = { uid, username: `${preferredUsername}@${hostname}`, diff --git a/src/controllers/activitypub.js b/src/controllers/activitypub.js index 9e4527c7ac..460d227208 100644 --- a/src/controllers/activitypub.js +++ b/src/controllers/activitypub.js @@ -33,8 +33,8 @@ Controller.getActor = async (req, res) => { image: cover ? `${nconf.get('url')}${cover}` : null, publicKey: { - id: `${nconf.get('url')}/user/${userslug}`, - owner: `${nconf.get('url')}/user/${userslug}#key`, + id: `${nconf.get('url')}/user/${userslug}#key`, + owner: `${nconf.get('url')}/user/${userslug}`, publicKeyPem: publicKey, }, }); @@ -97,6 +97,7 @@ Controller.getInbox = async (req, res) => { }; Controller.postInbox = async (req, res) => { - // stub — other activity-pub services will push stuff here. - res.sendStatus(405); + console.log(req.body); + + res.sendStatus(201); }; diff --git a/src/middleware/index.js b/src/middleware/index.js index 2d09dfcdf1..1c733c970d 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -18,6 +18,7 @@ const privileges = require('../privileges'); const cacheCreate = require('../cache/lru'); const helpers = require('./helpers'); const api = require('../api'); +const activitypub = require('../activitypub'); const controllers = { api: require('../controllers/api'), @@ -329,3 +330,13 @@ middleware.proceedOnActivityPub = (req, res, next) => { next(); }; + +middleware.validateActivity = helpers.try(async (req, res, next) => { + // Checks the validity of the incoming payload against the sender and rejects on failure + const verified = await activitypub.verify(req); + if (!verified) { + return res.sendStatus(400); + } + + next(); +}); diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 02fe9a2bf0..121d085d9b 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -12,5 +12,5 @@ module.exports = function (app, middleware, controllers) { app.post('/user/:userslug/outbox', middlewares, controllers.activitypub.postOutbox); app.get('/user/:userslug/inbox', middlewares, controllers.activitypub.getInbox); - app.post('/user/:userslug/inbox', middlewares, controllers.activitypub.postInbox); + app.post('/user/:userslug/inbox', [...middlewares, middleware.validateActivity], controllers.activitypub.postInbox); }; From a10df9873bca58d5de50f0961ab11654ecb2e3c8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 21 Jun 2023 15:45:29 -0400 Subject: [PATCH 0059/4744] test: added passing test cases for .sign() and .verify() --- test/activitypub.js | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/test/activitypub.js b/test/activitypub.js index ce1c51ab98..075fca975f 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -1,6 +1,7 @@ 'use strict'; const assert = require('assert'); +const { createHash } = require('crypto'); const nconf = require('nconf'); const request = require('request-promise-native'); @@ -11,6 +12,7 @@ const utils = require('../src/utils'); const meta = require('../src/meta'); const user = require('../src/user'); const privileges = require('../src/privileges'); +const activitypub = require('../src/activitypub'); describe('ActivityPub integration', () => { before(() => { @@ -213,4 +215,102 @@ describe('ActivityPub integration', () => { assert(['id', 'owner', 'publicKeyPem'].every(prop => response.body.publicKey.hasOwnProperty(prop))); }); }); + + describe.only('http signature signing and verification', () => { + describe('.sign()', () => { + let uid; + let username; + + before(async () => { + username = utils.generateUUID().slice(0, 10); + uid = await user.create({ username }); + }); + + it('should create a key-pair for a user if the user does not have one already', async () => { + const endpoint = `${nconf.get('url')}/user/${username}/inbox`; + await activitypub.sign(uid, endpoint); + const { publicKey, privateKey } = await db.getObject(`uid:${uid}:keys`); + + assert(publicKey); + assert(privateKey); + }); + + it('should return an object with date, a null digest, and signature, if no payload is passed in', async () => { + const endpoint = `${nconf.get('url')}/user/${username}/inbox`; + const { date, digest, signature } = await activitypub.sign(uid, endpoint); + const dateObj = new Date(date); + + assert(signature); + assert(dateObj); + assert.strictEqual(digest, null); + }); + + it('should also return a digest hash if payload is passed in', async () => { + const endpoint = `${nconf.get('url')}/user/${username}/inbox`; + const payload = { foo: 'bar' }; + const { digest } = await activitypub.sign(uid, endpoint, payload); + const hash = createHash('sha256'); + hash.update(JSON.stringify(payload)); + const checksum = hash.digest('hex'); + + assert(digest); + assert.strictEqual(digest, checksum); + }); + }); + + describe.only('.verify()', () => { + let uid; + let username; + const mockReqBase = { + method: 'GET', + // path: ... + headers: { + // host: ... + // date: ... + // signature: ... + // digest: ... + }, + }; + + before(async () => { + username = utils.generateUUID().slice(0, 10); + uid = await user.create({ username }); + }); + + it('should return true when the proper signature and relevant headers are passed in', async () => { + const endpoint = `${nconf.get('url')}/user/${username}/inbox`; + const path = `/user/${username}/inbox`; + const signature = await activitypub.sign(uid, endpoint); + const { host } = nconf.get('url_parsed'); + const req = { + ...mockReqBase, + ...{ + path, + headers: { ...signature, host }, + }, + }; + + const verified = await activitypub.verify(req); + assert.strictEqual(verified, true); + }); + + it('should return true when a digest is also passed in', async () => { + const endpoint = `${nconf.get('url')}/user/${username}/inbox`; + const path = `/user/${username}/inbox`; + const signature = await activitypub.sign(uid, endpoint, { foo: 'bar' }); + const { host } = nconf.get('url_parsed'); + const req = { + ...mockReqBase, + ...{ + method: 'POST', + path, + headers: { ...signature, host }, + }, + }; + + const verified = await activitypub.verify(req); + assert.strictEqual(verified, true); + }); + }); + }); }); From 2e8990088699ae248540f280fa8968793a5bd75e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 21 Jun 2023 17:16:37 -0400 Subject: [PATCH 0060/4744] chore: reorganize controllers for clarity --- src/controllers/accounts/profile.js | 31 ++----------------- .../{activitypub.js => activitypub/index.js} | 4 ++- src/controllers/activitypub/profiles.js | 30 ++++++++++++++++++ 3 files changed, 36 insertions(+), 29 deletions(-) rename src/controllers/{activitypub.js => activitypub/index.js} (96%) create mode 100644 src/controllers/activitypub/profiles.js diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index 7932ad78fa..ecad0d9b9c 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -10,16 +10,16 @@ const categories = require('../../categories'); const plugins = require('../../plugins'); const privileges = require('../../privileges'); const accountHelpers = require('./helpers'); -const { getActor } = require('../../activitypub'); const helpers = require('../helpers'); -const slugify = require('../../slugify'); const utils = require('../../utils'); +const activitypubController = require('../activitypub'); + const profileController = module.exports; profileController.get = async function (req, res, next) { if (res.locals.uid === -2) { - return profileController.getFederated(req, res, next); + return activitypubController.profiles.get(req, res, next); } const lowercaseSlug = req.params.userslug.toLowerCase(); @@ -64,31 +64,6 @@ profileController.get = async function (req, res, next) { res.render('account/profile', userData); }; -profileController.getFederated = async function (req, res, next) { - const { userslug: uid } = req.params; - const actor = await getActor(uid); - if (!actor) { - return next(); - } - - const { preferredUsername, published, icon, image, name, summary, hostname } = actor; - const payload = { - uid, - username: `${preferredUsername}@${hostname}`, - userslug: `${preferredUsername}@${hostname}`, - fullname: name, - joindate: new Date(published).getTime(), - picture: typeof icon === 'string' ? icon : icon.url, - uploadedpicture: typeof icon === 'string' ? icon : icon.url, - 'cover:url': typeof image === 'string' ? image : image.url, - 'cover:position': '50% 50%', - aboutme: summary, - aboutmeParsed: summary, - }; - - res.render('account/profile', payload); -}; - async function incrementProfileViews(req, userData) { if (req.uid >= 1) { req.session.uids_viewed = req.session.uids_viewed || {}; diff --git a/src/controllers/activitypub.js b/src/controllers/activitypub/index.js similarity index 96% rename from src/controllers/activitypub.js rename to src/controllers/activitypub/index.js index 460d227208..21e4264499 100644 --- a/src/controllers/activitypub.js +++ b/src/controllers/activitypub/index.js @@ -3,10 +3,12 @@ const nconf = require('nconf'); const user = require('../user'); -const activitypub = require('../activitypub'); +const activitypub = require('../../activitypub'); const Controller = module.exports; +Controller.profiles = require('./profiles'); + Controller.getActor = async (req, res) => { // todo: view:users priv gate const { userslug } = req.params; diff --git a/src/controllers/activitypub/profiles.js b/src/controllers/activitypub/profiles.js new file mode 100644 index 0000000000..5f0e484645 --- /dev/null +++ b/src/controllers/activitypub/profiles.js @@ -0,0 +1,30 @@ +'use strict'; + +const { getActor } = require('../../activitypub'); + +const controller = module.exports; + +controller.get = async function (req, res, next) { + const { userslug: uid } = req.params; + const actor = await getActor(uid); + if (!actor) { + return next(); + } + + const { preferredUsername, published, icon, image, name, summary, hostname } = actor; + const payload = { + uid, + username: `${preferredUsername}@${hostname}`, + userslug: `${preferredUsername}@${hostname}`, + fullname: name, + joindate: new Date(published).getTime(), + picture: typeof icon === 'string' ? icon : icon.url, + uploadedpicture: typeof icon === 'string' ? icon : icon.url, + 'cover:url': typeof image === 'string' ? image : image.url, + 'cover:position': '50% 50%', + aboutme: summary, + aboutmeParsed: summary, + }; + + res.render('account/profile', payload); +}; From cdc4275fec0ba4939c3967ccbd213aa525b87ed9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 23 Jun 2023 14:59:47 -0400 Subject: [PATCH 0061/4744] feat: actor cache, method to resolve inboxes, stub code for sending requests. Now base64 encoding digest as expected by Mastodon --- src/activitypub/helpers.js | 2 +- src/activitypub/index.js | 73 +++++++++++++++++++--------- src/controllers/activitypub/index.js | 23 ++++++++- src/controllers/write/users.js | 10 ++++ test/activitypub.js | 6 +-- 5 files changed, 87 insertions(+), 27 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 2be0f4fa5d..74ce768a74 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -1,7 +1,7 @@ 'use strict'; const request = require('request-promise-native'); -const { generateKeyPairSync, sign } = require('crypto'); +const { generateKeyPairSync } = require('crypto'); const winston = require('winston'); const db = require('../database'); diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 20d7b38967..878bfabc86 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -7,12 +7,18 @@ const { createHash, createSign, createVerify } = require('crypto'); const db = require('../database'); const user = require('../user'); +const ttl = require('../cache/ttl'); +const actorCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours const ActivityPub = module.exports; ActivityPub.helpers = require('./helpers'); ActivityPub.getActor = async (id) => { + if (actorCache.has(id)) { + return actorCache.get(id); + } + const { hostname, actorUri: uri } = await ActivityPub.helpers.query(id); if (!uri) { return false; @@ -28,9 +34,15 @@ ActivityPub.getActor = async (id) => { actor.hostname = hostname; + actorCache.set(id, actor); return actor; }; +ActivityPub.resolveInboxes = async ids => await Promise.all(ids.map(async (id) => { + const actor = await ActivityPub.getActor(id); + return actor.inbox; +})); + ActivityPub.getPublicKey = async (uid) => { let publicKey; @@ -84,7 +96,7 @@ ActivityPub.sign = async (uid, url, payload) => { if (payload) { const payloadHash = createHash('sha256'); payloadHash.update(JSON.stringify(payload)); - digest = payloadHash.digest('hex'); + digest = `sha-256=${payloadHash.digest('base64')}`; headers += ' digest'; signed_string += `\ndigest: ${digest}`; } @@ -146,26 +158,43 @@ ActivityPub.verify = async (req) => { } }; -/** - * This is just some code to test signing and verification. This should really be in the test suite. - */ -// setTimeout(async () => { -// const payload = { -// foo: 'bar', -// }; -// const signature = await ActivityPub.sign(1, 'http://127.0.0.1:4567/user/julian/inbox', payload); +ActivityPub.send = async (uid, targets, payload) => { + if (!Array.isArray(targets)) { + targets = [targets]; + } -// const res = await request({ -// uri: 'http://127.0.0.1:4567/user/julian/inbox', -// method: 'post', -// headers: { -// Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', -// ...signature, -// }, -// json: true, -// body: payload, -// simple: false, -// }); + const userslug = await user.getUserField(uid, 'userslug'); + const inboxes = await ActivityPub.resolveInboxes(targets); -// console.log(res); -// }, 1000); + payload = { + ...{ + '@context': 'https://www.w3.org/ns/activitystreams', + actor: { + type: 'Person', + name: `${userslug}@${nconf.get('url_parsed').host}`, + }, + }, + ...payload, + }; + + await Promise.all(inboxes.map(async (uri) => { + const { date, digest, signature } = await ActivityPub.sign(uid, uri, payload); + + const response = await request(uri, { + method: payload ? 'post' : 'get', + headers: { + date, + digest, + signature, + 'content-type': 'application/ld+json; profile="http://www.w3.org/ns/activitystreams', + accept: 'application/ld+json; profile="http://www.w3.org/ns/activitystreams', + }, + json: true, + body: payload, + simple: false, + resolveWithFullResponse: true, + }); + + console.log(response.statusCode, response.body); + })); +}; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 21e4264499..a90d5a5a1b 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -2,7 +2,7 @@ const nconf = require('nconf'); -const user = require('../user'); +const user = require('../../user'); const activitypub = require('../../activitypub'); const Controller = module.exports; @@ -103,3 +103,24 @@ Controller.postInbox = async (req, res) => { res.sendStatus(201); }; + +/** + * Main ActivityPub verbs + */ + +Controller.follow = async (req, res) => { + await activitypub.send(req.uid, req.params.uid, { + type: 'Follow', + object: { + type: 'Person', + name: req.params.uid, + }, + }); + + res.sendStatus(201); +}; + +Controller.unfollow = async (req, res) => { + console.log('got here'); + res.sendStatus(201); +}; diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index 715be0f48c..fe63ab9019 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -9,6 +9,8 @@ const user = require('../../user'); const helpers = require('../helpers'); +const activitypubController = require('../activitypub'); + const Users = module.exports; Users.redirectBySlug = async (req, res) => { @@ -92,11 +94,19 @@ Users.changePassword = async (req, res) => { }; Users.follow = async (req, res) => { + if (req.params.uid.indexOf('@') !== -1) { + return await activitypubController.follow(req, res); + } + await api.users.follow(req, req.params); helpers.formatApiResponse(200, res); }; Users.unfollow = async (req, res) => { + if (req.params.uid.indexOf('@') !== -1) { + return await activitypubController.unfollow(req, res); + } + await api.users.unfollow(req, req.params); helpers.formatApiResponse(200, res); }; diff --git a/test/activitypub.js b/test/activitypub.js index 075fca975f..ebca3a1d62 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -251,14 +251,14 @@ describe('ActivityPub integration', () => { const { digest } = await activitypub.sign(uid, endpoint, payload); const hash = createHash('sha256'); hash.update(JSON.stringify(payload)); - const checksum = hash.digest('hex'); + const checksum = hash.digest('base64'); assert(digest); - assert.strictEqual(digest, checksum); + assert.strictEqual(digest, `sha-256=${checksum}`); }); }); - describe.only('.verify()', () => { + describe('.verify()', () => { let uid; let username; const mockReqBase = { From 9f94653b3fc7bd814f8d3bb2e9b5ba03de9e7c2a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 23 Jun 2023 15:04:37 -0400 Subject: [PATCH 0062/4744] style: remove unused variable --- src/activitypub/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 878bfabc86..6edb7ae19e 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -1,7 +1,6 @@ 'use strict'; const request = require('request-promise-native'); -const url = require('url'); const nconf = require('nconf'); const { createHash, createSign, createVerify } = require('crypto'); From 5d95765ee7bf13ca563be39e3462bca8b146d4f4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 23 Jun 2023 15:25:00 -0400 Subject: [PATCH 0063/4744] fix: bugs, more prep to start making calls to self --- src/activitypub/index.js | 4 ++-- src/controllers/activitypub/profiles.js | 3 +-- src/controllers/well-known.js | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 6edb7ae19e..4994e344e9 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -185,8 +185,8 @@ ActivityPub.send = async (uid, targets, payload) => { date, digest, signature, - 'content-type': 'application/ld+json; profile="http://www.w3.org/ns/activitystreams', - accept: 'application/ld+json; profile="http://www.w3.org/ns/activitystreams', + 'content-type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', }, json: true, body: payload, diff --git a/src/controllers/activitypub/profiles.js b/src/controllers/activitypub/profiles.js index 5f0e484645..bc466048b6 100644 --- a/src/controllers/activitypub/profiles.js +++ b/src/controllers/activitypub/profiles.js @@ -10,7 +10,6 @@ controller.get = async function (req, res, next) { if (!actor) { return next(); } - const { preferredUsername, published, icon, image, name, summary, hostname } = actor; const payload = { uid, @@ -20,7 +19,7 @@ controller.get = async function (req, res, next) { joindate: new Date(published).getTime(), picture: typeof icon === 'string' ? icon : icon.url, uploadedpicture: typeof icon === 'string' ? icon : icon.url, - 'cover:url': typeof image === 'string' ? image : image.url, + 'cover:url': !image || typeof image === 'string' ? image : image.url, 'cover:position': '50% 50%', aboutme: summary, aboutmeParsed: summary, diff --git a/src/controllers/well-known.js b/src/controllers/well-known.js index 86caae4173..590f546b04 100644 --- a/src/controllers/well-known.js +++ b/src/controllers/well-known.js @@ -9,9 +9,9 @@ const Controller = module.exports; Controller.webfinger = async (req, res) => { const { resource } = req.query; - const { hostname } = nconf.get('url_parsed'); + const { host } = nconf.get('url_parsed'); - if (!resource || !resource.startsWith('acct:') || !resource.endsWith(hostname)) { + if (!resource || !resource.startsWith('acct:') || !resource.endsWith(host)) { return res.sendStatus(400); } @@ -21,7 +21,7 @@ Controller.webfinger = async (req, res) => { } // Get the slug - const slug = resource.slice(5, resource.length - (hostname.length + 1)); + const slug = resource.slice(5, resource.length - (host.length + 1)); const uid = await user.getUidByUserslug(slug); if (!uid) { @@ -29,7 +29,7 @@ Controller.webfinger = async (req, res) => { } const response = { - subject: `acct:${slug}@${hostname}`, + subject: `acct:${slug}@${host}`, aliases: [ `${nconf.get('url')}/uid/${uid}`, `${nconf.get('url')}/user/${slug}`, From 9dfa1b7209ebc981ca3c21e8a230cec23e968c23 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 26 Jun 2023 15:09:47 -0400 Subject: [PATCH 0064/4744] test: fix webfinger test --- test/activitypub.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/activitypub.js b/test/activitypub.js index ebca3a1d62..d56a57395b 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -26,7 +26,7 @@ describe('ActivityPub integration', () => { describe('WebFinger endpoint', () => { let uid; let slug; - const { hostname } = nconf.get('url_parsed'); + const { host } = nconf.get('url_parsed'); beforeEach(async () => { slug = slugify(utils.generateUUID().slice(0, 8)); @@ -34,7 +34,7 @@ describe('ActivityPub integration', () => { }); it('should return a 404 Not Found if no user exists by that username', async () => { - const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar@${hostname}`, { + const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar@${host}`, { method: 'get', json: true, followRedirect: true, @@ -61,7 +61,7 @@ describe('ActivityPub integration', () => { it('should return 403 Forbidden if the calling user is not allowed to view the user list/profiles', async () => { await privileges.global.rescind(['groups:view:users'], 'guests'); - const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${hostname}`, { + const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${host}`, { method: 'get', json: true, followRedirect: true, @@ -75,7 +75,7 @@ describe('ActivityPub integration', () => { }); it('should return a valid WebFinger response otherwise', async () => { - const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${hostname}`, { + const response = await request(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${host}`, { method: 'get', json: true, followRedirect: true, @@ -91,7 +91,7 @@ describe('ActivityPub integration', () => { assert(response.body[prop]); }); - assert.strictEqual(response.body.subject, `acct:${slug}@${hostname}`); + assert.strictEqual(response.body.subject, `acct:${slug}@${host}`); assert(Array.isArray(response.body.aliases)); assert([`${nconf.get('url')}/uid/${uid}`, `${nconf.get('url')}/user/${slug}`].every(url => response.body.aliases.includes(url))); @@ -216,7 +216,7 @@ describe('ActivityPub integration', () => { }); }); - describe.only('http signature signing and verification', () => { + describe('http signature signing and verification', () => { describe('.sign()', () => { let uid; let username; From e6753ce5dbf9132954158b3c0d3d4788033d06b8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 26 Jun 2023 16:15:25 -0400 Subject: [PATCH 0065/4744] fix: missing req.body when parsing ActivityPub requests --- src/activitypub/index.js | 5 ++++- src/controllers/activitypub/index.js | 2 +- src/webserver.js | 8 +++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 4994e344e9..3bb2177a3c 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -194,6 +194,9 @@ ActivityPub.send = async (uid, targets, payload) => { resolveWithFullResponse: true, }); - console.log(response.statusCode, response.body); + if (response.statusCode !== 201) { + // todo: i18n this + throw new Error('activity-failed'); + } })); }; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index a90d5a5a1b..f7e7dc460a 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -99,7 +99,7 @@ Controller.getInbox = async (req, res) => { }; Controller.postInbox = async (req, res) => { - console.log(req.body); + console.log('received', req.body); res.sendStatus(201); }; diff --git a/src/webserver.js b/src/webserver.js index ff5031ff41..1b6d62d752 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -229,7 +229,13 @@ function configureBodyParser(app) { } app.use(bodyParser.urlencoded(urlencodedOpts)); - const jsonOpts = nconf.get('bodyParser:json') || {}; + const jsonOpts = nconf.get('bodyParser:json') || { + type: [ + 'application/json', + 'application/ld+json', + 'application/activity+json', + ], + }; app.use(bodyParser.json(jsonOpts)); } From c02271c7afd7fa0699d405b9ead36419489f0d50 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 28 Jun 2023 14:59:39 -0400 Subject: [PATCH 0066/4744] feat: follow/unfollow logic and receipt --- src/activitypub/helpers.js | 12 +++++ src/activitypub/inbox.js | 61 +++++++++++++++++++++++ src/activitypub/index.js | 2 + src/activitypub/outbox.js | 13 +++++ src/controllers/activitypub/index.js | 66 ++++++++++++++++++++----- src/controllers/activitypub/profiles.js | 6 ++- src/user/follow.js | 8 +-- 7 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 src/activitypub/inbox.js create mode 100644 src/activitypub/outbox.js diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 74ce768a74..959c8a5419 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -3,9 +3,11 @@ const request = require('request-promise-native'); const { generateKeyPairSync } = require('crypto'); const winston = require('winston'); +const nconf = require('nconf'); const db = require('../database'); const ttl = require('../cache/ttl'); +const user = require('../user'); const webfingerCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours @@ -65,3 +67,13 @@ Helpers.generateKeys = async (uid) => { await db.setObject(`uid:${uid}:keys`, { publicKey, privateKey }); return { publicKey, privateKey }; }; + +Helpers.resolveLocalUid = async (id) => { + const [slug, host] = id.split('@'); + + if (id.indexOf('@') === -1 || host !== nconf.get('url_parsed').host) { + throw new Error('[[activitypub:invalid-id]]'); + } + + return await user.getUidByUserslug(slug); +}; diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js new file mode 100644 index 0000000000..6bbf25e748 --- /dev/null +++ b/src/activitypub/inbox.js @@ -0,0 +1,61 @@ +'use strict'; + +const db = require('../database'); +const user = require('../user'); + +const helpers = require('./helpers'); + +const inbox = module.exports; + +inbox.follow = async (actorId, objectId) => { + await handleFollow('follow', actorId, objectId); +}; + +inbox.unfollow = async (actorId, objectId) => { + await handleFollow('unfollow', actorId, objectId); +}; + +inbox.isFollowed = async (actorId, uid) => { + if (actorId.indexOf('@') === -1 || parseInt(uid, 10) <= 0) { + return false; + } + return await db.isSortedSetMember(`followersRemote:${uid}`, actorId); +}; + +async function handleFollow(type, actorId, objectId) { + // Sanity checks + const actorExists = await helpers.query(actorId); + if (!actorId || !actorExists) { + throw new Error('[[error:invalid-uid]]'); // should probably be AP specific + } + + if (!objectId) { + throw new Error('[[error:invalid-uid]]'); // should probably be AP specific + } + + const localUid = await helpers.resolveLocalUid(objectId); + if (!localUid) { + throw new Error('[[error:invalid-uid]]'); + } + + // matches toggleFollow() in src/user/follow.js + const isFollowed = await inbox.isFollowed(actorId, localUid); + if (type === 'follow') { + if (isFollowed) { + throw new Error('[[error:already-following]]'); + } + const now = Date.now(); + await db.sortedSetAdd(`followersRemote:${localUid}`, now, actorId); + } else { + if (!isFollowed) { + throw new Error('[[error:not-following]]'); + } + await db.sortedSetRemove(`followersRemote:${localUid}`, actorId); + } + + const [followerCount, followerRemoteCount] = await Promise.all([ + db.sortedSetCard(`followers:${localUid}`), + db.sortedSetCard(`followersRemote:${localUid}`), + ]); + await user.setUserField(localUid, 'followerCount', followerCount + followerRemoteCount); +} diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 3bb2177a3c..95f1e98024 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -12,6 +12,8 @@ const actorCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours const ActivityPub = module.exports; ActivityPub.helpers = require('./helpers'); +ActivityPub.inbox = require('./inbox'); +ActivityPub.outbox = require('./outbox'); ActivityPub.getActor = async (id) => { if (actorCache.has(id)) { diff --git a/src/activitypub/outbox.js b/src/activitypub/outbox.js new file mode 100644 index 0000000000..a21ef567fd --- /dev/null +++ b/src/activitypub/outbox.js @@ -0,0 +1,13 @@ +'use strict'; + +const db = require('../database'); + +const outbox = module.exports; + +outbox.isFollowing = async (uid, actorId) => { + if (parseInt(uid, 10) <= 0 || actorId.indexOf('@') === -1) { + return false; + } + return await db.isSortedSetMember(`followingRemote:${uid}`, actorId); +}; + diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index f7e7dc460a..1ecb86a697 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -2,8 +2,10 @@ const nconf = require('nconf'); +const db = require('../../database'); const user = require('../../user'); const activitypub = require('../../activitypub'); +const helpers = require('../helpers'); const Controller = module.exports; @@ -99,7 +101,17 @@ Controller.getInbox = async (req, res) => { }; Controller.postInbox = async (req, res) => { - console.log('received', req.body); + switch (req.body.type) { + case 'Follow': { + await activitypub.inbox.follow(req.body.actor.name, req.body.object.name); + break; + } + + case 'Unfollow': { + await activitypub.inbox.unfollow(req.body.actor.name, req.body.object.name); + break; + } + } res.sendStatus(201); }; @@ -109,18 +121,50 @@ Controller.postInbox = async (req, res) => { */ Controller.follow = async (req, res) => { - await activitypub.send(req.uid, req.params.uid, { - type: 'Follow', - object: { - type: 'Person', - name: req.params.uid, - }, - }); + try { + const { uid: objectId } = req.params; + await activitypub.send(req.uid, objectId, { + type: 'Follow', + object: { + type: 'Person', + name: objectId, + }, + }); - res.sendStatus(201); + const now = Date.now(); + await db.sortedSetAdd(`followingRemote:${req.uid}`, now, objectId); + await recountFollowing(req.uid); + + helpers.formatApiResponse(200, res); + } catch (e) { + helpers.formatApiResponse(400, res, e); + } }; Controller.unfollow = async (req, res) => { - console.log('got here'); - res.sendStatus(201); + try { + const { uid: objectId } = req.params; + await activitypub.send(req.uid, objectId, { + type: 'Unfollow', + object: { + type: 'Person', + name: objectId, + }, + }); + + await db.sortedSetRemove(`followingRemote:${req.uid}`, objectId); + await recountFollowing(req.uid); + + helpers.formatApiResponse(200, res); + } catch (e) { + helpers.formatApiResponse(400, res, e); + } }; + +async function recountFollowing(uid) { + const [followingCount, followingRemoteCount] = await Promise.all([ + db.sortedSetCard(`following:${uid}`), + db.sortedSetCard(`followingRemote:${uid}`), + ]); + await user.setUserField(uid, 'followingCount', followingCount + followingRemoteCount); +} diff --git a/src/controllers/activitypub/profiles.js b/src/controllers/activitypub/profiles.js index bc466048b6..3d05ce2c6f 100644 --- a/src/controllers/activitypub/profiles.js +++ b/src/controllers/activitypub/profiles.js @@ -1,6 +1,6 @@ 'use strict'; -const { getActor } = require('../../activitypub'); +const { getActor, outbox } = require('../../activitypub'); const controller = module.exports; @@ -11,6 +11,8 @@ controller.get = async function (req, res, next) { return next(); } const { preferredUsername, published, icon, image, name, summary, hostname } = actor; + const isFollowing = await outbox.isFollowing(req.uid, uid); + const payload = { uid, username: `${preferredUsername}@${hostname}`, @@ -23,6 +25,8 @@ controller.get = async function (req, res, next) { 'cover:position': '50% 50%', aboutme: summary, aboutmeParsed: summary, + + isFollowing, }; res.render('account/profile', payload); diff --git a/src/user/follow.js b/src/user/follow.js index f3b031a582..149e6d1151 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -49,13 +49,15 @@ module.exports = function (User) { ]); } - const [followingCount, followerCount] = await Promise.all([ + const [followingCount, followingRemoteCount, followerCount, followerRemoteCount] = await Promise.all([ db.sortedSetCard(`following:${uid}`), + db.sortedSetCard(`followingRemote:${uid}`), db.sortedSetCard(`followers:${theiruid}`), + db.sortedSetCard(`followersRemote:${theiruid}`), ]); await Promise.all([ - User.setUserField(uid, 'followingCount', followingCount), - User.setUserField(theiruid, 'followerCount', followerCount), + User.setUserField(uid, 'followingCount', followingCount + followingRemoteCount), + User.setUserField(theiruid, 'followerCount', followerCount + followerRemoteCount), ]); } From 4218ecc4a05589b9b8a876d231efa4cbf34a2a73 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 8 Aug 2023 12:05:08 -0400 Subject: [PATCH 0067/4744] fix: save remote follower count separately from local follower count --- src/activitypub/inbox.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 6bbf25e748..096bb481d6 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -53,9 +53,6 @@ async function handleFollow(type, actorId, objectId) { await db.sortedSetRemove(`followersRemote:${localUid}`, actorId); } - const [followerCount, followerRemoteCount] = await Promise.all([ - db.sortedSetCard(`followers:${localUid}`), - db.sortedSetCard(`followersRemote:${localUid}`), - ]); - await user.setUserField(localUid, 'followerCount', followerCount + followerRemoteCount); + const followerRemoteCount = await db.sortedSetCard(`followersRemote:${localUid}`); + await user.setUserField(localUid, 'followerRemoteCount', followerRemoteCount); } From bcee1c8dc8c06faf8278f65484e1c12e3738233a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 8 Aug 2023 14:30:15 -0400 Subject: [PATCH 0068/4744] fix: incorrect host/hostname usage in well-known test --- test/controllers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/controllers.js b/test/controllers.js index bd53d7c312..8a6ba3e204 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -2849,7 +2849,7 @@ describe('Controllers', () => { it('should deny access if view:users privilege is not enabled for guests', async () => { await privileges.global.rescind(['groups:view:users'], 'guests'); - const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${username}@${nconf.get('url_parsed').hostname}`, { + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${username}@${nconf.get('url_parsed').host}`, { json: true, simple: false, resolveWithFullResponse: true, @@ -2861,7 +2861,7 @@ describe('Controllers', () => { }); it('should respond appropriately if the user requested does not exist locally', async () => { - const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar@${nconf.get('url_parsed').hostname}`, { + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar@${nconf.get('url_parsed').host}`, { json: true, simple: false, resolveWithFullResponse: true, @@ -2871,7 +2871,7 @@ describe('Controllers', () => { }); it('should return a valid webfinger response if the user exists', async () => { - const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${username}@${nconf.get('url_parsed').hostname}`, { + const response = await requestAsync(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${username}@${nconf.get('url_parsed').host}`, { json: true, simple: false, resolveWithFullResponse: true, @@ -2879,7 +2879,7 @@ describe('Controllers', () => { assert.strictEqual(response.statusCode, 200); assert(['subject', 'aliases', 'links'].every(prop => response.body.hasOwnProperty(prop))); - assert(response.body.subject, `acct:${username}@${nconf.get('url_parsed').hostname}`); + assert(response.body.subject, `acct:${username}@${nconf.get('url_parsed').host}`); }); }); }); From cc0d18869a7485ba8b2bc80beeff323ff465c0ed Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 8 Aug 2023 15:33:35 -0400 Subject: [PATCH 0069/4744] test: fixed improper signed_string reconstruction in `.verify()` --- src/activitypub/index.js | 2 +- test/activitypub.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 95f1e98024..a986cb2335 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -136,7 +136,7 @@ ActivityPub.verify = async (req) => { // Re-construct signature string const signed_string = headers.split(' ').reduce((memo, cur) => { if (cur === '(request-target)') { - memo.push(`${cur}: ${String(req.method).toLowerCase()} ${req.path}`); + memo.push(`${cur}: ${String(req.method).toLowerCase()} ${req.baseUrl}${req.path}`); } else if (req.headers.hasOwnProperty(cur)) { memo.push(`${cur}: ${req.headers[cur]}`); } diff --git a/test/activitypub.js b/test/activitypub.js index d56a57395b..d59fe1cbc1 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -261,9 +261,11 @@ describe('ActivityPub integration', () => { describe('.verify()', () => { let uid; let username; + const baseUrl = nconf.get('relative_path'); const mockReqBase = { method: 'GET', // path: ... + baseUrl, headers: { // host: ... // date: ... From 99cc60c8d520bb92cbcb83280206ed9fe3099c72 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 6 Dec 2023 13:57:49 -0500 Subject: [PATCH 0070/4744] fix: add basic sanity-checking to middleware.validateActivity --- src/middleware/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/middleware/index.js b/src/middleware/index.js index 1c733c970d..27f07e895b 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -338,5 +338,11 @@ middleware.validateActivity = helpers.try(async (req, res, next) => { return res.sendStatus(400); } + // Sanity-check payload schema + const required = ['type']; + if (!required.every(prop => req.body.hasOwnProperty(prop))) { + return res.sendStatus(400); + } + next(); }); From 3c31ae239bbfb96c79e36fdc84d0b72dcfe40e6d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 7 Dec 2023 09:18:44 +0000 Subject: [PATCH 0071/4744] Latest translations and fallbacks --- public/language/hy/error.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/hy/error.json b/public/language/hy/error.json index 81d84ced2e..eafee7dc2e 100644 --- a/public/language/hy/error.json +++ b/public/language/hy/error.json @@ -185,7 +185,7 @@ "post-flagged-too-many-times": "Այս գրառումն արդեն նշվել է ուրիշների կողմից", "user-flagged-too-many-times": "Այս օգտատերն արդեն դրոշակվել է ուրիշների կողմից", "cant-flag-privileged": "Ձեզ չի թույլատրվում նշել արտոնյալ օգտատերերի պրոֆիլները կամ բովանդակությունը (մոդերատորներ/համաշխարհային մոդերատորներ/ադմիններ)", - "cant-locate-flag-report": "Cannot locate flag report", + "cant-locate-flag-report": "Հնարավոր չէ գտնել նշված հաշվետվությունը", "self-vote": "Դուք չեք կարող քվեարկել ձեր սեփական գրառման վրա", "too-many-upvotes-today": "Դուք կարող եք օրական միայն %1 անգամ կողմ քվեարկել", "too-many-upvotes-today-user": "Դուք կարող եք միայն օրական %1 անգամ կողմ քվեարկել օգտատիրոջը", From 5e693702a46ffbd4ac5858025ad520ce710c3c50 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 7 Dec 2023 12:36:30 -0500 Subject: [PATCH 0072/4744] chore: minor re-shuffling of code --- src/activitypub/index.js | 1 - src/activitypub/outbox.js | 13 ------------- src/controllers/activitypub/profiles.js | 5 +++-- src/routes/activitypub.js | 8 ++++---- 4 files changed, 7 insertions(+), 20 deletions(-) delete mode 100644 src/activitypub/outbox.js diff --git a/src/activitypub/index.js b/src/activitypub/index.js index a986cb2335..07b8da2d5a 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -13,7 +13,6 @@ const ActivityPub = module.exports; ActivityPub.helpers = require('./helpers'); ActivityPub.inbox = require('./inbox'); -ActivityPub.outbox = require('./outbox'); ActivityPub.getActor = async (id) => { if (actorCache.has(id)) { diff --git a/src/activitypub/outbox.js b/src/activitypub/outbox.js deleted file mode 100644 index a21ef567fd..0000000000 --- a/src/activitypub/outbox.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -const db = require('../database'); - -const outbox = module.exports; - -outbox.isFollowing = async (uid, actorId) => { - if (parseInt(uid, 10) <= 0 || actorId.indexOf('@') === -1) { - return false; - } - return await db.isSortedSetMember(`followingRemote:${uid}`, actorId); -}; - diff --git a/src/controllers/activitypub/profiles.js b/src/controllers/activitypub/profiles.js index 3d05ce2c6f..10ca171dc9 100644 --- a/src/controllers/activitypub/profiles.js +++ b/src/controllers/activitypub/profiles.js @@ -1,6 +1,7 @@ 'use strict'; -const { getActor, outbox } = require('../../activitypub'); +const db = require('../../database'); +const { getActor } = require('../../activitypub'); const controller = module.exports; @@ -11,7 +12,7 @@ controller.get = async function (req, res, next) { return next(); } const { preferredUsername, published, icon, image, name, summary, hostname } = actor; - const isFollowing = await outbox.isFollowing(req.uid, uid); + const isFollowing = await db.isSortedSetMember(`followingRemote:${req.uid}`, uid); const payload = { uid, diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 121d085d9b..8737d7baca 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -5,12 +5,12 @@ module.exports = function (app, middleware, controllers) { app.get('/user/:userslug', middlewares, controllers.activitypub.getActor); - app.get('/user/:userslug/following', middlewares, controllers.activitypub.getFollowing); - app.get('/user/:userslug/followers', middlewares, controllers.activitypub.getFollowers); + app.get('/user/:userslug/inbox', middlewares, controllers.activitypub.getInbox); + app.post('/user/:userslug/inbox', [...middlewares, middleware.validateActivity], controllers.activitypub.postInbox); app.get('/user/:userslug/outbox', middlewares, controllers.activitypub.getOutbox); app.post('/user/:userslug/outbox', middlewares, controllers.activitypub.postOutbox); - app.get('/user/:userslug/inbox', middlewares, controllers.activitypub.getInbox); - app.post('/user/:userslug/inbox', [...middlewares, middleware.validateActivity], controllers.activitypub.postInbox); + app.get('/user/:userslug/following', middlewares, controllers.activitypub.getFollowing); + app.get('/user/:userslug/followers', middlewares, controllers.activitypub.getFollowers); }; From e32eb8b3d7afd0abf7d1d35da4828d04df137f34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:48:56 -0500 Subject: [PATCH 0073/4744] fix(deps): update dependency @fontsource/inter to v5.0.16 (#12219) 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 003a896f9f..ac3cb4f319 100644 --- a/install/package.json +++ b/install/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@adactive/bootstrap-tagsinput": "0.8.2", - "@fontsource/inter": "5.0.15", + "@fontsource/inter": "5.0.16", "@fontsource/poppins": "5.0.8", "@fortawesome/fontawesome-free": "6.5.1", "@isaacs/ttlcache": "1.4.1", From daf2900a711bbca015ced3bed96cd899271f74a0 Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Fri, 8 Dec 2023 01:49:11 +0800 Subject: [PATCH 0074/4744] Update defaults.json (#12208) --- install/data/defaults.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/data/defaults.json b/install/data/defaults.json index d60984e99e..47a3e8d4a2 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -42,7 +42,7 @@ "registrationApprovalType": "normal", "allowAccountDelete": 1, "privateUploads": 0, - "allowedFileExtensions": "png,jpg,bmp,txt", + "allowedFileExtensions": "png,jpg,bmp,txt,webp,webm,mp4,gif", "uploadRateLimitThreshold": 10, "uploadRateLimitCooldown": 60, "allowUserHomePage": 1, @@ -188,4 +188,4 @@ "maxReconnectionAttempts": 5, "reconnectionDelay": 1500, "disableCustomUserSkins": 0 -} \ No newline at end of file +} From 4324f09c8500b8e29a61fd980d563edfec59a09e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 7 Dec 2023 13:10:06 -0500 Subject: [PATCH 0075/4744] fix: icon text and bgColor in remote profiles --- src/controllers/activitypub/profiles.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/controllers/activitypub/profiles.js b/src/controllers/activitypub/profiles.js index 10ca171dc9..738902998c 100644 --- a/src/controllers/activitypub/profiles.js +++ b/src/controllers/activitypub/profiles.js @@ -1,6 +1,7 @@ 'use strict'; const db = require('../../database'); +const user = require('../../user'); const { getActor } = require('../../activitypub'); const controller = module.exports; @@ -14,14 +15,24 @@ controller.get = async function (req, res, next) { const { preferredUsername, published, icon, image, name, summary, hostname } = actor; const isFollowing = await db.isSortedSetMember(`followingRemote:${req.uid}`, uid); + let picture; + if (icon) { + picture = typeof icon === 'string' ? icon : icon.url; + } + const iconBackgrounds = await user.getIconBackgrounds(); + let bgColor = Array.prototype.reduce.call(preferredUsername, (cur, next) => cur + next.charCodeAt(), 0); + bgColor = iconBackgrounds[bgColor % iconBackgrounds.length]; + const payload = { uid, username: `${preferredUsername}@${hostname}`, userslug: `${preferredUsername}@${hostname}`, fullname: name, joindate: new Date(published).getTime(), - picture: typeof icon === 'string' ? icon : icon.url, - uploadedpicture: typeof icon === 'string' ? icon : icon.url, + picture, + 'icon:text': (preferredUsername[0] || '').toUpperCase(), + 'icon:bgColor': bgColor, + uploadedpicture: undefined, 'cover:url': !image || typeof image === 'string' ? image : image.url, 'cover:position': '50% 50%', aboutme: summary, From e794f1d2ceb9af4cb1ea8a6059688287ff00182c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 7 Dec 2023 13:23:06 -0500 Subject: [PATCH 0076/4744] fix: store remote followed users count separately from local --- src/controllers/activitypub/index.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 1ecb86a697..d23081425c 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -132,8 +132,10 @@ Controller.follow = async (req, res) => { }); const now = Date.now(); - await db.sortedSetAdd(`followingRemote:${req.uid}`, now, objectId); - await recountFollowing(req.uid); + await Promise.all([ + db.sortedSetAdd(`followingRemote:${req.uid}`, now, objectId), + db.incrObjectField(`user:${req.uid}`, 'followingRemoteCount'), + ]); helpers.formatApiResponse(200, res); } catch (e) { @@ -152,19 +154,13 @@ Controller.unfollow = async (req, res) => { }, }); - await db.sortedSetRemove(`followingRemote:${req.uid}`, objectId); - await recountFollowing(req.uid); + await Promise.all([ + db.sortedSetRemove(`followingRemote:${req.uid}`, objectId), + db.decrObjectField(`user:${req.uid}`, 'followingRemoteCount'), + ]); helpers.formatApiResponse(200, res); } catch (e) { helpers.formatApiResponse(400, res, e); } }; - -async function recountFollowing(uid) { - const [followingCount, followingRemoteCount] = await Promise.all([ - db.sortedSetCard(`following:${uid}`), - db.sortedSetCard(`followingRemote:${uid}`), - ]); - await user.setUserField(uid, 'followingCount', followingCount + followingRemoteCount); -} From 255a67cd7f0f10e090e65232f0e9378a0fd5b9f1 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 8 Dec 2023 09:19:24 +0000 Subject: [PATCH 0077/4744] Latest translations and fallbacks --- public/language/hy/admin/extend/widgets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/hy/admin/extend/widgets.json b/public/language/hy/admin/extend/widgets.json index 2c477816ae..a8928471c5 100644 --- a/public/language/hy/admin/extend/widgets.json +++ b/public/language/hy/admin/extend/widgets.json @@ -5,7 +5,7 @@ "none-installed": "Վիջեթներ չեն գտնվել: Ակտիվացրեք վիջեթի հիմնական հավելվածը plugins կառավարման վահանակում:", "clone-from": "Կլոնավորել վիջեթներ-ից", "containers.available": "Առկա Containers", - "containers.explanation": "Drag and drop on top of any widget", + "containers.explanation": "Քաշեք և թողեք ցանկացած վիջեթի վերևում", "containers.none": "None", "container.well": "Well", "container.jumbotron": "Jumbotron", From 8a5fb86ddf548ac05631b0e2456f36ec275bd4e0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 8 Dec 2023 10:46:34 -0500 Subject: [PATCH 0078/4744] chore: small var rename --- src/controllers/activitypub/index.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index d23081425c..d366337b49 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -122,18 +122,18 @@ Controller.postInbox = async (req, res) => { Controller.follow = async (req, res) => { try { - const { uid: objectId } = req.params; - await activitypub.send(req.uid, objectId, { + const { uid: actorId } = req.params; + await activitypub.send(req.uid, actorId, { type: 'Follow', object: { type: 'Person', - name: objectId, + name: actorId, }, }); const now = Date.now(); await Promise.all([ - db.sortedSetAdd(`followingRemote:${req.uid}`, now, objectId), + db.sortedSetAdd(`followingRemote:${req.uid}`, now, actorId), db.incrObjectField(`user:${req.uid}`, 'followingRemoteCount'), ]); @@ -145,17 +145,17 @@ Controller.follow = async (req, res) => { Controller.unfollow = async (req, res) => { try { - const { uid: objectId } = req.params; - await activitypub.send(req.uid, objectId, { + const { uid: actorId } = req.params; + await activitypub.send(req.uid, actorId, { type: 'Unfollow', object: { type: 'Person', - name: objectId, + name: actorId, }, }); await Promise.all([ - db.sortedSetRemove(`followingRemote:${req.uid}`, objectId), + db.sortedSetRemove(`followingRemote:${req.uid}`, actorId), db.decrObjectField(`user:${req.uid}`, 'followingRemoteCount'), ]); From c803b2124c56edd8937bb73a154d74568e893070 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 8 Dec 2023 10:55:16 -0500 Subject: [PATCH 0079/4744] refactor: minor restructure to move logic out of main controller file to src/api --- src/api/activitypub.js | 53 ++++++++++++++++++++++++++++ src/api/index.js | 1 + src/controllers/activitypub/index.js | 45 ++++------------------- 3 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 src/api/activitypub.js diff --git a/src/api/activitypub.js b/src/api/activitypub.js new file mode 100644 index 0000000000..fd9f219e18 --- /dev/null +++ b/src/api/activitypub.js @@ -0,0 +1,53 @@ +'use strict'; + +/** + * DEVELOPMENT NOTE + * + * 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. + */ + +const db = require('../database'); +const activitypub = require('../activitypub'); + +const activitypubApi = module.exports; + +activitypubApi.follow = async (caller, { actorId } = {}) => { + if (!actorId) { + throw new Error('[[error:invalid-uid]]'); // should be activitypub-specific + } + + await activitypub.send(caller.uid, actorId, { + type: 'Follow', + object: { + type: 'Person', + name: actorId, + }, + }); + + const now = Date.now(); + await Promise.all([ + db.sortedSetAdd(`followingRemote:${caller.uid}`, now, actorId), + db.incrObjectField(`user:${caller.uid}`, 'followingRemoteCount'), + ]); +}; + +activitypubApi.unfollow = async (caller, { actorId }) => { + if (!actorId) { + throw new Error('[[error:invalid-uid]]'); // should be activitypub-specific + } + + await activitypub.send(caller.uid, actorId, { + type: 'Unfollow', + object: { + type: 'Person', + name: actorId, + }, + }); + + await Promise.all([ + db.sortedSetRemove(`followingRemote:${caller.uid}`, actorId), + db.decrObjectField(`user:${caller.uid}`, 'followingRemoteCount'), + ]); +}; diff --git a/src/api/index.js b/src/api/index.js index c454de93a5..18cd8678f1 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -11,6 +11,7 @@ module.exports = { categories: require('./categories'), search: require('./search'), flags: require('./flags'), + activitypub: require('./activitypub'), files: require('./files'), utils: require('./utils'), }; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index d366337b49..bfa07d33e3 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -5,6 +5,7 @@ const nconf = require('nconf'); const db = require('../../database'); const user = require('../../user'); const activitypub = require('../../activitypub'); +const api = require('../../api'); const helpers = require('../helpers'); const Controller = module.exports; @@ -101,6 +102,7 @@ Controller.getInbox = async (req, res) => { }; Controller.postInbox = async (req, res) => { + // Note: internal-only, hence no exposure via src/api switch (req.body.type) { case 'Follow': { await activitypub.inbox.follow(req.body.actor.name, req.body.object.name); @@ -121,46 +123,11 @@ Controller.postInbox = async (req, res) => { */ Controller.follow = async (req, res) => { - try { - const { uid: actorId } = req.params; - await activitypub.send(req.uid, actorId, { - type: 'Follow', - object: { - type: 'Person', - name: actorId, - }, - }); - - const now = Date.now(); - await Promise.all([ - db.sortedSetAdd(`followingRemote:${req.uid}`, now, actorId), - db.incrObjectField(`user:${req.uid}`, 'followingRemoteCount'), - ]); - - helpers.formatApiResponse(200, res); - } catch (e) { - helpers.formatApiResponse(400, res, e); - } + const { uid: actorId } = req.params; + helpers.formatApiResponse(200, res, await api.activitypub.follow(req, { actorId })); }; Controller.unfollow = async (req, res) => { - try { - const { uid: actorId } = req.params; - await activitypub.send(req.uid, actorId, { - type: 'Unfollow', - object: { - type: 'Person', - name: actorId, - }, - }); - - await Promise.all([ - db.sortedSetRemove(`followingRemote:${req.uid}`, actorId), - db.decrObjectField(`user:${req.uid}`, 'followingRemoteCount'), - ]); - - helpers.formatApiResponse(200, res); - } catch (e) { - helpers.formatApiResponse(400, res, e); - } + const { uid: actorId } = req.params; + helpers.formatApiResponse(200, res, await api.activitypub.unfollow(req, { actorId })); }; From c434262e2105617d83376ad12ac7977dad7c6f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 8 Dec 2023 10:58:13 -0500 Subject: [PATCH 0080/4744] fix: change translator escape remove \\\] and \\\[ match double ] and [ --- public/src/modules/translator.common.js | 7 ++++--- test/translator.js | 4 ---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/public/src/modules/translator.common.js b/public/src/modules/translator.common.js index 49a3b59c60..c69a9cc265 100644 --- a/public/src/modules/translator.common.js +++ b/public/src/modules/translator.common.js @@ -463,7 +463,9 @@ module.exports = function (utils, load, warn) { * @returns {string} */ Translator.escape = function escape(text) { - return typeof text === 'string' ? text.replace(/\[\[/g, '[[').replace(/\]\]/g, ']]') : text; + return typeof text === 'string' ? + text.replace(/\[\[/g, '[[').replace(/\]\]/g, ']]') : + text; }; /** @@ -473,8 +475,7 @@ module.exports = function (utils, load, warn) { */ Translator.unescape = function unescape(text) { return typeof text === 'string' ? - text.replace(/[/g, '[').replace(/\\\[/g, '[') - .replace(/]/g, ']').replace(/\\\]/g, ']') : + text.replace(/]]/g, ']]').replace(/[[/g, '[[') : text; }; diff --git a/test/translator.js b/test/translator.js index 6e34012a7a..61c3d5af8e 100644 --- a/test/translator.js +++ b/test/translator.js @@ -308,10 +308,6 @@ describe('Translator static methods', () => { describe('.unescape', () => { it('should unescape escaped translation patterns within text', (done) => { - assert.strictEqual( - Translator.unescape('some nice text \\[\\[global:home\\]\\] here'), - 'some nice text [[global:home]] here' - ); assert.strictEqual( Translator.unescape('some nice text [[global:home]] here'), 'some nice text [[global:home]] here' From c1f82b78a82fdbe0c174c5a94adac5d995d145a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 8 Dec 2023 11:07:11 -0500 Subject: [PATCH 0081/4744] chore: up composer default --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 5ef77abd22..300f0c3853 100644 --- a/install/package.json +++ b/install/package.json @@ -93,7 +93,7 @@ "multiparty": "4.2.3", "nconf": "0.12.1", "nodebb-plugin-2factor": "7.4.0", - "nodebb-plugin-composer-default": "10.2.27", + "nodebb-plugin-composer-default": "10.2.28", "nodebb-plugin-dbsearch": "6.2.2", "nodebb-plugin-emoji": "5.1.13", "nodebb-plugin-emoji-android": "4.0.0", From 245e5df38574af2499f223799404bc65e06ae174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 8 Dec 2023 13:54:57 -0500 Subject: [PATCH 0082/4744] chore: up composer --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 300f0c3853..14de04da6f 100644 --- a/install/package.json +++ b/install/package.json @@ -93,7 +93,7 @@ "multiparty": "4.2.3", "nconf": "0.12.1", "nodebb-plugin-2factor": "7.4.0", - "nodebb-plugin-composer-default": "10.2.28", + "nodebb-plugin-composer-default": "10.2.29", "nodebb-plugin-dbsearch": "6.2.2", "nodebb-plugin-emoji": "5.1.13", "nodebb-plugin-emoji-android": "4.0.0", From d96d4d0991094836092b4cce4598b27a505401fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:03:53 -0500 Subject: [PATCH 0083/4744] fix(deps): update dependency esbuild to v0.19.9 (#12224) 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 6fc35cacbc..2bfb43b8c4 100644 --- a/install/package.json +++ b/install/package.json @@ -63,7 +63,7 @@ "csrf-sync": "4.0.1", "daemon": "1.1.0", "diff": "5.1.0", - "esbuild": "0.19.8", + "esbuild": "0.19.9", "express": "4.18.2", "express-session": "1.17.3", "express-useragent": "1.0.15", From 1f79f5424154db808be46996d89220c851aae3b2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 11 Dec 2023 14:35:04 -0500 Subject: [PATCH 0084/4744] feat: update activitypub helper resolveLocalUid to accept both webfinger name and full URL as input --- src/activitypub/helpers.js | 23 ++++++++++++++++--- test/activitypub.js | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 959c8a5419..5e86939878 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -4,6 +4,7 @@ const request = require('request-promise-native'); const { generateKeyPairSync } = require('crypto'); const winston = require('winston'); const nconf = require('nconf'); +const validator = require('validator'); const db = require('../database'); const ttl = require('../cache/ttl'); @@ -68,10 +69,26 @@ Helpers.generateKeys = async (uid) => { return { publicKey, privateKey }; }; -Helpers.resolveLocalUid = async (id) => { - const [slug, host] = id.split('@'); +Helpers.resolveLocalUid = async (input) => { + let slug; - if (id.indexOf('@') === -1 || host !== nconf.get('url_parsed').host) { + if (validator.isURL(input, { + require_protocol: true, + require_host: true, + require_tld: false, + protocols: ['https'], + require_valid_protocol: true, + })) { + const { host, pathname } = new URL(input); + + if (host === nconf.get('url_parsed').host) { + slug = pathname.split('/').filter(Boolean)[1]; + } else { + throw new Error('[[activitypub:invalid-id]]'); + } + } else if (input.indexOf('@') !== -1) { // Webfinger + ([slug] = input.replace(/^acct:/, '').split('@')); + } else { throw new Error('[[activitypub:invalid-id]]'); } diff --git a/test/activitypub.js b/test/activitypub.js index d59fe1cbc1..7b6748bea2 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -100,6 +100,53 @@ describe('ActivityPub integration', () => { }); }); + describe.only('Helpers', () => { + describe('.query()', () => { + + }); + + describe('.generateKeys()', () => { + + }); + + describe('.resolveLocalUid()', () => { + let uid; + let slug; + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + uid = await user.create({ username: slug }); + }); + + it('should throw when an invalid input is passed in', async () => { + await assert.rejects( + activitypub.helpers.resolveLocalUid('ncl28h3qwhoiclwnevoinw3u'), + { message: '[[activitypub:invalid-id]]' } + ); + }); + + it('should return null when valid input is passed but does not resolve', async () => { + const uid = await activitypub.helpers.resolveLocalUid(`acct:foobar@${nconf.get('url_parsed').host}`); + assert.strictEqual(uid, null); + }); + + it('should resolve to a local uid when given a webfinger-style string', async () => { + const found = await activitypub.helpers.resolveLocalUid(`acct:${slug}@${nconf.get('url_parsed').host}`); + assert.strictEqual(found, uid); + }); + + it('should resolve even without the "acct:" prefix', async () => { + const found = await activitypub.helpers.resolveLocalUid(`${slug}@${nconf.get('url_parsed').host}`); + assert.strictEqual(found, uid); + }); + + it('should resolve when passed a full URL', async () => { + const found = await activitypub.helpers.resolveLocalUid(`${nconf.get('url')}/user/${slug}`); + assert.strictEqual(found, uid); + }); + }); + }); + describe('ActivityPub screener middleware', () => { let uid; let slug; From 6036d14463284257d778fc19bec319cf504d6510 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 20:22:36 -0500 Subject: [PATCH 0085/4744] fix(deps): update dependency ace-builds to v1.32.1 (#12226) 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 2bfb43b8c4..aed93413a7 100644 --- a/install/package.json +++ b/install/package.json @@ -34,7 +34,7 @@ "@fortawesome/fontawesome-free": "6.5.1", "@isaacs/ttlcache": "1.4.1", "@popperjs/core": "2.11.8", - "ace-builds": "1.32.0", + "ace-builds": "1.32.1", "archiver": "6.0.1", "async": "3.2.5", "autoprefixer": "10.4.16", From 3d7bad32749670a84e958d50f9457aa6e6c59d87 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 13 Dec 2023 09:18:42 +0000 Subject: [PATCH 0086/4744] Latest translations and fallbacks --- public/language/tr/notifications.json | 58 +++++++++++++-------------- public/language/tr/tags.json | 12 +++--- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json index 7feb6acdf3..9517894245 100644 --- a/public/language/tr/notifications.json +++ b/public/language/tr/notifications.json @@ -12,51 +12,51 @@ "you-have-unread-notifications": "Okunmamış bildirimleriniz var.", "all": "Hepsi", "topics": "Konular", - "tags": "Tags", - "categories": "Categories", + "tags": "Etiketler", + "categories": "Kategoriler", "replies": "Yanıtlar", "chat": "Sohbetler", "group-chat": "Grup Sohbetleri", - "public-chat": "Public Chats", + "public-chat": "Genel Sohbetler", "follows": "Takip Edilenler", "upvote": "Artı Oylananlar", - "awards": "Awards", + "awards": "Ödüller", "new-flags": "Yeni Şikayetler", "my-flags": "Vekil olarak atandığım şikayetler", "bans": "Yasaklamalar", "new-message-from": "%1 size bir mesaj gönderdi", - "new-messages-from": "%1 new messages from %2", - "new-message-in": "New message in %1", - "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "new-messages-from": "%2 kullanıcısından %1 yeni mesaj var", + "new-message-in": "%1 odasında yeni mesaj var", + "new-messages-in": "%2 odasında %1 yeni mesaj var", + "user-posted-in-public-room": "%1 şu odaya yazdı: %3", + "user-posted-in-public-room-dual": "%1 ve %2 şu odaya yazdı: %4", + "user-posted-in-public-room-triple": "%1, %2 ve %3 şu odaya yazdılar: %5", + "user-posted-in-public-room-multiple": "%1, %2 ve %3 diğer kullanıcı şu odaya yazdılar: %5", "upvoted-your-post-in": "%1 şu konudaki iletinizi beğendi: %2.", "upvoted-your-post-in-dual": "%1 ve %2 şu konudaki iletinizi beğendi: %3", - "upvoted-your-post-in-triple": "%1, %2 and %3 have upvoted your post in %4.", - "upvoted-your-post-in-multiple": "%1, %2 and %3 others have upvoted your post in %4.", + "upvoted-your-post-in-triple": "%1, %2 ve %3 şu konudaki iletinizi beğendi: %4.", + "upvoted-your-post-in-multiple": "%1, %2 ve %3 diğer kullanıcı şu konudaki iletinizi beğendi: %4.", "moved-your-post": "%1, iletinizi şuraya taşıdı: %2", "moved-your-topic": "%1 şuraya taşındı: %2", "user-flagged-post-in": "%1 şu konudaki bir iletiyi şikayet etti: %2", "user-flagged-post-in-dual": "%1 ve %2 şu konudaki bir iletiyi şikayet etti: %3", - "user-flagged-post-in-triple": "%1, %2 and %3 flagged a post in %4", - "user-flagged-post-in-multiple": "%1, %2 and %3 others flagged a post in %4", - "user-flagged-user": "%1 şu kullanıcının profilini şikayet etti: (%2)", - "user-flagged-user-dual": "%1 ve %2 şu kullanıcının profilini şikayet etti: (%3)", - "user-flagged-user-triple": "%1, %2 and %3 flagged a user profile (%4)", - "user-flagged-user-multiple": "%1, %2 and %3 others flagged a user profile (%4)", + "user-flagged-post-in-triple": "%1, %2 ve %3 şu konudaki bir iletiyi şikayet etti: %4", + "user-flagged-post-in-multiple": "%1, %2 ve %3 diğer kullanıcı şu konudaki bir iletiyi şikayet etti: %4", + "user-flagged-user": "%1 şu kullanıcıyı şikayet etti: (%2)", + "user-flagged-user-dual": "%1 ve %2 şu kullanıcıyı şikayet etti: (%3)", + "user-flagged-user-triple": "%1, %2 ve %3 şu kullanıcıyı şikayet etti: (%4)", + "user-flagged-user-multiple": "%1, %2 ve %3 diğer üye şu kullanıcıyı şikayet etti: (%4)", "user-posted-to": "%1 şu konuya bir ileti yazdı: %2", "user-posted-to-dual": "%1 ve %2 şu konuya ileti yazdılar: %3", - "user-posted-to-triple": "%1, %2 and %3 have posted replies to: %4", - "user-posted-to-multiple": "%1, %2 and %3 others have posted replies to: %4", + "user-posted-to-triple": "%1, %2 ve %3 şu konuya ileti yazdılar: %4", + "user-posted-to-multiple": "%1, %2 ve %3 diğer kullanıcı şu konuya ileti yazdılar: %4", "user-posted-topic": "%1 şu yeni konuyu oluşturdu: %2", "user-edited-post": "%1 şu konudaki bir iletiyi değiştirdi: %2", - "user-posted-topic-with-tag": "%1 has posted a new topic with tag %2", - "user-posted-topic-with-tag-dual": "%1 has posted a new topic with tags %2 and %3", - "user-posted-topic-with-tag-triple": "%1 has posted a new topic with tags %2, %3 and %4", - "user-posted-topic-with-tag-multiple": "%1 has posted a new topic with tags %2", - "user-posted-topic-in-category": "%1 has posted a new topic in %2", + "user-posted-topic-with-tag": "%1 şu etiketi kullanarak yeni bir konu oluşturdu: %2", + "user-posted-topic-with-tag-dual": "%1 şu etiketleri kullanarak yeni bir konu oluşturdu: %2 ve %3", + "user-posted-topic-with-tag-triple": "%1 şu etiketleri kullanarak yeni bir konu oluşturdu: %2, %3 ve %4", + "user-posted-topic-with-tag-multiple": "%1 şu etiketleri kullanarak yeni bir konu oluşturdu: %2", + "user-posted-topic-in-category": "%1 şu kategoride yeni bir başlık oluşturdu: %2", "user-started-following-you": "%1 sizi takip etmeye başladı.", "user-started-following-you-dual": "%1 ve %2 sizi takip etmeye başladı.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -82,8 +82,8 @@ "notification-and-email": "Bildirim & E-posta", "notificationType-upvote": "Biri iletinize artı oy verdiğinde", "notificationType-new-topic": "Takip ettiğiniz biri yeni bir konu oluşturduğunda", - "notificationType-new-topic-with-tag": "When a topic is posted with a tag you follow", - "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", + "notificationType-new-topic-with-tag": "Takip ettiğiniz etiket ile yeni bir başlık oluşturulduğunda", + "notificationType-new-topic-in-category": "Takip ettiğiniz kategoride yeni bir başlık oluşturulduğunda", "notificationType-new-reply": "Takip ettiğiniz bir konuya yeni bir ileti gönderildiğinde", "notificationType-post-edit": "Takip ettiğiniz bir konudaki bir ileti değiştirildiğinde", "notificationType-follow": "Biri sizi takip etmeye başlayınca", @@ -97,5 +97,5 @@ "notificationType-post-queue": "Yeni bir ileti sıraya alındığında", "notificationType-new-post-flag": "Bir ileti şikayet edildiğinde", "notificationType-new-user-flag": "Bir kullanıcı şikayet edildiğinde", - "notificationType-new-reward": "When you earn a new reward" + "notificationType-new-reward": "Yeni bir ödül kazanınca" } \ No newline at end of file diff --git a/public/language/tr/tags.json b/public/language/tr/tags.json index 9e39c64eb1..32b174112b 100644 --- a/public/language/tr/tags.json +++ b/public/language/tr/tags.json @@ -8,10 +8,10 @@ "no-tags": "Henüz etiket yok.", "select-tags": "Etiketleri Seç", "tag-whitelist": "Kullanılabilir etiket listesi", - "watching": "Watching", - "not-watching": "Not Watching", - "watching.description": "Notify me of new topics.", - "not-watching.description": "Do not notify me of new topics.", - "following-tag.message": "You will now be receiving notifications when somebody posts a topic with this tag.", - "not-following-tag.message": "You will not receive notifications when somebody posts a topic with this tag." + "watching": "Takip ediliyor", + "not-watching": "Takip edilmiyor", + "watching.description": "Yeni başlıkları bildir", + "not-watching.description": "Yeni başlıkları bildirme", + "following-tag.message": "Bu etiket ile yeni bir başlık oluşturulduğunda bildirim alacaksınız!", + "not-following-tag.message": "Bu etiket ile yeni bir başlık oluşturulduğunda bildirim almayacaksınız!" } \ No newline at end of file From e7e2a2f488f0de1f21f778fe569ccf925e52f055 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 13 Dec 2023 15:42:45 +0000 Subject: [PATCH 0087/4744] chore: incrementing version number - v3.5.3 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 14de04da6f..3e63d4c083 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "3.5.2", + "version": "3.5.3", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From e49ddaf815483747639a1d19ea72e5df55bb2faa Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 13 Dec 2023 15:42:45 +0000 Subject: [PATCH 0088/4744] chore: update changelog for v3.5.3 --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0993f03d..294dbd93ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,49 @@ +#### v3.5.3 (2023-12-13) + +##### Chores + +* up composer (245e5df3) +* up composer default (c1f82b78) +* incrementing version number - v3.5.2 (52fbb2da) +* update changelog for v3.5.2 (e2e85053) +* incrementing version number - v3.5.1 (4c543488) +* incrementing version number - v3.5.0 (d06fb4f0) +* incrementing version number - v3.4.3 (5c984250) +* incrementing version number - v3.4.2 (3f0dac38) +* incrementing version number - v3.4.1 (01e69574) +* incrementing version number - v3.4.0 (fd9247c5) +* incrementing version number - v3.3.9 (5805e770) +* incrementing version number - v3.3.8 (a5603565) +* incrementing version number - v3.3.7 (b26f1744) +* incrementing version number - v3.3.6 (7fb38792) +* incrementing version number - v3.3.4 (a67f84ea) +* incrementing version number - v3.3.3 (f94d239b) +* incrementing version number - v3.3.2 (ec9dac97) +* incrementing version number - v3.3.1 (151cc68f) +* incrementing version number - v3.3.0 (fc1ad70f) +* incrementing version number - v3.2.3 (b06d3e63) +* incrementing version number - v3.2.2 (758ecfcd) +* incrementing version number - v3.2.1 (20145074) +* incrementing version number - v3.2.0 (9ecac38e) +* incrementing version number - v3.1.7 (0b4e81ab) +* incrementing version number - v3.1.6 (b3a3b130) +* incrementing version number - v3.1.5 (ec19343a) +* incrementing version number - v3.1.4 (2452783c) +* incrementing version number - v3.1.3 (3b4e9d3f) +* incrementing version number - v3.1.2 (40fa3489) +* incrementing version number - v3.1.1 (40250733) +* incrementing version number - v3.1.0 (0cb386bd) +* incrementing version number - v3.0.1 (26f6ea49) +* incrementing version number - v3.0.0 (224e08cd) + +##### Bug Fixes + +* change translator escape (c434262e) + +##### Other Changes + +* add types for database abstration layer (#10762) (17cd19c7) + #### v3.5.2 (2023-11-29) ##### Chores From 68d5e4a8ab4f8249d04c4cd420c37d13b5ca3c0f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 13 Dec 2023 13:14:51 -0500 Subject: [PATCH 0089/4744] refactor: update activitypub.getActor to accept either url or webfinger id --- src/activitypub/index.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 07b8da2d5a..b257a7e5cf 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -3,6 +3,7 @@ const request = require('request-promise-native'); const nconf = require('nconf'); const { createHash, createSign, createVerify } = require('crypto'); +const validator = require('validator'); const db = require('../database'); const user = require('../user'); @@ -14,14 +15,24 @@ const ActivityPub = module.exports; ActivityPub.helpers = require('./helpers'); ActivityPub.inbox = require('./inbox'); -ActivityPub.getActor = async (id) => { - if (actorCache.has(id)) { - return actorCache.get(id); +ActivityPub.getActor = async (input) => { + // Can be a webfinger id, uri, or object, handle as appropriate + let uri; + if (validator.isURL(input, { + require_protocol: true, + require_host: true, + protocols: ['https'], + require_valid_protocol: true, + })) { + uri = input; + } else if (input.indexOf('@') !== -1) { // Webfinger + ({ actorUri: uri } = await ActivityPub.helpers.query(input)); + } else { + throw new Error('[[error:invalid-data]]'); } - const { hostname, actorUri: uri } = await ActivityPub.helpers.query(id); - if (!uri) { - return false; + if (actorCache.has(uri)) { + return actorCache.get(uri); } const actor = await request({ @@ -32,9 +43,9 @@ ActivityPub.getActor = async (id) => { json: true, }); - actor.hostname = hostname; + actor.hostname = new URL(uri).hostname; - actorCache.set(id, actor); + actorCache.set(uri, actor); return actor; }; @@ -181,7 +192,7 @@ ActivityPub.send = async (uid, targets, payload) => { const { date, digest, signature } = await ActivityPub.sign(uid, uri, payload); const response = await request(uri, { - method: payload ? 'post' : 'get', + method: 'post', headers: { date, digest, From 4c1b2b3fe6c320f633a76dff058a740be14f352f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 13 Dec 2023 13:15:03 -0500 Subject: [PATCH 0090/4744] feat: accept and undo support --- src/activitypub/inbox.js | 56 +++++++++++++++++++++++++++- src/activitypub/index.js | 10 ++--- src/controllers/activitypub/index.js | 15 ++++++++ 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 096bb481d6..de64dcde6c 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -2,6 +2,7 @@ const db = require('../database'); const user = require('../user'); +const activitypub = require('.'); const helpers = require('./helpers'); @@ -24,8 +25,8 @@ inbox.isFollowed = async (actorId, uid) => { async function handleFollow(type, actorId, objectId) { // Sanity checks - const actorExists = await helpers.query(actorId); - if (!actorId || !actorExists) { + const from = await helpers.query(actorId); + if (!actorId || !from) { throw new Error('[[error:invalid-uid]]'); // should probably be AP specific } @@ -46,13 +47,64 @@ async function handleFollow(type, actorId, objectId) { } const now = Date.now(); await db.sortedSetAdd(`followersRemote:${localUid}`, now, actorId); + await activitypub.send(localUid, actorId, { + type: 'Accept', + object: { + type: 'Follow', + actor: from.actorUri, + }, + }); } else { if (!isFollowed) { throw new Error('[[error:not-following]]'); } await db.sortedSetRemove(`followersRemote:${localUid}`, actorId); + await activitypub.send(localUid, actorId, { + type: 'Undo', + object: { + type: 'Follow', + actor: from.actorUri, + }, + }); } const followerRemoteCount = await db.sortedSetCard(`followersRemote:${localUid}`); await user.setUserField(localUid, 'followerRemoteCount', followerRemoteCount); } + +inbox.accept = async (req) => { + const { actor, object } = req.body; + const { type } = object; + + if (type === 'Follow') { + // todo: should check that actor and object.actor are the same person? + const uid = await helpers.resolveLocalUid(object.actor); + if (!uid) { + throw new Error('[[error:invalid-uid]]'); + } + + const now = Date.now(); + await Promise.all([ + db.sortedSetAdd(`followingRemote:${uid}`, now, actor.name), + db.incrObjectField(`user:${uid}`, 'followingRemoteCount'), + ]); + } +}; + +inbox.undo = async (req) => { + const { actor, object } = req.body; + const { type } = object; + + if (type === 'Follow') { + // todo: should check that actor and object.actor are the same person? + const uid = await helpers.resolveLocalUid(object.actor); + if (!uid) { + throw new Error('[[error:invalid-uid]]'); + } + + await Promise.all([ + db.sortedSetRemove(`followingRemote:${uid}`, actor.name), + db.decrObjectField(`user:${uid}`, 'followingRemoteCount'), + ]); + } +}; diff --git a/src/activitypub/index.js b/src/activitypub/index.js index b257a7e5cf..40370b4981 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -178,12 +178,10 @@ ActivityPub.send = async (uid, targets, payload) => { const inboxes = await ActivityPub.resolveInboxes(targets); payload = { - ...{ - '@context': 'https://www.w3.org/ns/activitystreams', - actor: { - type: 'Person', - name: `${userslug}@${nconf.get('url_parsed').host}`, - }, + '@context': 'https://www.w3.org/ns/activitystreams', + actor: { + type: 'Person', + name: `${userslug}@${nconf.get('url_parsed').host}`, }, ...payload, }; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index bfa07d33e3..153f97d701 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -113,6 +113,21 @@ Controller.postInbox = async (req, res) => { await activitypub.inbox.unfollow(req.body.actor.name, req.body.object.name); break; } + + case 'Accept': { + await activitypub.inbox.accept(req); + break; + } + + case 'Undo': { + await activitypub.inbox.undo(req); + break; + } + + default: { + console.log('Unhandled Activity!!!'); + console.log(req.body); + } } res.sendStatus(201); From 2dc1def51f89d9fe2fd7b80d2c632a764aa7c9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 13 Dec 2023 13:18:07 -0500 Subject: [PATCH 0091/4744] fix: #12227, fix crash in redirect --- src/middleware/header.js | 15 +-------------- src/middleware/user.js | 14 ++++++++++++++ src/routes/helpers.js | 1 + 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/middleware/header.js b/src/middleware/header.js index b6c4e47ec0..383ef8e94e 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -1,6 +1,5 @@ 'use strict'; -const user = require('../user'); const plugins = require('../plugins'); const helpers = require('./helpers'); @@ -27,17 +26,5 @@ async function doBuildHeader(req, res) { } await plugins.hooks.fire('filter:middleware.buildHeader', { req: req, locals: res.locals }); - const [config, canLoginIfBanned] = await Promise.all([ - controllers.api.loadConfig(req), - user.bans.canLoginIfBanned(req.uid), - ]); - - if (!canLoginIfBanned && req.loggedIn) { - req.logout(() => { - res.redirect('/'); - }); - return; - } - - res.locals.config = config; + res.locals.config = await controllers.api.loadConfig(req); } diff --git a/src/middleware/user.js b/src/middleware/user.js index 1220897ffc..a9573e397c 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -221,6 +221,20 @@ module.exports = function (middleware) { controllers.helpers.redirect(res, path); }); + middleware.redirectToHomeIfBanned = helpers.try(async (req, res, next) => { + if (req.loggedIn) { + const canLoginIfBanned = await user.bans.canLoginIfBanned(req.uid); + if (!canLoginIfBanned) { + req.logout(() => { + res.redirect('/'); + }); + return; + } + } + + next(); + }); + middleware.requireUser = function (req, res, next) { if (req.loggedIn) { return next(); diff --git a/src/routes/helpers.js b/src/routes/helpers.js index aff46467ec..b43f53fd3e 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -18,6 +18,7 @@ helpers.setupPageRoute = function (...args) { middlewares = [ middleware.applyBlacklist, middleware.authenticateRequest, + middleware.redirectToHomeIfBanned, middleware.maintenanceMode, middleware.registrationComplete, middleware.pluginHooks, From 7f46f07cb9c954cc47d6b930ed33dbb9347c4547 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 13 Dec 2023 13:21:29 -0500 Subject: [PATCH 0092/4744] fix: unused require --- src/controllers/activitypub/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 153f97d701..a8b3f88762 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -2,7 +2,6 @@ const nconf = require('nconf'); -const db = require('../../database'); const user = require('../../user'); const activitypub = require('../../activitypub'); const api = require('../../api'); From f91b823eccb6f132d040b59f8bbf9c9f0cfd5c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 14 Dec 2023 10:13:19 -0500 Subject: [PATCH 0093/4744] refactor: replace deprecated call with api call --- public/src/client/topic/replies.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/src/client/topic/replies.js b/public/src/client/topic/replies.js index 617eea3d0b..a70862c119 100644 --- a/public/src/client/topic/replies.js +++ b/public/src/client/topic/replies.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts'], function (posts, hooks, alerts) { +define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts', 'api'], function (posts, hooks, alerts, api) { const Replies = {}; Replies.init = function (button) { @@ -14,8 +14,8 @@ define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts'], function if (open.is(':not(.hidden)') && loading.is('.hidden')) { open.addClass('hidden'); loading.removeClass('hidden'); - - socket.emit('posts.getReplies', pid, function (err, postData) { + api.get(`/posts/${pid}/replies`, {}, function (err, { replies }) { + const postData = replies; loading.addClass('hidden'); if (err) { open.removeClass('hidden'); From b6ca117ae8bdf339810a0facd1b8a04f6712a9ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:09:33 -0500 Subject: [PATCH 0094/4744] fix(deps): update dependency ace-builds to v1.32.2 (#12228) 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 2dd2ad5e00..3f07a05609 100644 --- a/install/package.json +++ b/install/package.json @@ -34,7 +34,7 @@ "@fortawesome/fontawesome-free": "6.5.1", "@isaacs/ttlcache": "1.4.1", "@popperjs/core": "2.11.8", - "ace-builds": "1.32.1", + "ace-builds": "1.32.2", "archiver": "6.0.1", "async": "3.2.5", "autoprefixer": "10.4.16", From 24c1dfac8cf949788373b4d00d1299301dd0b635 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 13 Dec 2023 13:38:52 -0500 Subject: [PATCH 0095/4744] test: allow http proto on ci --- src/activitypub/helpers.js | 15 +++++++++++++-- test/activitypub.js | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 5e86939878..aa28652d36 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -71,18 +71,29 @@ Helpers.generateKeys = async (uid) => { Helpers.resolveLocalUid = async (input) => { let slug; + const protocols = ['https']; + if (process.env.CI === 'true') { + protocols.push('http'); + } + console.log(input, nconf.get('url'), nconf.get('url_parsed'), protocols, validator.isURL(input, { + require_protocol: true, + require_host: true, + require_tld: false, + protocols, + require_valid_protocol: true, + }), nconf.get('ci')); if (validator.isURL(input, { require_protocol: true, require_host: true, require_tld: false, - protocols: ['https'], + protocols, require_valid_protocol: true, })) { const { host, pathname } = new URL(input); if (host === nconf.get('url_parsed').host) { - slug = pathname.split('/').filter(Boolean)[1]; + slug = pathname.replace(nconf.get('relative_path'), '').split('/').filter(Boolean)[1]; } else { throw new Error('[[activitypub:invalid-id]]'); } diff --git a/test/activitypub.js b/test/activitypub.js index 7b6748bea2..49eec8b21c 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -100,7 +100,7 @@ describe('ActivityPub integration', () => { }); }); - describe.only('Helpers', () => { + describe('Helpers', () => { describe('.query()', () => { }); From a21110fd882ac39aadb2f3a6f77778e6e2058f80 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 14 Dec 2023 13:47:28 -0500 Subject: [PATCH 0096/4744] fix: handle null actor uri in helpers.query --- src/activitypub/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 40370b4981..81f3334e04 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -31,6 +31,10 @@ ActivityPub.getActor = async (input) => { throw new Error('[[error:invalid-data]]'); } + if (!uri) { + throw new Error('[[error:invalid-uid]]'); + } + if (actorCache.has(uri)) { return actorCache.get(uri); } From 7b7bfdb7622aae1e21994af5132493b89246ca93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 15 Dec 2023 20:08:22 -0500 Subject: [PATCH 0097/4744] text-break on uploads --- src/views/admin/manage/uploads.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/admin/manage/uploads.tpl b/src/views/admin/manage/uploads.tpl index 51bbba917a..e2f6e3a179 100644 --- a/src/views/admin/manage/uploads.tpl +++ b/src/views/admin/manage/uploads.tpl @@ -36,7 +36,7 @@ {{{ if files.isFile }}} - {files.name} + {files.name} {{{ end }}} @@ -53,7 +53,7 @@ {{{ if files.isFile }}}{files.sizeHumanReadable}{{{ else }}}[[admin/manage/uploads:filecount, {files.fileCount}]]{{{ end }}} - + From 9e2a6f8625267c8c276a842b4cd20a24abcdf694 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:06:30 -0500 Subject: [PATCH 0098/4744] fix(deps): update dependency csrf-sync to v4.0.3 (#12232) 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 3f07a05609..f1a2eea117 100644 --- a/install/package.json +++ b/install/package.json @@ -60,7 +60,7 @@ "cookie-parser": "1.4.6", "cron": "3.1.6", "cropperjs": "1.6.1", - "csrf-sync": "4.0.1", + "csrf-sync": "4.0.3", "daemon": "1.1.0", "diff": "5.1.0", "esbuild": "0.19.9", From 451430006e0c922e27c1b6c98e4aa60d7c31c009 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:08:43 -0500 Subject: [PATCH 0099/4744] fix(deps): update dependency sharp to v0.33.1 (#12233) 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 f1a2eea117..def00b78e1 100644 --- a/install/package.json +++ b/install/package.json @@ -128,7 +128,7 @@ "sass": "1.69.5", "semver": "7.5.4", "serve-favicon": "2.5.0", - "sharp": "0.33.0", + "sharp": "0.33.1", "sitemap": "7.1.1", "socket.io": "4.7.2", "socket.io-client": "4.7.2", From c15bdd4cf019493f8e857e093b42b132d8b9b716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 18 Dec 2023 12:08:34 -0500 Subject: [PATCH 0100/4744] =?UTF-8?q?=F0=9F=91=8BRequest,=20=F0=9F=90=B6?= =?UTF-8?q?=20Fetch,=20closes=20#10341=20(#12236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * axios migration * controller tests * add missing deps * feeds * remove unused async * flags * locale-detect * messaging/middleware * remove log * meta * plugins * posts * search * topics/thumbs * user/emails * uploads.js * socket.io * cleaunup * test native fetch * cleanup * increase engine to 18 fix remaining tests * remove testing file * fix comments,typo * revert debug --- install/package.json | 6 +- public/src/client/flags/detail.js | 3 - src/admin/versions.js | 42 +- src/cli/upgrade-plugins.js | 11 +- src/plugins/index.js | 25 +- src/plugins/install.js | 21 +- src/plugins/usage.js | 45 +- src/request.js | 79 + src/socket.io/admin.js | 4 +- test/api.js | 57 +- test/authentication.js | 444 ++--- test/categories.js | 15 +- test/controllers-admin.js | 838 ++++----- test/controllers.js | 2663 ++++++++++------------------- test/feeds.js | 209 +-- test/flags.js | 133 +- test/helpers/index.js | 226 +-- test/locale-detect.js | 47 +- test/messaging.js | 173 +- test/meta.js | 88 +- test/middleware.js | 80 +- test/plugins.js | 37 +- test/posts.js | 118 +- test/posts/uploads.js | 148 +- test/search.js | 232 +-- test/socket.io.js | 179 +- test/topics.js | 1103 ++++-------- test/topics/thumbs.js | 86 +- test/uploads.js | 373 ++-- test/user.js | 674 +++----- test/user/emails.js | 37 +- 31 files changed, 2895 insertions(+), 5301 deletions(-) create mode 100644 src/request.js diff --git a/install/package.json b/install/package.json index def00b78e1..2ab68ae48d 100644 --- a/install/package.json +++ b/install/package.json @@ -67,6 +67,7 @@ "express": "4.18.2", "express-session": "1.17.3", "express-useragent": "1.0.15", + "fetch-cookie": "2.1.0", "file-loader": "6.2.0", "fs-extra": "11.2.0", "graceful-fs": "4.2.11", @@ -119,8 +120,6 @@ "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", "ioredis": "5.3.2", - "request": "2.88.2", - "request-promise-native": "1.0.9", "rimraf": "5.0.5", "rss": "1.2.2", "rtlcss": "4.1.1", @@ -142,6 +141,7 @@ "timeago": "1.6.7", "tinycon": "0.6.8", "toobusy-js": "0.5.1", + "tough-cookie": "4.1.3", "validator": "13.11.0", "webpack": "5.89.0", "webpack-merge": "5.10.0", @@ -181,7 +181,7 @@ "url": "https://github.com/NodeBB/NodeBB/issues" }, "engines": { - "node": ">=16" + "node": ">=18" }, "maintainers": [ { diff --git a/public/src/client/flags/detail.js b/public/src/client/flags/detail.js index 5122d022e8..4567520eda 100644 --- a/public/src/client/flags/detail.js +++ b/public/src/client/flags/detail.js @@ -62,9 +62,6 @@ define('forum/flags/detail', [ Detail.reloadHistory(payload.history); }).catch(alerts.error); }, - onShown: (e) => { - console.log(e); - }, }); break; } diff --git a/src/admin/versions.js b/src/admin/versions.js index aeb3e7e21c..1277108f75 100644 --- a/src/admin/versions.js +++ b/src/admin/versions.js @@ -1,15 +1,15 @@ 'use strict'; -const request = require('request'); - +const request = require('../request'); const meta = require('../meta'); let versionCache = ''; let versionCacheLastModified = ''; const isPrerelease = /^v?\d+\.\d+\.\d+-.+$/; +const latestReleaseUrl = 'https://api.github.com/repos/NodeBB/NodeBB/releases/latest'; -function getLatestVersion(callback) { +async function getLatestVersion() { const headers = { Accept: 'application/vnd.github.v3+json', 'User-Agent': encodeURIComponent(`NodeBB Admin Control Panel/${meta.config.title}`), @@ -19,31 +19,23 @@ function getLatestVersion(callback) { headers['If-Modified-Since'] = versionCacheLastModified; } - request('https://api.github.com/repos/NodeBB/NodeBB/releases/latest', { - json: true, + const { body: latestRelease, response } = await request.get(latestReleaseUrl, { headers: headers, timeout: 2000, - }, (err, res, latestRelease) => { - if (err) { - return callback(err); - } - - if (res.statusCode === 304) { - return callback(null, versionCache); - } - - if (res.statusCode !== 200) { - return callback(new Error(res.statusMessage)); - } - - if (!latestRelease || !latestRelease.tag_name) { - return callback(new Error('[[error:cant-get-latest-release]]')); - } - const tagName = latestRelease.tag_name.replace(/^v/, ''); - versionCache = tagName; - versionCacheLastModified = res.headers['last-modified']; - callback(null, versionCache); }); + if (response.statusCode === 304) { + return versionCache; + } + if (response.statusCode !== 200) { + throw new Error(response.statusText); + } + if (!latestRelease || !latestRelease.tag_name) { + throw new Error('[[error:cant-get-latest-release]]'); + } + const tagName = latestRelease.tag_name.replace(/^v/, ''); + versionCache = tagName; + versionCacheLastModified = response.headers['last-modified']; + return versionCache; } exports.getLatestVersion = getLatestVersion; diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index 2c76a6c5b1..024d922647 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -1,13 +1,13 @@ 'use strict'; const prompt = require('prompt'); -const request = require('request-promise-native'); const cproc = require('child_process'); const semver = require('semver'); const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); +const request = require('../request'); const { paths, pluginNamePattern } = require('../constants'); const pkgInstall = require('./package-install'); @@ -74,11 +74,10 @@ async function getCurrentVersion() { } async function getSuggestedModules(nbbVersion, toCheck) { - let body = await request({ - method: 'GET', - url: `https://packages.nodebb.org/api/v1/suggest?version=${nbbVersion}&package[]=${toCheck.join('&package[]=')}`, - json: true, - }); + let { response, body } = await request.get(`https://packages.nodebb.org/api/v1/suggest?version=${nbbVersion}&package[]=${toCheck.join('&package[]=')}`); + if (!response.ok) { + throw new Error(`Unable to get suggested module for NodeBB(${nbbVersion}) ${toCheck.join(',')}`); + } if (!Array.isArray(body) && toCheck.length === 1) { body = [body]; } diff --git a/src/plugins/index.js b/src/plugins/index.js index be634bb8fa..f3a42aa01a 100644 --- a/src/plugins/index.js +++ b/src/plugins/index.js @@ -6,8 +6,8 @@ const winston = require('winston'); const semver = require('semver'); const nconf = require('nconf'); const chalk = require('chalk'); -const request = require('request-promise-native'); +const request = require('../request'); const user = require('../user'); const posts = require('../posts'); @@ -153,10 +153,10 @@ Plugins.reloadRoutes = async function (params) { Plugins.get = async function (id) { const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins/${id}`; - const body = await request(url, { - json: true, - }); - + const { response, body } = await request.get(url); + if (!response.ok) { + throw new Error(`[[error:unable-to-load-plugin, ${id}]]`); + } let normalised = await Plugins.normalise([body ? body.payload : {}]); normalised = normalised.filter(plugin => plugin.id === id); return normalised.length ? normalised[0] : undefined; @@ -169,9 +169,10 @@ Plugins.list = async function (matching) { const { version } = require(paths.currentPackage); const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins${matching !== false ? `?version=${version}` : ''}`; try { - const body = await request(url, { - json: true, - }); + const { response, body } = await request.get(url); + if (!response.ok) { + throw new Error(`[[error:unable-to-load-plugins-from-nbbpm]]`); + } return await Plugins.normalise(body); } catch (err) { winston.error(`Error loading ${url}`, err); @@ -181,9 +182,11 @@ Plugins.list = async function (matching) { Plugins.listTrending = async () => { const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/analytics/top/week`; - return await request(url, { - json: true, - }); + const { response, body } = await request.get(url); + if (!response.ok) { + throw new Error(`[[error:unable-to-load-trending-plugins]]`); + } + return body; }; Plugins.normalise = async function (apiReturn) { diff --git a/src/plugins/install.js b/src/plugins/install.js index 82b078403e..91a39da76e 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -7,8 +7,8 @@ const nconf = require('nconf'); const os = require('os'); const cproc = require('child_process'); const util = require('util'); -const request = require('request-promise-native'); +const request = require('../request'); const db = require('../database'); const meta = require('../meta'); const pubsub = require('../pubsub'); @@ -74,12 +74,10 @@ module.exports = function (Plugins) { }; Plugins.checkWhitelist = async function (id, version) { - const body = await request({ - method: 'GET', - url: `https://packages.nodebb.org/api/v1/plugins/${encodeURIComponent(id)}`, - json: true, - }); - + const { response, body } = await request.get(`https://packages.nodebb.org/api/v1/plugins/${encodeURIComponent(id)}`); + if (!response.ok) { + throw new Error(`[[error:cant-connect-to-nbbpm]]`); + } if (body && body.code === 'ok' && (version === 'latest' || body.payload.valid.includes(version))) { return; } @@ -88,11 +86,10 @@ module.exports = function (Plugins) { }; Plugins.suggest = async function (pluginId, nbbVersion) { - const body = await request({ - method: 'GET', - url: `https://packages.nodebb.org/api/v1/suggest?package=${encodeURIComponent(pluginId)}&version=${encodeURIComponent(nbbVersion)}`, - json: true, - }); + const { response, body } = await request.get(`https://packages.nodebb.org/api/v1/suggest?package=${encodeURIComponent(pluginId)}&version=${encodeURIComponent(nbbVersion)}`); + if (!response.ok) { + throw new Error(`[[error:cant-connect-to-nbbpm]]`); + } return body; }; diff --git a/src/plugins/usage.js b/src/plugins/usage.js index 43a66f2b54..69e3a44441 100644 --- a/src/plugins/usage.js +++ b/src/plugins/usage.js @@ -1,48 +1,45 @@ 'use strict'; const nconf = require('nconf'); -const request = require('request'); const winston = require('winston'); const crypto = require('crypto'); const cronJob = require('cron').CronJob; +const request = require('../request'); const pkg = require('../../package.json'); const meta = require('../meta'); module.exports = function (Plugins) { Plugins.startJobs = function () { - new cronJob('0 0 0 * * *', (() => { - Plugins.submitUsageData(); + new cronJob('0 0 0 * * *', (async () => { + await Plugins.submitUsageData(); }), null, true); }; - Plugins.submitUsageData = function (callback) { - callback = callback || function () {}; + Plugins.submitUsageData = async function () { if (!meta.config.submitPluginUsage || !Plugins.loadedPlugins.length || global.env !== 'production') { - return callback(); + return; } const hash = crypto.createHash('sha256'); hash.update(nconf.get('url')); - request.post(`${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugin/usage`, { - form: { - id: hash.digest('hex'), - version: pkg.version, - plugins: Plugins.loadedPlugins, - }, - timeout: 5000, - }, (err, res, body) => { - if (err) { - winston.error(err.stack); - return callback(err); + const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugin/usage`; + try { + const { response, body } = await request.post(url, { + body: { + id: hash.digest('hex'), + version: pkg.version, + plugins: Plugins.loadedPlugins, + }, + timeout: 5000, + }); + + if (!response.ok) { + winston.error(`[plugins.submitUsageData] received ${response.status} ${body}`); } - if (res.statusCode !== 200) { - winston.error(`[plugins.submitUsageData] received ${res.statusCode} ${body}`); - callback(new Error(`[[error:nbbpm-${res.statusCode}]]`)); - } else { - callback(); - } - }); + } catch (err) { + winston.error(err.stack); + } }; }; diff --git a/src/request.js b/src/request.js new file mode 100644 index 0000000000..241163f099 --- /dev/null +++ b/src/request.js @@ -0,0 +1,79 @@ +'use strict'; + +const { CookieJar } = require('tough-cookie'); +const fetchCookie = require('fetch-cookie'); + +exports.jar = function () { + return new CookieJar(); +}; + +async function call(url, method, { body, timeout, jar, ...config } = {}) { + let fetchImpl = fetch; + if (jar) { + fetchImpl = fetchCookie(fetch, jar); + } + + const opts = { + ...config, + method, + headers: { + 'content-type': 'application/json', + ...config.headers, + }, + }; + if (timeout > 0) { + opts.signal = AbortSignal.timeout(timeout); + } + + if (body && ['POST', 'PUT', 'PATCH', 'DEL', 'DELETE'].includes(method)) { + if (opts.headers['content-type'] && opts.headers['content-type'].startsWith('application/json')) { + opts.body = JSON.stringify(body); + } else { + opts.body = body; + } + } + + const response = await fetchImpl(url, opts); + + const { headers } = response; + const contentType = headers.get('content-type'); + const isJSON = contentType && contentType.indexOf('application/json') !== -1; + let respBody = await response.text(); + if (isJSON && respBody) { + try { + respBody = JSON.parse(respBody); + } catch (err) { + throw new Error('invalid json in response body', url); + } + } + + return { + body: respBody, + response: { + ok: response.ok, + status: response.status, + statusCode: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + }, + }; +} + +/* +const { body, response } = await request.get('someurl?foo=1&baz=2') +*/ +exports.get = async (url, config) => call(url, 'GET', config); + +exports.head = async (url, config) => call(url, 'HEAD', config); +exports.del = async (url, config) => call(url, 'DELETE', config); +exports.delete = exports.del; +exports.options = async (url, config) => call(url, 'OPTIONS', config); + +/* +const { body, response } = await request.post('someurl', { body: { foo: 1, baz: 2}}) +*/ +exports.post = async (url, config) => call(url, 'POST', config); +exports.put = async (url, config) => call(url, 'PUT', config); +exports.patch = async (url, config) => call(url, 'PATCH', config); + + diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index e5efe90482..6e5093d9a1 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -100,8 +100,8 @@ SocketAdmin.getSearchDict = async function (socket) { return await getAdminSearchDict(lang); }; -SocketAdmin.deleteAllSessions = function (socket, data, callback) { - user.auth.deleteAllSessions(callback); +SocketAdmin.deleteAllSessions = async function () { + await user.auth.deleteAllSessions(); }; SocketAdmin.reloadAllSessions = function (socket, data, callback) { diff --git a/test/api.js b/test/api.js index e3d420c83a..47961742ff 100644 --- a/test/api.js +++ b/test/api.js @@ -5,13 +5,13 @@ const assert = require('assert'); const path = require('path'); const fs = require('fs'); const SwaggerParser = require('@apidevtools/swagger-parser'); -const request = require('request-promise-native'); const nconf = require('nconf'); const jwt = require('jsonwebtoken'); const util = require('util'); const wait = util.promisify(setTimeout); +const request = require('../src/request'); const db = require('./mocks/databasemock'); const helpers = require('./helpers'); const meta = require('../src/meta'); @@ -314,12 +314,7 @@ describe('API', async () => { ({ jar } = await helpers.loginUser('admin', '123456')); // Retrieve CSRF token using cookie, to test Write API - const config = await request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar, - }); - csrfToken = config.csrf_token; + csrfToken = await helpers.getCsrfToken(jar); setup = true; } @@ -409,7 +404,7 @@ describe('API', async () => { paths.forEach((path) => { const context = api.paths[path]; let schema; - let response; + let result; let url; let method; const headers = {}; @@ -498,26 +493,16 @@ describe('API', async () => { try { if (type === 'json') { - response = await request(url, { - method: method, + const searchParams = new URLSearchParams(qs); + result = await request[method](`${url}?${searchParams}`, { jar: !unauthenticatedRoutes.includes(path) ? jar : undefined, - json: true, - followRedirect: false, // all responses are significant (e.g. 302) - simple: false, // don't throw on non-200 (e.g. 302) - resolveWithFullResponse: true, // send full request back (to check statusCode) + maxRedirect: 0, + redirect: 'manual', headers: headers, - qs: qs, body: body, }); } else if (type === 'form') { - response = await new Promise((resolve, reject) => { - helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken, (err, res) => { - if (err) { - return reject(err); - } - resolve(res); - }); - }); + result = await helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken); } } catch (e) { assert(!e, `${method.toUpperCase()} ${path} errored with: ${e.message}`); @@ -526,13 +511,18 @@ describe('API', async () => { it('response status code should match one of the schema defined responses', () => { // HACK: allow HTTP 418 I am a teapot, for now 👇 - assert(context[method].responses.hasOwnProperty('418') || Object.keys(context[method].responses).includes(String(response.statusCode)), `${method.toUpperCase()} ${path} sent back unexpected HTTP status code: ${response.statusCode}`); + const { responses } = context[method]; + assert( + responses.hasOwnProperty('418') || + Object.keys(responses).includes(String(result.response.statusCode)), + `${method.toUpperCase()} ${path} sent back unexpected HTTP status code: ${result.response.statusCode}` + ); }); // Recursively iterate through schema properties, comparing type it('response body should match schema definition', () => { const http302 = context[method].responses['302']; - if (http302 && response.statusCode === 302) { + if (http302 && result.response.statusCode === 302) { // Compare headers instead const expectedHeaders = Object.keys(http302.headers).reduce((memo, name) => { const value = http302.headers[name].schema.example; @@ -541,13 +531,13 @@ describe('API', async () => { }, {}); for (const header of Object.keys(expectedHeaders)) { - assert(response.headers[header.toLowerCase()]); - assert.strictEqual(response.headers[header.toLowerCase()], expectedHeaders[header]); + assert(result.response.headers[header.toLowerCase()]); + assert.strictEqual(result.response.headers[header.toLowerCase()], expectedHeaders[header]); } return; } - if (response.statusCode === 400 && context[method].responses['400']) { + if (result.response.statusCode === 400 && context[method].responses['400']) { // TODO: check 400 schema to response.body? return; } @@ -557,12 +547,12 @@ describe('API', async () => { return; } - assert.strictEqual(response.statusCode, 200, `HTTP 200 expected (path: ${method} ${path}`); + assert.strictEqual(result.response.statusCode, 200, `HTTP 200 expected (path: ${method} ${path}`); const hasJSON = http200.content && http200.content['application/json']; if (hasJSON) { schema = context[method].responses['200'].content['application/json'].schema; - compare(schema, response.body, method.toUpperCase(), path, 'root'); + compare(schema, result.body, method.toUpperCase(), path, 'root'); } // TODO someday: text/csv, binary file type checking? @@ -576,12 +566,7 @@ describe('API', async () => { mocks.delete['/users/{uid}/sessions/{uuid}'][1].example = Object.keys(sessionUUIDs).pop(); // Retrieve CSRF token using cookie, to test Write API - const config = await request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar, - }); - csrfToken = config.csrf_token; + csrfToken = await helpers.getCsrfToken(jar); } }); }); diff --git a/test/authentication.js b/test/authentication.js index 8f0ba9389c..1dcbe176a8 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -3,12 +3,9 @@ const assert = require('assert'); const url = require('url'); -const async = require('async'); const nconf = require('nconf'); -const request = require('request'); -const requestAsync = require('request-promise-native'); -const util = require('util'); +const request = require('../src/request'); const db = require('./mocks/databasemock'); const user = require('../src/user'); const utils = require('../src/utils'); @@ -45,8 +42,8 @@ describe('authentication', () => { it('should allow login with email for uid 1', async () => { const oldValue = meta.config.allowLoginWith; meta.config.allowLoginWith = 'username-email'; - const { res } = await helpers.loginUser('regular@nodebb.org', 'regularpwd'); - assert.strictEqual(res.statusCode, 200); + const { response } = await helpers.loginUser('regular@nodebb.org', 'regularpwd'); + assert.strictEqual(response.statusCode, 200); meta.config.allowLoginWith = oldValue; }); @@ -54,150 +51,112 @@ describe('authentication', () => { const oldValue = meta.config.allowLoginWith; meta.config.allowLoginWith = 'username-email'; const uid = await user.create({ username: '2nduser', password: '2ndpassword', email: '2nduser@nodebb.org' }); - const { res, body } = await helpers.loginUser('2nduser@nodebb.org', '2ndpassword'); - assert.strictEqual(res.statusCode, 403); + const { response, body } = await helpers.loginUser('2nduser@nodebb.org', '2ndpassword'); + assert.strictEqual(response.statusCode, 403); assert.strictEqual(body, '[[error:invalid-login-credentials]]'); meta.config.allowLoginWith = oldValue; }); - it('should fail to create user if username is too short', (done) => { - helpers.registerUser({ + it('should fail to create user if username is too short', async () => { + const { response, body } = await helpers.registerUser({ username: 'a', password: '123456', - }, (err, jar, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 400); - assert.equal(body, '[[error:username-too-short]]'); - done(); }); + assert.equal(response.statusCode, 400); + assert.equal(body, '[[error:username-too-short]]'); }); - it('should fail to create user if userslug is too short', (done) => { - helpers.registerUser({ + it('should fail to create user if userslug is too short', async () => { + const { response, body } = await helpers.registerUser({ username: '----a-----', password: '123456', - }, (err, jar, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 400); - assert.equal(body, '[[error:username-too-short]]'); - done(); }); + assert.equal(response.statusCode, 400); + assert.equal(body, '[[error:username-too-short]]'); }); - it('should fail to create user if userslug is too short', (done) => { - helpers.registerUser({ + it('should fail to create user if userslug is too short', async () => { + const { response, body } = await helpers.registerUser({ username: ' a', password: '123456', - }, (err, jar, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 400); - assert.equal(body, '[[error:username-too-short]]'); - done(); }); + assert.equal(response.statusCode, 400); + assert.equal(body, '[[error:username-too-short]]'); }); - it('should fail to create user if userslug is too short', (done) => { - helpers.registerUser({ + it('should fail to create user if userslug is too short', async () => { + const { response, body } = await helpers.registerUser({ username: 'a ', password: '123456', - }, (err, jar, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 400); - assert.equal(body, '[[error:username-too-short]]'); - done(); }); + assert.equal(response.statusCode, 400); + assert.equal(body, '[[error:username-too-short]]'); }); - it('should register and login a user', (done) => { - request({ - url: `${nconf.get('url')}/api/config`, - json: true, + it('should register and login a user', async () => { + const jar = request.jar(); + const csrf_token = await helpers.getCsrfToken(jar); + + const { body } = await request.post(`${nconf.get('url')}/register`, { + jar, + body: { + email: 'admin@nodebb.org', + username: 'admin', + password: 'adminpwd', + 'password-confirm': 'adminpwd', + userLang: 'it', + gdpr_consent: true, + }, + headers: { + 'x-csrf-token': csrf_token, + }, + }); + + const validationPending = await user.email.isValidationPending(body.uid, 'admin@nodebb.org'); + assert.strictEqual(validationPending, true); + + assert(body); + assert(body.hasOwnProperty('uid') && body.uid > 0); + const newUid = body.uid; + const { body: self } = await request.get(`${nconf.get('url')}/api/self`, { + jar, + }); + assert(self); + assert.equal(self.username, 'admin'); + assert.equal(self.uid, newUid); + const settings = await user.getSettings(body.uid); + assert.equal(settings.userLang, 'it'); + }); + + it('should logout a user', async () => { + await helpers.logoutUser(jar); + + const { response, body } = await request.get(`${nconf.get('url')}/api/me`, { jar: jar, - }, (err, response, body) => { - assert.ifError(err); - - request.post(`${nconf.get('url')}/register`, { - form: { - email: 'admin@nodebb.org', - username: 'admin', - password: 'adminpwd', - 'password-confirm': 'adminpwd', - userLang: 'it', - gdpr_consent: true, - }, - json: true, - jar: jar, - headers: { - 'x-csrf-token': body.csrf_token, - }, - }, async (err, response, body) => { - const validationPending = await user.email.isValidationPending(body.uid, 'admin@nodebb.org'); - assert.strictEqual(validationPending, true); - assert.ifError(err); - assert(body); - assert(body.hasOwnProperty('uid') && body.uid > 0); - const newUid = body.uid; - request({ - url: `${nconf.get('url')}/api/self`, - json: true, - jar: jar, - }, (err, response, body) => { - assert.ifError(err); - assert(body); - assert.equal(body.username, 'admin'); - assert.equal(body.uid, newUid); - user.getSettings(body.uid, (err, settings) => { - assert.ifError(err); - assert.equal(settings.userLang, 'it'); - done(); - }); - }); - }); - }); - }); - - it('should logout a user', (done) => { - helpers.logoutUser(jar, (err) => { - assert.ifError(err); - request({ - url: `${nconf.get('url')}/api/me`, - json: true, - jar: jar, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 401); - assert.strictEqual(body.status.code, 'not-authorised'); - done(); - }); }); + assert.equal(response.statusCode, 401); + assert.strictEqual(body.status.code, 'not-authorised'); }); it('should regenerate the session identifier on successful login', async () => { const matchRegexp = /express\.sid=s%3A(.+?);/; const { hostname, path } = url.parse(nconf.get('url')); - - const sid = String(jar._jar.store.idx[hostname][path]['express.sid']).match(matchRegexp)[1]; + const sid = String(jar.store.idx[hostname][path]['express.sid']).match(matchRegexp)[1]; await helpers.logoutUser(jar); const newJar = (await helpers.loginUser('regular', 'regularpwd')).jar; - const newSid = String(newJar._jar.store.idx[hostname][path]['express.sid']).match(matchRegexp)[1]; + const newSid = String(newJar.store.idx[hostname][path]['express.sid']).match(matchRegexp)[1]; assert.notStrictEqual(newSid, sid); }); - it('should revoke all sessions', (done) => { + + it('should revoke all sessions', async () => { const socketAdmin = require('../src/socket.io/admin'); - db.sortedSetCard(`uid:${regularUid}:sessions`, (err, count) => { - assert.ifError(err); - assert(count); - socketAdmin.deleteAllSessions({ uid: 1 }, {}, (err) => { - assert.ifError(err); - db.sortedSetCard(`uid:${regularUid}:sessions`, (err, count) => { - assert.ifError(err); - assert(!count); - done(); - }); - }); - }); + let sessionCount = await db.sortedSetCard(`uid:${regularUid}:sessions`); + assert(sessionCount); + await socketAdmin.deleteAllSessions({ uid: 1 }, {}); + sessionCount = await db.sortedSetCard(`uid:${regularUid}:sessions`); + assert(!sessionCount); }); describe('login', () => { @@ -205,11 +164,12 @@ describe('authentication', () => { let password; let uid; - function getCookieExpiry(res) { - assert(res.headers['set-cookie']); - assert.strictEqual(res.headers['set-cookie'][0].includes('Expires'), true); + function getCookieExpiry(response) { + const { headers } = response; + assert(headers['set-cookie']); + assert.strictEqual(headers['set-cookie'].includes('Expires'), true); - const values = res.headers['set-cookie'][0].split(';'); + const values = headers['set-cookie'].split(';'); return values.reduce((memo, cur) => { if (!memo) { const [name, value] = cur.split('='); @@ -230,9 +190,7 @@ describe('authentication', () => { it('should login a user', async () => { const { jar, body: loginBody } = await helpers.loginUser(username, password); assert(loginBody); - const body = await requestAsync({ - url: `${nconf.get('url')}/api/self`, - json: true, + const { body } = await request.get(`${nconf.get('url')}/api/self`, { jar, }); assert(body); @@ -243,11 +201,11 @@ describe('authentication', () => { }); it('should set a cookie that only lasts for the life of the browser session', async () => { - const { res } = await helpers.loginUser(username, password); + const { response } = await helpers.loginUser(username, password); - assert(res.headers); - assert(res.headers['set-cookie']); - assert.strictEqual(res.headers['set-cookie'][0].includes('Expires'), false); + assert(response.headers); + assert(response.headers['set-cookie']); + assert.strictEqual(response.headers['set-cookie'].includes('Expires'), false); }); it('should set a different expiry if sessionDuration is set', async () => { @@ -255,9 +213,9 @@ describe('authentication', () => { const days = 1; meta.config.sessionDuration = days * 24 * 60 * 60; - const { res } = await helpers.loginUser(username, password); + const { response } = await helpers.loginUser(username, password); - const expiry = getCookieExpiry(res); + const expiry = getCookieExpiry(response); const expected = new Date(); expected.setUTCDate(expected.getUTCDate() + days); @@ -267,9 +225,9 @@ describe('authentication', () => { }); it('should set a cookie that lasts for x days where x is loginDays setting, if asked to remember', async () => { - const { res } = await helpers.loginUser(username, password, { remember: 'on' }); + const { response } = await helpers.loginUser(username, password, { remember: 'on' }); - const expiry = getCookieExpiry(res); + const expiry = getCookieExpiry(response); const expected = new Date(); expected.setUTCDate(expected.getUTCDate() + meta.config.loginDays); @@ -280,9 +238,9 @@ describe('authentication', () => { const _loginDays = meta.config.loginDays; meta.config.loginDays = 5; - const { res } = await helpers.loginUser(username, password, { remember: 'on' }); + const { response } = await helpers.loginUser(username, password, { remember: 'on' }); - const expiry = getCookieExpiry(res); + const expiry = getCookieExpiry(response); const expected = new Date(); expected.setUTCDate(expected.getUTCDate() + meta.config.loginDays); @@ -295,9 +253,9 @@ describe('authentication', () => { const _loginSeconds = meta.config.loginSeconds; meta.config.loginSeconds = 60; - const { res } = await helpers.loginUser(username, password, { remember: 'on' }); + const { response } = await helpers.loginUser(username, password, { remember: 'on' }); - const expiry = getCookieExpiry(res); + const expiry = getCookieExpiry(response); const expected = new Date(); expected.setUTCSeconds(expected.getUTCSeconds() + meta.config.loginSeconds); @@ -308,158 +266,128 @@ describe('authentication', () => { }); }); - it('should fail to login if ip address is invalid', (done) => { + it('should fail to login if ip address is invalid', async () => { const jar = request.jar(); - request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar, - }, (err, response, body) => { - if (err) { - return done(err); - } + const csrf_token = await helpers.getCsrfToken(jar); - request.post(`${nconf.get('url')}/login`, { - form: { - username: 'regular', - password: 'regularpwd', - }, - json: true, - jar: jar, - headers: { - 'x-csrf-token': body.csrf_token, - 'x-forwarded-for': '', - }, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 500); - done(); - }); + const { response } = await request.post(`${nconf.get('url')}/login`, { + body: { + username: 'regular', + password: 'regularpwd', + }, + jar: jar, + headers: { + 'x-csrf-token': csrf_token, + 'x-forwarded-for': '', + }, }); + assert.equal(response.status, 500); }); it('should fail to login if user does not exist', async () => { - const { res, body } = await helpers.loginUser('doesnotexist', 'nopassword'); - assert.equal(res.statusCode, 403); + const { response, body } = await helpers.loginUser('doesnotexist', 'nopassword'); + assert.equal(response.statusCode, 403); assert.equal(body, '[[error:invalid-login-credentials]]'); }); it('should fail to login if username is empty', async () => { - const { res, body } = await helpers.loginUser('', 'some password'); - assert.equal(res.statusCode, 403); + const { response, body } = await helpers.loginUser('', 'some password'); + assert.equal(response.statusCode, 403); assert.equal(body, '[[error:invalid-username-or-password]]'); }); it('should fail to login if password is empty', async () => { - const { res, body } = await helpers.loginUser('someuser', ''); - assert.equal(res.statusCode, 403); + const { response, body } = await helpers.loginUser('someuser', ''); + assert.equal(response.statusCode, 403); assert.equal(body, '[[error:invalid-username-or-password]]'); }); it('should fail to login if username and password are empty', async () => { - const { res, body } = await helpers.loginUser('', ''); - assert.equal(res.statusCode, 403); + const { response, body } = await helpers.loginUser('', ''); + assert.equal(response.statusCode, 403); assert.equal(body, '[[error:invalid-username-or-password]]'); }); it('should fail to login if user does not have password field in db', async () => { await user.create({ username: 'hasnopassword', email: 'no@pass.org' }); - const { res, body } = await helpers.loginUser('hasnopassword', 'doesntmatter'); - assert.equal(res.statusCode, 403); + const { response, body } = await helpers.loginUser('hasnopassword', 'doesntmatter'); + assert.equal(response.statusCode, 403); assert.equal(body, '[[error:invalid-login-credentials]]'); }); it('should fail to login if password is longer than 4096', async () => { - let longPassword; + let longPassword = ''; for (let i = 0; i < 5000; i++) { longPassword += 'a'; } - const { res, body } = await helpers.loginUser('someuser', longPassword); - assert.equal(res.statusCode, 403); + const { response, body } = await helpers.loginUser('someuser', longPassword); + assert.equal(response.statusCode, 403); assert.equal(body, '[[error:password-too-long]]'); }); it('should fail to login if local login is disabled', async () => { await privileges.global.rescind(['groups:local:login'], 'registered-users'); - const { res, body } = await helpers.loginUser('regular', 'regularpwd'); - assert.equal(res.statusCode, 403); + const { response, body } = await helpers.loginUser('regular', 'regularpwd'); + assert.equal(response.statusCode, 403); assert.equal(body, '[[error:local-login-disabled]]'); await privileges.global.give(['groups:local:login'], 'registered-users'); }); - it('should fail to register if registraton is disabled', (done) => { + it('should fail to register if registraton is disabled', async () => { meta.config.registrationType = 'disabled'; - helpers.registerUser({ + const { response, body } = await helpers.registerUser({ username: 'someuser', password: 'somepassword', - }, (err, jar, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 403); - assert.equal(body, 'Forbidden'); - done(); }); + assert.equal(response.statusCode, 403); + assert.equal(body, 'Forbidden'); }); - it('should return error if invitation is not valid', (done) => { + it('should return error if invitation is not valid', async () => { meta.config.registrationType = 'invite-only'; - helpers.registerUser({ + const { response, body } = await helpers.registerUser({ username: 'someuser', password: 'somepassword', - }, (err, jar, response, body) => { - meta.config.registrationType = 'normal'; - assert.ifError(err); - assert.equal(response.statusCode, 400); - assert.equal(body, '[[register:invite.error-invite-only]]'); - done(); }); + meta.config.registrationType = 'normal'; + assert.equal(response.statusCode, 400); + assert.equal(body, '[[register:invite.error-invite-only]]'); }); - it('should fail to register if username is falsy or too short', (done) => { - helpers.registerUser({ - username: '', - password: 'somepassword', - }, (err, jar, response, body) => { - assert.ifError(err); + it('should fail to register if username is falsy or too short', async () => { + const userData = [ + { username: '', password: 'somepassword' }, + { username: 'a', password: 'somepassword' }, + ]; + for (const user of userData) { + // eslint-disable-next-line no-await-in-loop + const { response, body } = await helpers.registerUser(user); assert.equal(response.statusCode, 400); assert.equal(body, '[[error:username-too-short]]'); - helpers.registerUser({ - username: 'a', - password: 'somepassword', - }, (err, jar, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 400); - assert.equal(body, '[[error:username-too-short]]'); - done(); - }); - }); + } }); - it('should fail to register if username is too long', (done) => { - helpers.registerUser({ + it('should fail to register if username is too long', async () => { + const { response, body } = await helpers.registerUser({ username: 'thisisareallylongusername', password: '123456', - }, (err, jar, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 400); - assert.equal(body, '[[error:username-too-long]]'); - done(); }); + + assert.equal(response.statusCode, 400); + assert.equal(body, '[[error:username-too-long]]'); }); - it('should queue user if ip is used before', (done) => { + it('should queue user if ip is used before', async () => { meta.config.registrationApprovalType = 'admin-approval-ip'; - helpers.registerUser({ + const { response, body } = await helpers.registerUser({ email: 'another@user.com', username: 'anotheruser', password: 'anotherpwd', gdpr_consent: 1, - }, (err, jar, response, body) => { - meta.config.registrationApprovalType = 'normal'; - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert.equal(body.message, '[[register:registration-added-to-queue]]'); - done(); }); + meta.config.registrationApprovalType = 'normal'; + assert.equal(response.statusCode, 200); + assert.equal(body.message, '[[register:registration-added-to-queue]]'); }); @@ -468,41 +396,32 @@ describe('authentication', () => { const uid = await user.create({ username: 'ginger', password: '123456', email }); await user.setUserField(uid, 'email', email); await user.email.confirmByUid(uid); - const { res } = await helpers.loginUser('ginger@nodebb.org', '123456'); - assert.equal(res.statusCode, 200); + const { response } = await helpers.loginUser('ginger@nodebb.org', '123456'); + assert.equal(response.statusCode, 200); }); it('should fail to login if login type is username and an email is sent', async () => { meta.config.allowLoginWith = 'username'; - const { res, body } = await helpers.loginUser('ginger@nodebb.org', '123456'); + const { response, body } = await helpers.loginUser('ginger@nodebb.org', '123456'); meta.config.allowLoginWith = 'username-email'; - assert.equal(res.statusCode, 400); + assert.equal(response.statusCode, 400); assert.equal(body, '[[error:wrong-login-type-username]]'); }); - it('should send 200 if not logged in', (done) => { + it('should send 200 if not logged in', async () => { const jar = request.jar(); - request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar, - }, (err, response, body) => { - assert.ifError(err); + const csrf_token = await helpers.getCsrfToken(jar); - request.post(`${nconf.get('url')}/logout`, { - form: {}, - json: true, - jar: jar, - headers: { - 'x-csrf-token': body.csrf_token, - }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body, 'not-logged-in'); - done(); - }); + const { response, body } = await request.post(`${nconf.get('url')}/logout`, { + data: {}, + jar: jar, + headers: { + 'x-csrf-token': csrf_token, + }, }); + + assert.equal(response.statusCode, 200); + assert.equal(body, 'not-logged-in'); }); describe('banned user authentication', () => { @@ -518,7 +437,7 @@ describe('authentication', () => { it('should prevent banned user from logging in', async () => { await user.bans.ban(bannedUser.uid, 0, 'spammer'); - const { res: res1, body: body1 } = await helpers.loginUser(bannedUser.username, bannedUser.pw); + const { response: res1, body: body1 } = await helpers.loginUser(bannedUser.username, bannedUser.pw); assert.equal(res1.statusCode, 403); delete body1.timestamp; assert.deepStrictEqual(body1, { @@ -532,7 +451,7 @@ describe('authentication', () => { await user.bans.unban(bannedUser.uid); const expiry = Date.now() + 10000; await user.bans.ban(bannedUser.uid, expiry, ''); - const { res: res2, body: body2 } = await helpers.loginUser(bannedUser.username, bannedUser.pw); + const { response: res2, body: body2 } = await helpers.loginUser(bannedUser.username, bannedUser.pw); assert.equal(res2.statusCode, 403); assert(body2.banned_until); assert(body2.reason, '[[user:info.banned-no-reason]]'); @@ -540,15 +459,15 @@ describe('authentication', () => { it('should allow banned user to log in if the "banned-users" group has "local-login" privilege', async () => { await privileges.global.give(['groups:local:login'], 'banned-users'); - const { res } = await helpers.loginUser(bannedUser.username, bannedUser.pw); - assert.strictEqual(res.statusCode, 200); + const { response } = await helpers.loginUser(bannedUser.username, bannedUser.pw); + assert.strictEqual(response.statusCode, 200); }); it('should allow banned user to log in if the user herself has "local-login" privilege', async () => { await privileges.global.rescind(['groups:local:login'], 'banned-users'); await privileges.categories.give(['local:login'], 0, bannedUser.uid); - const { res } = await helpers.loginUser(bannedUser.username, bannedUser.pw); - assert.strictEqual(res.statusCode, 200); + const { response } = await helpers.loginUser(bannedUser.username, bannedUser.pw); + assert.strictEqual(response.statusCode, 200); }); }); @@ -561,10 +480,10 @@ describe('authentication', () => { let data = await helpers.loginUser('lockme', 'abcdef'); meta.config.loginAttempts = 5; - assert.equal(data.res.statusCode, 403); + assert.equal(data.response.statusCode, 403); assert.equal(data.body, '[[error:account-locked]]'); data = await helpers.loginUser('lockme', 'abcdef'); - assert.equal(data.res.statusCode, 403); + assert.equal(data.response.statusCode, 403); assert.equal(data.body, '[[error:account-locked]]'); const locked = await db.exists(`lockout:${uid}`); assert(locked); @@ -594,57 +513,46 @@ describe('authentication', () => { }); it('should fail with invalid token', async () => { - const { res, body } = await helpers.request('get', `/api/self`, { - form: { - _uid: newUid, - }, - json: true, + const { response, body } = await helpers.request('get', `/api/self?_uid${newUid}`, { jar: jar, headers: { Authorization: `Bearer sdfhaskfdja-jahfdaksdf`, }, }); - assert.strictEqual(res.statusCode, 401); + assert.strictEqual(response.statusCode, 401); assert.strictEqual(body, 'not-authorized'); }); it('should use a token tied to an uid', async () => { - const { res, body } = await helpers.request('get', `/api/self`, { - json: true, + const { response, body } = await helpers.request('get', `/api/self`, { headers: { Authorization: `Bearer ${userToken}`, }, }); - assert.strictEqual(res.statusCode, 200); + assert.strictEqual(response.statusCode, 200); assert.strictEqual(body.username, 'apiUserTarget'); }); it('should fail if _uid is not passed in with master token', async () => { - const { res, body } = await helpers.request('get', `/api/self`, { - form: {}, - json: true, + const { response, body } = await helpers.request('get', `/api/self`, { headers: { Authorization: `Bearer ${masterToken}`, }, }); - assert.strictEqual(res.statusCode, 500); + assert.strictEqual(response.statusCode, 500); assert.strictEqual(body.error, '[[error:api.master-token-no-uid]]'); }); it('should use master api token and _uid', async () => { - const { res, body } = await helpers.request('get', `/api/self`, { - form: { - _uid: newUid, - }, - json: true, + const { response, body } = await helpers.request('get', `/api/self?_uid=${newUid}`, { headers: { Authorization: `Bearer ${masterToken}`, }, }); - assert.strictEqual(res.statusCode, 200); + assert.strictEqual(response.statusCode, 200); assert.strictEqual(body.username, 'apiUserTarget'); }); }); diff --git a/test/categories.js b/test/categories.js index 7ce3fd41c4..c81323d9a2 100644 --- a/test/categories.js +++ b/test/categories.js @@ -2,8 +2,8 @@ const assert = require('assert'); const nconf = require('nconf'); -const request = require('request'); +const request = require('../src/request'); const db = require('./mocks/databasemock'); const Categories = require('../src/categories'); const Topics = require('../src/topics'); @@ -76,14 +76,11 @@ describe('Categories', () => { }); }); - it('should load a category route', (done) => { - request(`${nconf.get('url')}/api/category/${categoryObj.cid}/test-category`, { json: true }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert.equal(body.name, 'Test Category & NodeBB'); - assert(body); - done(); - }); + it('should load a category route', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/category/${categoryObj.cid}/test-category`); + assert.equal(response.statusCode, 200); + assert.equal(body.name, 'Test Category & NodeBB'); + assert(body); }); describe('Categories.getRecentTopicReplies', () => { diff --git a/test/controllers-admin.js b/test/controllers-admin.js index 8578f0c52a..7760bf128e 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -3,9 +3,8 @@ const async = require('async'); const assert = require('assert'); const nconf = require('nconf'); -const request = require('request'); -const requestAsync = require('request-promise-native'); +const request = require('../src/request'); const db = require('./mocks/databasemock'); const categories = require('../src/categories'); const topics = require('../src/topics'); @@ -68,600 +67,427 @@ describe('Admin Controllers', () => { it('should 403 if user is not admin', async () => { ({ jar } = await helpers.loginUser('admin', 'barbar')); - const { statusCode, body } = await requestAsync(`${nconf.get('url')}/admin`, { + const { response, body } = await request.get(`${nconf.get('url')}/admin`, { jar: jar, - simple: false, - resolveWithFullResponse: true, }); - assert.equal(statusCode, 403); + assert.equal(response.statusCode, 403); assert(body); }); - it('should load admin dashboard', (done) => { - groups.join('administrators', adminUid, (err) => { - assert.ifError(err); - const dashboards = [ - '/admin', '/admin/dashboard/logins', '/admin/dashboard/users', '/admin/dashboard/topics', '/admin/dashboard/searches', - ]; - async.each(dashboards, (url, next) => { - request(`${nconf.get('url')}${url}`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200, url); - assert(body); - - next(); - }); - }, done); - }); - }); - - it('should load admin analytics', (done) => { - request(`${nconf.get('url')}/api/admin/analytics?units=hours`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); + it('should load admin dashboard', async () => { + await groups.join('administrators', adminUid); + const dashboards = [ + '/admin', '/admin/dashboard/logins', '/admin/dashboard/users', '/admin/dashboard/topics', '/admin/dashboard/searches', + ]; + await async.each(dashboards, async (url) => { + const { response, body } = await request.get(`${nconf.get('url')}${url}`, { jar: jar }); + assert.equal(response.statusCode, 200, url); assert(body); - assert(body.query); - assert(body.result); - done(); }); }); - it('should load groups page', (done) => { - request(`${nconf.get('url')}/admin/manage/groups`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load admin analytics', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/analytics?units=hours`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); + assert(body.query); + assert(body.result); }); - it('should load groups detail page', (done) => { - request(`${nconf.get('url')}/admin/manage/groups/administrators`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load groups page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/manage/groups`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load global privileges page', (done) => { - request(`${nconf.get('url')}/admin/manage/privileges`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load groups detail page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/manage/groups/administrators`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load admin privileges page', (done) => { - request(`${nconf.get('url')}/admin/manage/privileges/admin`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load global privileges page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/manage/privileges`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load privileges page for category 1', (done) => { - request(`${nconf.get('url')}/admin/manage/privileges/1`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load admin privileges page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/manage/privileges/admin`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load manage digests', (done) => { - request(`${nconf.get('url')}/admin/manage/digest`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load privileges page for category 1', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/manage/privileges/1`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load manage uploads', (done) => { - request(`${nconf.get('url')}/admin/manage/uploads`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load manage digests', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/manage/digest`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load general settings page', (done) => { - request(`${nconf.get('url')}/admin/settings`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load manage uploads', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/manage/uploads`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load email settings page', (done) => { - request(`${nconf.get('url')}/admin/settings/email`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load general settings page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/settings`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load user settings page', (done) => { - request(`${nconf.get('url')}/admin/settings/user`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load email settings page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/settings/email`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load info page for a user', (done) => { - request(`${nconf.get('url')}/api/user/regular/info`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.history); - assert(Array.isArray(body.history.flags)); - assert(Array.isArray(body.history.bans)); - assert(Array.isArray(body.sessions)); - done(); - }); + it('should load user settings page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/settings/user`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should 404 for edit/email page if user does not exist', (done) => { - request(`${nconf.get('url')}/api/user/doesnotexist/edit/email`, { jar: jar, json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should load info page for a user', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/regular/info`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body.history); + assert(Array.isArray(body.history.flags)); + assert(Array.isArray(body.history.bans)); + assert(Array.isArray(body.sessions)); }); - it('should load /admin/settings/homepage', (done) => { - request(`${nconf.get('url')}/api/admin/settings/general`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.routes); - done(); - }); + it('should 404 for edit/email page if user does not exist', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/user/doesnotexist/edit/email`, { jar }); + assert.equal(response.statusCode, 404); }); - it('should load /admin/advanced/database', (done) => { - request(`${nconf.get('url')}/api/admin/advanced/database`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - - if (nconf.get('redis')) { - assert(body.redis); - } else if (nconf.get('mongo')) { - assert(body.mongo); - } else if (nconf.get('postgres')) { - assert(body.postgres); - } - done(); - }); + it('should load /admin/settings/homepage', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/settings/general`, { jar: jar, json: true }); + assert.equal(response.statusCode, 200); + assert(body.routes); }); - it('should load /admin/extend/plugins', function (done) { + it('should load /admin/advanced/database', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/database`, { jar: jar, json: true }); + + assert.equal(response.statusCode, 200); + + if (nconf.get('redis')) { + assert(body.redis); + } else if (nconf.get('mongo')) { + assert(body.mongo); + } else if (nconf.get('postgres')) { + assert(body.postgres); + } + }); + + it('should load /admin/extend/plugins', async function () { this.timeout(50000); - request(`${nconf.get('url')}/api/admin/extend/plugins`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert(body.hasOwnProperty('installed')); - assert(body.hasOwnProperty('upgradeCount')); - assert(body.hasOwnProperty('download')); - assert(body.hasOwnProperty('incompatible')); - done(); - }); + const { body } = await request.get(`${nconf.get('url')}/api/admin/extend/plugins`, { jar: jar }); + + assert(body.hasOwnProperty('installed')); + assert(body.hasOwnProperty('upgradeCount')); + assert(body.hasOwnProperty('download')); + assert(body.hasOwnProperty('incompatible')); }); - it('should load /admin/manage/users', (done) => { - request(`${nconf.get('url')}/api/admin/manage/users`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body); - assert(body.users.length > 0); - done(); - }); + it('should load /admin/manage/users', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/users`, { jar: jar, json: true }); + assert.strictEqual(response.statusCode, 200); + assert(body); + assert(body.users.length > 0); }); - it('should load /admin/manage/users?filters=banned', (done) => { - request(`${nconf.get('url')}/api/admin/manage/users?filters=banned`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body); - assert.strictEqual(body.users.length, 0); - done(); - }); + it('should load /admin/manage/users?filters=banned', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/users?filters=banned`, { jar: jar, json: true }); + assert.strictEqual(response.statusCode, 200); + assert(body); + assert.strictEqual(body.users.length, 0); }); - it('should load /admin/manage/users?filters=banned&filters=verified', (done) => { - request(`${nconf.get('url')}/api/admin/manage/users?filters=banned&filters=verified`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body); - assert.strictEqual(body.users.length, 0); - done(); - }); + it('should load /admin/manage/users?filters=banned&filters=verified', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/users?filters=banned&filters=verified`, { jar: jar, json: true }); + assert.strictEqual(response.statusCode, 200); + assert(body); + assert.strictEqual(body.users.length, 0); }); - it('should load /admin/manage/users?query=admin', (done) => { - request(`${nconf.get('url')}/api/admin/manage/users?query=admin`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body); - assert.strictEqual(body.users[0].username, 'admin'); - done(); - }); + it('should load /admin/manage/users?query=admin', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/users?query=admin`, { jar: jar, json: true }); + assert.strictEqual(response.statusCode, 200); + assert(body); + assert.strictEqual(body.users[0].username, 'admin'); }); - it('should return empty results if query is too short', (done) => { - request(`${nconf.get('url')}/api/admin/manage/users?query=a`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body); - assert.strictEqual(body.users.length, 0); - done(); - }); + it('should return empty results if query is too short', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/users?query=a`, { jar: jar }); + assert.strictEqual(response.statusCode, 200); + assert(body); + assert.strictEqual(body.users.length, 0); }); - it('should load /admin/manage/registration', (done) => { - request(`${nconf.get('url')}/api/admin/manage/registration`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/manage/registration', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/registration`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should 404 if users is not privileged', (done) => { - request(`${nconf.get('url')}/api/registration-queue`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + it('should 404 if users is not privileged', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/registration-queue`); + assert.equal(response.statusCode, 404); + assert(body); }); - it('should load /api/registration-queue', (done) => { - request(`${nconf.get('url')}/api/registration-queue`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /api/registration-queue', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/registration-queue`, { jar: jar, json: true }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/manage/admins-mods', (done) => { - request(`${nconf.get('url')}/api/admin/manage/admins-mods`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/manage/admins-mods', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/admins-mods`, { jar: jar, json: true }); + assert.equal(response.statusCode, 200); + assert(body); }); it('should load /admin/users/csv', (done) => { const socketAdmin = require('../src/socket.io/admin'); socketAdmin.user.exportUsersCSV({ uid: adminUid }, {}, (err) => { assert.ifError(err); - setTimeout(() => { - request(`${nconf.get('url')}/api/admin/users/csv`, { + setTimeout(async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/users/csv`, { jar: jar, headers: { referer: `${nconf.get('url')}/admin/manage/users`, }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); }); + assert.equal(response.statusCode, 200); + assert(body); + done(); }, 2000); }); }); - it('should return 403 if no referer', (done) => { - request(`${nconf.get('url')}/api/admin/groups/administrators/csv`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 403); - assert.equal(body, '[[error:invalid-origin]]'); - done(); - }); + it('should return 403 if no referer', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/groups/administrators/csv`, { jar }); + assert.equal(response.statusCode, 403); + assert.equal(body, '[[error:invalid-origin]]'); }); - it('should return 403 if referer is not /api/admin/groups/administrators/csv', (done) => { - request(`${nconf.get('url')}/api/admin/groups/administrators/csv`, { + it('should return 403 if referer is not /api/admin/groups/administrators/csv', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/groups/administrators/csv`, { jar: jar, headers: { referer: '/topic/1/test', }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 403); - assert.equal(body, '[[error:invalid-origin]]'); - done(); }); + assert.equal(response.statusCode, 403); + assert.equal(body, '[[error:invalid-origin]]'); }); - it('should load /api/admin/groups/administrators/csv', (done) => { - request(`${nconf.get('url')}/api/admin/groups/administrators/csv`, { + it('should load /api/admin/groups/administrators/csv', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/groups/administrators/csv`, { jar: jar, headers: { referer: `${nconf.get('url')}/admin/manage/groups`, }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/advanced/hooks', (done) => { - request(`${nconf.get('url')}/api/admin/advanced/hooks`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/advanced/hooks', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/hooks`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/advanced/cache', (done) => { - request(`${nconf.get('url')}/api/admin/advanced/cache`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/advanced/cache', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/cache`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /api/admin/advanced/cache/dump and 404 with no query param', (done) => { - request(`${nconf.get('url')}/api/admin/advanced/cache/dump`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + it('should load /api/admin/advanced/cache/dump and 404 with no query param', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/cache/dump`, { jar }); + assert.equal(response.statusCode, 404); + assert(body); }); - it('should load /api/admin/advanced/cache/dump', (done) => { - request(`${nconf.get('url')}/api/admin/advanced/cache/dump?name=post`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /api/admin/advanced/cache/dump', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/cache/dump?name=post`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/advanced/errors', (done) => { - request(`${nconf.get('url')}/api/admin/advanced/errors`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/advanced/errors', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/errors`, { jar: jar, json: true }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/advanced/errors/export', (done) => { - meta.errors.clear((err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/admin/advanced/errors/export`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.strictEqual(body, ''); - done(); - }); - }); + it('should load /admin/advanced/errors/export', async () => { + await meta.errors.clear(); + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/errors/export`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert.strictEqual(body, ''); }); - it('should load /admin/advanced/logs', (done) => { + it('should load /admin/advanced/logs', async () => { const fs = require('fs'); - fs.appendFile(meta.logs.path, 'dummy log', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/admin/advanced/logs`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + await fs.promises.appendFile(meta.logs.path, 'dummy log'); + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/logs`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/settings/navigation', (done) => { + it('should load /admin/settings/navigation', async () => { const navigation = require('../src/navigation/admin'); const data = require('../install/data/navigation.json'); + await navigation.save(data); - navigation.save(data, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/admin/settings/navigation`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert(body); - assert(body.available); - assert(body.enabled); - done(); - }); - }); + const { body } = await request.get(`${nconf.get('url')}/api/admin/settings/navigation`, { jar }); + assert(body); + assert(body.available); + assert(body.enabled); }); - it('should load /admin/development/info', (done) => { - request(`${nconf.get('url')}/api/admin/development/info`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/development/info', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/development/info`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/development/logger', (done) => { - request(`${nconf.get('url')}/api/admin/development/logger`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/development/logger', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/development/logger`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/advanced/events', (done) => { - request(`${nconf.get('url')}/api/admin/advanced/events`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/advanced/events', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/events`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/manage/categories', (done) => { - request(`${nconf.get('url')}/api/admin/manage/categories`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/manage/categories', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/categories`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/manage/categories/1', (done) => { - request(`${nconf.get('url')}/api/admin/manage/categories/1`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/manage/categories/1', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/categories/1`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); it('should load /admin/manage/catgories?cid=', async () => { const { cid: rootCid } = await categories.create({ name: 'parent category' }); const { cid: childCid } = await categories.create({ name: 'child category', parentCid: rootCid }); - const { res, body } = await helpers.request('get', `/api/admin/manage/categories?cid=${rootCid}`, { + const { response, body } = await helpers.request('get', `/api/admin/manage/categories?cid=${rootCid}`, { jar: jar, json: true, }); - assert.strictEqual(res.statusCode, 200); + assert.strictEqual(response.statusCode, 200); assert.strictEqual(body.categoriesTree[0].cid, rootCid); assert.strictEqual(body.categoriesTree[0].children[0].cid, childCid); assert.strictEqual(body.breadcrumbs[0].text, '[[admin/manage/categories:top-level]]'); assert.strictEqual(body.breadcrumbs[1].text, 'parent category'); }); - it('should load /admin/manage/categories/1/analytics', (done) => { - request(`${nconf.get('url')}/api/admin/manage/categories/1/analytics`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/manage/categories/1/analytics', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/categories/1/analytics`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/extend/rewards', (done) => { - request(`${nconf.get('url')}/api/admin/extend/rewards`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/extend/rewards', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/extend/rewards`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/extend/widgets', (done) => { - request(`${nconf.get('url')}/api/admin/extend/widgets`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/extend/widgets', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/extend/widgets`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/settings/languages', (done) => { - request(`${nconf.get('url')}/api/admin/settings/languages`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/settings/languages', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/settings/languages`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/settings/social', (done) => { - request(`${nconf.get('url')}/api/admin/settings/general`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert(body); - body = body.postSharing.map(network => network && network.id); - assert(body.includes('facebook')); - assert(body.includes('twitter')); - done(); - }); + it('should load /admin/settings/social', async () => { + const { body } = await request.get(`${nconf.get('url')}/api/admin/settings/general`, { jar }); + assert(body); + const sharing = body.postSharing.map(network => network && network.id); + assert(sharing.includes('facebook')); + assert(sharing.includes('twitter')); + assert(sharing.includes('linkedin')); + assert(sharing.includes('whatsapp')); + assert(sharing.includes('telegram')); }); - it('should load /admin/manage/tags', (done) => { - request(`${nconf.get('url')}/api/admin/manage/tags`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/manage/tags', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/tags`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('/post-queue should 404 for regular user', (done) => { - request(`${nconf.get('url')}/api/post-queue`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert(body); - assert.equal(res.statusCode, 404); - done(); - }); + it('/post-queue should 404 for regular user', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/post-queue`); + assert(body); + assert.equal(response.statusCode, 404); }); - it('should load /post-queue', (done) => { - request(`${nconf.get('url')}/api/post-queue`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /post-queue', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('/ip-blacklist should 404 for regular user', (done) => { - request(`${nconf.get('url')}/api/ip-blacklist`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert(body); - assert.equal(res.statusCode, 404); - done(); - }); + it('/ip-blacklist should 404 for regular user', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/ip-blacklist`); + assert(body); + assert.equal(response.statusCode, 404); }); - it('should load /ip-blacklist', (done) => { - request(`${nconf.get('url')}/api/ip-blacklist`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /ip-blacklist', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/ip-blacklist`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/appearance/themes', (done) => { - request(`${nconf.get('url')}/api/admin/appearance/themes`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/appearance/themes', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/appearance/themes`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /admin/appearance/customise', (done) => { - request(`${nconf.get('url')}/api/admin/appearance/customise`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /admin/appearance/customise', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin/appearance/customise`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /recent in maintenance mode', (done) => { + it('should load /recent in maintenance mode', async () => { meta.config.maintenanceMode = 1; - request(`${nconf.get('url')}/api/recent`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - meta.config.maintenanceMode = 0; - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/api/recent`, { jar }); + assert.equal(response.statusCode, 200); + meta.config.maintenanceMode = 0; + assert(body); }); describe('mods page', () => { @@ -673,56 +499,46 @@ describe('Admin Controllers', () => { await groups.join(`cid:${cid}:privileges:moderate`, moderatorUid); }); - it('should error with no privileges', (done) => { - request(`${nconf.get('url')}/api/flags`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.deepStrictEqual(body, { - status: { - code: 'not-authorised', - message: 'A valid login session was not found. Please log in and try again.', - }, - response: {}, - }); - done(); + it('should error with no privileges', async () => { + const { body } = await request.get(`${nconf.get('url')}/api/flags`); + + assert.deepStrictEqual(body, { + status: { + code: 'not-authorised', + message: 'A valid login session was not found. Please log in and try again.', + }, + response: {}, }); }); - it('should load flags page data', (done) => { - request(`${nconf.get('url')}/api/flags`, { jar: moderatorJar, json: true }, (err, res, body) => { - assert.ifError(err); - assert(body); - assert(body.flags); - assert(body.filters); - assert.equal(body.filters.cid.indexOf(cid), -1); - done(); - }); + it('should load flags page data', async () => { + const { body } = await request.get(`${nconf.get('url')}/api/flags`, { jar: moderatorJar }); + assert(body); + assert(body.flags); + assert(body.filters); + assert.equal(body.filters.cid.indexOf(cid), -1); }); - it('should return a 404 if flag does not exist', (done) => { - request(`${nconf.get('url')}/api/flags/123123123`, { + it('should return a 404 if flag does not exist', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/flags/123123123`, { jar: moderatorJar, - json: true, headers: { Accept: 'text/html, application/json', }, - }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 404); - done(); }); + assert.strictEqual(response.statusCode, 404); }); it('should error when you attempt to flag a privileged user\'s post', async () => { - const { res, body } = await helpers.request('post', '/api/v3/flags', { - json: true, + const { response, body } = await helpers.request('post', '/api/v3/flags', { jar: regularJar, - form: { + body: { id: pid, type: 'post', reason: 'spam', }, }); - assert.strictEqual(res.statusCode, 400); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.code, 'bad-request'); assert.strictEqual(body.status.message, 'You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)'); }); @@ -730,16 +546,15 @@ describe('Admin Controllers', () => { it('should error with not enough reputation to flag', async () => { const oldValue = meta.config['min:rep:flag']; meta.config['min:rep:flag'] = 1000; - const { res, body } = await helpers.request('post', '/api/v3/flags', { - json: true, + const { response, body } = await helpers.request('post', '/api/v3/flags', { jar: regularJar, - form: { + body: { id: regularPid, type: 'post', reason: 'spam', }, }); - assert.strictEqual(res.statusCode, 400); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.code, 'bad-request'); assert.strictEqual(body.status.message, 'You need 1000 reputation to flag this post'); @@ -749,10 +564,9 @@ describe('Admin Controllers', () => { it('should return flag details', async () => { const oldValue = meta.config['min:rep:flag']; meta.config['min:rep:flag'] = 0; - const result = await helpers.request('post', '/api/v3/flags', { - json: true, + await helpers.request('post', '/api/v3/flags', { jar: regularJar, - form: { + body: { id: regularPid, type: 'post', reason: 'spam', @@ -770,7 +584,6 @@ describe('Admin Controllers', () => { const { flagId } = flagsResult.body.flags[0]; const { body } = await helpers.request('get', `/api/flags/${flagId}`, { - json: true, jar: moderatorJar, }); assert(body.reports); @@ -779,7 +592,7 @@ describe('Admin Controllers', () => { }); }); - it('should escape special characters in config', (done) => { + it('should escape special characters in config', async () => { const plugins = require('../src/plugins'); function onConfigGet(config, callback) { config.someValue = '"foo"'; @@ -788,46 +601,34 @@ describe('Admin Controllers', () => { callback(null, config); } plugins.hooks.register('somePlugin', { hook: 'filter:config.get', method: onConfigGet }); - request(`${nconf.get('url')}/admin`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(body.includes('"someValue":"\\\\"foo\\\\""')); - assert(body.includes('"otherValue":"\\\'123\\\'"')); - assert(body.includes('"script":"<\\/script>"')); - request(nconf.get('url'), { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(body.includes('"someValue":"\\\\"foo\\\\""')); - assert(body.includes('"otherValue":"\\\'123\\\'"')); - assert(body.includes('"script":"<\\/script>"')); - plugins.hooks.unregister('somePlugin', 'filter:config.get', onConfigGet); - done(); - }); - }); + const { response, body } = await request.get(`${nconf.get('url')}/admin`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); + assert(body.includes('"someValue":"\\\\"foo\\\\""')); + assert(body.includes('"otherValue":"\\\'123\\\'"')); + assert(body.includes('"script":"<\\/script>"')); + const { response: res2, body: body2 } = await request.get(nconf.get('url'), { jar }); + assert.equal(res2.statusCode, 200); + assert(body2); + assert(body2.includes('"someValue":"\\\\"foo\\\\""')); + assert(body2.includes('"otherValue":"\\\'123\\\'"')); + assert(body2.includes('"script":"<\\/script>"')); + plugins.hooks.unregister('somePlugin', 'filter:config.get', onConfigGet); }); describe('admin page privileges', () => { - let userJar; let uid; const privileges = require('../src/privileges'); + const requestOpts = {}; before(async () => { uid = await user.create({ username: 'regularjoe', password: 'barbar' }); - userJar = (await helpers.loginUser('regularjoe', 'barbar')).jar; + requestOpts.jar = (await helpers.loginUser('regularjoe', 'barbar')).jar; }); describe('routeMap parsing', () => { it('should allow normal user access to admin pages', async function () { this.timeout(50000); - function makeRequest(url) { - return new Promise((resolve, reject) => { - request(url, { jar: userJar, json: true }, (err, res, body) => { - if (err) reject(err); - else resolve(res); - }); - }); - } + const uploadRoutes = [ 'category/uploadpicture', 'uploadfavicon', @@ -842,11 +643,11 @@ describe('Admin Controllers', () => { for (const route of adminRoutes) { /* eslint-disable no-await-in-loop */ await privileges.admin.rescind([privileges.admin.routeMap[route]], uid); - let res = await makeRequest(`${nconf.get('url')}/api/admin/${route}`); + let { response: res } = await request.get(`${nconf.get('url')}/api/admin/${route}`, requestOpts); assert.strictEqual(res.statusCode, 403); await privileges.admin.give([privileges.admin.routeMap[route]], uid); - res = await makeRequest(`${nconf.get('url')}/api/admin/${route}`); + ({ response: res } = await request.get(`${nconf.get('url')}/api/admin/${route}`, requestOpts)); assert.strictEqual(res.statusCode, 200); await privileges.admin.rescind([privileges.admin.routeMap[route]], uid); @@ -855,11 +656,11 @@ describe('Admin Controllers', () => { for (const route of adminRoutes) { /* eslint-disable no-await-in-loop */ await privileges.admin.rescind([privileges.admin.routeMap[route]], uid); - let res = await makeRequest(`${nconf.get('url')}/api/admin`); + let { response: res } = await await request.get(`${nconf.get('url')}/api/admin`, requestOpts); assert.strictEqual(res.statusCode, 403); await privileges.admin.give([privileges.admin.routeMap[route]], uid); - res = await makeRequest(`${nconf.get('url')}/api/admin`); + ({ response: res } = await await request.get(`${nconf.get('url')}/api/admin`, requestOpts)); assert.strictEqual(res.statusCode, 200); await privileges.admin.rescind([privileges.admin.routeMap[route]], uid); @@ -869,23 +670,14 @@ describe('Admin Controllers', () => { describe('routePrefixMap parsing', () => { it('should allow normal user access to admin pages', async () => { - // this.timeout(50000); - function makeRequest(url) { - return new Promise((resolve, reject) => { - request(url, { jar: userJar, json: true }, (err, res, body) => { - if (err) reject(err); - else resolve(res); - }); - }); - } for (const route of Object.keys(privileges.admin.routePrefixMap)) { /* eslint-disable no-await-in-loop */ await privileges.admin.rescind([privileges.admin.routePrefixMap[route]], uid); - let res = await makeRequest(`${nconf.get('url')}/api/admin/${route}foobar/derp`); + let { response: res } = await request.get(`${nconf.get('url')}/api/admin/${route}foobar/derp`, requestOpts); assert.strictEqual(res.statusCode, 403); await privileges.admin.give([privileges.admin.routePrefixMap[route]], uid); - res = await makeRequest(`${nconf.get('url')}/api/admin/${route}foobar/derp`); + ({ response: res } = await request.get(`${nconf.get('url')}/api/admin/${route}foobar/derp`, requestOpts)); assert.strictEqual(res.statusCode, 404); await privileges.admin.rescind([privileges.admin.routePrefixMap[route]], uid); diff --git a/test/controllers.js b/test/controllers.js index 82e517640a..418420303f 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -1,14 +1,12 @@ 'use strict'; -const async = require('async'); const assert = require('assert'); const nconf = require('nconf'); -const request = require('request'); -const requestAsync = require('request-promise-native'); const fs = require('fs'); const path = require('path'); const util = require('util'); +const request = require('../src/request'); const db = require('./mocks/databasemock'); const api = require('../src/api'); const categories = require('../src/categories'); @@ -33,6 +31,7 @@ describe('Controllers', () => { let fooUid; let adminUid; let category; + let testRoutes = []; before(async () => { category = await categories.create({ @@ -55,36 +54,68 @@ describe('Controllers', () => { const result = await topics.post({ uid: fooUid, title: 'test topic title', content: 'test topic content', cid: cid }); tid = result.topicData.tid; + pid = result.postData.pid; + + testRoutes = [ + { it: 'should load /reset without code', url: '/reset' }, + { it: 'should load /reset with invalid code', url: '/reset/123123' }, + { it: 'should load /login', url: '/login' }, + { it: 'should load /register', url: '/register' }, + { it: 'should load /robots.txt', url: '/robots.txt' }, + { it: 'should load /manifest.webmanifest', url: '/manifest.webmanifest' }, + { it: 'should load /outgoing?url=', url: '/outgoing?url=http://youtube.com' }, + { it: 'should 404 on /outgoing with no url', url: '/outgoing', status: 404 }, + { it: 'should 404 on /outgoing with javascript: protocol', url: '/outgoing?url=javascript:alert(1);', status: 404 }, + { it: 'should 404 on /outgoing with invalid url', url: '/outgoing?url=derp', status: 404 }, + { it: 'should load /sping', url: '/sping', body: 'healthy' }, + { it: 'should load /ping', url: '/ping', body: '200' }, + { it: 'should handle 404', url: '/arouteinthevoid', status: 404 }, + { it: 'should load topic rss feed', url: `/topic/${tid}.rss` }, + { it: 'should load category rss feed', url: `/category/${cid}.rss` }, + { it: 'should load topics rss feed', url: `/topics.rss` }, + { it: 'should load recent rss feed', url: `/recent.rss` }, + { it: 'should load top rss feed', url: `/top.rss` }, + { it: 'should load popular rss feed', url: `/popular.rss` }, + { it: 'should load popular rss feed with term', url: `/popular/day.rss` }, + { it: 'should load recent posts rss feed', url: `/recentposts.rss` }, + { it: 'should load category recent posts rss feed', url: `/category/${cid}/recentposts.rss` }, + { it: 'should load user topics rss feed', url: `/user/foo/topics.rss` }, + { it: 'should load tag rss feed', url: `/tags/nodebb.rss` }, + { it: 'should load client.css', url: `/assets/client.css` }, + { it: 'should load admin.css', url: `/assets/admin.css` }, + { it: 'should load sitemap.xml', url: `/sitemap.xml` }, + { it: 'should load sitemap/pages.xml', url: `/sitemap/pages.xml` }, + { it: 'should load sitemap/categories.xml', url: `/sitemap/categories.xml` }, + { it: 'should load sitemap/topics.1.xml', url: `/sitemap/topics.1.xml` }, + { it: 'should load theme screenshot', url: `/css/previews/nodebb-theme-harmony` }, + { it: 'should load users page', url: `/users` }, + { it: 'should load users page section', url: `/users?section=online` }, + { it: 'should load groups page', url: `/groups` }, + { it: 'should get recent posts', url: `/api/recent/posts/month` }, + { it: 'should get post data', url: `/api/v3/posts/${pid}` }, + { it: 'should get topic data', url: `/api/v3/topics/${tid}` }, + { it: 'should get category data', url: `/api/v3/categories/${cid}` }, + { it: 'should return osd data', url: `/osd.xml` }, + ]; }); - it('should load /config with csrf_token', (done) => { - request({ - url: `${nconf.get('url')}/api/config`, - json: true, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert(body.csrf_token); - done(); - }); + it('should load /config with csrf_token', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/config`); + assert.equal(response.statusCode, 200); + assert(body.csrf_token); }); - it('should load /config with no csrf_token as spider', (done) => { - request({ - url: `${nconf.get('url')}/api/config`, - json: true, + it('should load /config with no csrf_token as spider', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/config`, { headers: { 'user-agent': 'yandex', }, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert.strictEqual(body.csrf_token, false); - assert.strictEqual(body.uid, -1); - assert.strictEqual(body.loggedIn, false); - done(); }); + assert.equal(response.statusCode, 200); + assert.strictEqual(body.csrf_token, false); + assert.strictEqual(body.uid, -1); + assert.strictEqual(body.loggedIn, false); }); describe('homepage', () => { @@ -111,145 +142,75 @@ describe('Controllers', () => { await meta.templates.compileTemplate(name, message); }); - it('should load default', (done) => { - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + async function assertHomeUrl() { + const { response, body } = await request.get(nconf.get('url')); + assert.equal(response.statusCode, 200); + assert(body); + } + + it('should load default', async () => { + await assertHomeUrl(); }); - it('should load unread', (done) => { - meta.configs.set('homePageRoute', 'unread', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + it('should load unread', async () => { + await meta.configs.set('homePageRoute', 'unread'); + await assertHomeUrl(); }); - it('should load recent', (done) => { - meta.configs.set('homePageRoute', 'recent', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + it('should load recent', async () => { + await meta.configs.set('homePageRoute', 'recent'); + await assertHomeUrl(); }); - it('should load top', (done) => { - meta.configs.set('homePageRoute', 'top', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + it('should load top', async () => { + await meta.configs.set('homePageRoute', 'top'); + await assertHomeUrl(); }); - it('should load popular', (done) => { - meta.configs.set('homePageRoute', 'popular', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + it('should load popular', async () => { + await meta.configs.set('homePageRoute', 'popular'); + await assertHomeUrl(); }); - it('should load category', (done) => { - meta.configs.set('homePageRoute', 'category/1/test-category', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + it('should load category', async () => { + await meta.configs.set('homePageRoute', 'category/1/test-category'); + await assertHomeUrl(); }); - it('should not load breadcrumbs on home page route', (done) => { - request(`${nconf.get('url')}/api`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(!body.breadcrumbs); - done(); - }); + it('should not load breadcrumbs on home page route', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api`); + assert.equal(response.statusCode, 200); + assert(body); + assert(!body.breadcrumbs); }); - it('should redirect to custom', (done) => { - meta.configs.set('homePageRoute', 'groups', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + it('should redirect to custom', async () => { + await meta.configs.set('homePageRoute', 'groups'); + await assertHomeUrl(); }); - it('should 404 if custom does not exist', (done) => { - meta.configs.set('homePageRoute', 'this-route-does-not-exist', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); - }); + it('should 404 if custom does not exist', async () => { + await meta.configs.set('homePageRoute', 'this-route-does-not-exist'); + const { response, body } = await request.get(nconf.get('url')); + assert.equal(response.statusCode, 404); + assert(body); }); - it('api should work with hook', (done) => { - meta.configs.set('homePageRoute', 'mycustompage', (err) => { - assert.ifError(err); - - request(`${nconf.get('url')}/api`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.works, true); - assert.equal(body.template.mycustompage, true); - - done(); - }); - }); + it('api should work with hook', async () => { + await meta.configs.set('homePageRoute', 'mycustompage'); + const { response, body } = await request.get(`${nconf.get('url')}/api`); + assert.equal(response.statusCode, 200); + assert.equal(body.works, true); + assert.equal(body.template.mycustompage, true); }); - it('should render with hook', (done) => { - meta.configs.set('homePageRoute', 'mycustompage', (err) => { - assert.ifError(err); - - request(nconf.get('url'), (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.ok(body); - assert.ok(body.indexOf('
{ + await meta.configs.set('homePageRoute', 'mycustompage'); + const { response, body } = await request.get(nconf.get('url')); + assert.equal(response.statusCode, 200); + assert.ok(body); + assert.ok(body.indexOf('
{ @@ -259,84 +220,49 @@ describe('Controllers', () => { }); }); - it('should load /reset without code', (done) => { - request(`${nconf.get('url')}/reset`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load /reset with invalid code', (done) => { - request(`${nconf.get('url')}/reset/123123`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load /login', (done) => { - request(`${nconf.get('url')}/login`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load /register', (done) => { - request(`${nconf.get('url')}/register`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load /register/complete', (done) => { - const data = { - username: 'interstitial', - password: '123456', - 'password-confirm': '123456', - email: 'test@me.com', - }; - - const jar = request.jar(); - request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar, - }, (err, response, body) => { - assert.ifError(err); - - request.post(`${nconf.get('url')}/register`, { - form: data, - json: true, - jar: jar, - headers: { - 'x-csrf-token': body.csrf_token, - }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.strictEqual(body.next, `${nconf.get('relative_path')}/register/complete`); - request(`${nconf.get('url')}/api/register/complete`, { - jar: jar, - json: true, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.sections); - assert(body.errors); - assert(body.title); - done(); - }); + describe('routes that should 200/404 etc.', () => { + const baseUrl = nconf.get('url'); + testRoutes.forEach((route) => { + it(route.it, async () => { + const { response, body } = await request.get(`${baseUrl}/${route.url}`); + assert.equal(response.statusCode, route.status || 200); + if (route.body) { + assert.strictEqual(String(body), route.body); + } else { + assert(body); + } }); }); }); + it('should load /register/complete', async () => { + const jar = request.jar(); + const csrf_token = await helpers.getCsrfToken(jar); + const { response, body } = await request.post(`${nconf.get('url')}/register`, { + body: { + username: 'interstitial', + password: '123456', + 'password-confirm': '123456', + email: 'test@me.com', + }, + jar, + headers: { + 'x-csrf-token': csrf_token, + }, + }); + assert.equal(response.statusCode, 200); + assert.strictEqual(body.next, `${nconf.get('relative_path')}/register/complete`); + + const { response: res2, body: body2 } = await request.get(`${nconf.get('url')}/api/register/complete`, { + jar: jar, + json: true, + }); + assert.equal(res2.statusCode, 200); + assert(body2.sections); + assert(body2.errors); + assert(body2.title); + }); + describe('registration interstitials', () => { describe('email update', () => { let jar; @@ -350,10 +276,10 @@ describe('Controllers', () => { method: dummyEmailerHook, }); - jar = await helpers.registerUser({ + jar = (await helpers.registerUser({ username: utils.generateUUID().slice(0, 10), password: utils.generateUUID(), - }); + })).jar; token = await helpers.getCsrfToken(jar); meta.config.requireEmailAddress = 1; @@ -365,44 +291,37 @@ describe('Controllers', () => { }); it('email interstitial should still apply if empty email entered and requireEmailAddress is enabled', async () => { - let res = await requestAsync(`${nconf.get('url')}/register/complete`, { - method: 'post', + const { response: res } = await request.post(`${nconf.get('url')}/register/complete`, { jar, - json: true, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': token, }, - form: { + body: { email: '', }, }); assert.strictEqual(res.headers.location, `${nconf.get('relative_path')}/register/complete`); - res = await requestAsync(`${nconf.get('url')}/api/register/complete`, { + const { response, body } = await request.get(`${nconf.get('url')}/api/register/complete`, { jar, - json: true, - resolveWithFullResponse: true, }); - assert.strictEqual(res.statusCode, 200); - assert(res.body.errors.length); - assert(res.body.errors.includes('[[error:invalid-email]]')); + assert.strictEqual(response.statusCode, 200); + assert(body.errors.length); + assert(body.errors.includes('[[error:invalid-email]]')); }); it('gdpr interstitial should still apply if email requirement is disabled', async () => { meta.config.requireEmailAddress = 0; - const res = await requestAsync(`${nconf.get('url')}/api/register/complete`, { + const { body } = await request.get(`${nconf.get('url')}/api/register/complete`, { jar, - json: true, - resolveWithFullResponse: true, }); - assert(!res.body.errors.includes('[[error:invalid-email]]')); - assert(!res.body.errors.includes('[[error:gdpr-consent-denied]]')); + assert(!body.errors.includes('[[error:invalid-email]]')); + assert(!body.errors.includes('[[error:gdpr-consent-denied]]')); meta.config.requireEmailAddress = 1; }); @@ -575,18 +494,16 @@ describe('Controllers', () => { const username = utils.generateUUID().slice(0, 10); before(async () => { - jar = await helpers.registerUser({ + jar = (await helpers.registerUser({ username, password: utils.generateUUID(), - }); + })).jar; token = await helpers.getCsrfToken(jar); }); async function abortInterstitial() { - await requestAsync(`${nconf.get('url')}/register/abort`, { - method: 'post', + await request.post(`${nconf.get('url')}/register/abort`, { jar, - simple: false, headers: { 'x-csrf-token': token, }, @@ -596,38 +513,32 @@ describe('Controllers', () => { it('should not apply if requireEmailAddress is not enabled', async () => { meta.config.requireEmailAddress = 0; - const res = await requestAsync(`${nconf.get('url')}/register/complete`, { - method: 'post', + const { response } = await request.post(`${nconf.get('url')}/register/complete`, { jar, - json: true, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': token, }, - form: { + body: { email: `${utils.generateUUID().slice(0, 10)}@example.org`, gdpr_agree_data: 'on', gdpr_agree_email: 'on', }, }); - console.log(res.headers.location); - assert.strictEqual(res.headers.location, `${nconf.get('relative_path')}/`); + + assert.strictEqual(response.headers.location, `${nconf.get('relative_path')}/`); meta.config.requireEmailAddress = 1; }); it('should allow access to regular resources after an email is entered, even if unconfirmed', async () => { - const res = await requestAsync(`${nconf.get('url')}/recent`, { + const { response } = await request.get(`${nconf.get('url')}/recent`, { jar, - json: true, - resolveWithFullResponse: true, - followRedirect: false, - simple: false, + maxRedirect: 0, }); - assert.strictEqual(res.statusCode, 200); + assert.strictEqual(response.statusCode, 200); }); it('should redirect back to interstitial for categories requiring validated email', async () => { @@ -635,16 +546,14 @@ describe('Controllers', () => { const { cid } = await categories.create({ name }); await privileges.categories.rescind(['groups:read'], cid, ['registered-users']); await privileges.categories.give(['groups:read'], cid, ['verified-users']); - const res = await requestAsync(`${nconf.get('url')}/category/${cid}/${slugify(name)}`, { + const { response } = await request.get(`${nconf.get('url')}/category/${cid}/${slugify(name)}`, { jar, - json: true, - resolveWithFullResponse: true, - followRedirect: false, - simple: false, + maxRedirect: 0, + redirect: 'manual', }); - assert.strictEqual(res.statusCode, 307); - assert.strictEqual(res.headers.location, `${nconf.get('relative_path')}/register/complete`); + assert.strictEqual(response.statusCode, 307); + assert.strictEqual(response.headers.location, `${nconf.get('relative_path')}/register/complete`); await abortInterstitial(); }); @@ -653,25 +562,21 @@ describe('Controllers', () => { const { cid } = await categories.create({ name }); await privileges.categories.rescind(['groups:topics:read'], cid, 'registered-users'); await privileges.categories.give(['groups:topics:read'], cid, 'verified-users'); - const res = await requestAsync(`${nconf.get('url')}/category/${cid}/${slugify(name)}`, { + const { response } = await request.get(`${nconf.get('url')}/category/${cid}/${slugify(name)}`, { jar, - json: true, - resolveWithFullResponse: true, - followRedirect: false, - simple: false, + maxRedirect: 0, + redirect: 'manual', }); - assert.strictEqual(res.statusCode, 200); + assert.strictEqual(response.statusCode, 200); const title = utils.generateUUID(); const uid = await user.getUidByUsername(username); const { topicData } = await topics.post({ uid, cid, title, content: utils.generateUUID() }); - const res2 = await requestAsync(`${nconf.get('url')}/topic/${topicData.tid}/${slugify(title)}`, { + const { response: res2 } = await request.get(`${nconf.get('url')}/topic/${topicData.tid}/${slugify(title)}`, { jar, - json: true, - resolveWithFullResponse: true, - followRedirect: false, - simple: false, + maxRedirect: 0, + redirect: 'manual', }); assert.strictEqual(res2.statusCode, 307); assert.strictEqual(res2.headers.location, `${nconf.get('relative_path')}/register/complete`); @@ -686,32 +591,29 @@ describe('Controllers', () => { let token; before(async () => { - jar = await helpers.registerUser({ + jar = (await helpers.registerUser({ username: utils.generateUUID().slice(0, 10), password: utils.generateUUID(), - }); + })).jar; token = await helpers.getCsrfToken(jar); }); it('registration should succeed once gdpr prompts are agreed to', async () => { - const res = await requestAsync(`${nconf.get('url')}/register/complete`, { - method: 'post', + const { response } = await request.post(`${nconf.get('url')}/register/complete`, { jar, - json: true, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': token, }, - form: { + body: { gdpr_agree_data: 'on', gdpr_agree_email: 'on', }, }); - assert.strictEqual(res.statusCode, 302); - assert.strictEqual(res.headers.location, `${nconf.get('relative_path')}/`); + assert.strictEqual(response.statusCode, 302); + assert.strictEqual(response.headers.location, `${nconf.get('relative_path')}/`); }); }); @@ -720,480 +622,131 @@ describe('Controllers', () => { let token; beforeEach(async () => { - jar = await helpers.registerUser({ + jar = (await helpers.registerUser({ username: utils.generateUUID().slice(0, 10), password: utils.generateUUID(), - }); + })).jar; token = await helpers.getCsrfToken(jar); }); it('should terminate the session and send user back to index if interstitials remain', async () => { - const res = await requestAsync(`${nconf.get('url')}/register/abort`, { - method: 'post', + const { response } = await request.post(`${nconf.get('url')}/register/abort`, { jar, - json: true, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': token, }, }); - assert.strictEqual(res.statusCode, 302); - assert.strictEqual(res.headers['set-cookie'][0], `express.sid=; Path=${nconf.get('relative_path') || '/'}; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`); - assert.strictEqual(res.headers.location, `${nconf.get('relative_path')}/`); + assert.strictEqual(response.statusCode, 302); + assert.strictEqual(response.headers['set-cookie'], `express.sid=; Path=${nconf.get('relative_path') || '/'}; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`); + assert.strictEqual(response.headers.location, `${nconf.get('relative_path')}/`); }); it('should preserve the session and send user back to user profile if no interstitials remain (e.g. GDPR OK + email change cancellation)', async () => { // Submit GDPR consent - await requestAsync(`${nconf.get('url')}/register/complete`, { - method: 'post', + await request.post(`${nconf.get('url')}/register/complete`, { jar, - json: true, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': token, }, - form: { + body: { gdpr_agree_data: 'on', gdpr_agree_email: 'on', }, }); // Start email change flow - await requestAsync(`${nconf.get('url')}/me/edit/email`, { jar }); + await request.get(`${nconf.get('url')}/me/edit/email`, { jar }); - const res = await requestAsync(`${nconf.get('url')}/register/abort`, { - method: 'post', + const { response } = await request.post(`${nconf.get('url')}/register/abort`, { jar, - json: true, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': token, }, }); - assert.strictEqual(res.statusCode, 302); - assert(res.headers.location.match(/\/uid\/\d+$/)); + assert.strictEqual(response.statusCode, 302); + assert(response.headers.location.match(/\/uid\/\d+$/)); }); }); }); - it('should load /robots.txt', (done) => { - request(`${nconf.get('url')}/robots.txt`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - it('should load /manifest.webmanifest', (done) => { - request(`${nconf.get('url')}/manifest.webmanifest`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load /outgoing?url=', (done) => { - request(`${nconf.get('url')}/outgoing?url=http://youtube.com`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should 404 on /outgoing with no url', (done) => { - request(`${nconf.get('url')}/outgoing`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); - }); - - it('should 404 on /outgoing with javascript: protocol', (done) => { - request(`${nconf.get('url')}/outgoing?url=javascript:alert(1);`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); - }); - - it('should 404 on /outgoing with invalid url', (done) => { - request(`${nconf.get('url')}/outgoing?url=derp`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); - }); - - it('should load /tos', (done) => { + it('should load /tos', async () => { meta.config.termsOfUse = 'please accept our tos'; - request(`${nconf.get('url')}/tos`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/tos`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load 404 if meta.config.termsOfUse is empty', (done) => { + it('should return 404 if meta.config.termsOfUse is empty', async () => { meta.config.termsOfUse = ''; - request(`${nconf.get('url')}/tos`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/tos`); + assert.equal(response.statusCode, 404); + assert(body); }); - it('should load /sping', (done) => { - request(`${nconf.get('url')}/sping`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body, 'healthy'); - done(); - }); + + it('should error if guests do not have search privilege', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/users?query=bar§ion=sort-posts`); + assert.equal(response.statusCode, 500); + assert(body); + assert.equal(body.error, '[[error:no-privileges]]'); }); - it('should load /ping', (done) => { - request(`${nconf.get('url')}/ping`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body, '200'); - done(); - }); + it('should load users search page', async () => { + await privileges.global.give(['groups:search:users'], 'guests'); + const { response, body } = await request.get(`${nconf.get('url')}/users?query=bar§ion=sort-posts`); + assert.equal(response.statusCode, 200); + assert(body); + await privileges.global.rescind(['groups:search:users'], 'guests'); }); - it('should handle 404', (done) => { - request(`${nconf.get('url')}/arouteinthevoid`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); - }); - - it('should load topic rss feed', (done) => { - request(`${nconf.get('url')}/topic/${tid}.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load category rss feed', (done) => { - request(`${nconf.get('url')}/category/${cid}.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load topics rss feed', (done) => { - request(`${nconf.get('url')}/topics.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load recent rss feed', (done) => { - request(`${nconf.get('url')}/recent.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load top rss feed', (done) => { - request(`${nconf.get('url')}/top.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load popular rss feed', (done) => { - request(`${nconf.get('url')}/popular.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load popular rss feed with term', (done) => { - request(`${nconf.get('url')}/popular/day.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load recent posts rss feed', (done) => { - request(`${nconf.get('url')}/recentposts.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load category recent posts rss feed', (done) => { - request(`${nconf.get('url')}/category/${cid}/recentposts.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load user topics rss feed', (done) => { - request(`${nconf.get('url')}/user/foo/topics.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load tag rss feed', (done) => { - request(`${nconf.get('url')}/tags/nodebb.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load client.css', (done) => { - request(`${nconf.get('url')}/assets/client.css`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load admin.css', (done) => { - request(`${nconf.get('url')}/assets/admin.css`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load sitemap.xml', (done) => { - request(`${nconf.get('url')}/sitemap.xml`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load sitemap/pages.xml', (done) => { - request(`${nconf.get('url')}/sitemap/pages.xml`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load sitemap/categories.xml', (done) => { - request(`${nconf.get('url')}/sitemap/categories.xml`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load sitemap/topics/1.xml', (done) => { - request(`${nconf.get('url')}/sitemap/topics.1.xml`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load robots.txt', (done) => { - request(`${nconf.get('url')}/robots.txt`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load theme screenshot', (done) => { - request(`${nconf.get('url')}/css/previews/nodebb-theme-persona`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load users page', (done) => { - request(`${nconf.get('url')}/users`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load users page', (done) => { - request(`${nconf.get('url')}/users?section=online`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should error if guests do not have search privilege', (done) => { - request(`${nconf.get('url')}/api/users?query=bar§ion=sort-posts`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 500); - assert(body); - assert.equal(body.error, '[[error:no-privileges]]'); - done(); - }); - }); - - it('should load users search page', (done) => { - privileges.global.give(['groups:search:users'], 'guests', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/users?query=bar§ion=sort-posts`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - privileges.global.rescind(['groups:search:users'], 'guests', done); - }); - }); - }); - - it('should load groups page', (done) => { - request(`${nconf.get('url')}/groups`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should load group details page', (done) => { - groups.create({ + it('should load group details page', async () => { + await groups.create({ name: 'group-details', description: 'Foobar!', hidden: 0, - }, (err) => { - assert.ifError(err); - groups.join('group-details', fooUid, (err) => { - assert.ifError(err); - topics.post({ - uid: fooUid, - title: 'topic title', - content: 'test topic content', - cid: cid, - }, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/groups/group-details`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert.equal(body.posts[0].content, 'test topic content'); - done(); - }); - }); - }); }); + await groups.join('group-details', fooUid); + + await topics.post({ + uid: fooUid, + title: 'topic title', + content: 'test topic content', + cid: cid, + }); + + const { response, body } = await request.get(`${nconf.get('url')}/api/groups/group-details`); + assert.equal(response.statusCode, 200); + assert(body); + assert.equal(body.posts[0].content, 'test topic content'); }); - it('should load group members page', (done) => { - request(`${nconf.get('url')}/groups/group-details/members`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load group members page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/groups/group-details/members`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should 404 when trying to load group members of hidden group', (done) => { + it('should 404 when trying to load group members of hidden group', async () => { const groups = require('../src/groups'); - groups.create({ + await groups.create({ name: 'hidden-group', description: 'Foobar!', hidden: 1, - }, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/groups/hidden-group/members`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); }); + const { response } = await request.get(`${nconf.get('url')}/groups/hidden-group/members`); + assert.equal(response.statusCode, 404); }); - it('should get recent posts', (done) => { - request(`${nconf.get('url')}/api/recent/posts/month`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should get post data', (done) => { - request(`${nconf.get('url')}/api/v3/posts/${pid}`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should get topic data', (done) => { - request(`${nconf.get('url')}/api/v3/topics/${tid}`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); - - it('should get category data', (done) => { - request(`${nconf.get('url')}/api/v3/categories/${cid}`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); describe('revoke session', () => { @@ -1208,63 +761,52 @@ describe('Controllers', () => { csrf_token = login.csrf_token; }); - it('should fail to revoke session with missing uuid', (done) => { - request.del(`${nconf.get('url')}/api/user/revokeme/session`, { + it('should fail to revoke session with missing uuid', async () => { + const { response } = await request.del(`${nconf.get('url')}/api/user/revokeme/session`, { jar: jar, headers: { 'x-csrf-token': csrf_token, }, - }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); }); + assert.equal(response.statusCode, 404); }); - it('should fail if user doesn\'t exist', (done) => { - request.del(`${nconf.get('url')}/api/v3/users/doesnotexist/sessions/1112233`, { + it('should fail if user doesn\'t exist', async () => { + const { response, body } = await request.del(`${nconf.get('url')}/api/v3/users/doesnotexist/sessions/1112233`, { jar: jar, headers: { 'x-csrf-token': csrf_token, }, - }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 404); - const parsedResponse = JSON.parse(body); - assert.deepStrictEqual(parsedResponse.response, {}); - assert.deepStrictEqual(parsedResponse.status, { - code: 'not-found', - message: 'User does not exist', - }); - done(); + }); + + assert.strictEqual(response.statusCode, 404); + // const parsedResponse = JSON.parse(body); + assert.deepStrictEqual(body.response, {}); + assert.deepStrictEqual(body.status, { + code: 'not-found', + message: 'User does not exist', }); }); - it('should revoke user session', (done) => { - db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1, (err, sids) => { - assert.ifError(err); - const sid = sids[0]; + it('should revoke user session', async () => { + const sids = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1); + const sid = sids[0]; + const sessionObj = await db.sessionStoreGet(sid); - db.sessionStore.get(sid, (err, sessionObj) => { - assert.ifError(err); - request.del(`${nconf.get('url')}/api/v3/users/${uid}/sessions/${sessionObj.meta.uuid}`, { - jar: jar, - headers: { - 'x-csrf-token': csrf_token, - }, - }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert.deepStrictEqual(JSON.parse(body), { - status: { - code: 'ok', - message: 'OK', - }, - response: {}, - }); - done(); - }); - }); + const { response, body } = await request.del(`${nconf.get('url')}/api/v3/users/${uid}/sessions/${sessionObj.meta.uuid}`, { + jar: jar, + headers: { + 'x-csrf-token': csrf_token, + }, + }); + + assert.strictEqual(response.statusCode, 200); + assert.deepStrictEqual(body, { + status: { + code: 'ok', + message: 'OK', + }, + response: {}, }); }); }); @@ -1272,114 +814,83 @@ describe('Controllers', () => { describe('widgets', () => { const widgets = require('../src/widgets'); - before((done) => { - async.waterfall([ - function (next) { - widgets.reset(next); - }, - function (next) { - const data = { - template: 'categories.tpl', - location: 'sidebar', - widgets: [ - { - widget: 'html', - data: { - html: 'test', - title: '', - container: '', - }, - }, - ], - }; + before(async () => { + await widgets.reset(); + const data = { + template: 'categories.tpl', + location: 'sidebar', + widgets: [ + { + widget: 'html', + data: { + html: 'test', + title: '', + container: '', + }, + }, + ], + }; - widgets.setArea(data, next); - }, - ], done); + await widgets.setArea(data); }); - it('should return {} if there are no widgets', (done) => { - request(`${nconf.get('url')}/api/category/${cid}`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.widgets); - assert.equal(Object.keys(body.widgets).length, 0); - done(); - }); + it('should return {} if there are no widgets', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/category/${cid}`); + assert.equal(response.statusCode, 200); + assert(body.widgets); + assert.equal(Object.keys(body.widgets).length, 0); }); - it('should render templates', (done) => { + it('should render templates', async () => { const url = `${nconf.get('url')}/api/categories`; - request(url, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.widgets); - assert(body.widgets.sidebar); - assert.equal(body.widgets.sidebar[0].html, 'test'); - done(); - }); + const { response, body } = await request.get(url); + assert.equal(response.statusCode, 200); + assert(body.widgets); + assert(body.widgets.sidebar); + assert.equal(body.widgets.sidebar[0].html, 'test'); }); - it('should reset templates', (done) => { - widgets.resetTemplates(['categories', 'category'], (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/categories`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.widgets); - assert.equal(Object.keys(body.widgets).length, 0); - done(); - }); - }); + it('should reset templates', async () => { + await widgets.resetTemplates(['categories', 'category']); + const { response, body } = await request.get(`${nconf.get('url')}/api/categories`); + assert.equal(response.statusCode, 200); + assert(body.widgets); + assert.equal(Object.keys(body.widgets).length, 0); }); }); describe('tags', () => { - let tid; - before((done) => { - topics.post({ + before(async () => { + await topics.post({ uid: fooUid, title: 'topic title', content: 'test topic content', cid: cid, tags: ['nodebb', 'bug', 'test'], - }, (err, result) => { - assert.ifError(err); - tid = result.topicData.tid; - done(); }); }); - it('should render tags page', (done) => { - request(`${nconf.get('url')}/api/tags`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(Array.isArray(body.tags)); - done(); - }); + it('should render tags page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/tags`); + assert.equal(response.statusCode, 200); + assert(body); + assert(Array.isArray(body.tags)); }); - it('should render tag page with no topics', (done) => { - request(`${nconf.get('url')}/api/tags/notag`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(Array.isArray(body.topics)); - assert.equal(body.topics.length, 0); - done(); - }); + it('should render tag page with no topics', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/tags/notag`); + assert.equal(response.statusCode, 200); + assert(body); + assert(Array.isArray(body.topics)); + assert.equal(body.topics.length, 0); }); - it('should render tag page with 1 topic', (done) => { - request(`${nconf.get('url')}/api/tags/nodebb`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(Array.isArray(body.topics)); - assert.equal(body.topics.length, 1); - done(); - }); + it('should render tag page with 1 topic', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/tags/nodebb`); + assert.equal(response.statusCode, 200); + assert(body); + assert(Array.isArray(body.topics)); + assert.equal(body.topics.length, 1); }); }); @@ -1394,42 +905,30 @@ describe('Controllers', () => { done(); }); - it('should return 503 in maintenance mode', (done) => { - request(`${nconf.get('url')}/recent`, { json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 503); - done(); - }); + it('should return 503 in maintenance mode', async () => { + const { response } = await request.get(`${nconf.get('url')}/recent`); + assert.equal(response.statusCode, 503); }); - it('should return 503 in maintenance mode', (done) => { - request(`${nconf.get('url')}/api/recent`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 503); - assert(body); - done(); - }); + it('should return 503 in maintenance mode', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/recent`); + assert.equal(response.statusCode, 503); + assert(body); }); - it('should return 200 in maintenance mode', (done) => { - request(`${nconf.get('url')}/api/login`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should return 200 in maintenance mode', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/login`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should return 200 if guests are allowed', (done) => { + it('should return 200 if guests are allowed', async () => { const oldValue = meta.config.groupsExemptFromMaintenanceMode; meta.config.groupsExemptFromMaintenanceMode.push('guests'); - request(`${nconf.get('url')}/api/recent`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body); - meta.config.groupsExemptFromMaintenanceMode = oldValue; - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/api/recent`); + assert.strictEqual(response.statusCode, 200); + assert(body); + meta.config.groupsExemptFromMaintenanceMode = oldValue; }); }); @@ -1441,239 +940,170 @@ describe('Controllers', () => { ({ jar, csrf_token } = await helpers.loginUser('foo', 'barbar')); }); - it('should redirect to account page with logged in user', (done) => { - request(`${nconf.get('url')}/api/login`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/user/foo'); - assert.equal(body, '/user/foo'); - done(); - }); + it('should redirect to account page with logged in user', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/login`, { jar }); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/user/foo'); + assert.equal(body, '/user/foo'); }); - it('should 404 if uid is not a number', (done) => { - request(`${nconf.get('url')}/api/uid/test`, { json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if uid is not a number', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/uid/test`, { jar }); + assert.equal(response.statusCode, 404); }); - it('should redirect to userslug', (done) => { - request(`${nconf.get('url')}/api/uid/${fooUid}`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/user/foo'); - assert.equal(body, '/user/foo'); - done(); - }); + it('should redirect to userslug', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/uid/${fooUid}`); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/user/foo'); + assert.equal(body, '/user/foo'); }); - it('should redirect to userslug and keep query params', (done) => { - request(`${nconf.get('url')}/api/uid/${fooUid}/topics?foo=bar`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/user/foo/topics?foo=bar'); - assert.equal(body, '/user/foo/topics?foo=bar'); - done(); - }); + it('should redirect to userslug and keep query params', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/uid/${fooUid}/topics?foo=bar`); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/user/foo/topics?foo=bar'); + assert.equal(body, '/user/foo/topics?foo=bar'); }); - it('should 404 if user does not exist', (done) => { - request(`${nconf.get('url')}/api/uid/123123`, { json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if user does not exist', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/uid/123123`); + assert.equal(response.statusCode, 404); }); describe('/me/*', () => { - it('should redirect to user profile', (done) => { - request(`${nconf.get('url')}/me`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.includes('"template":{"name":"account/profile","account/profile":true}')); - assert(body.includes('"username":"foo"')); - done(); - }); + it('should redirect to user profile', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/me`, { jar }); + assert.equal(response.statusCode, 200); + assert(body.includes('"template":{"name":"account/profile","account/profile":true}')); + assert(body.includes('"username":"foo"')); }); - it('api should redirect to /user/[userslug]/bookmarks', (done) => { - request(`${nconf.get('url')}/api/me/bookmarks`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/user/foo/bookmarks'); - assert.equal(body, '/user/foo/bookmarks'); - done(); - }); + + it('api should redirect to /user/[userslug]/bookmarks', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/me/bookmarks`, { jar }); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/user/foo/bookmarks'); + assert.equal(body, '/user/foo/bookmarks'); }); - it('api should redirect to /user/[userslug]/edit/username', (done) => { - request(`${nconf.get('url')}/api/me/edit/username`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/user/foo/edit/username'); - assert.equal(body, '/user/foo/edit/username'); - done(); - }); + + it('api should redirect to /user/[userslug]/edit/username', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/me/edit/username`, { jar }); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/user/foo/edit/username'); + assert.equal(body, '/user/foo/edit/username'); }); - it('should redirect to login if user is not logged in', (done) => { - request(`${nconf.get('url')}/me/bookmarks`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.includes('Login to your account'), body.slice(0, 500)); - done(); - }); + + it('should redirect to login if user is not logged in', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/me/bookmarks`); + assert.equal(response.statusCode, 200); + assert(body.includes('Login to your account'), body.slice(0, 500)); }); }); - it('should 401 if user is not logged in', (done) => { - request(`${nconf.get('url')}/api/admin`, { json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 401); - done(); - }); + it('should 401 if user is not logged in', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/admin`); + assert.equal(response.statusCode, 401); }); - it('should 403 if user is not admin', (done) => { - request(`${nconf.get('url')}/api/admin`, { jar: jar, json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 403); - done(); - }); + it('should 403 if user is not admin', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/admin`, { jar }); + assert.equal(response.statusCode, 403); }); - it('should load /user/foo/posts', (done) => { - request(`${nconf.get('url')}/api/user/foo/posts`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/posts', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/posts`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should 401 if not logged in', (done) => { - request(`${nconf.get('url')}/api/user/foo/bookmarks`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 401); - assert(body); - done(); - }); + it('should 401 if not logged in', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/admin`); + assert.equal(response.statusCode, 401); + assert(body); }); - it('should load /user/foo/bookmarks', (done) => { - request(`${nconf.get('url')}/api/user/foo/bookmarks`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/bookmarks', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/bookmarks`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/upvoted', (done) => { - request(`${nconf.get('url')}/api/user/foo/upvoted`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/upvoted', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/upvoted`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/downvoted', (done) => { - request(`${nconf.get('url')}/api/user/foo/downvoted`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/downvoted', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/downvoted`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/best', (done) => { - request(`${nconf.get('url')}/api/user/foo/best`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/best', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/best`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/controversial', (done) => { - request(`${nconf.get('url')}/api/user/foo/controversial`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/controversial', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/controversial`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/watched', (done) => { - request(`${nconf.get('url')}/api/user/foo/watched`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/watched', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/watched`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/ignored', (done) => { - request(`${nconf.get('url')}/api/user/foo/ignored`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/ignored', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/ignored`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/topics', (done) => { - request(`${nconf.get('url')}/api/user/foo/topics`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/topics', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/topics`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/blocks', (done) => { - request(`${nconf.get('url')}/api/user/foo/blocks`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/blocks', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/blocks`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/consent', (done) => { - request(`${nconf.get('url')}/api/user/foo/consent`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/consent', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/consent`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/sessions', (done) => { - request(`${nconf.get('url')}/api/user/foo/sessions`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/sessions', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/sessions`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/categories', (done) => { - request(`${nconf.get('url')}/api/user/foo/categories`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/categories', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/categories`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load /user/foo/uploads', (done) => { - request(`${nconf.get('url')}/api/user/foo/uploads`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load /user/foo/tags', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/tags`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); + }); + + it('should load /user/foo/uploads', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/uploads`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); describe('user data export routes', () => { @@ -1685,35 +1115,26 @@ describe('Controllers', () => { await sleep(10000); }); - it('should export users posts', (done) => { - request(`${nconf.get('url')}/api/v3/users/${fooUid}/exports/posts`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should export users posts', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/v3/users/${fooUid}/exports/posts`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should export users uploads', (done) => { - request(`${nconf.get('url')}/api/v3/users/${fooUid}/exports/uploads`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should export users uploads', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/v3/users/${fooUid}/exports/uploads`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should export users profile', (done) => { - request(`${nconf.get('url')}/api/v3/users/${fooUid}/exports/profile`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should export users profile', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/v3/users/${fooUid}/exports/profile`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); }); - it('should load notifications page', (done) => { + it('should load notifications page', async () => { const notifications = require('../src/notifications'); const notifData = { bodyShort: '[[notifications:user-posted-to, test1, test2]]', @@ -1726,294 +1147,192 @@ describe('Controllers', () => { mergeId: `notifications:user-posted-to|${1}`, topicTitle: 'topic title', }; - async.waterfall([ - function (next) { - notifications.create(notifData, next); - }, - function (notification, next) { - notifications.push(notification, fooUid, next); - }, - function (next) { - setTimeout(next, 2500); - }, - function (next) { - request(`${nconf.get('url')}/api/notifications`, { jar: jar, json: true }, next); - }, - function (res, body, next) { - assert.equal(res.statusCode, 200); - assert(body); - const notif = body.notifications[0]; - assert.equal(notif.bodyShort, 'test1 has posted a reply to: test2'); - assert.equal(notif.bodyLong, notifData.bodyLong); - assert.equal(notif.pid, notifData.pid); - assert.equal(notif.path, nconf.get('relative_path') + notifData.path); - assert.equal(notif.nid, notifData.nid); - next(); - }, - ], done); + const notification = await notifications.create(notifData); + await notifications.push(notification, fooUid); + await sleep(2500); + const { response, body } = await request.get(`${nconf.get('url')}/api/notifications`, { + jar, + }); + assert.equal(response.statusCode, 200); + assert(body); + const notif = body.notifications[0]; + assert.equal(notif.bodyShort, 'test1 has posted a reply to: test2'); + assert.equal(notif.bodyLong, notifData.bodyLong); + assert.equal(notif.pid, notifData.pid); + assert.equal(notif.path, nconf.get('relative_path') + notifData.path); + assert.equal(notif.nid, notifData.nid); }); - it('should 404 if user does not exist', (done) => { - request(`${nconf.get('url')}/api/user/email/doesnotexist`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + it('should 404 if user does not exist', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/email/doesnotexist`); + assert.equal(response.statusCode, 404); + assert(body); }); - it('should load user by uid', (done) => { - request(`${nconf.get('url')}/api/user/uid/${fooUid}`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load user by uid', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/uid/${fooUid}`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load user by username', (done) => { - request(`${nconf.get('url')}/api/user/username/foo`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load user by username', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/username/foo`); + assert.equal(response.statusCode, 200); + assert(body); }); it('should NOT load user by email (by default)', async () => { - const res = await requestAsync(`${nconf.get('url')}/api/user/email/foo@test.com`, { - resolveWithFullResponse: true, - simple: false, - }); + const { response } = await request.get(`${nconf.get('url')}/api/user/email/foo@test.com`); - assert.strictEqual(res.statusCode, 404); + assert.strictEqual(response.statusCode, 404); }); it('should load user by email if user has elected to show their email', async () => { await user.setSetting(fooUid, 'showemail', 1); - const res = await requestAsync(`${nconf.get('url')}/api/user/email/foo@test.com`, { - resolveWithFullResponse: true, - }); - assert.strictEqual(res.statusCode, 200); - assert(res.body); + const { response, body } = await request.get(`${nconf.get('url')}/api/user/email/foo@test.com`); + assert.strictEqual(response.statusCode, 200); + assert(body); await user.setSetting(fooUid, 'showemail', 0); }); - it('should return 401 if user does not have view:users privilege', (done) => { - privileges.global.rescind(['groups:view:users'], 'guests', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/user/foo`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 401); - assert.deepEqual(body, { - response: {}, - status: { - code: 'not-authorised', - message: 'A valid login session was not found. Please log in and try again.', - }, - }); - privileges.global.give(['groups:view:users'], 'guests', done); - }); + it('should return 401 if user does not have view:users privilege', async () => { + await privileges.global.rescind(['groups:view:users'], 'guests'); + + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo`); + assert.equal(response.statusCode, 401); + assert.deepEqual(body, { + response: {}, + status: { + code: 'not-authorised', + message: 'A valid login session was not found. Please log in and try again.', + }, }); + await privileges.global.give(['groups:view:users'], 'guests'); }); it('should return false if user can not edit user', async () => { await user.create({ username: 'regularJoe', password: 'barbar' }); const { jar } = await helpers.loginUser('regularJoe', 'barbar'); - let { statusCode } = await requestAsync(`${nconf.get('url')}/api/user/foo/info`, { jar: jar, json: true, simple: false, resolveWithFullResponse: true }); - assert.equal(statusCode, 403); - ({ statusCode } = await requestAsync(`${nconf.get('url')}/api/user/foo/edit`, { jar: jar, json: true, simple: false, resolveWithFullResponse: true })); - assert.equal(statusCode, 403); + let { response } = await request.get(`${nconf.get('url')}/api/user/foo/info`, { jar }); + assert.equal(response.statusCode, 403); + ({ response } = await request.get(`${nconf.get('url')}/api/user/foo/edit`, { jar })); + assert.equal(response.statusCode, 403); }); - it('should load correct user', (done) => { - request(`${nconf.get('url')}/api/user/FOO`, { jar: jar, json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - done(); - }); + it('should load correct user', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/user/FOO`, { jar: jar }); + assert.equal(response.statusCode, 200); }); - it('should redirect', (done) => { - request(`${nconf.get('url')}/user/FOO`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should redirect', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/user/FOO`, { jar: jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should 404 if user does not exist', (done) => { - request(`${nconf.get('url')}/api/user/doesnotexist`, { jar: jar }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if user does not exist', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/doesnotexist`, { jar }); + assert.equal(response.statusCode, 404); }); - it('should not increase profile view if you visit your own profile', (done) => { - request(`${nconf.get('url')}/api/user/foo`, { jar: jar }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - setTimeout(() => { - user.getUserField(fooUid, 'profileviews', (err, viewcount) => { - assert.ifError(err); - assert(viewcount === 0); - done(); - }); - }, 500); - }); + it('should not increase profile view if you visit your own profile', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/user/foo`, { jar }); + assert.equal(response.statusCode, 200); + await sleep(500); + const viewcount = await user.getUserField(fooUid, 'profileviews'); + assert(viewcount === 0); }); - it('should not increase profile view if a guest visits a profile', (done) => { - request(`${nconf.get('url')}/api/user/foo`, {}, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - setTimeout(() => { - user.getUserField(fooUid, 'profileviews', (err, viewcount) => { - assert.ifError(err); - assert(viewcount === 0); - done(); - }); - }, 500); - }); + it('should not increase profile view if a guest visits a profile', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/user/foo`, {}); + assert.equal(response.statusCode, 200); + await sleep(500); + const viewcount = await user.getUserField(fooUid, 'profileviews'); + assert(viewcount === 0); }); it('should increase profile view', async () => { const { jar } = await helpers.loginUser('regularJoe', 'barbar'); - const { statusCode } = await requestAsync(`${nconf.get('url')}/api/user/foo`, { - jar: jar, - simple: false, - resolveWithFullResponse: true, + const { response } = await request.get(`${nconf.get('url')}/api/user/foo`, { + jar, }); - assert.equal(statusCode, 200); - + assert.equal(response.statusCode, 200); await sleep(500); const viewcount = await user.getUserField(fooUid, 'profileviews'); assert(viewcount > 0); }); - it('should parse about me', (done) => { - user.setUserFields(fooUid, { picture: '/path/to/picture', aboutme: 'hi i am a bot' }, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/user/foo`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.aboutme, 'hi i am a bot'); - assert.equal(body.picture, '/path/to/picture'); - done(); - }); - }); + it('should parse about me', async () => { + await user.setUserFields(fooUid, { picture: '/path/to/picture', aboutme: 'hi i am a bot' }); + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo`); + assert.equal(response.statusCode, 200); + assert.equal(body.aboutme, 'hi i am a bot'); + assert.equal(body.picture, '/path/to/picture'); }); - it('should not return reputation if reputation is disabled', (done) => { + it('should not return reputation if reputation is disabled', async () => { meta.config['reputation:disabled'] = 1; - request(`${nconf.get('url')}/api/user/foo`, { json: true }, (err, res, body) => { - meta.config['reputation:disabled'] = 0; - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(!body.hasOwnProperty('reputation')); - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo`); + meta.config['reputation:disabled'] = 0; + assert.equal(response.statusCode, 200); + assert(!body.hasOwnProperty('reputation')); }); - it('should only return posts that are not deleted', (done) => { - let topicData; - let pidToDelete; - async.waterfall([ - function (next) { - topics.post({ uid: fooUid, title: 'visible', content: 'some content', cid: cid }, next); - }, - function (data, next) { - topicData = data.topicData; - topics.reply({ uid: fooUid, content: '1st reply', tid: topicData.tid }, next); - }, - function (postData, next) { - pidToDelete = postData.pid; - topics.reply({ uid: fooUid, content: '2nd reply', tid: topicData.tid }, next); - }, - function (postData, next) { - posts.delete(pidToDelete, fooUid, next); - }, - function (next) { - request(`${nconf.get('url')}/api/user/foo`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - const contents = body.posts.map(p => p.content); - assert(!contents.includes('1st reply')); - done(); - }); - }, - ], done); + it('should only return posts that are not deleted', async () => { + const { topicData } = await topics.post({ uid: fooUid, title: 'visible', content: 'some content', cid: cid }); + const { pid: pidToDelete } = await topics.reply({ uid: fooUid, content: '1st reply', tid: topicData.tid }); + await topics.reply({ uid: fooUid, content: '2nd reply', tid: topicData.tid }); + await posts.delete(pidToDelete, fooUid); + + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo`); + assert.equal(response.statusCode, 200); + const contents = body.posts.map(p => p.content); + assert(!contents.includes('1st reply')); }); - it('should return selected group title', (done) => { - groups.create({ + it('should return selected group title', async () => { + await groups.create({ name: 'selectedGroup', - }, (err) => { - assert.ifError(err); - user.create({ username: 'groupie' }, (err, uid) => { - assert.ifError(err); - groups.join('selectedGroup', uid, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/user/groupie`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(Array.isArray(body.selectedGroup)); - assert.equal(body.selectedGroup[0].name, 'selectedGroup'); - done(); - }); - }); - }); }); + const uid = await user.create({ username: 'groupie' }); + await groups.join('selectedGroup', uid); + + const { response, body } = await request.get(`${nconf.get('url')}/api/user/groupie`); + assert.equal(response.statusCode, 200); + assert(Array.isArray(body.selectedGroup)); + assert.equal(body.selectedGroup[0].name, 'selectedGroup'); }); - it('should 404 if user does not exist', (done) => { - groups.join('administrators', fooUid, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/user/doesnotexist/edit`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - groups.leave('administrators', fooUid, done); - }); - }); + it('should 404 if user does not exist', async () => { + await groups.join('administrators', fooUid); + + const { response } = await request.get(`${nconf.get('url')}/api/user/doesnotexist/edit`, { jar }); + assert.equal(response.statusCode, 404); + await groups.leave('administrators', fooUid); }); - it('should render edit/password', (done) => { - request(`${nconf.get('url')}/api/user/foo/edit/password`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - done(); - }); + it('should render edit/password', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/user/foo/edit/password`, { jar }); + assert.equal(response.statusCode, 200); }); it('should render edit/email', async () => { - const res = await requestAsync(`${nconf.get('url')}/api/user/foo/edit/email`, { - jar, - json: true, - resolveWithFullResponse: true, - }); + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/edit/email`, { jar }); - assert.strictEqual(res.statusCode, 200); - assert.strictEqual(res.body, '/register/complete'); + assert.strictEqual(response.statusCode, 200); + assert.strictEqual(body, '/register/complete'); - await requestAsync({ - uri: `${nconf.get('url')}/register/abort`, - method: 'post', + await request.post(`${nconf.get('url')}/register/abort`, { jar, - simple: false, headers: { 'x-csrf-token': csrf_token, }, }); }); - it('should render edit/username', (done) => { - request(`${nconf.get('url')}/api/user/foo/edit/username`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - done(); - }); + it('should render edit/username', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/user/foo/edit/username`, { jar }); + assert.equal(response.statusCode, 200); }); }); @@ -2028,28 +1347,22 @@ describe('Controllers', () => { assert(isFollowing); }); - it('should get followers page', (done) => { - request(`${nconf.get('url')}/api/user/foo/followers`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.users[0].username, 'follower'); - done(); - }); + it('should get followers page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/followers`); + assert.equal(response.statusCode, 200); + assert.equal(body.users[0].username, 'follower'); }); - it('should get following page', (done) => { - request(`${nconf.get('url')}/api/user/follower/following`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.users[0].username, 'foo'); - done(); - }); + it('should get following page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/follower/following`); + assert.equal(response.statusCode, 200); + assert.equal(body.users[0].username, 'foo'); }); it('should return empty after unfollow', async () => { await apiUser.unfollow({ uid: uid }, { uid: fooUid }); - const { res, body } = await helpers.request('get', `/api/user/foo/followers`, { json: true }); - assert.equal(res.statusCode, 200); + const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/followers`); + assert.equal(response.statusCode, 200); assert.equal(body.users.length, 0); }); }); @@ -2060,86 +1373,41 @@ describe('Controllers', () => { ({ jar } = await helpers.loginUser('foo', 'barbar')); }); - it('should 404 for invalid pid', (done) => { - request(`${nconf.get('url')}/api/post/fail`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 for invalid pid', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/post/fail`); + assert.equal(response.statusCode, 404); }); - it('should 403 if user does not have read privilege', (done) => { - privileges.categories.rescind(['groups:topics:read'], category.cid, 'registered-users', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/post/${pid}`, { jar: jar }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 403); - privileges.categories.give(['groups:topics:read'], category.cid, 'registered-users', done); - }); - }); + it('should 403 if user does not have read privilege', async () => { + await privileges.categories.rescind(['groups:topics:read'], category.cid, 'registered-users'); + const { response } = await request.get(`${nconf.get('url')}/api/post/${pid}`, { jar }); + assert.equal(response.statusCode, 403); + await privileges.categories.give(['groups:topics:read'], category.cid, 'registered-users'); }); - it('should return correct post path', (done) => { - request(`${nconf.get('url')}/api/post/${pid}`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/topic/1/test-topic-title'); - assert.equal(body, '/topic/1/test-topic-title'); - done(); - }); + it('should return correct post path', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/post/${pid}`); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/topic/1/test-topic-title'); + assert.equal(body, '/topic/1/test-topic-title'); }); }); describe('cookie consent', () => { - it('should return relevant data in configs API route', (done) => { - request(`${nconf.get('url')}/api/config`, (err, res, body) => { - let parsed; - assert.ifError(err); - assert.equal(res.statusCode, 200); - - try { - parsed = JSON.parse(body); - } catch (e) { - assert.ifError(e); - } - - assert.ok(parsed.cookies); - assert.equal(translator.escape('[[global:cookies.message]]'), parsed.cookies.message); - assert.equal(translator.escape('[[global:cookies.accept]]'), parsed.cookies.dismiss); - assert.equal(translator.escape('[[global:cookies.learn-more]]'), parsed.cookies.link); - - done(); - }); + it('should return relevant data in configs API route', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/config`); + assert.equal(response.statusCode, 200); + assert.ok(body.cookies); + assert.equal(translator.escape('[[global:cookies.message]]'), body.cookies.message); + assert.equal(translator.escape('[[global:cookies.accept]]'), body.cookies.dismiss); + assert.equal(translator.escape('[[global:cookies.learn-more]]'), body.cookies.link); }); - it('response should be parseable when entries have apostrophes', (done) => { - meta.configs.set('cookieConsentMessage', 'Julian\'s Message', (err) => { - assert.ifError(err); - - request(`${nconf.get('url')}/api/config`, (err, res, body) => { - let parsed; - assert.ifError(err); - assert.equal(res.statusCode, 200); - - try { - parsed = JSON.parse(body); - } catch (e) { - assert.ifError(e); - } - - assert.equal('Julian's Message', parsed.cookies.message); - done(); - }); - }); - }); - }); - - it('should return osd data', (done) => { - request(`${nconf.get('url')}/osd.xml`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); + it('response should be parseable when entries have apostrophes', async () => { + await meta.configs.set('cookieConsentMessage', 'Julian\'s Message'); + const { response, body } = await request.get(`${nconf.get('url')}/api/config`); + assert.equal(response.statusCode, 200); + assert.equal('Julian's Message', body.cookies.message); }); }); @@ -2150,43 +1418,31 @@ describe('Controllers', () => { done(); }); - it('should handle topic malformed uri', (done) => { - request(`${nconf.get('url')}/topic/1/a%AFc`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should handle topic malformed uri', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/topic/1/a%AFc`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should handle category malformed uri', (done) => { - request(`${nconf.get('url')}/category/1/a%AFc`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should handle category malformed uri', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/category/1/a%AFc`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should handle malformed uri ', (done) => { - request(`${nconf.get('url')}/user/a%AFc`, (err, res, body) => { - assert.ifError(err); - assert(body); - assert.equal(res.statusCode, 400); - done(); - }); + it('should handle malformed uri ', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/user/a%AFc`); + assert(body); + assert.equal(response.statusCode, 400); }); - it('should handle malformed uri in api', (done) => { - request(`${nconf.get('url')}/api/user/a%AFc`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 400); - assert.equal(body.error, '[[global:400.title]]'); - done(); - }); + it('should handle malformed uri in api', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/a%AFc`); + assert.equal(response.statusCode, 400); + assert.equal(body.error, '[[global:400.title]]'); }); - it('should handle CSRF error', (done) => { + it('should handle CSRF error', async () => { plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; plugins.loadedHooks['filter:router.page'].push({ method: function (req, res, next) { @@ -2196,15 +1452,12 @@ describe('Controllers', () => { }, }); - request(`${nconf.get('url')}/users`, {}, (err, res) => { - plugins.loadedHooks['filter:router.page'] = []; - assert.ifError(err); - assert.equal(res.statusCode, 403); - done(); - }); + const { response } = await request.get(`${nconf.get('url')}/users`); + plugins.loadedHooks['filter:router.page'] = []; + assert.equal(response.statusCode, 403); }); - it('should handle black-list error', (done) => { + it('should handle black-list error', async () => { plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; plugins.loadedHooks['filter:router.page'].push({ method: function (req, res, next) { @@ -2213,17 +1466,13 @@ describe('Controllers', () => { next(err); }, }); - - request(`${nconf.get('url')}/users`, {}, (err, res, body) => { - plugins.loadedHooks['filter:router.page'] = []; - assert.ifError(err); - assert.equal(res.statusCode, 403); - assert.equal(body, 'blacklist error message'); - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/users`); + plugins.loadedHooks['filter:router.page'] = []; + assert.equal(response.statusCode, 403); + assert.equal(body, 'blacklist error message'); }); - it('should handle page redirect through error', (done) => { + it('should handle page redirect through error', async () => { plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; plugins.loadedHooks['filter:router.page'].push({ method: function (req, res, next) { @@ -2234,16 +1483,12 @@ describe('Controllers', () => { next(err); }, }); - - request(`${nconf.get('url')}/users`, {}, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/users`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should handle api page redirect through error', (done) => { + it('should handle api page redirect through error', async () => { plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; plugins.loadedHooks['filter:router.page'].push({ method: function (req, res, next) { @@ -2254,17 +1499,13 @@ describe('Controllers', () => { next(err); }, }); - - request(`${nconf.get('url')}/api/users`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/api/popular'); - assert(body, '/api/popular'); - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/api/users`); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/api/popular'); + assert(body, '/api/popular'); }); - it('should handle error page', (done) => { + it('should handle error page', async () => { plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; plugins.loadedHooks['filter:router.page'].push({ method: function (req, res, next) { @@ -2272,14 +1513,10 @@ describe('Controllers', () => { next(err); }, }); - - request(`${nconf.get('url')}/users`, (err, res, body) => { - plugins.loadedHooks['filter:router.page'] = []; - assert.ifError(err); - assert.equal(res.statusCode, 500); - assert(body); - done(); - }); + const { response, body } = await request.get(`${nconf.get('url')}/users`); + plugins.loadedHooks['filter:router.page'] = []; + assert.equal(response.statusCode, 500); + assert(body); }); }); @@ -2289,265 +1526,115 @@ describe('Controllers', () => { ({ jar } = await helpers.loginUser('foo', 'barbar')); }); - it('should return 404 if cid is not a number', (done) => { - request(`${nconf.get('url')}/api/category/fail`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should return 404 if cid is not a number', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/category/fail`); + assert.equal(response.statusCode, 404); }); - it('should return 404 if topic index is not a number', (done) => { - request(`${nconf.get('url')}/api/category/${category.slug}/invalidtopicindex`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should return 404 if topic index is not a number', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}/invalidtopicindex`); + assert.equal(response.statusCode, 404); }); - it('should 404 if category does not exist', (done) => { - request(`${nconf.get('url')}/api/category/123123`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if category does not exist', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/category/123123`); + assert.equal(response.statusCode, 404); }); - it('should 404 if category is disabled', (done) => { - categories.create({ name: 'disabled' }, (err, category) => { - assert.ifError(err); - categories.setCategoryField(category.cid, 'disabled', 1, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/category/${category.slug}`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); - }); - }); + it('should 404 if category is disabled', async () => { + const category = await categories.create({ name: 'disabled' }); + await categories.setCategoryField(category.cid, 'disabled', 1); + const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`); + assert.equal(response.statusCode, 404); }); - it('should return 401 if not allowed to read', (done) => { - categories.create({ name: 'hidden' }, (err, category) => { - assert.ifError(err); - privileges.categories.rescind(['groups:read'], category.cid, 'guests', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/category/${category.slug}`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 401); - done(); - }); - }); - }); + it('should return 401 if not allowed to read', async () => { + const category = await categories.create({ name: 'hidden' }); + await privileges.categories.rescind(['groups:read'], category.cid, 'guests'); + const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`); + assert.equal(response.statusCode, 401); }); - it('should redirect if topic index is negative', (done) => { - request(`${nconf.get('url')}/api/category/${category.slug}/-10`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.ok(res.headers['x-redirect']); - done(); - }); + it('should redirect if topic index is negative', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}/-10`); + assert.equal(response.statusCode, 200); + assert.ok(response.headers['x-redirect']); }); - it('should 404 if page is not found', (done) => { - user.setSetting(fooUid, 'usePagination', 1, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/category/${category.slug}?page=100`, { jar: jar, json: true }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); - }); + it('should 404 if page is not found', async () => { + await user.setSetting(fooUid, 'usePagination', 1); + const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}?page=100`, { jar }); + assert.equal(response.statusCode, 404); }); - it('should load page 1 if req.query.page is not sent', (done) => { - request(`${nconf.get('url')}/api/category/${category.slug}`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.pagination.currentPage, 1); - done(); - }); + it('should load page 1 if req.query.page is not sent', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`, { jar }); + assert.equal(response.statusCode, 200); + assert.equal(body.pagination.currentPage, 1); }); - it('should sort topics by most posts', (done) => { - async.waterfall([ - function (next) { - categories.create({ name: 'most-posts-category' }, next); - }, - function (category, next) { - async.waterfall([ - function (next) { - topics.post({ uid: fooUid, cid: category.cid, title: 'topic 1', content: 'topic 1 OP' }, next); - }, - function (data, next) { - topics.post({ uid: fooUid, cid: category.cid, title: 'topic 2', content: 'topic 2 OP' }, next); - }, - function (data, next) { - topics.reply({ uid: fooUid, content: 'topic 2 reply', tid: data.topicData.tid }, next); - }, - function (postData, next) { - request(`${nconf.get('url')}/api/category/${category.slug}?sort=most_posts`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.topics[0].title, 'topic 2'); - assert.equal(body.topics[0].postcount, 2); - assert.equal(body.topics[1].postcount, 1); - next(); - }); - }, - ], (err) => { - next(err); - }); - }, - ], done); + it('should sort topics by most posts', async () => { + const category = await categories.create({ name: 'most-posts-category' }); + await topics.post({ uid: fooUid, cid: category.cid, title: 'topic 1', content: 'topic 1 OP' }); + const t2 = await topics.post({ uid: fooUid, cid: category.cid, title: 'topic 2', content: 'topic 2 OP' }); + await topics.reply({ uid: fooUid, content: 'topic 2 reply', tid: t2.topicData.tid }); + + const { response, body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}?sort=most_posts`, { jar }); + assert.equal(response.statusCode, 200); + assert.equal(body.topics[0].title, 'topic 2'); + assert.equal(body.topics[0].postcount, 2); + assert.equal(body.topics[1].postcount, 1); }); - it('should load a specific users topics from a category with tags', (done) => { - async.waterfall([ - function (next) { - categories.create({ name: 'filtered-category' }, next); - }, - function (category, next) { - async.waterfall([ - function (next) { - topics.post({ uid: fooUid, cid: category.cid, title: 'topic 1', content: 'topic 1 OP', tags: ['java', 'cpp'] }, next); - }, - function (data, next) { - topics.post({ uid: fooUid, cid: category.cid, title: 'topic 2', content: 'topic 2 OP', tags: ['node', 'javascript'] }, next); - }, - function (data, next) { - topics.post({ uid: fooUid, cid: category.cid, title: 'topic 3', content: 'topic 3 OP', tags: ['java', 'cpp', 'best'] }, next); - }, - function (data, next) { - request(`${nconf.get('url')}/api/category/${category.slug}?tag=node&author=foo`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.topics[0].title, 'topic 2'); - next(); - }); - }, - function (next) { - request(`${nconf.get('url')}/api/category/${category.slug}?tag[]=java&tag[]=cpp`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.topics[0].title, 'topic 3'); - assert.equal(body.topics[1].title, 'topic 1'); - next(); - }); - }, - ], (err) => { - next(err); - }); - }, - ], done); + it('should load a specific users topics from a category with tags', async () => { + const category = await categories.create({ name: 'filtered-category' }); + await topics.post({ uid: fooUid, cid: category.cid, title: 'topic 1', content: 'topic 1 OP', tags: ['java', 'cpp'] }); + await topics.post({ uid: fooUid, cid: category.cid, title: 'topic 2', content: 'topic 2 OP', tags: ['node', 'javascript'] }); + await topics.post({ uid: fooUid, cid: category.cid, title: 'topic 3', content: 'topic 3 OP', tags: ['java', 'cpp', 'best'] }); + + let { body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}?tag=node&author=foo`, { jar }); + assert.equal(body.topics[0].title, 'topic 2'); + + ({ body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}?tag[]=java&tag[]=cpp`, { jar })); + assert.equal(body.topics[0].title, 'topic 3'); + assert.equal(body.topics[1].title, 'topic 1'); }); - it('should redirect if category is a link', (done) => { - let cid; - let category; - async.waterfall([ - function (next) { - categories.create({ name: 'redirect', link: 'https://nodebb.org' }, next); - }, - function (_category, next) { - category = _category; - cid = category.cid; - request(`${nconf.get('url')}/api/category/${category.slug}`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], 'https://nodebb.org'); - assert.equal(body, 'https://nodebb.org'); - next(); - }); - }, - function (next) { - categories.setCategoryField(cid, 'link', '/recent', next); - }, - function (next) { - request(`${nconf.get('url')}/api/category/${category.slug}`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/recent'); - assert.equal(body, '/recent'); - next(); - }); - }, - ], done); + it('should redirect if category is a link', async () => { + const category = await categories.create({ name: 'redirect', link: 'https://nodebb.org' }); + const { cid } = category; + + let result = await request.get(`${nconf.get('url')}/api/category/${category.slug}`, { jar }); + assert.equal(result.response.headers['x-redirect'], 'https://nodebb.org'); + assert.equal(result.body, 'https://nodebb.org'); + await categories.setCategoryField(cid, 'link', '/recent'); + + result = await request.get(`${nconf.get('url')}/api/category/${category.slug}`, { jar }); + assert.equal(result.response.headers['x-redirect'], '/recent'); + assert.equal(result.body, '/recent'); }); - it('should get recent topic replies from children categories', (done) => { - let parentCategory; - let childCategory1; - let childCategory2; + it('should get recent topic replies from children categories', async () => { + const parentCategory = await categories.create({ name: 'parent category', backgroundImage: 'path/to/some/image' }); + const childCategory1 = await categories.create({ name: 'child category 1', parentCid: category.cid }); + const childCategory2 = await categories.create({ name: 'child category 2', parentCid: parentCategory.cid }); + await topics.post({ uid: fooUid, cid: childCategory2.cid, title: 'topic 1', content: 'topic 1 OP' }); - async.waterfall([ - function (next) { - categories.create({ name: 'parent category', backgroundImage: 'path/to/some/image' }, next); - }, - function (category, next) { - parentCategory = category; - async.waterfall([ - function (next) { - categories.create({ name: 'child category 1', parentCid: category.cid }, next); - }, - function (category, next) { - childCategory1 = category; - categories.create({ name: 'child category 2', parentCid: parentCategory.cid }, next); - }, - function (category, next) { - childCategory2 = category; - topics.post({ uid: fooUid, cid: childCategory2.cid, title: 'topic 1', content: 'topic 1 OP' }, next); - }, - function (data, next) { - request(`${nconf.get('url')}/api/category/${parentCategory.slug}`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.children[0].posts[0].content, 'topic 1 OP'); - next(); - }); - }, - ], (err) => { - next(err); - }); - }, - ], done); + const { body } = await request.get(`${nconf.get('url')}/api/category/${parentCategory.slug}`, { jar }); + assert.equal(body.children[0].posts[0].content, 'topic 1 OP'); }); - it('should create 2 pages of topics', (done) => { - async.waterfall([ - function (next) { - categories.create({ name: 'category with 2 pages' }, next); - }, - function (category, next) { - const titles = []; - for (let i = 0; i < 30; i++) { - titles.push(`topic title ${i}`); - } + it('should create 2 pages of topics', async () => { + const category = await categories.create({ name: 'category with 2 pages' }); + for (let i = 0; i < 30; i++) { + // eslint-disable-next-line no-await-in-loop + await topics.post({ uid: fooUid, cid: category.cid, title: `topic title ${i}`, content: 'does not really matter' }); + } + const userSettings = await user.getSettings(fooUid); - async.waterfall([ - function (next) { - async.eachSeries(titles, (title, next) => { - topics.post({ uid: fooUid, cid: category.cid, title: title, content: 'does not really matter' }, next); - }, next); - }, - function (next) { - user.getSettings(fooUid, next); - }, - function (settings, next) { - request(`${nconf.get('url')}/api/category/${category.slug}`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body.topics.length, settings.topicsPerPage); - assert.equal(body.pagination.pageCount, 2); - next(); - }); - }, - ], (err) => { - next(err); - }); - }, - ], done); + const { body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`, { jar }); + assert.equal(body.topics.length, userSettings.topicsPerPage); + assert.equal(body.pagination.pageCount, 2); }); it('should load categories', async () => { @@ -2580,58 +1667,40 @@ describe('Controllers', () => { ({ jar } = await helpers.loginUser('foo', 'barbar')); }); - it('should load unread page', (done) => { - request(`${nconf.get('url')}/api/unread`, { jar: jar }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - done(); - }); + it('should load unread page', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/unread`, { jar }); + assert.equal(response.statusCode, 200); }); - it('should 404 if filter is invalid', (done) => { - request(`${nconf.get('url')}/api/unread/doesnotexist`, { jar: jar }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if filter is invalid', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/unread/doesnotexist`, { jar }); + assert.equal(response.statusCode, 404); }); - it('should return total unread count', (done) => { - request(`${nconf.get('url')}/api/unread/total?filter=new`, { jar: jar }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body, 0); - done(); - }); + it('should return total unread count', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/unread/total?filter=new`, { jar }); + assert.equal(response.statusCode, 200); + assert.equal(body, 0); }); - it('should redirect if page is out of bounds', (done) => { - request(`${nconf.get('url')}/api/unread?page=-1`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/unread?page=1'); - assert.equal(body, '/unread?page=1'); - done(); - }); + it('should redirect if page is out of bounds', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/unread?page=-1`, { jar }); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/unread?page=1'); + assert.equal(body, '/unread?page=1'); }); }); describe('admin middlewares', () => { - it('should redirect to login', (done) => { - request(`${nconf.get('url')}//api/admin/advanced/database`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 401); - done(); - }); + it('should redirect to login', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/admin/advanced/database`); + assert.equal(response.statusCode, 401); }); - it('should redirect to login', (done) => { - request(`${nconf.get('url')}//admin/advanced/database`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.includes('Login to your account')); - done(); - }); + it('should redirect to login', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/admin/advanced/database`); + assert.equal(response.statusCode, 200); + assert(body.includes('Login to your account')); }); }); @@ -2645,18 +1714,15 @@ describe('Controllers', () => { csrf_token = login.csrf_token; }); - it('should load the composer route', (done) => { - request(`${nconf.get('url')}/api/compose?cid=1`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.title); - assert(body.template); - assert.equal(body.url, `${nconf.get('relative_path')}/compose`); - done(); - }); + it('should load the composer route', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/compose?cid=1`); + assert.equal(response.statusCode, 200); + assert(body.title); + assert(body.template); + assert.equal(body.url, `${nconf.get('relative_path')}/compose`); }); - it('should load the composer route if disabled by plugin', (done) => { + it('should load the composer route if disabled by plugin', async () => { function hookMethod(hookData, callback) { hookData.templateData.disabled = true; callback(null, hookData); @@ -2667,151 +1733,134 @@ describe('Controllers', () => { method: hookMethod, }); - request(`${nconf.get('url')}/api/compose?cid=1`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.title); - assert.strictEqual(body.template.name, ''); - assert.strictEqual(body.url, `${nconf.get('relative_path')}/compose`); + const { response, body } = await request.get(`${nconf.get('url')}/api/compose?cid=1`); + assert.equal(response.statusCode, 200); + assert(body.title); + assert.strictEqual(body.template.name, ''); + assert.strictEqual(body.url, `${nconf.get('relative_path')}/compose`); - plugins.hooks.unregister('myTestPlugin', 'filter:composer.build', hookMethod); - done(); - }); + plugins.hooks.unregister('myTestPlugin', 'filter:composer.build', hookMethod); }); - it('should error with invalid data', (done) => { - request.post(`${nconf.get('url')}/compose`, { - form: { + it('should error with invalid data', async () => { + let result = await request.post(`${nconf.get('url')}/compose`, { + data: { content: 'a new reply', }, jar: jar, headers: { 'x-csrf-token': csrf_token, }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 400); - request.post(`${nconf.get('url')}/compose`, { - form: { - tid: tid, - }, - jar: jar, - headers: { - 'x-csrf-token': csrf_token, - }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 400); - done(); - }); }); - }); - it('should create a new topic and reply by composer route', (done) => { - const data = { - cid: cid, - title: 'no js is good', - content: 'a topic with noscript', - }; - request.post(`${nconf.get('url')}/compose`, { - form: data, + assert.equal(result.response.statusCode, 400); + result = await request.post(`${nconf.get('url')}/compose`, { + body: { + tid: tid, + }, jar: jar, headers: { 'x-csrf-token': csrf_token, }, - }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 302); - request.post(`${nconf.get('url')}/compose`, { - form: { - tid: tid, - content: 'a new reply', - }, - jar: jar, - headers: { - 'x-csrf-token': csrf_token, - }, - }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 302); - done(); - }); }); + assert.equal(result.response.statusCode, 400); + }); + + it('should create a new topic and reply by composer route', async () => { + let result = await request.post(`${nconf.get('url')}/compose`, { + body: { + cid: cid, + title: 'no js is good', + content: 'a topic with noscript', + }, + jar: jar, + maxRedirect: 0, + redirect: 'manual', + headers: { + 'x-csrf-token': csrf_token, + }, + }); + + assert.equal(result.response.statusCode, 302); + result = await request.post(`${nconf.get('url')}/compose`, { + body: { + tid: tid, + content: 'a new reply', + }, + jar: jar, + maxRedirect: 0, + redirect: 'manual', + headers: { + 'x-csrf-token': csrf_token, + }, + }); + assert.equal(result.response.statusCode, 302); }); it('should create a new topic and reply by composer route as a guest', async () => { const jar = request.jar(); const csrf_token = await helpers.getCsrfToken(jar); - const data = { - cid: cid, - title: 'no js is good', - content: 'a topic with noscript', - handle: 'guest1', - }; await privileges.categories.give(['groups:topics:create', 'groups:topics:reply'], cid, 'guests'); const result = await helpers.request('post', `/compose`, { - form: data, + body: { + cid: cid, + title: 'no js is good', + content: 'a topic with noscript', + handle: 'guest1', + }, jar, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': csrf_token, }, }); - assert.strictEqual(result.res.statusCode, 302); + assert.strictEqual(result.response.statusCode, 302); const replyResult = await helpers.request('post', `/compose`, { - form: { + body: { tid: tid, content: 'a new reply', handle: 'guest2', }, jar, + maxRedirect: 0, + redirect: 'manual', headers: { 'x-csrf-token': csrf_token, }, }); - assert.equal(replyResult.res.statusCode, 302); + assert.equal(replyResult.response.statusCode, 302); await privileges.categories.rescind(['groups:topics:post', 'groups:topics:reply'], cid, 'guests'); }); }); describe('test routes', () => { if (process.env.NODE_ENV === 'development') { - it('should load debug route', (done) => { - request(`${nconf.get('url')}/debug/test`, {}, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + it('should load debug route', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/debug/test`); + assert.equal(response.statusCode, 404); + assert(body); }); - it('should load redoc read route', (done) => { - request(`${nconf.get('url')}/debug/spec/read`, {}, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load redoc read route', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/debug/spec/read`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load redoc write route', (done) => { - request(`${nconf.get('url')}/debug/spec/write`, {}, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load redoc write route', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/debug/spec/write`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load 404 for invalid type', (done) => { - request(`${nconf.get('url')}/debug/spec/doesnotexist`, {}, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + it('should load 404 for invalid type', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/debug/spec/doesnotexist`); + assert.equal(response.statusCode, 404); + assert(body); }); } }); diff --git a/test/feeds.js b/test/feeds.js index 6f1c9d0b08..a54b485981 100644 --- a/test/feeds.js +++ b/test/feeds.js @@ -1,14 +1,12 @@ 'use strict'; const assert = require('assert'); -const async = require('async'); -const request = require('request'); const nconf = require('nconf'); const db = require('./mocks/databasemock'); +const request = require('../src/request'); const topics = require('../src/topics'); const categories = require('../src/categories'); -const groups = require('../src/groups'); const user = require('../src/user'); const meta = require('../src/meta'); const privileges = require('../src/privileges'); @@ -16,38 +14,27 @@ const helpers = require('./helpers'); describe('feeds', () => { let tid; - let pid; let fooUid; let cid; - before((done) => { + before(async () => { meta.config['feeds:disableRSS'] = 1; - async.series({ - category: function (next) { - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - }, next); - }, - user: function (next) { - user.create({ username: 'foo', password: 'barbar', email: 'foo@test.com' }, next); - }, - }, (err, results) => { - if (err) { - return done(err); - } - cid = results.category.cid; - fooUid = results.user; - - topics.post({ uid: results.user, title: 'test topic title', content: 'test topic content', cid: results.category.cid }, (err, result) => { - tid = result.topicData.tid; - pid = result.postData.pid; - done(err); - }); + const category = await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', }); + cid = category.cid; + fooUid = await user.create({ username: 'foo', password: 'barbar', email: 'foo@test.com' }); + + const result = await topics.post({ + cid: cid, + uid: fooUid, + title: 'test topic title', + content: 'test topic content', + }); + tid = result.topicData.tid; }); - - it('should 404', (done) => { + it('should 404', async () => { const feedUrls = [ `${nconf.get('url')}/topic/${tid}.rss`, `${nconf.get('url')}/category/${cid}.rss`, @@ -61,67 +48,45 @@ describe('feeds', () => { `${nconf.get('url')}/user/foo/topics.rss`, `${nconf.get('url')}/tags/nodebb.rss`, ]; - async.eachSeries(feedUrls, (url, next) => { - request(url, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - next(); - }); - }, (err) => { - assert.ifError(err); - meta.config['feeds:disableRSS'] = 0; - done(); - }); + for (const url of feedUrls) { + // eslint-disable-next-line no-await-in-loop + const { response } = await request.get(url); + assert.equal(response.statusCode, 404); + } + meta.config['feeds:disableRSS'] = 0; }); - it('should 404 if topic does not exist', (done) => { - request(`${nconf.get('url')}/topic/${1000}.rss`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if topic does not exist', async () => { + const { response } = await request.get(`${nconf.get('url')}/topic/${1000}.rss`); + assert.equal(response.statusCode, 404); }); - it('should 404 if category id is not a number', (done) => { - request(`${nconf.get('url')}/category/invalid.rss`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if category id is not a number', async () => { + const { response } = await request.get(`${nconf.get('url')}/category/invalid.rss`); + assert.equal(response.statusCode, 404); }); - it('should redirect if we do not have read privilege', (done) => { - privileges.categories.rescind(['groups:topics:read'], cid, 'guests', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/topic/${tid}.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(body.includes('Login to your account')); - privileges.categories.give(['groups:topics:read'], cid, 'guests', done); - }); - }); + it('should redirect if we do not have read privilege', async () => { + await privileges.categories.rescind(['groups:topics:read'], cid, 'guests'); + const { response, body } = await request.get(`${nconf.get('url')}/topic/${tid}.rss`); + assert.equal(response.statusCode, 200); + assert(body); + assert(body.includes('Login to your account')); + await privileges.categories.give(['groups:topics:read'], cid, 'guests'); }); - it('should 404 if user is not found', (done) => { - request(`${nconf.get('url')}/user/doesnotexist/topics.rss`, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - done(); - }); + it('should 404 if user is not found', async () => { + const { response } = await request.get(`${nconf.get('url')}/user/doesnotexist/topics.rss`); + assert.equal(response.statusCode, 404); }); - it('should redirect if we do not have read privilege', (done) => { - privileges.categories.rescind(['groups:read'], cid, 'guests', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/category/${cid}.rss`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - assert(body.includes('Login to your account')); - privileges.categories.give(['groups:read'], cid, 'guests', done); - }); - }); + it('should redirect if we do not have read privilege', async () => { + await privileges.categories.rescind(['groups:read'], cid, 'guests'); + const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}.rss`); + assert.equal(response.statusCode, 200); + assert(body); + assert(body.includes('Login to your account')); + await privileges.categories.give(['groups:read'], cid, 'guests'); }); describe('private feeds and tokens', () => { @@ -131,69 +96,45 @@ describe('feeds', () => { ({ jar } = await helpers.loginUser('foo', 'barbar')); }); - it('should load feed if its not private', (done) => { - request(`${nconf.get('url')}/category/${cid}.rss`, { }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load feed if its not private', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}.rss`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should not allow access if uid or token is missing', (done) => { - privileges.categories.rescind(['groups:read'], cid, 'guests', (err) => { - assert.ifError(err); - async.parallel({ - test1: function (next) { - request(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}`, { }, next); - }, - test2: function (next) { - request(`${nconf.get('url')}/category/${cid}.rss?token=sometoken`, { }, next); - }, - }, (err, results) => { - assert.ifError(err); - assert.equal(results.test1[0].statusCode, 200); - assert.equal(results.test2[0].statusCode, 200); - assert(results.test1[0].body.includes('Login to your account')); - assert(results.test2[0].body.includes('Login to your account')); - done(); - }); - }); + it('should not allow access if uid or token is missing', async () => { + await privileges.categories.rescind(['groups:read'], cid, 'guests'); + const [test1, test2] = await Promise.all([ + request.get(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}`, { }), + request.get(`${nconf.get('url')}/category/${cid}.rss?token=sometoken`, { }), + ]); + + assert.equal(test1.response.statusCode, 200); + assert.equal(test2.response.statusCode, 200); + assert(test1.body.includes('Login to your account')); + assert(test2.body.includes('Login to your account')); }); - it('should not allow access if token is wrong', (done) => { - request(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}&token=sometoken`, { }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.includes('Login to your account')); - done(); - }); + it('should not allow access if token is wrong', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}&token=sometoken`); + assert.equal(response.statusCode, 200); + assert(body.includes('Login to your account')); }); - it('should allow access if token is correct', (done) => { - request(`${nconf.get('url')}/api/category/${cid}`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - rssToken = body.rssFeedUrl.split('token')[1].slice(1); - request(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}&token=${rssToken}`, { }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.startsWith(' { + const { body: body1 } = await request.get(`${nconf.get('url')}/api/category/${cid}`, { jar }); + rssToken = body1.rssFeedUrl.split('token')[1].slice(1); + const { response, body: body2 } = await request.get(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}&token=${rssToken}`); + assert.equal(response.statusCode, 200); + assert(body2.startsWith(' { - privileges.categories.rescind(['groups:read'], cid, 'registered-users', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}&token=${rssToken}`, { }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.includes('Login to your account')); - done(); - }); - }); + it('should not allow access if token is correct but has no privilege', async () => { + await privileges.categories.rescind(['groups:read'], cid, 'registered-users'); + const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}.rss?uid=${fooUid}&token=${rssToken}`); + assert.equal(response.statusCode, 200); + assert(body.includes('Login to your account')); }); }); }); diff --git a/test/flags.js b/test/flags.js index 65dd23ef66..ee150a10c4 100644 --- a/test/flags.js +++ b/test/flags.js @@ -2,15 +2,13 @@ const assert = require('assert'); const nconf = require('nconf'); -const async = require('async'); -const request = require('request-promise-native'); const util = require('util'); const sleep = util.promisify(setTimeout); const db = require('./mocks/databasemock'); const helpers = require('./helpers'); - +const request = require('../src/request'); const Flags = require('../src/flags'); const Categories = require('../src/categories'); const Topics = require('../src/topics'); @@ -243,13 +241,11 @@ describe('Flags', () => { it('should show user history for admins', async () => { await Groups.join('administrators', moderatorUid); - const flagData = await request({ - uri: `${nconf.get('url')}/api/flags/1`, + const { body: flagData } = await request.get(`${nconf.get('url')}/api/flags/1`, { jar, headers: { 'x-csrf-token': csrfToken, }, - json: true, }); assert(flagData.history); @@ -260,13 +256,11 @@ describe('Flags', () => { it('should show user history for global moderators', async () => { await Groups.join('Global Moderators', moderatorUid); - const flagData = await request({ - uri: `${nconf.get('url')}/api/flags/1`, + const { body: flagData } = await request.get(`${nconf.get('url')}/api/flags/1`, { jar, headers: { 'x-csrf-token': csrfToken, }, - json: true, }); assert(flagData.history); @@ -895,9 +889,7 @@ describe('Flags', () => { describe('.create()', () => { it('should create a flag with no errors', async () => { - await request({ - method: 'post', - uri: `${nconf.get('url')}/api/v3/flags`, + await request.post(`${nconf.get('url')}/api/v3/flags`, { jar, headers: { 'x-csrf-token': csrfToken, @@ -907,7 +899,6 @@ describe('Flags', () => { id: pid, reason: 'foobar', }, - json: true, }); const exists = await Flags.exists('post', pid, 2); @@ -921,9 +912,7 @@ describe('Flags', () => { content: 'This is flaggable content', }); - const { response } = await request({ - method: 'post', - uri: `${nconf.get('url')}/api/v3/flags`, + const { body } = await request.post(`${nconf.get('url')}/api/v3/flags`, { jar, headers: { 'x-csrf-token': csrfToken, @@ -933,10 +922,9 @@ describe('Flags', () => { id: postData.pid, reason: '"', }, - json: true, }); - const flagData = await Flags.get(response.flagId); + const flagData = await Flags.get(body.response.flagId); assert.strictEqual(flagData.reports[0].value, '"<script>alert('ok');</script>'); }); @@ -953,15 +941,9 @@ describe('Flags', () => { }); const login = await helpers.loginUser('unprivileged', 'abcdef'); const jar3 = login.jar; - const config = await request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar3, - }); - const csrfToken = config.csrf_token; - const { statusCode, body } = await request({ - method: 'post', - uri: `${nconf.get('url')}/api/v3/flags`, + const csrfToken = await helpers.getCsrfToken(jar3); + + const { response, body } = await request.post(`${nconf.get('url')}/api/v3/flags`, { jar: jar3, headers: { 'x-csrf-token': csrfToken, @@ -971,11 +953,8 @@ describe('Flags', () => { id: result.postData.pid, reason: 'foobar', }, - json: true, - simple: false, - resolveWithFullResponse: true, }); - assert.strictEqual(statusCode, 403); + assert.strictEqual(response.statusCode, 403); // Handle dev mode test delete body.stack; @@ -992,9 +971,7 @@ describe('Flags', () => { describe('.update()', () => { it('should update a flag\'s properties', async () => { - const { response } = await request({ - method: 'put', - uri: `${nconf.get('url')}/api/v3/flags/4`, + const { body } = await request.put(`${nconf.get('url')}/api/v3/flags/4`, { jar, headers: { 'x-csrf-token': csrfToken, @@ -1002,10 +979,9 @@ describe('Flags', () => { body: { state: 'wip', }, - json: true, }); - const { history } = response; + const { history } = body.response; assert(Array.isArray(history)); assert(history[0].fields.hasOwnProperty('state')); assert.strictEqual('[[flags:state-wip]]', history[0].fields.state); @@ -1014,14 +990,11 @@ describe('Flags', () => { describe('.rescind()', () => { it('should remove a flag\'s report', async () => { - const response = await request({ - method: 'delete', - uri: `${nconf.get('url')}/api/v3/flags/4/report`, + const { response } = await request.del(`${nconf.get('url')}/api/v3/flags/4/report`, { jar, headers: { 'x-csrf-token': csrfToken, }, - resolveWithFullResponse: true, }); assert.strictEqual(response.statusCode, 200); @@ -1030,9 +1003,7 @@ describe('Flags', () => { describe('.appendNote()', () => { it('should append a note to the flag', async () => { - const { response } = await request({ - method: 'post', - uri: `${nconf.get('url')}/api/v3/flags/4/notes`, + const { body } = await request.post(`${nconf.get('url')}/api/v3/flags/4/notes`, { jar, headers: { 'x-csrf-token': csrfToken, @@ -1041,9 +1012,8 @@ describe('Flags', () => { note: 'lorem ipsum dolor sit amet', datetime: 1626446956652, }, - json: true, }); - + const { response } = body; assert(response.hasOwnProperty('notes')); assert(Array.isArray(response.notes)); assert.strictEqual('lorem ipsum dolor sit amet', response.notes[0].content); @@ -1058,16 +1028,13 @@ describe('Flags', () => { describe('.deleteNote()', () => { it('should delete a note from a flag', async () => { - const { response } = await request({ - method: 'delete', - uri: `${nconf.get('url')}/api/v3/flags/4/notes/1626446956652`, + const { body } = await request.del(`${nconf.get('url')}/api/v3/flags/4/notes/1626446956652`, { jar, headers: { 'x-csrf-token': csrfToken, }, - json: true, }); - + const { response } = body; assert(Array.isArray(response.history)); assert(Array.isArray(response.notes)); assert.strictEqual(response.notes.length, 0); @@ -1088,7 +1055,7 @@ describe('Flags', () => { before(async () => { uid = await User.create({ username: 'flags-access-control', password: 'abcdef' }); ({ jar, csrf_token } = await helpers.loginUser('flags-access-control', 'abcdef')); - + console.log('cs', csrfToken); flaggerUid = await User.create({ username: 'flags-access-control-flagger', password: 'abcdef' }); }); @@ -1106,68 +1073,44 @@ describe('Flags', () => { }); ({ flagId } = await Flags.create('post', postData.pid, flaggerUid, 'spam')); + const commonOpts = { + jar, + headers: { + 'x-csrf-token': csrf_token, + }, + }; requests = new Set([ { + ...commonOpts, method: 'get', uri: `${nconf.get('url')}/api/v3/flags/${flagId}`, - jar, - headers: { - 'x-csrf-token': csrf_token, - }, - json: true, - simple: false, - resolveWithFullResponse: true, }, { + ...commonOpts, method: 'put', uri: `${nconf.get('url')}/api/v3/flags/${flagId}`, - jar, - headers: { - 'x-csrf-token': csrf_token, - }, body: { state: 'wip', }, - json: true, - simple: false, - resolveWithFullResponse: true, }, { + ...commonOpts, method: 'post', uri: `${nconf.get('url')}/api/v3/flags/${flagId}/notes`, - jar, - headers: { - 'x-csrf-token': csrf_token, - }, body: { note: 'test note', datetime: noteTime, }, - json: true, - simple: false, - resolveWithFullResponse: true, }, { + ...commonOpts, method: 'delete', uri: `${nconf.get('url')}/api/v3/flags/${flagId}/notes/${noteTime}`, - jar, - headers: { - 'x-csrf-token': csrf_token, - }, - json: true, - simple: false, - resolveWithFullResponse: true, }, { + ...commonOpts, method: 'delete', uri: `${nconf.get('url')}/api/v3/flags/${flagId}`, - jar, - headers: { - 'x-csrf-token': csrf_token, - }, - json: true, - simple: false, - resolveWithFullResponse: true, }, ]); }); @@ -1179,7 +1122,8 @@ describe('Flags', () => { delete opts.headers; // eslint-disable-next-line no-await-in-loop - const { statusCode } = await request(opts); + const { response } = await request[opts.method](opts.uri, opts); + const { statusCode } = response; assert(statusCode.toString().startsWith(4), `${opts.method.toUpperCase()} ${opts.uri} => ${statusCode}`); } }); @@ -1187,7 +1131,8 @@ describe('Flags', () => { it('should not allow access to privileged flag endpoints to regular users', async () => { for (const opts of requests) { // eslint-disable-next-line no-await-in-loop - const { statusCode } = await request(opts); + const { response } = await request[opts.method](opts.uri, opts); + const { statusCode } = response; assert(statusCode.toString().startsWith(4), `${opts.method.toUpperCase()} ${opts.uri} => ${statusCode}`); } }); @@ -1197,7 +1142,8 @@ describe('Flags', () => { for (const opts of requests) { // eslint-disable-next-line no-await-in-loop - const { statusCode } = await request(opts); + const { response } = await request[opts.method](opts.uri, opts); + const { statusCode } = response; assert.strictEqual(statusCode, 200, `${opts.method.toUpperCase()} ${opts.uri} => ${statusCode}`); } }); @@ -1207,7 +1153,8 @@ describe('Flags', () => { for (const opts of requests) { // eslint-disable-next-line no-await-in-loop - const { statusCode } = await request(opts); + const { response } = await request[opts.method](opts.uri, opts); + const { statusCode } = response; assert.strictEqual(statusCode, 200, `${opts.method.toUpperCase()} ${opts.uri} => ${statusCode}`); } }); @@ -1217,7 +1164,8 @@ describe('Flags', () => { for (const opts of requests) { // eslint-disable-next-line no-await-in-loop - const { statusCode } = await request(opts); + const { response } = await request[opts.method](opts.uri, opts); + const { statusCode } = response; assert.strictEqual(statusCode, 200, `${opts.method.toUpperCase()} ${opts.uri} => ${statusCode}`); } }); @@ -1231,7 +1179,8 @@ describe('Flags', () => { for (const opts of requests) { // eslint-disable-next-line no-await-in-loop - const { statusCode } = await request(opts); + const { response } = await request[opts.method](opts.uri, opts); + const { statusCode } = response; assert(statusCode.toString().startsWith(4), `${opts.method.toUpperCase()} ${opts.uri} => ${statusCode}`); } }); diff --git a/test/helpers/index.js b/test/helpers/index.js index aea7761e17..e71a05edaa 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -1,26 +1,22 @@ 'use strict'; -const request = require('request'); -const requestAsync = require('request-promise-native'); const nconf = require('nconf'); const fs = require('fs'); +const path = require('path'); const winston = require('winston'); -const utils = require('../../src/utils'); +const request = require('../../src/request'); const helpers = module.exports; helpers.getCsrfToken = async (jar) => { - const { csrf_token: token } = await requestAsync({ - url: `${nconf.get('url')}/api/config`, - json: true, + const { body } = await request.get(`${nconf.get('url')}/api/config`, { jar, }); - - return token; + return body.csrf_token; }; -helpers.request = async function (method, uri, options) { +helpers.request = async function (method, uri, options = {}) { const ignoreMethods = ['GET', 'HEAD', 'OPTIONS']; const lowercaseMethod = String(method).toLowerCase(); let csrf_token; @@ -28,79 +24,44 @@ helpers.request = async function (method, uri, options) { csrf_token = await helpers.getCsrfToken(options.jar); } - return new Promise((resolve, reject) => { - options.headers = options.headers || {}; - if (csrf_token) { - options.headers['x-csrf-token'] = csrf_token; - } - request[lowercaseMethod](`${nconf.get('url')}${uri}`, options, (err, res, body) => { - if (err) reject(err); - else resolve({ res, body }); - }); - }); + options.headers = options.headers || {}; + if (csrf_token) { + options.headers['x-csrf-token'] = csrf_token; + } + return await request[lowercaseMethod](`${nconf.get('url')}${uri}`, options); }; helpers.loginUser = async (username, password, payload = {}) => { const jar = request.jar(); - const form = { username, password, ...payload }; + const data = { username, password, ...payload }; - const { statusCode, body: configBody } = await requestAsync({ - url: `${nconf.get('url')}/api/config`, - json: true, + const csrf_token = await helpers.getCsrfToken(jar); + const { response, body } = await request.post(`${nconf.get('url')}/login`, { + body: data, jar: jar, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, - }); - - if (statusCode !== 200) { - throw new Error('[[error:invalid-response]]'); - } - - const { csrf_token } = configBody; - const res = await requestAsync.post(`${nconf.get('url')}/login`, { - form, - json: true, - jar: jar, - followRedirect: false, - simple: false, - resolveWithFullResponse: true, headers: { 'x-csrf-token': csrf_token, }, }); - return { jar, res, body: res.body, csrf_token: csrf_token }; + return { jar, response, body, csrf_token }; }; -helpers.logoutUser = function (jar, callback) { - request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar, - }, (err, response, body) => { - if (err) { - return callback(err, response, body); - } - - request.post(`${nconf.get('url')}/logout`, { - form: {}, - json: true, - jar: jar, - headers: { - 'x-csrf-token': body.csrf_token, - }, - }, (err, response, body) => { - callback(err, response, body); - }); +helpers.logoutUser = async function (jar) { + const csrf_token = await helpers.getCsrfToken(jar); + const { response, body } = await request.post(`${nconf.get('url')}/logout`, { + body: {}, + jar, + headers: { + 'x-csrf-token': csrf_token, + }, }); + return { response, body }; }; -helpers.connectSocketIO = function (res, csrf_token, callback) { +helpers.connectSocketIO = function (res, csrf_token) { const io = require('socket.io-client'); - let cookies = res.headers['set-cookie']; - cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c)); - const cookie = cookies[0]; + const cookie = res.headers['set-cookie']; const socket = io(nconf.get('base_url'), { path: `${nconf.get('relative_path')}/socket.io`, extraHeaders: { @@ -111,73 +72,71 @@ helpers.connectSocketIO = function (res, csrf_token, callback) { _csrf: csrf_token, }, }); - let error; - socket.on('connect', () => { - if (error) { - return; - } - callback(null, socket); - }); + return new Promise((resolve, reject) => { + let error; + socket.on('connect', () => { + if (error) { + return; + } + resolve(socket); + }); - socket.on('error', (err) => { - error = err; - console.log('socket.io error', err.stack); - callback(err); + socket.on('error', (err) => { + error = err; + console.log('socket.io error', err.stack); + reject(err); + }); }); }; -helpers.uploadFile = function (uploadEndPoint, filePath, body, jar, csrf_token, callback) { - let formData = { - files: [ - fs.createReadStream(filePath), - ], +helpers.uploadFile = async function (uploadEndPoint, filePath, data, jar, csrf_token) { + const mime = require('mime'); + const form = new FormData(); + const file = await fs.promises.readFile(filePath); + const blob = new Blob([file], { type: mime.getType(filePath) }); + + form.append('files', blob, path.basename(filePath)); + + if (data && data.params) { + form.append('params', data.params); + } + + const response = await fetch(uploadEndPoint, { + method: 'post', + body: form, + headers: { + 'x-csrf-token': csrf_token, + cookie: await jar.getCookieString(uploadEndPoint), + }, + }); + const body = await response.json(); + return { + body, + response: { + status: response.status, + statusCode: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + }, }; - formData = utils.merge(formData, body); - request.post({ - url: uploadEndPoint, - formData: formData, - json: true, - jar: jar, +}; + +helpers.registerUser = async function (data) { + const jar = request.jar(); + const csrf_token = await helpers.getCsrfToken(jar); + + if (!data.hasOwnProperty('password-confirm')) { + data['password-confirm'] = data.password; + } + + const { response, body } = await request.post(`${nconf.get('url')}/register`, { + body: data, + jar, headers: { 'x-csrf-token': csrf_token, }, - }, (err, res, body) => { - if (err) { - return callback(err); - } - if (res.statusCode !== 200) { - winston.error(JSON.stringify(body)); - } - callback(null, res, body); - }); -}; - -helpers.registerUser = function (data, callback) { - const jar = request.jar(); - request({ - url: `${nconf.get('url')}/api/config`, - json: true, - jar: jar, - }, (err, response, body) => { - if (err) { - return callback(err); - } - - if (!data.hasOwnProperty('password-confirm')) { - data['password-confirm'] = data.password; - } - - request.post(`${nconf.get('url')}/register`, { - form: data, - json: true, - jar: jar, - headers: { - 'x-csrf-token': body.csrf_token, - }, - }, (err, response, body) => { - callback(err, jar, response, body); - }); }); + return { jar, response, body }; }; // http://stackoverflow.com/a/14387791/583363 @@ -205,37 +164,26 @@ helpers.copyFile = function (source, target, callback) { } }; -helpers.invite = async function (body, uid, jar, csrf_token) { - console.log('making call'); - const res = await requestAsync.post(`${nconf.get('url')}/api/v3/users/${uid}/invites`, { +helpers.invite = async function (data, uid, jar, csrf_token) { + return await request.post(`${nconf.get('url')}/api/v3/users/${uid}/invites`, { jar: jar, - // using "form" since client "api" module make requests with "application/x-www-form-urlencoded" content-type - form: body, + body: data, headers: { 'x-csrf-token': csrf_token, }, - simple: false, - resolveWithFullResponse: true, }); - console.log(res.statusCode, res.body); - - res.body = JSON.parse(res.body); - return { res, body }; }; -helpers.createFolder = function (path, folderName, jar, csrf_token) { - return requestAsync.put(`${nconf.get('url')}/api/v3/files/folder`, { +helpers.createFolder = async function (path, folderName, jar, csrf_token) { + return await request.put(`${nconf.get('url')}/api/v3/files/folder`, { jar, body: { path, folderName, }, - json: true, headers: { 'x-csrf-token': csrf_token, }, - simple: false, - resolveWithFullResponse: true, }); }; diff --git a/test/locale-detect.js b/test/locale-detect.js index 91c3e94194..c6f98142c4 100644 --- a/test/locale-detect.js +++ b/test/locale-detect.js @@ -2,45 +2,34 @@ const assert = require('assert'); const nconf = require('nconf'); -const request = require('request'); const db = require('./mocks/databasemock'); const meta = require('../src/meta'); +const request = require('../src/request'); describe('Language detection', () => { - it('should detect the language for a guest', (done) => { - meta.configs.set('autoDetectLang', 1, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/config`, { - headers: { - 'Accept-Language': 'de-DE,de;q=0.5', - }, - json: true, - }, (err, res, body) => { - assert.ifError(err); - assert.ok(body); + it('should detect the language for a guest', async () => { + await meta.configs.set('autoDetectLang', 1); - assert.strictEqual(body.userLang, 'de'); - done(); - }); + const { body } = await request.get(`${nconf.get('url')}/api/config`, { + headers: { + 'Accept-Language': 'de-DE,de;q=0.5', + }, }); + assert.ok(body); + assert.strictEqual(body.userLang, 'de'); }); - it('should do nothing when disabled', (done) => { - meta.configs.set('autoDetectLang', 0, (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/config`, { - headers: { - 'Accept-Language': 'de-DE,de;q=0.5', - }, - json: true, - }, (err, res, body) => { - assert.ifError(err); - assert.ok(body); + it('should do nothing when disabled', async () => { + await meta.configs.set('autoDetectLang', 0); - assert.strictEqual(body.userLang, 'en-GB'); - done(); - }); + const { body } = await request.get(`${nconf.get('url')}/api/config`, { + headers: { + 'Accept-Language': 'de-DE,de;q=0.5', + }, }); + + assert.ok(body); + assert.strictEqual(body.userLang, 'en-GB'); }); }); diff --git a/test/messaging.js b/test/messaging.js index 4709aff351..86008e2021 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -1,7 +1,7 @@ 'use strict'; const assert = require('assert'); -const request = require('request-promise-native'); + const nconf = require('nconf'); const util = require('util'); @@ -14,7 +14,7 @@ const Groups = require('../src/groups'); const Messaging = require('../src/messaging'); const api = require('../src/api'); const helpers = require('./helpers'); -const socketModules = require('../src/socket.io/modules'); +const request = require('../src/request'); const utils = require('../src/utils'); const translator = require('../src/translator'); @@ -33,12 +33,8 @@ describe('Messaging Library', () => { const callv3API = async (method, path, body, user) => { const options = { - method, body, - json: true, jar: mocks.users[user].jar, - resolveWithFullResponse: true, - simple: false, }; if (method !== 'get') { @@ -47,7 +43,7 @@ describe('Messaging Library', () => { }; } - return request(`${nconf.get('url')}/api/v3${path}`, options); + return request[method](`${nconf.get('url')}/api/v3${path}`, options); }; before(async () => { @@ -162,11 +158,11 @@ describe('Messaging Library', () => { uids: [mocks.users.baz.uid], }, 'foo'); - const { statusCode, body } = await callv3API('post', `/chats`, { + const { response, body } = await callv3API('post', `/chats`, { uids: [mocks.users.baz.uid], }, 'foo'); - assert.equal(statusCode, 400); + assert.equal(response.statusCode, 400); assert.equal(body.status.code, 'bad-request'); assert.equal(body.status.message, await translator.translate('[[error:too-many-messages]]')); meta.config.chatMessageDelay = oldValue; @@ -190,20 +186,20 @@ describe('Messaging Library', () => { assert.strictEqual(messages[0].system, 1); assert.strictEqual(messages[0].content, 'user-join'); - const { statusCode, body: body2 } = await callv3API('put', `/chats/${roomId}/messages/${messages[0].messageId}`, { + const { response, body: body2 } = await callv3API('put', `/chats/${roomId}/messages/${messages[0].messageId}`, { message: 'test', }, 'foo'); - assert.strictEqual(statusCode, 400); + assert.strictEqual(response.statusCode, 400); assert.equal(body2.status.message, await translator.translate('[[error:cant-edit-chat-message]]')); }); it('should fail to add user to room with invalid data', async () => { - let { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, {}, 'foo'); - assert.strictEqual(statusCode, 400); + let { response, body } = await callv3API('post', `/chats/${roomId}/users`, {}, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:required-parameters-missing, uids]]')); - ({ statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [null] }, 'foo')); - assert.strictEqual(statusCode, 400); + ({ response, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [null] }, 'foo')); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]')); }); @@ -220,38 +216,38 @@ describe('Messaging Library', () => { }); it('should throw error if user is not in room', async () => { - const { statusCode, body } = await callv3API('get', `/chats/${roomId}/users`, {}, 'bar'); - assert.strictEqual(statusCode, 403); + const { response, body } = await callv3API('get', `/chats/${roomId}/users`, {}, 'bar'); + assert.strictEqual(response.statusCode, 403); assert.equal(body.status.message, await translator.translate('[[error:no-privileges]]')); }); it('should fail to add users to room if max is reached', async () => { meta.config.maximumUsersInChatRoom = 2; - const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.bar.uid] }, 'foo'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.bar.uid] }, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.equal(body.status.message, await translator.translate('[[error:cant-add-more-users-to-chat-room]]')); meta.config.maximumUsersInChatRoom = 0; }); it('should fail to add users to room if user does not exist', async () => { - const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [98237498234] }, 'foo'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [98237498234] }, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]')); }); it('should fail to add self to room', async () => { - const { statusCode, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.foo.uid] }, 'foo'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('post', `/chats/${roomId}/users`, { uids: [mocks.users.foo.uid] }, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:cant-chat-with-yourself]]')); }); it('should fail to leave room with invalid data', async () => { - let { statusCode, body } = await callv3API('delete', `/chats/${roomId}/users`, {}, 'foo'); - assert.strictEqual(statusCode, 400); + let { response, body } = await callv3API('delete', `/chats/${roomId}/users`, {}, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:required-parameters-missing, uids]]')); - ({ statusCode, body } = await callv3API('delete', `/chats/${roomId}/users`, { uids: [98237423] }, 'foo')); - assert.strictEqual(statusCode, 400); + ({ response, body } = await callv3API('delete', `/chats/${roomId}/users`, { uids: [98237423] }, 'foo')); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]')); }); @@ -303,9 +299,7 @@ describe('Messaging Library', () => { const { jar: senderJar, csrf_token: senderCsrf } = await helpers.loginUser('deleted_chat_user', 'barbar'); const receiver = await User.create({ username: 'receiver' }); - const { response } = await request(`${nconf.get('url')}/api/v3/chats`, { - method: 'post', - json: true, + const { body } = await request.post(`${nconf.get('url')}/api/v3/chats`, { jar: senderJar, body: { uids: [receiver], @@ -315,31 +309,31 @@ describe('Messaging Library', () => { }, }); await User.deleteAccount(sender); - assert(await Messaging.isRoomOwner(receiver, response.roomId)); + assert(await Messaging.isRoomOwner(receiver, body.response.roomId)); }); it('should fail to remove user from room', async () => { - let { statusCode, body } = await callv3API('delete', `/chats/${roomId}/users`, {}, 'foo'); - assert.strictEqual(statusCode, 400); + let { response, body } = await callv3API('delete', `/chats/${roomId}/users`, {}, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:required-parameters-missing, uids]]')); - ({ statusCode, body } = await callv3API('delete', `/chats/${roomId}/users`, { uids: [null] }, 'foo')); - assert.strictEqual(statusCode, 400); + ({ response, body } = await callv3API('delete', `/chats/${roomId}/users`, { uids: [null] }, 'foo')); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]')); }); it('should fail to remove user from room if user does not exist', async () => { - const { statusCode, body } = await callv3API('delete', `/chats/${roomId}/users`, { uids: [99] }, 'foo'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('delete', `/chats/${roomId}/users`, { uids: [99] }, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:no-user]]')); }); it('should remove user from room', async () => { - const { statusCode, body } = await callv3API('post', `/chats`, { + const { response, body } = await callv3API('post', `/chats`, { uids: [mocks.users.herp.uid], }, 'foo'); const { roomId } = body.response; - assert.strictEqual(statusCode, 200); + assert.strictEqual(response.statusCode, 200); let isInRoom = await Messaging.isUserInRoom(mocks.users.herp.uid, roomId); assert(isInRoom); @@ -488,8 +482,8 @@ describe('Messaging Library', () => { }); it('should rename room', async () => { - const { statusCode } = await callv3API('put', `/chats/${roomId}`, { name: 'new room name' }, 'foo'); - assert.strictEqual(statusCode, 200); + const { response } = await callv3API('put', `/chats/${roomId}`, { name: 'new room name' }, 'foo'); + assert.strictEqual(response.statusCode, 200); }); it('should send a room-rename system message when a room is renamed', async () => { @@ -638,46 +632,46 @@ describe('Messaging Library', () => { }); it('should fail to edit message with invalid data', async () => { - let { statusCode, body } = await callv3API('put', `/chats/1/messages/10000`, { message: 'foo' }, 'foo'); - assert.strictEqual(statusCode, 400); + let { response, body } = await callv3API('put', `/chats/1/messages/10000`, { message: 'foo' }, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:invalid-mid]]')); - ({ statusCode, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, {}, 'foo')); - assert.strictEqual(statusCode, 400); + ({ response, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, {}, 'foo')); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:invalid-chat-message]]')); }); it('should fail to edit message if new content is empty string', async () => { - const { statusCode, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, { message: ' ' }, 'foo'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, { message: ' ' }, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:invalid-chat-message]]')); }); it('should fail to edit message if not own message', async () => { - const { statusCode, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, { message: 'message edited' }, 'herp'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, { message: 'message edited' }, 'herp'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:cant-edit-chat-message]]')); }); it('should fail to edit message if message not in room', async () => { - const { statusCode, body } = await callv3API('put', `/chats/${roomId}/messages/1014`, { message: 'message edited' }, 'herp'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('put', `/chats/${roomId}/messages/1014`, { message: 'message edited' }, 'herp'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, await translator.translate('[[error:invalid-mid]]')); }); it('should edit message', async () => { - let { statusCode, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, { message: 'message edited' }, 'foo'); - assert.strictEqual(statusCode, 200); + let { response, body } = await callv3API('put', `/chats/${roomId}/messages/${mid}`, { message: 'message edited' }, 'foo'); + assert.strictEqual(response.statusCode, 200); assert.strictEqual(body.response.content, 'message edited'); - ({ statusCode, body } = await callv3API('get', `/chats/${roomId}/messages/${mid}`, {}, 'foo')); - assert.strictEqual(statusCode, 200); + ({ response, body } = await callv3API('get', `/chats/${roomId}/messages/${mid}`, {}, 'foo')); + assert.strictEqual(response.statusCode, 200); assert.strictEqual(body.response.content, 'message edited'); }); it('should fail to delete message if not owner', async () => { - const { statusCode, body } = await callv3API('delete', `/chats/${roomId}/messages/${mid}`, {}, 'herp'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('delete', `/chats/${roomId}/messages/${mid}`, {}, 'herp'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, 'You are not allowed to delete this message'); }); @@ -716,8 +710,8 @@ describe('Messaging Library', () => { }); it('should error out if a message is deleted again', async () => { - const { statusCode, body } = await callv3API('delete', `/chats/${roomId}/messages/${mid}`, {}, 'foo'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('delete', `/chats/${roomId}/messages/${mid}`, {}, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, 'This chat message has already been deleted.'); }); @@ -728,8 +722,8 @@ describe('Messaging Library', () => { }); it('should error out if a message is restored again', async () => { - const { statusCode, body } = await callv3API('post', `/chats/${roomId}/messages/${mid}`, {}, 'foo'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('post', `/chats/${roomId}/messages/${mid}`, {}, 'foo'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, 'This chat message has already been restored.'); }); @@ -743,8 +737,8 @@ describe('Messaging Library', () => { }); it('should error out for regular users', async () => { - const { statusCode, body } = await callv3API('delete', `/chats/${roomId}/messages/${mid2}`, {}, 'baz'); - assert.strictEqual(statusCode, 400); + const { response, body } = await callv3API('delete', `/chats/${roomId}/messages/${mid2}`, {}, 'baz'); + assert.strictEqual(response.statusCode, 400); assert.strictEqual(body.status.message, 'chat-message-editing-disabled'); }); @@ -767,33 +761,21 @@ describe('Messaging Library', () => { describe('controller', () => { it('should 404 if chat is disabled', async () => { meta.config.disableChat = 1; - const response = await request(`${nconf.get('url')}/user/baz/chats`, { - resolveWithFullResponse: true, - simple: false, - }); + const { response } = await request.get(`${nconf.get('url')}/user/baz/chats`); assert.equal(response.statusCode, 404); }); it('should 401 for guest with not-authorised status code', async () => { meta.config.disableChat = 0; - const response = await request(`${nconf.get('url')}/api/user/baz/chats`, { - resolveWithFullResponse: true, - simple: false, - json: true, - }); - const { body } = response; + const { response, body } = await request.get(`${nconf.get('url')}/api/user/baz/chats`); assert.equal(response.statusCode, 401); assert.equal(body.status.code, 'not-authorised'); }); it('should 404 for non-existent user', async () => { - const response = await request(`${nconf.get('url')}/user/doesntexist/chats`, { - resolveWithFullResponse: true, - simple: false, - }); - + const { response } = await request.get(`${nconf.get('url')}/user/doesntexist/chats`); assert.equal(response.statusCode, 404); }); }); @@ -805,13 +787,7 @@ describe('Messaging Library', () => { }); it('should return chats page data', async () => { - const response = await request(`${nconf.get('url')}/api/user/herp/chats`, { - resolveWithFullResponse: true, - simple: false, - json: true, - jar, - }); - const { body } = response; + const { response, body } = await request.get(`${nconf.get('url')}/api/user/herp/chats`, { jar }); assert.equal(response.statusCode, 200); assert(Array.isArray(body.rooms)); @@ -820,13 +796,7 @@ describe('Messaging Library', () => { }); it('should return room data', async () => { - const response = await request(`${nconf.get('url')}/api/user/herp/chats/${roomId}`, { - resolveWithFullResponse: true, - simple: false, - json: true, - jar, - }); - const { body } = response; + const { response, body } = await request.get(`${nconf.get('url')}/api/user/herp/chats/${roomId}`, { jar }); assert.equal(response.statusCode, 200); assert.equal(body.roomId, roomId); @@ -834,27 +804,16 @@ describe('Messaging Library', () => { }); it('should redirect to chats page', async () => { - const res = await request(`${nconf.get('url')}/api/chats`, { - resolveWithFullResponse: true, - simple: false, - jar, - json: true, - }); - const { body } = res; + const { response, body } = await request.get(`${nconf.get('url')}/api/chats`, { jar }); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], '/user/herp/chats'); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], '/user/herp/chats'); assert.equal(body, '/user/herp/chats'); }); it('should return 404 if user is not in room', async () => { const data = await helpers.loginUser('baz', 'quuxquux'); - const response = await request(`${nconf.get('url')}/api/user/baz/chats/${roomId}`, { - resolveWithFullResponse: true, - simple: false, - json: true, - jar: data.jar, - }); + const { response } = await request.get(`${nconf.get('url')}/api/user/baz/chats/${roomId}`, { jar: data.jar }); assert.equal(response.statusCode, 404); }); diff --git a/test/meta.js b/test/meta.js index 84452561d4..77667ddbf2 100644 --- a/test/meta.js +++ b/test/meta.js @@ -2,13 +2,14 @@ const assert = require('assert'); const async = require('async'); -const request = require('request'); + const nconf = require('nconf'); const db = require('./mocks/databasemock'); const meta = require('../src/meta'); const User = require('../src/user'); const Groups = require('../src/groups'); +const request = require('../src/request'); describe('meta', () => { let fooUid; @@ -489,117 +490,86 @@ describe('meta', () => { }); describe('Access-Control-Allow-Origin', () => { - it('Access-Control-Allow-Origin header should be empty', (done) => { + it('Access-Control-Allow-Origin header should be empty', async () => { const jar = request.jar(); - request.get(`${nconf.get('url')}/api/search?term=bug`, { - form: {}, - json: true, + const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, { jar: jar, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.headers['access-control-allow-origin'], undefined); - done(); }); + + assert.equal(response.headers['access-control-allow-origin'], undefined); }); - it('should set proper Access-Control-Allow-Origin header', (done) => { + it('should set proper Access-Control-Allow-Origin header', async () => { const jar = request.jar(); const oldValue = meta.config['access-control-allow-origin']; meta.config['access-control-allow-origin'] = 'test.com, mydomain.com'; - request.get(`${nconf.get('url')}/api/search?term=bug`, { - form: { - }, - json: true, + const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, { jar: jar, headers: { origin: 'mydomain.com', }, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.headers['access-control-allow-origin'], 'mydomain.com'); - meta.config['access-control-allow-origin'] = oldValue; - done(err); }); + + assert.equal(response.headers['access-control-allow-origin'], 'mydomain.com'); + meta.config['access-control-allow-origin'] = oldValue; }); - it('Access-Control-Allow-Origin header should be empty if origin does not match', (done) => { + it('Access-Control-Allow-Origin header should be empty if origin does not match', async () => { const jar = request.jar(); const oldValue = meta.config['access-control-allow-origin']; meta.config['access-control-allow-origin'] = 'test.com, mydomain.com'; - request.get(`${nconf.get('url')}/api/search?term=bug`, { - form: { - }, - json: true, + const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, { + data: {}, jar: jar, headers: { origin: 'notallowed.com', }, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.headers['access-control-allow-origin'], undefined); - meta.config['access-control-allow-origin'] = oldValue; - done(err); }); + assert.equal(response.headers['access-control-allow-origin'], undefined); + meta.config['access-control-allow-origin'] = oldValue; }); - it('should set proper Access-Control-Allow-Origin header', (done) => { + it('should set proper Access-Control-Allow-Origin header', async () => { const jar = request.jar(); const oldValue = meta.config['access-control-allow-origin-regex']; meta.config['access-control-allow-origin-regex'] = 'match\\.this\\..+\\.domain.com, mydomain\\.com'; - request.get(`${nconf.get('url')}/api/search?term=bug`, { - form: { - }, - json: true, + const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, { jar: jar, headers: { origin: 'match.this.anything123.domain.com', }, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.headers['access-control-allow-origin'], 'match.this.anything123.domain.com'); - meta.config['access-control-allow-origin-regex'] = oldValue; - done(err); }); + + assert.equal(response.headers['access-control-allow-origin'], 'match.this.anything123.domain.com'); + meta.config['access-control-allow-origin-regex'] = oldValue; }); - it('Access-Control-Allow-Origin header should be empty if origin does not match', (done) => { + it('Access-Control-Allow-Origin header should be empty if origin does not match', async () => { const jar = request.jar(); const oldValue = meta.config['access-control-allow-origin-regex']; meta.config['access-control-allow-origin-regex'] = 'match\\.this\\..+\\.domain.com, mydomain\\.com'; - request.get(`${nconf.get('url')}/api/search?term=bug`, { - form: { - }, - json: true, + const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, { jar: jar, headers: { origin: 'notallowed.com', }, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.headers['access-control-allow-origin'], undefined); - meta.config['access-control-allow-origin-regex'] = oldValue; - done(err); }); + assert.equal(response.headers['access-control-allow-origin'], undefined); + meta.config['access-control-allow-origin-regex'] = oldValue; }); - it('should not error with invalid regexp', (done) => { + it('should not error with invalid regexp', async () => { const jar = request.jar(); const oldValue = meta.config['access-control-allow-origin-regex']; meta.config['access-control-allow-origin-regex'] = '[match\\.this\\..+\\.domain.com, mydomain\\.com'; - request.get(`${nconf.get('url')}/api/search?term=bug`, { - form: { - }, - json: true, + const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, { jar: jar, headers: { origin: 'mydomain.com', }, - }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.headers['access-control-allow-origin'], 'mydomain.com'); - meta.config['access-control-allow-origin-regex'] = oldValue; - done(err); }); + assert.equal(response.headers['access-control-allow-origin'], 'mydomain.com'); + meta.config['access-control-allow-origin-regex'] = oldValue; }); }); diff --git a/test/middleware.js b/test/middleware.js index 818ef371cd..5941488d94 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -2,13 +2,13 @@ const assert = require('assert'); const nconf = require('nconf'); -const request = require('request-promise-native'); + const db = require('./mocks/databasemock'); const user = require('../src/user'); const groups = require('../src/groups'); const utils = require('../src/utils'); - +const request = require('../src/request'); const helpers = require('./helpers'); describe('Middlewares', () => { @@ -116,81 +116,61 @@ describe('Middlewares', () => { }); it('should be absent on non-existent routes, for guests', async () => { - const res = await request(`${nconf.get('url')}/${utils.generateUUID()}`, { - simple: false, - resolveWithFullResponse: true, - }); + const { response } = await request.get(`${nconf.get('url')}/${utils.generateUUID()}`); - assert.strictEqual(res.statusCode, 404); - assert(!Object.keys(res.headers).includes('cache-control')); + assert.strictEqual(response.statusCode, 404); + assert(!Object.keys(response.headers).includes('cache-control')); }); it('should be set to "private" on non-existent routes, for logged in users', async () => { - const res = await request(`${nconf.get('url')}/${utils.generateUUID()}`, { - simple: false, - resolveWithFullResponse: true, + const { response } = await request.get(`${nconf.get('url')}/${utils.generateUUID()}`, { jar, + headers: { + accept: 'text/html', + }, }); - assert.strictEqual(res.statusCode, 404); - assert(Object.keys(res.headers).includes('cache-control')); - assert.strictEqual(res.headers['cache-control'], 'private'); + assert.strictEqual(response.statusCode, 404); + assert(Object.keys(response.headers).includes('cache-control')); + assert.strictEqual(response.headers['cache-control'], 'private'); }); it('should be absent on regular routes, for guests', async () => { - const res = await request(nconf.get('url'), { - simple: false, - resolveWithFullResponse: true, - }); + const { response } = await request.get(nconf.get('url')); - assert.strictEqual(res.statusCode, 200); - assert(!Object.keys(res.headers).includes('cache-control')); + assert.strictEqual(response.statusCode, 200); + assert(!Object.keys(response.headers).includes('cache-control')); }); it('should be absent on api routes, for guests', async () => { - const res = await request(`${nconf.get('url')}/api`, { - simple: false, - resolveWithFullResponse: true, - }); + const { response } = await request.get(`${nconf.get('url')}/api`); - assert.strictEqual(res.statusCode, 200); - assert(!Object.keys(res.headers).includes('cache-control')); + assert.strictEqual(response.statusCode, 200); + assert(!Object.keys(response.headers).includes('cache-control')); }); it('should be set to "private" on regular routes, for logged-in users', async () => { - const res = await request(nconf.get('url'), { - simple: false, - resolveWithFullResponse: true, - jar, - }); + const { response } = await request.get(nconf.get('url'), { jar }); - assert.strictEqual(res.statusCode, 200); - assert(Object.keys(res.headers).includes('cache-control')); - assert.strictEqual(res.headers['cache-control'], 'private'); + assert.strictEqual(response.statusCode, 200); + assert(Object.keys(response.headers).includes('cache-control')); + assert.strictEqual(response.headers['cache-control'], 'private'); }); it('should be set to "private" on api routes, for logged-in users', async () => { - const res = await request(`${nconf.get('url')}/api`, { - simple: false, - resolveWithFullResponse: true, - jar, - }); + const { response } = await request.get(`${nconf.get('url')}/api`, { jar }); - assert.strictEqual(res.statusCode, 200); - assert(Object.keys(res.headers).includes('cache-control')); - assert.strictEqual(res.headers['cache-control'], 'private'); + assert.strictEqual(response.statusCode, 200); + assert(Object.keys(response.headers).includes('cache-control')); + assert.strictEqual(response.headers['cache-control'], 'private'); }); it('should be set to "private" on apiv3 routes, for logged-in users', async () => { - const res = await request(`${nconf.get('url')}/api/v3/users/${uid}`, { - simple: false, - resolveWithFullResponse: true, - jar, - }); + const { response } = await request.get(`${nconf.get('url')}/api/v3/users/${uid}`, { jar }); - assert.strictEqual(res.statusCode, 200); - assert(Object.keys(res.headers).includes('cache-control')); - assert.strictEqual(res.headers['cache-control'], 'private'); + assert.strictEqual(response.statusCode, 200); + assert(Object.keys(response.headers).includes('cache-control')); + assert.strictEqual(response.headers['cache-control'], 'private'); }); }); }); diff --git a/test/plugins.js b/test/plugins.js index 2ffbf604c3..1260f8fd71 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -3,11 +3,12 @@ const assert = require('assert'); const path = require('path'); const nconf = require('nconf'); -const request = require('request'); + const fs = require('fs'); const db = require('./mocks/databasemock'); const plugins = require('../src/plugins'); +const request = require('../src/request'); describe('Plugins', () => { it('should load plugin data', (done) => { @@ -290,33 +291,24 @@ describe('Plugins', () => { }); describe('static assets', () => { - it('should 404 if resource does not exist', (done) => { - request.get(`${nconf.get('url')}/plugins/doesnotexist/should404.tpl`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + it('should 404 if resource does not exist', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/plugins/doesnotexist/should404.tpl`); + assert.equal(response.statusCode, 404); + assert(body); }); - it('should 404 if resource does not exist', (done) => { + it('should 404 if resource does not exist', async () => { const url = `${nconf.get('url')}/plugins/nodebb-plugin-dbsearch/dbsearch/templates/admin/plugins/should404.tpl`; - request.get(url, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 404); - assert(body); - done(); - }); + const { response, body } = await request.get(url); + assert.equal(response.statusCode, 404); + assert(body); }); - it('should get resource', (done) => { + it('should get resource', async () => { const url = `${nconf.get('url')}/assets/templates/admin/plugins/dbsearch.tpl`; - request.get(url, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + const { response, body } = await request.get(url); + assert.equal(response.statusCode, 200); + assert(body); }); }); @@ -371,7 +363,6 @@ describe('Plugins', () => { assert.ifError(err); assert(Array.isArray(data)); data.forEach((pluginData) => { - console.log(pluginData); assert(activePlugins.includes(pluginData)); }); done(); diff --git a/test/posts.js b/test/posts.js index 1289f56eea..e52b5cdf23 100644 --- a/test/posts.js +++ b/test/posts.js @@ -2,8 +2,7 @@ const assert = require('assert'); -const async = require('async'); -const request = require('request-promise-native'); + const nconf = require('nconf'); const path = require('path'); const util = require('util'); @@ -24,6 +23,7 @@ const meta = require('../src/meta'); const file = require('../src/file'); const helpers = require('./helpers'); const utils = require('../src/utils'); +const request = require('../src/request'); describe('Post\'s', () => { let voterUid; @@ -33,52 +33,26 @@ describe('Post\'s', () => { let topicData; let cid; - before((done) => { - async.series({ - voterUid: function (next) { - user.create({ username: 'upvoter' }, next); - }, - voteeUid: function (next) { - user.create({ username: 'upvotee' }, next); - }, - globalModUid: function (next) { - user.create({ username: 'globalmod', password: 'globalmodpwd' }, next); - }, - category: function (next) { - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - }, next); - }, - }, (err, results) => { - if (err) { - return done(err); - } + before(async () => { + voterUid = await user.create({ username: 'upvoter' }); + voteeUid = await user.create({ username: 'upvotee' }); + globalModUid = await user.create({ username: 'globalmod', password: 'globalmodpwd' }); + ({ cid } = await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + })); - voterUid = results.voterUid; - voteeUid = results.voteeUid; - globalModUid = results.globalModUid; - cid = results.category.cid; - - topics.post({ - uid: results.voteeUid, - cid: results.category.cid, - title: 'Test Topic Title', - content: 'The content of test topic', - }, (err, data) => { - if (err) { - return done(err); - } - postData = data.postData; - topicData = data.topicData; - - groups.join('Global Moderators', globalModUid, done); - }); - }); + ({ topicData, postData } = await topics.post({ + uid: voteeUid, + cid: cid, + title: 'Test Topic Title', + content: 'The content of test topic', + })); + await groups.join('Global Moderators', globalModUid); }); it('should update category teaser properly', async () => { - const getCategoriesAsync = async () => await request(`${nconf.get('url')}/api/categories`, { json: true }); + const getCategoriesAsync = async () => (await request.get(`${nconf.get('url')}/api/categories`, { })).body; const postResult = await topics.post({ uid: globalModUid, cid: cid, title: 'topic title', content: '123456789' }); let data = await getCategoriesAsync(); @@ -372,24 +346,14 @@ describe('Post\'s', () => { assert.strictEqual(isDeleted, 1); }); - it('should not see post content if global mod does not have posts:view_deleted privilege', (done) => { - async.waterfall([ - function (next) { - user.create({ username: 'global mod', password: '123456' }, next); - }, - function (uid, next) { - groups.join('Global Moderators', uid, next); - }, - function (next) { - privileges.categories.rescind(['groups:posts:view_deleted'], cid, 'Global Moderators', next); - }, - async () => { - const { jar } = await helpers.loginUser('global mod', '123456'); - const { posts } = await request(`${nconf.get('url')}/api/topic/${tid}`, { jar, json: true }); - assert.equal(posts[1].content, '[[topic:post-is-deleted]]'); - await privileges.categories.give(['groups:posts:view_deleted'], cid, 'Global Moderators'); - }, - ], done); + it('should not see post content if global mod does not have posts:view_deleted privilege', async () => { + const uid = await user.create({ username: 'global mod', password: '123456' }); + await groups.join('Global Moderators', uid); + await privileges.categories.rescind(['groups:posts:view_deleted'], cid, 'Global Moderators'); + const { jar } = await helpers.loginUser('global mod', '123456'); + const { body } = await request.get(`${nconf.get('url')}/api/topic/${tid}`, { jar }); + assert.equal(body.posts[1].content, '[[topic:post-is-deleted]]'); + await privileges.categories.give(['groups:posts:view_deleted'], cid, 'Global Moderators'); }); it('should restore a post', async () => { @@ -1013,7 +977,8 @@ describe('Post\'s', () => { it('should load queued posts', async () => { ({ jar } = await helpers.loginUser('globalmod', 'globalmodpwd')); - const { posts } = await request(`${nconf.get('url')}/api/post-queue`, { jar: jar, json: true }); + const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar }); + const { posts } = body; assert.equal(posts[0].type, 'topic'); assert.equal(posts[0].data.content, 'queued topic content'); assert.equal(posts[1].type, 'reply'); @@ -1029,21 +994,24 @@ describe('Post\'s', () => { it('should edit post in queue', async () => { await socketPosts.editQueuedContent({ uid: globalModUid }, { id: queueId, content: 'newContent' }); - const { posts } = await request(`${nconf.get('url')}/api/post-queue`, { jar: jar, json: true }); + const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar }); + const { posts } = body; assert.equal(posts[1].type, 'reply'); assert.equal(posts[1].data.content, 'newContent'); }); it('should edit topic title in queue', async () => { await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, title: 'new topic title' }); - const { posts } = await request(`${nconf.get('url')}/api/post-queue`, { jar: jar, json: true }); + const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar }); + const { posts } = body; assert.equal(posts[0].type, 'topic'); assert.equal(posts[0].data.title, 'new topic title'); }); it('should edit topic category in queue', async () => { await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: 2 }); - const { posts } = await request(`${nconf.get('url')}/api/post-queue`, { jar: jar, json: true }); + const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar }); + const { posts } = body; assert.equal(posts[0].type, 'topic'); assert.equal(posts[0].data.cid, 2); await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: cid }); @@ -1063,20 +1031,10 @@ describe('Post\'s', () => { }); }); - it('should accept queued posts and submit', (done) => { - let ids; - async.waterfall([ - function (next) { - db.getSortedSetRange('post:queue', 0, -1, next); - }, - function (_ids, next) { - ids = _ids; - socketPosts.accept({ uid: globalModUid }, { id: ids[0] }, next); - }, - function (next) { - socketPosts.accept({ uid: globalModUid }, { id: ids[1] }, next); - }, - ], done); + it('should accept queued posts and submit', async () => { + const ids = await db.getSortedSetRange('post:queue', 0, -1); + await socketPosts.accept({ uid: globalModUid }, { id: ids[0] }); + await socketPosts.accept({ uid: globalModUid }, { id: ids[1] }); }); it('should not crash if id does not exist', (done) => { diff --git a/test/posts/uploads.js b/test/posts/uploads.js index 875397aaea..9471bca2f7 100644 --- a/test/posts/uploads.js +++ b/test/posts/uploads.js @@ -6,7 +6,6 @@ const path = require('path'); const os = require('os'); const nconf = require('nconf'); -const async = require('async'); const crypto = require('crypto'); const db = require('../mocks/databasemock'); @@ -75,24 +74,15 @@ describe('upload methods', () => { }); }); - it('should remove an image if it is edited out of the post', (done) => { - async.series([ - function (next) { - posts.edit({ - pid: pid, - uid, - content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png)... AND NO MORE!', - }, next); - }, - async.apply(posts.uploads.sync, pid), - ], (err) => { - assert.ifError(err); - db.sortedSetCard(`post:${pid}:uploads`, (err, length) => { - assert.ifError(err); - assert.strictEqual(1, length); - done(); - }); + it('should remove an image if it is edited out of the post', async () => { + await posts.edit({ + pid: pid, + uid, + content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png)... AND NO MORE!', }); + await posts.uploads.sync(pid); + const length = await db.sortedSetCard(`post:${pid}:uploads`); + assert.strictEqual(1, length); }); }); @@ -127,85 +117,52 @@ describe('upload methods', () => { }); describe('.associate()', () => { - it('should add an image to the post\'s maintained list of uploads', (done) => { - async.waterfall([ - async.apply(posts.uploads.associate, pid, 'files/whoa.gif'), - async.apply(posts.uploads.list, pid), - ], (err, uploads) => { - assert.ifError(err); - assert.strictEqual(2, uploads.length); - assert.strictEqual(true, uploads.includes('files/whoa.gif')); - done(); - }); + it('should add an image to the post\'s maintained list of uploads', async () => { + await posts.uploads.associate(pid, 'files/whoa.gif'); + const uploads = await posts.uploads.list(pid); + assert.strictEqual(2, uploads.length); + assert.strictEqual(true, uploads.includes('files/whoa.gif')); }); - it('should allow arrays to be passed in', (done) => { - async.waterfall([ - async.apply(posts.uploads.associate, pid, ['files/amazeballs.jpg', 'files/wut.txt']), - async.apply(posts.uploads.list, pid), - ], (err, uploads) => { - assert.ifError(err); - assert.strictEqual(4, uploads.length); - assert.strictEqual(true, uploads.includes('files/amazeballs.jpg')); - assert.strictEqual(true, uploads.includes('files/wut.txt')); - done(); - }); + it('should allow arrays to be passed in', async () => { + await posts.uploads.associate(pid, ['files/amazeballs.jpg', 'files/wut.txt']); + const uploads = await posts.uploads.list(pid); + assert.strictEqual(4, uploads.length); + assert.strictEqual(true, uploads.includes('files/amazeballs.jpg')); + assert.strictEqual(true, uploads.includes('files/wut.txt')); }); - it('should save a reverse association of md5sum to pid', (done) => { + it('should save a reverse association of md5sum to pid', async () => { const md5 = filename => crypto.createHash('md5').update(filename).digest('hex'); - - async.waterfall([ - async.apply(posts.uploads.associate, pid, ['files/test.bmp']), - function (next) { - db.getSortedSetRange(`upload:${md5('files/test.bmp')}:pids`, 0, -1, next); - }, - ], (err, pids) => { - assert.ifError(err); - assert.strictEqual(true, Array.isArray(pids)); - assert.strictEqual(true, pids.length > 0); - assert.equal(pid, pids[0]); - done(); - }); + await posts.uploads.associate(pid, ['files/test.bmp']); + const pids = await db.getSortedSetRange(`upload:${md5('files/test.bmp')}:pids`, 0, -1); + assert.strictEqual(true, Array.isArray(pids)); + assert.strictEqual(true, pids.length > 0); + assert.equal(pid, pids[0]); }); - it('should not associate a file that does not exist on the local disk', (done) => { - async.waterfall([ - async.apply(posts.uploads.associate, pid, ['files/nonexistant.xls']), - async.apply(posts.uploads.list, pid), - ], (err, uploads) => { - assert.ifError(err); - assert.strictEqual(uploads.length, 5); - assert.strictEqual(false, uploads.includes('files/nonexistant.xls')); - done(); - }); + it('should not associate a file that does not exist on the local disk', async () => { + await posts.uploads.associate(pid, ['files/nonexistant.xls']); + const uploads = await posts.uploads.list(pid); + assert.strictEqual(uploads.length, 5); + assert.strictEqual(false, uploads.includes('files/nonexistant.xls')); }); }); describe('.dissociate()', () => { - it('should remove an image from the post\'s maintained list of uploads', (done) => { - async.waterfall([ - async.apply(posts.uploads.dissociate, pid, 'files/whoa.gif'), - async.apply(posts.uploads.list, pid), - ], (err, uploads) => { - assert.ifError(err); - assert.strictEqual(4, uploads.length); - assert.strictEqual(false, uploads.includes('files/whoa.gif')); - done(); - }); + it('should remove an image from the post\'s maintained list of uploads', async () => { + await posts.uploads.dissociate(pid, 'files/whoa.gif'); + const uploads = await posts.uploads.list(pid); + assert.strictEqual(4, uploads.length); + assert.strictEqual(false, uploads.includes('files/whoa.gif')); }); - it('should allow arrays to be passed in', (done) => { - async.waterfall([ - async.apply(posts.uploads.dissociate, pid, ['files/amazeballs.jpg', 'files/wut.txt']), - async.apply(posts.uploads.list, pid), - ], (err, uploads) => { - assert.ifError(err); - assert.strictEqual(2, uploads.length); - assert.strictEqual(false, uploads.includes('files/amazeballs.jpg')); - assert.strictEqual(false, uploads.includes('files/wut.txt')); - done(); - }); + it('should allow arrays to be passed in', async () => { + await posts.uploads.dissociate(pid, ['files/amazeballs.jpg', 'files/wut.txt']); + const uploads = await posts.uploads.list(pid); + assert.strictEqual(2, uploads.length); + assert.strictEqual(false, uploads.includes('files/amazeballs.jpg')); + assert.strictEqual(false, uploads.includes('files/wut.txt')); }); it('should remove the image\'s user association, if present', async () => { @@ -397,21 +354,14 @@ describe('post uploads management', () => { }); }); - it('should automatically sync uploads on post edit', (done) => { - async.waterfall([ - async.apply(posts.edit, { - pid: reply.pid, - uid, - content: 'no uploads', - }), - function (postData, next) { - posts.uploads.list(reply.pid, next); - }, - ], (err, uploads) => { - assert.ifError(err); - assert.strictEqual(true, Array.isArray(uploads)); - assert.strictEqual(0, uploads.length); - done(); + it('should automatically sync uploads on post edit', async () => { + await posts.edit({ + pid: reply.pid, + uid, + content: 'no uploads', }); + const uploads = await posts.uploads.list(reply.pid); + assert.strictEqual(true, Array.isArray(uploads)); + assert.strictEqual(0, uploads.length); }); }); diff --git a/test/search.js b/test/search.js index 14179ad5cd..f0e285cb9d 100644 --- a/test/search.js +++ b/test/search.js @@ -2,8 +2,6 @@ const assert = require('assert'); -const async = require('async'); -const request = require('request'); const nconf = require('nconf'); const db = require('./mocks/databasemock'); @@ -12,6 +10,7 @@ const categories = require('../src/categories'); const user = require('../src/user'); const search = require('../src/search'); const privileges = require('../src/privileges'); +const request = require('../src/request'); describe('Search', () => { let phoebeUid; @@ -26,103 +25,60 @@ describe('Search', () => { let cid2; let cid3; - before((done) => { - async.waterfall([ - function (next) { - async.series({ - phoebe: function (next) { - user.create({ username: 'phoebe' }, next); - }, - ginger: function (next) { - user.create({ username: 'ginger' }, next); - }, - category1: function (next) { - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - }, next); - }, - category2: function (next) { - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - }, next); - }, - }, next); - }, - function (results, next) { - phoebeUid = results.phoebe; - gingerUid = results.ginger; - cid1 = results.category1.cid; - cid2 = results.category2.cid; + before(async () => { + phoebeUid = await user.create({ username: 'phoebe' }); + gingerUid = await user.create({ username: 'ginger' }); + cid1 = (await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + })).cid; - async.waterfall([ - function (next) { - categories.create({ - name: 'Child Test Category', - description: 'Test category created by testing script', - parentCid: cid2, - }, next); - }, - function (category, next) { - cid3 = category.cid; - topics.post({ - uid: phoebeUid, - cid: cid1, - title: 'nodebb mongodb bugs', - content: 'avocado cucumber apple orange fox', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'], - }, next); - }, - function (results, next) { - topic1Data = results.topicData; - post1Data = results.postData; + cid2 = (await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + })).cid; - topics.post({ - uid: gingerUid, - cid: cid2, - title: 'java mongodb redis', - content: 'avocado cucumber carrot armadillo', - tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'], - }, next); - }, - function (results, next) { - topic2Data = results.topicData; - post2Data = results.postData; - topics.reply({ - uid: phoebeUid, - content: 'reply post apple', - tid: topic2Data.tid, - }, next); - }, - function (_post3Data, next) { - post3Data = _post3Data; - setTimeout(next, 500); - }, - ], next); - }, - ], done); + cid3 = (await categories.create({ + name: 'Child Test Category', + description: 'Test category created by testing script', + parentCid: cid2, + })).cid; + + ({ topicData: topic1Data, postData: post1Data } = await topics.post({ + uid: phoebeUid, + cid: cid1, + title: 'nodebb mongodb bugs', + content: 'avocado cucumber apple orange fox', + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'], + })); + + ({ topicData: topic2Data, postData: post2Data } = await topics.post({ + uid: gingerUid, + cid: cid2, + title: 'java mongodb redis', + content: 'avocado cucumber carrot armadillo', + tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'], + })); + post3Data = await topics.reply({ + uid: phoebeUid, + content: 'reply post apple', + tid: topic2Data.tid, + }); }); - it('should search term in titles and posts', (done) => { + it('should search term in titles and posts', async () => { const meta = require('../src/meta'); const qs = `/api/search?term=cucumber&in=titlesposts&categories[]=${cid1}&by=phoebe&replies=1&repliesFilter=atleast&sortBy=timestamp&sortDirection=desc&showAs=posts`; - privileges.global.give(['groups:search:content'], 'guests', (err) => { - assert.ifError(err); - request({ - url: nconf.get('url') + qs, - json: true, - }, (err, response, body) => { - assert.ifError(err); - assert(body); - assert.equal(body.matchCount, 1); - assert.equal(body.posts.length, 1); - assert.equal(body.posts[0].pid, post1Data.pid); - assert.equal(body.posts[0].uid, phoebeUid); + await privileges.global.give(['groups:search:content'], 'guests'); - privileges.global.rescind(['groups:search:content'], 'guests', done); - }); - }); + const { body } = await request.get(nconf.get('url') + qs); + assert(body); + assert.equal(body.matchCount, 1); + assert.equal(body.posts.length, 1); + assert.equal(body.posts[0].pid, post1Data.pid); + assert.equal(body.posts[0].uid, phoebeUid); + + await privileges.global.rescind(['groups:search:content'], 'guests'); }); it('should search for a user', (done) => { @@ -225,69 +181,47 @@ describe('Search', () => { }); }); - it('should search child categories', (done) => { - async.waterfall([ - function (next) { - topics.post({ - uid: gingerUid, - cid: cid3, - title: 'child category topic', - content: 'avocado cucumber carrot armadillo', - }, next); - }, - function (result, next) { - search.search({ - query: 'avocado', - searchIn: 'titlesposts', - categories: [cid2], - searchChildren: true, - sortBy: 'topic.timestamp', - sortDirection: 'desc', - }, next); - }, - function (result, next) { - assert(result.posts.length, 2); - assert(result.posts[0].topic.title === 'child category topic'); - assert(result.posts[1].topic.title === 'java mongodb redis'); - next(); - }, - ], done); + it('should search child categories', async () => { + await topics.post({ + uid: gingerUid, + cid: cid3, + title: 'child category topic', + content: 'avocado cucumber carrot armadillo', + }); + const result = await search.search({ + query: 'avocado', + searchIn: 'titlesposts', + categories: [cid2], + searchChildren: true, + sortBy: 'topic.timestamp', + sortDirection: 'desc', + }); + assert(result.posts.length, 2); + assert(result.posts[0].topic.title === 'child category topic'); + assert(result.posts[1].topic.title === 'java mongodb redis'); }); - it('should return json search data with no categories', (done) => { + it('should return json search data with no categories', async () => { const qs = '/api/search?term=cucumber&in=titlesposts&searchOnly=1'; - privileges.global.give(['groups:search:content'], 'guests', (err) => { - assert.ifError(err); - request({ - url: nconf.get('url') + qs, - json: true, - }, (err, response, body) => { - assert.ifError(err); - assert(body); - assert(body.hasOwnProperty('matchCount')); - assert(body.hasOwnProperty('pagination')); - assert(body.hasOwnProperty('pageCount')); - assert(body.hasOwnProperty('posts')); - assert(!body.hasOwnProperty('categories')); + await privileges.global.give(['groups:search:content'], 'guests'); - privileges.global.rescind(['groups:search:content'], 'guests', done); - }); - }); + const { body } = await request.get(nconf.get('url') + qs); + assert(body); + assert(body.hasOwnProperty('matchCount')); + assert(body.hasOwnProperty('pagination')); + assert(body.hasOwnProperty('pageCount')); + assert(body.hasOwnProperty('posts')); + assert(!body.hasOwnProperty('categories')); + + await privileges.global.rescind(['groups:search:content'], 'guests'); }); - it('should not crash without a search term', (done) => { + it('should not crash without a search term', async () => { const qs = '/api/search'; - privileges.global.give(['groups:search:content'], 'guests', (err) => { - assert.ifError(err); - request({ - url: nconf.get('url') + qs, - json: true, - }, (err, response, body) => { - assert.ifError(err); - assert(body); - assert.strictEqual(response.statusCode, 200); - privileges.global.rescind(['groups:search:content'], 'guests', done); - }); - }); + await privileges.global.give(['groups:search:content'], 'guests'); + const { response, body } = await request.get(nconf.get('url') + qs); + assert(body); + assert.strictEqual(response.statusCode, 200); + await privileges.global.rescind(['groups:search:content'], 'guests'); }); }); diff --git a/test/socket.io.js b/test/socket.io.js index f9b8b677df..c2a0a68ad6 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -9,11 +9,7 @@ const util = require('util'); const sleep = util.promisify(setTimeout); const assert = require('assert'); -const async = require('async'); const nconf = require('nconf'); -const request = require('request'); - -const cookies = request.jar(); const db = require('./mocks/databasemock'); const user = require('../src/user'); @@ -52,35 +48,11 @@ describe('socket.io', () => { }); - it('should connect and auth properly', (done) => { - request.get({ - url: `${nconf.get('url')}/api/config`, - jar: cookies, - json: true, - }, (err, res, body) => { - assert.ifError(err); - - request.post(`${nconf.get('url')}/login`, { - jar: cookies, - form: { - username: 'admin', - password: 'adminpwd', - }, - headers: { - 'x-csrf-token': body.csrf_token, - }, - json: true, - }, (err, res) => { - assert.ifError(err); - - helpers.connectSocketIO(res, body.csrf_token, (err, _io) => { - io = _io; - assert.ifError(err); - - done(); - }); - }); - }); + it('should connect and auth properly', async () => { + const { response, csrf_token } = await helpers.loginUser('admin', 'adminpwd'); + io = await helpers.connectSocketIO(response, csrf_token); + assert(io); + assert(io.emit); }); it('should return error for unknown event', (done) => { @@ -459,20 +431,38 @@ describe('socket.io', () => { }); }); - it('should toggle plugin install', function (done) { - this.timeout(0); - const oldValue = process.env.NODE_ENV; - process.env.NODE_ENV = 'development'; - socketAdmin.plugins.toggleInstall({ - uid: adminUid, - }, { - id: 'nodebb-plugin-location-to-map', - version: 'latest', - }, (err, data) => { - assert.ifError(err); - assert.equal(data.name, 'nodebb-plugin-location-to-map'); - process.env.NODE_ENV = oldValue; - done(); + describe('install/upgrade plugin', () => { + it('should toggle plugin install', function (done) { + this.timeout(0); + const oldValue = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + socketAdmin.plugins.toggleInstall({ + uid: adminUid, + }, { + id: 'nodebb-plugin-location-to-map', + version: 'latest', + }, (err, data) => { + assert.ifError(err); + assert.equal(data.name, 'nodebb-plugin-location-to-map'); + process.env.NODE_ENV = oldValue; + done(); + }); + }); + + it('should upgrade plugin', function (done) { + this.timeout(0); + const oldValue = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + socketAdmin.plugins.upgrade({ + uid: adminUid, + }, { + id: 'nodebb-plugin-location-to-map', + version: 'latest', + }, (err) => { + assert.ifError(err); + process.env.NODE_ENV = oldValue; + done(); + }); }); }); @@ -501,22 +491,6 @@ describe('socket.io', () => { }); }); - it('should upgrade plugin', function (done) { - this.timeout(0); - const oldValue = process.env.NODE_ENV; - process.env.NODE_ENV = 'development'; - socketAdmin.plugins.upgrade({ - uid: adminUid, - }, { - id: 'nodebb-plugin-location-to-map', - version: 'latest', - }, (err) => { - assert.ifError(err); - process.env.NODE_ENV = oldValue; - done(); - }); - }); - it('should error with invalid data', (done) => { socketAdmin.widgets.set({ uid: adminUid }, null, (err) => { assert.equal(err.message, '[[error:invalid-data]]'); @@ -709,60 +683,43 @@ describe('socket.io', () => { assert(pwExpiry > then && pwExpiry < Date.now()); }); - it('should not error on valid email', (done) => { - socketUser.reset.send({ uid: 0 }, 'regular@test.com', (err) => { - assert.ifError(err); + it('should not error on valid email', async () => { + await socketUser.reset.send({ uid: 0 }, 'regular@test.com'); + const [count, eventsData] = await Promise.all([ + db.sortedSetCount('reset:issueDate', 0, Date.now()), + events.getEvents('', 0, 0), + ]); + assert.strictEqual(count, 2); - async.parallel({ - count: async.apply(db.sortedSetCount.bind(db), 'reset:issueDate', 0, Date.now()), - event: async.apply(events.getEvents, '', 0, 0), - }, (err, data) => { - assert.ifError(err); - assert.strictEqual(data.count, 2); - - // Event validity - assert.strictEqual(data.event.length, 1); - const event = data.event[0]; - assert.strictEqual(event.type, 'password-reset'); - assert.strictEqual(event.text, '[[success:success]]'); - - done(); - }); - }); + // Event validity + assert.strictEqual(eventsData.length, 1); + const event = eventsData[0]; + assert.strictEqual(event.type, 'password-reset'); + assert.strictEqual(event.text, '[[success:success]]'); }); - it('should not generate code if rate limited', (done) => { - socketUser.reset.send({ uid: 0 }, 'regular@test.com', (err) => { - assert(err); + it('should not generate code if rate limited', async () => { + await assert.rejects( + socketUser.reset.send({ uid: 0 }, 'regular@test.com'), + { message: '[[error:reset-rate-limited]]' }, + ); + const [count, eventsData] = await Promise.all([ + db.sortedSetCount('reset:issueDate', 0, Date.now()), + events.getEvents('', 0, 0), + ]); + assert.strictEqual(count, 2); - async.parallel({ - count: async.apply(db.sortedSetCount.bind(db), 'reset:issueDate', 0, Date.now()), - event: async.apply(events.getEvents, '', 0, 0), - }, (err, data) => { - assert.ifError(err); - assert.strictEqual(data.count, 2); - - // Event validity - assert.strictEqual(data.event.length, 1); - const event = data.event[0]; - assert.strictEqual(event.type, 'password-reset'); - assert.strictEqual(event.text, '[[error:reset-rate-limited]]'); - - done(); - }); - }); + // Event validity + assert.strictEqual(eventsData.length, 1); + const event = eventsData[0]; + assert.strictEqual(event.type, 'password-reset'); + assert.strictEqual(event.text, '[[error:reset-rate-limited]]'); }); - it('should not error on invalid email (but not generate reset code)', (done) => { - socketUser.reset.send({ uid: 0 }, 'irregular@test.com', (err) => { - assert.ifError(err); - - db.sortedSetCount('reset:issueDate', 0, Date.now(), (err, count) => { - assert.ifError(err); - assert.strictEqual(count, 2); - done(); - }); - }); + it('should not error on invalid email (but not generate reset code)', async () => { + await socketUser.reset.send({ uid: 0 }, 'irregular@test.com'); + const count = await db.sortedSetCount('reset:issueDate', 0, Date.now()); + assert.strictEqual(count, 2); }); it('should error on no email', (done) => { diff --git a/test/topics.js b/test/topics.js index f40925103a..8a32e445f5 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1,12 +1,10 @@ 'use strict'; -const async = require('async'); const path = require('path'); const assert = require('assert'); const validator = require('validator'); const mockdate = require('mockdate'); const nconf = require('nconf'); -const request = require('request'); const util = require('util'); const sleep = util.promisify(setTimeout); @@ -22,14 +20,10 @@ const User = require('../src/user'); const groups = require('../src/groups'); const utils = require('../src/utils'); const helpers = require('./helpers'); -const socketPosts = require('../src/socket.io/posts'); const socketTopics = require('../src/socket.io/topics'); const apiTopics = require('../src/api/topics'); const apiPosts = require('../src/api/posts'); - -const requestType = util.promisify((type, url, opts, cb) => { - request[type](url, opts, (err, res, body) => cb(err, { res: res, body: body })); -}); +const request = require('../src/request'); describe('Topic\'s', () => { let topic; @@ -142,8 +136,8 @@ describe('Topic\'s', () => { }); await privileges.categories.give(['groups:topics:create'], categoryObj.cid, 'guests'); await privileges.categories.give(['groups:topics:reply'], categoryObj.cid, 'guests'); - const result = await requestType('post', `${nconf.get('url')}/api/v3/topics`, { - form: { + const result = await request.post(`${nconf.get('url')}/api/v3/topics`, { + data: { title: 'just a title', cid: categoryObj.cid, content: 'content for the main post', @@ -151,9 +145,8 @@ describe('Topic\'s', () => { headers: { 'x-csrf-token': 'invalid', }, - json: true, }); - assert.strictEqual(result.res.statusCode, 403); + assert.strictEqual(result.response.statusCode, 403); assert.strictEqual(result.body, 'Forbidden'); }); @@ -164,13 +157,12 @@ describe('Topic\'s', () => { }); const jar = request.jar(); const result = await helpers.request('post', `/api/v3/topics`, { - form: { + body: { title: 'just a title', cid: categoryObj.cid, content: 'content for the main post', }, jar: jar, - json: true, }); assert.strictEqual(result.body.status.message, 'You do not have enough privileges for this action.'); }); @@ -185,7 +177,7 @@ describe('Topic\'s', () => { const jar = request.jar(); const result = await helpers.request('post', `/api/v3/topics`, { - form: { + body: { title: 'just a title', cid: categoryObj.cid, content: 'content for the main post', @@ -199,11 +191,10 @@ describe('Topic\'s', () => { assert.strictEqual(result.body.response.user.username, '[[global:guest]]'); const replyResult = await helpers.request('post', `/api/v3/topics/${result.body.response.tid}`, { - form: { + body: { content: 'a reply by guest', }, jar: jar, - json: true, }); assert.strictEqual(replyResult.body.response.content, 'a reply by guest'); assert.strictEqual(replyResult.body.response.user.username, '[[global:guest]]'); @@ -219,14 +210,13 @@ describe('Topic\'s', () => { const oldValue = meta.config.allowGuestHandles; meta.config.allowGuestHandles = 1; const result = await helpers.request('post', `/api/v3/topics`, { - form: { + body: { title: 'just a title', cid: categoryObj.cid, content: 'content for the main post', handle: 'guest123', }, jar: request.jar(), - json: true, }); assert.strictEqual(result.body.status.code, 'ok'); @@ -235,12 +225,11 @@ describe('Topic\'s', () => { assert.strictEqual(result.body.response.user.displayname, 'guest123'); const replyResult = await helpers.request('post', `/api/v3/topics/${result.body.response.tid}`, { - form: { + body: { content: 'a reply by guest', handle: 'guest124', }, jar: request.jar(), - json: true, }); assert.strictEqual(replyResult.body.response.content, 'a reply by guest'); assert.strictEqual(replyResult.body.response.user.username, 'guest124'); @@ -651,40 +640,20 @@ describe('Topic\'s', () => { let followerUid; let moveCid; - before((done) => { - async.waterfall([ - function (next) { - topics.post({ - uid: topic.userId, - title: topic.title, - content: topic.content, - cid: topic.categoryId, - }, (err, result) => { - assert.ifError(err); - newTopic = result.topicData; - next(); - }); - }, - function (next) { - User.create({ username: 'topicFollower', password: '123456' }, next); - }, - function (_uid, next) { - followerUid = _uid; - topics.follow(newTopic.tid, _uid, next); - }, - function (next) { - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - }, (err, category) => { - if (err) { - return next(err); - } - moveCid = category.cid; - next(); - }); - }, - ], done); + before(async () => { + ({ topicData: newTopic } = await topics.post({ + uid: topic.userId, + title: topic.title, + content: topic.content, + cid: topic.categoryId, + })); + followerUid = await User.create({ username: 'topicFollower', password: '123456' }); + await topics.follow(newTopic.tid, followerUid); + + ({ cid: moveCid } = await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + })); }); it('should load topic tools', (done) => { @@ -753,118 +722,68 @@ describe('Topic\'s', () => { }); }); - it('should properly update sets when post is moved', (done) => { - let movedPost; - let previousPost; - let topic2LastReply; - let tid1; - let tid2; + it('should properly update sets when post is moved', async () => { const cid1 = topic.categoryId; - let cid2; - function checkCidSets(post1, post2, callback) { - async.waterfall([ - function (next) { - async.parallel({ - topicData: function (next) { - topics.getTopicsFields([tid1, tid2], ['lastposttime', 'postcount'], next); - }, - scores1: function (next) { - db.sortedSetsScore([ - `cid:${cid1}:tids`, - `cid:${cid1}:tids:lastposttime`, - `cid:${cid1}:tids:posts`, - ], tid1, next); - }, - scores2: function (next) { - db.sortedSetsScore([ - `cid:${cid2}:tids`, - `cid:${cid2}:tids:lastposttime`, - `cid:${cid2}:tids:posts`, - ], tid2, next); - }, - posts1: function (next) { - db.getSortedSetRangeWithScores(`tid:${tid1}:posts`, 0, -1, next); - }, - posts2: function (next) { - db.getSortedSetRangeWithScores(`tid:${tid2}:posts`, 0, -1, next); - }, - }, next); - }, - function (results, next) { - const assertMsg = `${JSON.stringify(results.posts1)}\n${JSON.stringify(results.posts2)}`; - assert.equal(results.topicData[0].postcount, results.scores1[2], assertMsg); - assert.equal(results.topicData[1].postcount, results.scores2[2], assertMsg); - assert.equal(results.topicData[0].lastposttime, post1.timestamp, assertMsg); - assert.equal(results.topicData[1].lastposttime, post2.timestamp, assertMsg); - assert.equal(results.topicData[0].lastposttime, results.scores1[0], assertMsg); - assert.equal(results.topicData[1].lastposttime, results.scores2[0], assertMsg); - assert.equal(results.topicData[0].lastposttime, results.scores1[1], assertMsg); - assert.equal(results.topicData[1].lastposttime, results.scores2[1], assertMsg); + const category = await categories.create({ + name: 'move to this category', + description: 'Test category created by testing script', + }); + const cid2 = category.cid; + const { topicData } = await topics.post({ uid: adminUid, title: 'topic1', content: 'topic 1 mainPost', cid: cid1 }); + const tid1 = topicData.tid; + const previousPost = await topics.reply({ uid: adminUid, content: 'topic 1 reply 1', tid: tid1 }); + const movedPost = await topics.reply({ uid: adminUid, content: 'topic 1 reply 2', tid: tid1 }); - next(); - }, - ], callback); + const { topicData: anotherTopic } = await topics.post({ uid: adminUid, title: 'topic2', content: 'topic 2 mainpost', cid: cid2 }); + const tid2 = anotherTopic.tid; + const topic2LastReply = await topics.reply({ uid: adminUid, content: 'topic 2 reply 1', tid: tid2 }); + + async function checkCidSets(post1, post2) { + const [topicData, scores1, scores2, posts1, posts2] = await Promise.all([ + topics.getTopicsFields([tid1, tid2], ['lastposttime', 'postcount']), + db.sortedSetsScore([ + `cid:${cid1}:tids`, + `cid:${cid1}:tids:lastposttime`, + `cid:${cid1}:tids:posts`, + ], tid1), + db.sortedSetsScore([ + `cid:${cid2}:tids`, + `cid:${cid2}:tids:lastposttime`, + `cid:${cid2}:tids:posts`, + ], tid2), + db.getSortedSetRangeWithScores(`tid:${tid1}:posts`, 0, -1), + db.getSortedSetRangeWithScores(`tid:${tid2}:posts`, 0, -1), + ]); + const assertMsg = `${JSON.stringify(posts1)}\n${JSON.stringify(posts2)}`; + assert.equal(topicData[0].postcount, scores1[2], assertMsg); + assert.equal(topicData[1].postcount, scores2[2], assertMsg); + assert.equal(topicData[0].lastposttime, post1.timestamp, assertMsg); + assert.equal(topicData[1].lastposttime, post2.timestamp, assertMsg); + assert.equal(topicData[0].lastposttime, scores1[0], assertMsg); + assert.equal(topicData[1].lastposttime, scores2[0], assertMsg); + assert.equal(topicData[0].lastposttime, scores1[1], assertMsg); + assert.equal(topicData[1].lastposttime, scores2[1], assertMsg); } - async.waterfall([ - function (next) { - categories.create({ - name: 'move to this category', - description: 'Test category created by testing script', - }, next); - }, - function (category, next) { - cid2 = category.cid; - topics.post({ uid: adminUid, title: 'topic1', content: 'topic 1 mainPost', cid: cid1 }, next); - }, - function (result, next) { - tid1 = result.topicData.tid; - topics.reply({ uid: adminUid, content: 'topic 1 reply 1', tid: tid1 }, next); - }, - function (postData, next) { - previousPost = postData; - topics.reply({ uid: adminUid, content: 'topic 1 reply 2', tid: tid1 }, next); - }, - function (postData, next) { - movedPost = postData; - topics.post({ uid: adminUid, title: 'topic2', content: 'topic 2 mainpost', cid: cid2 }, next); - }, - function (results, next) { - tid2 = results.topicData.tid; - topics.reply({ uid: adminUid, content: 'topic 2 reply 1', tid: tid2 }, next); - }, - function (postData, next) { - topic2LastReply = postData; - checkCidSets(movedPost, postData, next); - }, - function (next) { - db.isMemberOfSortedSets([`cid:${cid1}:pids`, `cid:${cid2}:pids`], movedPost.pid, next); - }, - function (isMember, next) { - assert.deepEqual(isMember, [true, false]); - categories.getCategoriesFields([cid1, cid2], ['post_count'], next); - }, - function (categoryData, next) { - assert.equal(categoryData[0].post_count, 4); - assert.equal(categoryData[1].post_count, 2); - topics.movePostToTopic(1, movedPost.pid, tid2, next); - }, - function (next) { - checkCidSets(previousPost, topic2LastReply, next); - }, - function (next) { - db.isMemberOfSortedSets([`cid:${cid1}:pids`, `cid:${cid2}:pids`], movedPost.pid, next); - }, - function (isMember, next) { - assert.deepEqual(isMember, [false, true]); - categories.getCategoriesFields([cid1, cid2], ['post_count'], next); - }, - function (categoryData, next) { - assert.equal(categoryData[0].post_count, 3); - assert.equal(categoryData[1].post_count, 3); - next(); - }, - ], done); + await checkCidSets(movedPost, topic2LastReply); + + let isMember = await db.isMemberOfSortedSets([`cid:${cid1}:pids`, `cid:${cid2}:pids`], movedPost.pid); + assert.deepEqual(isMember, [true, false]); + + let categoryData = await categories.getCategoriesFields([cid1, cid2], ['post_count']); + assert.equal(categoryData[0].post_count, 4); + assert.equal(categoryData[1].post_count, 2); + + await topics.movePostToTopic(1, movedPost.pid, tid2); + + await checkCidSets(previousPost, topic2LastReply); + + isMember = await db.isMemberOfSortedSets([`cid:${cid1}:pids`, `cid:${cid2}:pids`], movedPost.pid); + assert.deepEqual(isMember, [false, true]); + + categoryData = await categories.getCategoriesFields([cid1, cid2], ['post_count']); + assert.equal(categoryData[0].post_count, 3); + assert.equal(categoryData[1].post_count, 3); }); it('should fail to purge topic if user does not have privilege', async () => { @@ -915,42 +834,22 @@ describe('Topic\'s', () => { let tid1; let tid2; let tid3; - before((done) => { - function createTopic(callback) { - topics.post({ + before(async () => { + async function createTopic() { + return (await topics.post({ uid: topic.userId, title: 'topic for test', content: 'topic content', cid: topic.categoryId, - }, callback); + })).topicData.tid; } - async.series({ - topic1: function (next) { - createTopic(next); - }, - topic2: function (next) { - createTopic(next); - }, - topic3: function (next) { - createTopic(next); - }, - }, (err, results) => { - assert.ifError(err); - tid1 = results.topic1.topicData.tid; - tid2 = results.topic2.topicData.tid; - tid3 = results.topic3.topicData.tid; - async.series([ - function (next) { - topics.tools.pin(tid1, adminUid, next); - }, - function (next) { - // artificial timeout so pin time is different on redis sometimes scores are indentical - setTimeout(() => { - topics.tools.pin(tid2, adminUid, next); - }, 5); - }, - ], done); - }); + tid1 = await createTopic(); + tid2 = await createTopic(); + tid3 = await createTopic(); + await topics.tools.pin(tid1, adminUid); + // artificial timeout so pin time is different on redis sometimes scores are indentical + await sleep(5); + await topics.tools.pin(tid2, adminUid); }); const socketTopics = require('../src/socket.io/topics'); @@ -1009,110 +908,48 @@ describe('Topic\'s', () => { let newTid; let uid; let newTopic; - before((done) => { + before(async () => { uid = topic.userId; - async.waterfall([ - function (done) { - topics.post({ uid: topic.userId, title: 'Topic to be ignored', content: 'Just ignore me, please!', cid: topic.categoryId }, (err, result) => { - if (err) { - return done(err); - } - - newTopic = result.topicData; - newTid = newTopic.tid; - done(); - }); - }, - function (done) { - topics.markUnread(newTid, uid, done); - }, - ], done); + const result = await topics.post({ uid: topic.userId, title: 'Topic to be ignored', content: 'Just ignore me, please!', cid: topic.categoryId }); + newTopic = result.topicData; + newTid = newTopic.tid; + await topics.markUnread(newTid, uid); }); - it('should not appear in the unread list', (done) => { - async.waterfall([ - function (done) { - topics.ignore(newTid, uid, done); - }, - function (done) { - topics.getUnreadTopics({ cid: 0, uid: uid, start: 0, stop: -1, filter: '' }, done); - }, - function (results, done) { - const { topics } = results; - const tids = topics.map(topic => topic.tid); - assert.equal(tids.indexOf(newTid), -1, 'The topic appeared in the unread list.'); - done(); - }, - ], done); + it('should not appear in the unread list', async () => { + await topics.ignore(newTid, uid); + const { topics: topicData } = await topics.getUnreadTopics({ cid: 0, uid: uid, start: 0, stop: -1, filter: '' }); + const tids = topicData.map(topic => topic.tid); + assert.equal(tids.indexOf(newTid), -1, 'The topic appeared in the unread list.'); }); - it('should not appear as unread in the recent list', (done) => { - async.waterfall([ - function (done) { - topics.ignore(newTid, uid, done); - }, - function (done) { - topics.getLatestTopics({ - uid: uid, - start: 0, - stop: -1, - term: 'year', - }, done); - }, - function (results, done) { - const { topics } = results; - let topic; - let i; - for (i = 0; i < topics.length; i += 1) { - if (topics[i].tid === parseInt(newTid, 10)) { - assert.equal(false, topics[i].unread, 'ignored topic was marked as unread in recent list'); - return done(); - } - } - assert.ok(topic, 'topic didn\'t appear in the recent list'); - done(); - }, - ], done); + it('should not appear as unread in the recent list', async () => { + await topics.ignore(newTid, uid); + const results = await topics.getLatestTopics({ + uid: uid, + start: 0, + stop: -1, + term: 'year', + }); + + const { topics: topicsData } = results; + let topic; + let i; + for (i = 0; i < topicsData.length; i += 1) { + if (topicsData[i].tid === parseInt(newTid, 10)) { + assert.equal(false, topicsData[i].unread, 'ignored topic was marked as unread in recent list'); + return; + } + } + assert.ok(topic, 'topic didn\'t appear in the recent list'); }); - it('should appear as unread again when marked as reading', (done) => { - async.waterfall([ - function (done) { - topics.ignore(newTid, uid, done); - }, - function (done) { - topics.follow(newTid, uid, done); - }, - function (done) { - topics.getUnreadTopics({ cid: 0, uid: uid, start: 0, stop: -1, filter: '' }, done); - }, - function (results, done) { - const { topics } = results; - const tids = topics.map(topic => topic.tid); - assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); - done(); - }, - ], done); - }); - - it('should appear as unread again when marked as following', (done) => { - async.waterfall([ - function (done) { - topics.ignore(newTid, uid, done); - }, - function (done) { - topics.follow(newTid, uid, done); - }, - function (done) { - topics.getUnreadTopics({ cid: 0, uid: uid, start: 0, stop: -1, filter: '' }, done); - }, - function (results, done) { - const { topics } = results; - const tids = topics.map(topic => topic.tid); - assert.notEqual(tids.indexOf(newTid), -1, 'The topic did not appear in the unread list.'); - done(); - }, - ], done); + it('should appear as unread again when marked as following', async () => { + await topics.ignore(newTid, uid); + await topics.follow(newTid, uid); + const results = await topics.getUnreadTopics({ cid: 0, uid: uid, start: 0, stop: -1, filter: '' }); + const tids = results.topics.map(topic => topic.tid); + assert.ok(tids.includes(newTid), 'The topic did not appear in the unread list.'); }); }); @@ -1121,49 +958,26 @@ describe('Topic\'s', () => { const replies = []; let topicPids; const originalBookmark = 6; - function postReply(next) { - topics.reply({ uid: topic.userId, content: `test post ${replies.length}`, tid: newTopic.tid }, (err, result) => { - assert.equal(err, null, 'was created with error'); - assert.ok(result); - replies.push(result); - next(); - }); + async function postReply() { + const result = await topics.reply({ uid: topic.userId, content: `test post ${replies.length}`, tid: newTopic.tid }); + assert.ok(result); + replies.push(result); } - before((done) => { - async.waterfall([ - function (next) { - groups.join('administrators', topic.userId, next); - }, - function (next) { - topics.post({ - uid: topic.userId, - title: topic.title, - content: topic.content, - cid: topic.categoryId, - }, (err, result) => { - assert.ifError(err); - newTopic = result.topicData; - next(); - }); - }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { postReply(next); }, - function (next) { - topicPids = replies.map(reply => reply.pid); - socketTopics.bookmark({ uid: topic.userId }, { tid: newTopic.tid, index: originalBookmark }, next); - }, - ], done); + before(async () => { + await groups.join('administrators', topic.userId); + ({ topicData: newTopic } = await topics.post({ + uid: topic.userId, + title: topic.title, + content: topic.content, + cid: topic.categoryId, + })); + for (let i = 0; i < 12; i++) { + // eslint-disable-next-line no-await-in-loop + await postReply(); + } + topicPids = replies.map(reply => reply.pid); + await socketTopics.bookmark({ uid: topic.userId }, { tid: newTopic.tid, index: originalBookmark }); }); it('should fail with invalid data', (done) => { @@ -1192,44 +1006,25 @@ describe('Topic\'s', () => { }); }); - it('should not update the user\'s bookmark', (done) => { - async.waterfall([ - function (next) { - socketTopics.createTopicFromPosts({ uid: topic.userId }, { - title: 'Fork test, no bookmark update', - pids: topicPids.slice(-2), - fromTid: newTopic.tid, - }, next); - }, - function (forkedTopicData, next) { - topics.getUserBookmark(newTopic.tid, topic.userId, next); - }, - function (bookmark, next) { - assert.equal(originalBookmark, bookmark); - next(); - }, - ], done); + it('should not update the user\'s bookmark', async () => { + await socketTopics.createTopicFromPosts({ uid: topic.userId }, { + title: 'Fork test, no bookmark update', + pids: topicPids.slice(-2), + fromTid: newTopic.tid, + }); + const bookmark = await topics.getUserBookmark(newTopic.tid, topic.userId); + assert.equal(originalBookmark, bookmark); }); - it('should update the user\'s bookmark ', (done) => { - async.waterfall([ - function (next) { - topics.createTopicFromPosts( - topic.userId, - 'Fork test, no bookmark update', - topicPids.slice(1, 3), - newTopic.tid, - next - ); - }, - function (forkedTopicData, next) { - topics.getUserBookmark(newTopic.tid, topic.userId, next); - }, - function (bookmark, next) { - assert.equal(originalBookmark - 2, bookmark); - next(); - }, - ], done); + it('should update the user\'s bookmark ', async () => { + await topics.createTopicFromPosts( + topic.userId, + 'Fork test, no bookmark update', + topicPids.slice(1, 3), + newTopic.tid, + ); + const bookmark = await topics.getUserBookmark(newTopic.tid, topic.userId); + assert.equal(originalBookmark - 2, bookmark); }); it('should properly update topic vote count after forking', async () => { @@ -1267,162 +1062,114 @@ describe('Topic\'s', () => { }); }); - it('should load topic', (done) => { - request(`${nconf.get('url')}/topic/${topicData.slug}`, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert(body); - done(); - }); + it('should load topic', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load topic api data', (done) => { - request(`${nconf.get('url')}/api/topic/${topicData.slug}`, { json: true }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert.strictEqual(body._header.tags.meta.find(t => t.name === 'description').content, 'topic content'); - assert.strictEqual(body._header.tags.meta.find(t => t.property === 'og:description').content, 'topic content'); - done(); - }); + it('should load topic api data', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/topic/${topicData.slug}`); + assert.equal(response.statusCode, 200); + assert.strictEqual(body._header.tags.meta.find(t => t.name === 'description').content, 'topic content'); + assert.strictEqual(body._header.tags.meta.find(t => t.property === 'og:description').content, 'topic content'); }); - it('should 404 if post index is invalid', (done) => { - request(`${nconf.get('url')}/topic/${topicData.slug}/derp`, (err, response) => { - assert.ifError(err); - assert.equal(response.statusCode, 404); - done(); - }); + it('should 404 if post index is invalid', async () => { + const { response } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}/derp`); + assert.equal(response.statusCode, 404); }); - it('should 404 if topic does not exist', (done) => { - request(`${nconf.get('url')}/topic/123123/does-not-exist`, (err, response) => { - assert.ifError(err); - assert.equal(response.statusCode, 404); - done(); - }); + it('should 404 if topic does not exist', async () => { + const { response } = await request.get(`${nconf.get('url')}/topic/123123/does-not-exist`); + assert.equal(response.statusCode, 404); }); - it('should 401 if not allowed to read as guest', (done) => { + it('should 401 if not allowed to read as guest', async () => { const privileges = require('../src/privileges'); - privileges.categories.rescind(['groups:topics:read'], topicData.cid, 'guests', (err) => { - assert.ifError(err); - request(`${nconf.get('url')}/api/topic/${topicData.slug}`, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 401); - assert(body); - privileges.categories.give(['groups:topics:read'], topicData.cid, 'guests', done); - }); - }); + await privileges.categories.rescind(['groups:topics:read'], topicData.cid, 'guests'); + + const { response, body } = await request.get(`${nconf.get('url')}/api/topic/${topicData.slug}`); + assert.equal(response.statusCode, 401); + assert(body); + await privileges.categories.give(['groups:topics:read'], topicData.cid, 'guests'); }); - it('should redirect to correct topic if slug is missing', (done) => { - request(`${nconf.get('url')}/topic/${topicData.tid}/herpderp/1?page=2`, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert(body); - done(); - }); + it('should redirect to correct topic if slug is missing', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/topic/${topicData.tid}/herpderp/1?page=2`); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should redirect if post index is out of range', (done) => { - request(`${nconf.get('url')}/api/topic/${topicData.slug}/-1`, { json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(res.headers['x-redirect'], `/topic/${topicData.tid}/topic-for-controller-test`); - assert.equal(body, `/topic/${topicData.tid}/topic-for-controller-test`); - done(); - }); + it('should redirect if post index is out of range', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/topic/${topicData.slug}/-1`); + assert.equal(response.statusCode, 200); + assert.equal(response.headers['x-redirect'], `/topic/${topicData.tid}/topic-for-controller-test`); + assert.equal(body, `/topic/${topicData.tid}/topic-for-controller-test`); }); - it('should 404 if page is out of bounds', (done) => { + it('should 404 if page is out of bounds', async () => { const meta = require('../src/meta'); meta.config.usePagination = 1; - request(`${nconf.get('url')}/topic/${topicData.slug}?page=100`, (err, response) => { - assert.ifError(err); - assert.equal(response.statusCode, 404); - done(); - }); + const { response } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}?page=100`); + assert.equal(response.statusCode, 404); }); - it('should mark topic read', (done) => { - request(`${nconf.get('url')}/topic/${topicData.slug}`, { + it('should mark topic read', async () => { + const { response } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}`, { jar: adminJar, - }, (err, res) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - topics.hasReadTopics([topicData.tid], adminUid, (err, hasRead) => { - assert.ifError(err); - assert.equal(hasRead[0], true); - done(); - }); }); + assert.equal(response.statusCode, 200); + const hasRead = await topics.hasReadTopics([topicData.tid], adminUid); + assert.equal(hasRead[0], true); }); - it('should 404 if tid is not a number', (done) => { - request(`${nconf.get('url')}/api/topic/teaser/nan`, { json: true }, (err, response) => { - assert.ifError(err); - assert.equal(response.statusCode, 404); - done(); - }); + it('should 404 if tid is not a number', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/topic/teaser/nan`); + assert.equal(response.statusCode, 404); }); - it('should 403 if cant read', (done) => { - request(`${nconf.get('url')}/api/topic/teaser/${123123}`, { json: true }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 403); - assert.equal(body, '[[error:no-privileges]]'); - - done(); - }); + it('should 403 if cant read', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/topic/teaser/${123123}`); + assert.equal(response.statusCode, 403); + assert.equal(body, '[[error:no-privileges]]'); }); - it('should load topic teaser', (done) => { - request(`${nconf.get('url')}/api/topic/teaser/${topicData.tid}`, { json: true }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert(body); - assert.equal(body.tid, topicData.tid); - assert.equal(body.content, 'topic content'); - assert(body.user); - assert(body.topic); - assert(body.category); - done(); - }); + it('should load topic teaser', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/topic/teaser/${topicData.tid}`); + assert.equal(response.statusCode, 200); + assert(body); + assert.equal(body.tid, topicData.tid); + assert.equal(body.content, 'topic content'); + assert(body.user); + assert(body.topic); + assert(body.category); }); - it('should 404 if tid is not a number', (done) => { - request(`${nconf.get('url')}/api/topic/pagination/nan`, { json: true }, (err, response) => { - assert.ifError(err); - assert.equal(response.statusCode, 404); - done(); - }); + it('should 404 if tid is not a number', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/topic/pagination/nan`); + assert.equal(response.statusCode, 404); }); - it('should 404 if tid does not exist', (done) => { - request(`${nconf.get('url')}/api/topic/pagination/1231231`, { json: true }, (err, response) => { - assert.ifError(err); - assert.equal(response.statusCode, 404); - done(); - }); + it('should 404 if tid does not exist', async () => { + const { response } = await request.get(`${nconf.get('url')}/api/topic/pagination/1231231`); + assert.equal(response.statusCode, 404); }); - it('should load pagination', (done) => { - request(`${nconf.get('url')}/api/topic/pagination/${topicData.tid}`, { json: true }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert(body); - assert.deepEqual(body.pagination, { - prev: { page: 1, active: false }, - next: { page: 1, active: false }, - first: { page: 1, active: true }, - last: { page: 1, active: true }, - rel: [], - pages: [], - currentPage: 1, - pageCount: 1, - }); - done(); + it('should load pagination', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/topic/pagination/${topicData.tid}`); + assert.equal(response.statusCode, 200); + assert(body); + assert.deepEqual(body.pagination, { + prev: { page: 1, active: false }, + next: { page: 1, active: false }, + first: { page: 1, active: true }, + last: { page: 1, active: true }, + rel: [], + pages: [], + currentPage: 1, + pageCount: 1, }); }); }); @@ -1464,23 +1211,12 @@ describe('Topic\'s', () => { describe('suggested topics', () => { let tid1; let tid3; - before((done) => { - async.series({ - topic1: function (next) { - topics.post({ uid: adminUid, tags: ['nodebb'], title: 'topic title 1', content: 'topic 1 content', cid: topic.categoryId }, next); - }, - topic2: function (next) { - topics.post({ uid: adminUid, tags: ['nodebb'], title: 'topic title 2', content: 'topic 2 content', cid: topic.categoryId }, next); - }, - topic3: function (next) { - topics.post({ uid: adminUid, tags: [], title: 'topic title 3', content: 'topic 3 content', cid: topic.categoryId }, next); - }, - }, (err, results) => { - assert.ifError(err); - tid1 = results.topic1.topicData.tid; - tid3 = results.topic3.topicData.tid; - done(); - }); + before(async () => { + const topic1 = await topics.post({ uid: adminUid, tags: ['nodebb'], title: 'topic title 1', content: 'topic 1 content', cid: topic.categoryId }); + const topic2 = await topics.post({ uid: adminUid, tags: ['nodebb'], title: 'topic title 2', content: 'topic 2 content', cid: topic.categoryId }); + const topic3 = await topics.post({ uid: adminUid, tags: [], title: 'topic title 3', content: 'topic 3 content', cid: topic.categoryId }); + tid1 = topic1.topicData.tid; + tid3 = topic3.topicData.tid; }); it('should return suggested topics', (done) => { @@ -1503,23 +1239,11 @@ describe('Topic\'s', () => { describe('unread', () => { const socketTopics = require('../src/socket.io/topics'); let tid; - let mainPid; let uid; - before((done) => { - async.parallel({ - topic: function (next) { - topics.post({ uid: topic.userId, title: 'unread topic', content: 'unread topic content', cid: topic.categoryId }, next); - }, - joeUid: function (next) { - User.create({ username: 'regularJoe' }, next); - }, - }, (err, results) => { - assert.ifError(err); - tid = results.topic.topicData.tid; - mainPid = results.topic.postData.pid; - uid = results.joeUid; - done(); - }); + before(async () => { + const { topicData } = await topics.post({ uid: topic.userId, title: 'unread topic', content: 'unread topic content', cid: topic.categoryId }); + uid = await User.create({ username: 'regularJoe' }); + tid = topicData.tid; }); it('should fail with invalid data', async () => { @@ -1651,93 +1375,45 @@ describe('Topic\'s', () => { }); }); - it('should not return topics in category you cant read', (done) => { - let privateCid; - let privateTid; - async.waterfall([ - function (next) { - categories.create({ - name: 'private category', - description: 'private category', - }, next); - }, - function (category, next) { - privateCid = category.cid; - privileges.categories.rescind(['groups:topics:read'], category.cid, 'registered-users', next); - }, - function (next) { - topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: privateCid }, next); - }, - function (data, next) { - privateTid = data.topicData.tid; - topics.getUnreadTids({ uid: uid }, next); - }, - function (unreadTids, next) { - unreadTids = unreadTids.map(String); - assert(!unreadTids.includes(String(privateTid))); - next(); - }, - ], done); + it('should not return topics in category you cant read', async () => { + const { cid: privateCid } = await categories.create({ + name: 'private category', + description: 'private category', + }); + privileges.categories.rescind(['groups:topics:read'], privateCid, 'registered-users'); + + const { topicData } = await topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: privateCid }); + const privateTid = topicData.tid; + + const unreadTids = (await topics.getUnreadTids({ uid: uid })).map(String); + assert(!unreadTids.includes(String(privateTid))); }); - it('should not return topics in category you ignored/not watching', (done) => { - let ignoredCid; - let tid; - async.waterfall([ - function (next) { - categories.create({ - name: 'ignored category', - description: 'ignored category', - }, next); - }, - function (category, next) { - ignoredCid = category.cid; - privileges.categories.rescind(['groups:topics:read'], category.cid, 'registered-users', next); - }, - function (next) { - topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: ignoredCid }, next); - }, - function (data, next) { - tid = data.topicData.tid; - User.ignoreCategory(uid, ignoredCid, next); - }, - function (next) { - topics.getUnreadTids({ uid: uid }, next); - }, - function (unreadTids, next) { - unreadTids = unreadTids.map(String); - assert(!unreadTids.includes(String(tid))); - next(); - }, - ], done); + it('should not return topics in category you ignored/not watching', async () => { + const category = await categories.create({ + name: 'ignored category', + description: 'ignored category', + }); + const ignoredCid = category.cid; + await privileges.categories.rescind(['groups:topics:read'], ignoredCid, 'registered-users'); + + const { topicData } = await topics.post({ uid: adminUid, title: 'topic in private category', content: 'registered-users cant see this', cid: ignoredCid }); + const { tid } = topicData; + + await User.ignoreCategory(uid, ignoredCid); + const unreadTids = (await topics.getUnreadTids({ uid: uid })).map(String); + assert(!unreadTids.includes(String(tid))); }); - it('should not return topic as unread if new post is from blocked user', (done) => { - let blockedUid; - let topic; - async.waterfall([ - function (next) { - topics.post({ uid: adminUid, title: 'will not get as unread', content: 'not unread', cid: categoryObj.cid }, next); - }, - function (result, next) { - topic = result.topicData; - User.create({ username: 'blockedunread' }, next); - }, - function (uid, next) { - blockedUid = uid; - User.blocks.add(uid, adminUid, next); - }, - function (next) { - topics.reply({ uid: blockedUid, content: 'post from blocked user', tid: topic.tid }, next); - }, - function (result, next) { - topics.getUnreadTids({ cid: 0, uid: adminUid }, next); - }, - function (unreadTids, next) { - assert(!unreadTids.includes(topic.tid)); - User.blocks.remove(blockedUid, adminUid, next); - }, - ], done); + it('should not return topic as unread if new post is from blocked user', async () => { + const { topicData } = await topics.post({ uid: adminUid, title: 'will not get as unread', content: 'not unread', cid: categoryObj.cid }); + const blockedUid = await User.create({ username: 'blockedunread' }); + await User.blocks.add(blockedUid, adminUid); + await topics.reply({ uid: blockedUid, content: 'post from blocked user', tid: topic.tid }); + + const unreadTids = await topics.getUnreadTids({ cid: 0, uid: adminUid }); + assert(!unreadTids.includes(topicData.tid)); + await User.blocks.remove(blockedUid, adminUid); }); it('should not return topic as unread if topic is deleted', async () => { @@ -1753,18 +1429,9 @@ describe('Topic\'s', () => { const socketTopics = require('../src/socket.io/topics'); const socketAdmin = require('../src/socket.io/admin'); - before((done) => { - async.series([ - function (next) { - topics.post({ uid: adminUid, tags: ['php', 'nosql', 'psql', 'nodebb', 'node icon'], title: 'topic title 1', content: 'topic 1 content', cid: topic.categoryId }, next); - }, - function (next) { - topics.post({ uid: adminUid, tags: ['javascript', 'mysql', 'python', 'nodejs'], title: 'topic title 2', content: 'topic 2 content', cid: topic.categoryId }, next); - }, - ], (err) => { - assert.ifError(err); - done(); - }); + before(async () => { + await topics.post({ uid: adminUid, tags: ['php', 'nosql', 'psql', 'nodebb', 'node icon'], title: 'topic title 1', content: 'topic 1 content', cid: topic.categoryId }); + await topics.post({ uid: adminUid, tags: ['javascript', 'mysql', 'python', 'nodejs'], title: 'topic title 2', content: 'topic 2 content', cid: topic.categoryId }); }); it('should return empty array if query is falsy', (done) => { @@ -2309,20 +1976,9 @@ describe('Topic\'s', () => { describe('teasers', () => { let topic1; let topic2; - before((done) => { - async.series([ - function (next) { - topics.post({ uid: adminUid, title: 'topic 1', content: 'content 1', cid: categoryObj.cid }, next); - }, - function (next) { - topics.post({ uid: adminUid, title: 'topic 2', content: 'content 2', cid: categoryObj.cid }, next); - }, - ], (err, results) => { - assert.ifError(err); - topic1 = results[0]; - topic2 = results[1]; - done(); - }); + before(async () => { + topic1 = await topics.post({ uid: adminUid, title: 'topic 1', content: 'content 1', cid: categoryObj.cid }); + topic2 = await topics.post({ uid: adminUid, title: 'topic 2', content: 'content 2', cid: categoryObj.cid }); }); after((done) => { @@ -2416,47 +2072,23 @@ describe('Topic\'s', () => { }); }); - it('should not return teaser if user is blocked', (done) => { - let blockedUid; - async.waterfall([ - function (next) { - User.create({ username: 'blocked' }, next); - }, - function (uid, next) { - blockedUid = uid; - User.blocks.add(uid, adminUid, next); - }, - function (next) { - topics.reply({ uid: blockedUid, content: 'post from blocked user', tid: topic2.topicData.tid }, next); - }, - function (result, next) { - topics.getTeaser(topic2.topicData.tid, adminUid, next); - }, - function (teaser, next) { - assert.equal(teaser.content, 'content 2'); - User.blocks.remove(blockedUid, adminUid, next); - }, - ], done); + it('should not return teaser if user is blocked', async () => { + const blockedUid = await User.create({ username: 'blocked' }); + await User.blocks.add(blockedUid, adminUid); + await topics.reply({ uid: blockedUid, content: 'post from blocked user', tid: topic2.topicData.tid }); + const teaser = await topics.getTeaser(topic2.topicData.tid, adminUid); + assert.equal(teaser.content, 'content 2'); + await User.blocks.remove(blockedUid, adminUid); }); }); describe('tag privilege', () => { let uid; let cid; - before((done) => { - async.waterfall([ - function (next) { - User.create({ username: 'tag_poster' }, next); - }, - function (_uid, next) { - uid = _uid; - categories.create({ name: 'tag category' }, next); - }, - function (categoryObj, next) { - cid = categoryObj.cid; - next(); - }, - ], done); + before(async () => { + uid = await User.create({ username: 'tag_poster' }); + const category = await categories.create({ name: 'tag category' }); + cid = category.cid; }); it('should fail to post if user does not have tag privilege', (done) => { @@ -2507,27 +2139,14 @@ describe('Topic\'s', () => { return await topics.getTopicWithPosts(topicData, `tid:${topicData.tid}:posts`, adminUid, 0, 19, false); } - before((done) => { - async.waterfall([ - function (next) { - User.create({ username: 'mergevictim' }, next); - }, - function (_uid, next) { - uid = _uid; - topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 1', content: 'topic 1 OP' }, next); - }, - function (result, next) { - topic1Data = result.topicData; - topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 2', content: 'topic 2 OP' }, next); - }, - function (result, next) { - topic2Data = result.topicData; - topics.reply({ uid: uid, content: 'topic 1 reply', tid: topic1Data.tid }, next); - }, - function (postData, next) { - topics.reply({ uid: uid, content: 'topic 2 reply', tid: topic2Data.tid }, next); - }, - ], done); + before(async () => { + uid = await User.create({ username: 'mergevictim' }); + let result = await topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 1', content: 'topic 1 OP' }); + topic1Data = result.topicData; + result = await topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 2', content: 'topic 2 OP' }); + topic2Data = result.topicData; + await topics.reply({ uid: uid, content: 'topic 1 reply', tid: topic1Data.tid }); + await topics.reply({ uid: uid, content: 'topic 2 reply', tid: topic2Data.tid }); }); it('should error if data is not an array', (done) => { @@ -2565,14 +2184,11 @@ describe('Topic\'s', () => { assert.equal(topic1.title, 'topic 1'); }); - it('should return properly for merged topic', (done) => { - request(`${nconf.get('url')}/api/topic/${topic2Data.slug}`, { jar: adminJar, json: true }, (err, response, body) => { - assert.ifError(err); - assert.equal(response.statusCode, 200); - assert(body); - assert.deepStrictEqual(body.posts, []); - done(); - }); + it('should return properly for merged topic', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/topic/${topic2Data.slug}`, { jar: adminJar }); + assert.equal(response.statusCode, 200); + assert(body); + assert.deepStrictEqual(body.posts, []); }); it('should merge 2 topics with options mainTid', async () => { @@ -2645,26 +2261,24 @@ describe('Topic\'s', () => { await topics.reply({ uid: topic.userId, content: 'topic 2 reply', tid: topic2Result.topicData.tid }); }); - it('should get sorted topics in category', (done) => { + it('should get sorted topics in category', async () => { const filters = ['', 'watched', 'unreplied', 'new']; - async.map(filters, (filter, next) => { - topics.getSortedTopics({ + const data = await Promise.all(filters.map( + async filter => topics.getSortedTopics({ cids: [category.cid], uid: topic.userId, start: 0, stop: -1, filter: filter, sort: 'votes', - }, next); - }, (err, data) => { - assert.ifError(err); - assert(data); - data.forEach((filterTopics) => { - assert(Array.isArray(filterTopics.topics)); - }); - done(); + }) + )); + assert(data); + data.forEach((filterTopics) => { + assert(Array.isArray(filterTopics.topics)); }); }); + it('should get topics recent replied first', async () => { const data = await topics.getSortedTopics({ cids: [category.cid], @@ -2697,15 +2311,13 @@ describe('Topic\'s', () => { let adminApiOpts; let postData; const replyData = { - form: { + body: { content: 'a reply by guest', }, - json: true, }; before(async () => { adminApiOpts = { - json: true, jar: adminJar, headers: { 'x-csrf-token': csrf_token, @@ -2750,85 +2362,85 @@ describe('Topic\'s', () => { }); it('should not load topic for an unprivileged user', async () => { - const response = await requestType('get', `${nconf.get('url')}/topic/${topicData.slug}`); + const { response, body } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}`); assert.strictEqual(response.statusCode, 404); - assert(response.body); + assert(body); }); it('should load topic for a privileged user', async () => { - const response = (await requestType('get', `${nconf.get('url')}/topic/${topicData.slug}`, { jar: adminJar })).res; + const { response, body } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}`, { jar: adminJar }); assert.strictEqual(response.statusCode, 200); - assert(response.body); + assert(body); }); it('should not be amongst topics of the category for an unprivileged user', async () => { - const response = await requestType('get', `${nconf.get('url')}/api/category/${categoryObj.slug}`, { json: true }); - assert.strictEqual(response.body.topics.filter(topic => topic.tid === topicData.tid).length, 0); + const { body } = await request.get(`${nconf.get('url')}/api/category/${categoryObj.slug}`); + assert.strictEqual(body.topics.filter(topic => topic.tid === topicData.tid).length, 0); }); it('should be amongst topics of the category for a privileged user', async () => { - const response = await requestType('get', `${nconf.get('url')}/api/category/${categoryObj.slug}`, { json: true, jar: adminJar }); - const topic = response.body.topics.filter(topic => topic.tid === topicData.tid)[0]; + const { body } = await request.get(`${nconf.get('url')}/api/category/${categoryObj.slug}`, { jar: adminJar }); + const topic = body.topics.filter(topic => topic.tid === topicData.tid)[0]; assert.strictEqual(topic && topic.tid, topicData.tid); }); it('should load topic for guests if privilege is given', async () => { await privileges.categories.give(['groups:topics:schedule'], categoryObj.cid, 'guests'); - const response = await requestType('get', `${nconf.get('url')}/topic/${topicData.slug}`); + const { response, body } = await request.get(`${nconf.get('url')}/topic/${topicData.slug}`); assert.strictEqual(response.statusCode, 200); - assert(response.body); + assert(body); }); it('should be amongst topics of the category for guests if privilege is given', async () => { - const response = await requestType('get', `${nconf.get('url')}/api/category/${categoryObj.slug}`, { json: true }); - const topic = response.body.topics.filter(topic => topic.tid === topicData.tid)[0]; + const { body } = await request.get(`${nconf.get('url')}/api/category/${categoryObj.slug}`); + const topic = body.topics.filter(topic => topic.tid === topicData.tid)[0]; assert.strictEqual(topic && topic.tid, topicData.tid); }); it('should not allow deletion of a scheduled topic', async () => { - const response = await requestType('delete', `${nconf.get('url')}/api/v3/topics/${topicData.tid}/state`, adminApiOpts); - assert.strictEqual(response.res.statusCode, 400); + const { response } = await request.delete(`${nconf.get('url')}/api/v3/topics/${topicData.tid}/state`, adminApiOpts); + assert.strictEqual(response.statusCode, 400); }); it('should not allow to unpin a scheduled topic', async () => { - const response = await requestType('delete', `${nconf.get('url')}/api/v3/topics/${topicData.tid}/pin`, adminApiOpts); - assert.strictEqual(response.res.statusCode, 400); + const { response } = await request.delete(`${nconf.get('url')}/api/v3/topics/${topicData.tid}/pin`, adminApiOpts); + assert.strictEqual(response.statusCode, 400); }); it('should not allow to restore a scheduled topic', async () => { - const response = await requestType('put', `${nconf.get('url')}/api/v3/topics/${topicData.tid}/state`, adminApiOpts); - assert.strictEqual(response.res.statusCode, 400); + const { response } = await request.put(`${nconf.get('url')}/api/v3/topics/${topicData.tid}/state`, adminApiOpts); + assert.strictEqual(response.statusCode, 400); }); it('should not allow unprivileged to reply', async () => { await privileges.categories.rescind(['groups:topics:schedule'], categoryObj.cid, 'guests'); await privileges.categories.give(['groups:topics:reply'], categoryObj.cid, 'guests'); - const response = await requestType('post', `${nconf.get('url')}/api/v3/topics/${topicData.tid}`, replyData); - assert.strictEqual(response.res.statusCode, 403); + const { response } = await request.post(`${nconf.get('url')}/api/v3/topics/${topicData.tid}`, replyData); + assert.strictEqual(response.statusCode, 403); }); it('should allow guests to reply if privilege is given', async () => { await privileges.categories.give(['groups:topics:schedule'], categoryObj.cid, 'guests'); - const response = await helpers.request('post', `/api/v3/topics/${topicData.tid}`, { + const { body } = await helpers.request('post', `/api/v3/topics/${topicData.tid}`, { ...replyData, jar: request.jar(), }); - assert.strictEqual(response.body.response.content, 'a reply by guest'); - assert.strictEqual(response.body.response.user.username, '[[global:guest]]'); + assert.strictEqual(body.response.content, 'a reply by guest'); + assert.strictEqual(body.response.user.username, '[[global:guest]]'); }); it('should have replies with greater timestamp than the scheduled topics itself', async () => { - const response = await requestType('get', `${nconf.get('url')}/api/topic/${topicData.slug}`, { json: true }); - postData = response.body.posts[1]; - assert(postData.timestamp > response.body.posts[0].timestamp); + const { body } = await request.get(`${nconf.get('url')}/api/topic/${topicData.slug}`); + postData = body.posts[1]; + assert(postData.timestamp > body.posts[0].timestamp); }); it('should have post edits with greater timestamp than the original', async () => { - const editData = { ...adminApiOpts, form: { content: 'an edit by the admin' } }; - const result = await requestType('put', `${nconf.get('url')}/api/v3/posts/${postData.pid}`, editData); + const editData = { ...adminApiOpts, body: { content: 'an edit by the admin' } }; + const result = await request.put(`${nconf.get('url')}/api/v3/posts/${postData.pid}`, editData); assert(result.body.response.edited > postData.timestamp); - const diffsResult = await requestType('get', `${nconf.get('url')}/api/v3/posts/${postData.pid}/diffs`, adminApiOpts); + const diffsResult = await request.get(`${nconf.get('url')}/api/v3/posts/${postData.pid}/diffs`, adminApiOpts); const { revisions } = diffsResult.body.response; // diffs are LIFO assert(revisions[0].timestamp > revisions[1].timestamp); @@ -2836,8 +2448,8 @@ describe('Topic\'s', () => { it('should able to reschedule', async () => { const newDate = new Date(Date.now() + (5 * 86400000)).getTime(); - const editData = { ...adminApiOpts, form: { ...topic, pid: topicData.mainPid, timestamp: newDate } }; - const response = await requestType('put', `${nconf.get('url')}/api/v3/posts/${topicData.mainPid}`, editData); + const editData = { ...adminApiOpts, body: { ...topic, pid: topicData.mainPid, timestamp: newDate } }; + await request.put(`${nconf.get('url')}/api/v3/posts/${topicData.mainPid}`, editData); const editedTopic = await topics.getTopicFields(topicData.tid, ['lastposttime', 'timestamp']); const editedPost = await posts.getPostFields(postData.pid, ['timestamp']); @@ -2875,17 +2487,16 @@ describe('Topic\'s', () => { it('should not be able to schedule a "published" topic', async () => { const newDate = new Date(Date.now() + 86400000).getTime(); - const editData = { ...adminApiOpts, form: { ...topic, pid: topicData.mainPid, timestamp: newDate } }; - const response = await requestType('put', `${nconf.get('url')}/api/v3/posts/${topicData.mainPid}`, editData); - assert.strictEqual(response.body.response.timestamp, Date.now()); - + const editData = { ...adminApiOpts, body: { ...topic, pid: topicData.mainPid, timestamp: newDate } }; + const { body } = await request.put(`${nconf.get('url')}/api/v3/posts/${topicData.mainPid}`, editData); + assert.strictEqual(body.response.timestamp, Date.now()); mockdate.reset(); }); it('should allow to purge a scheduled topic', async () => { topicData = (await topics.post(topic)).topicData; - const response = await requestType('delete', `${nconf.get('url')}/api/v3/topics/${topicData.tid}`, adminApiOpts); - assert.strictEqual(response.res.statusCode, 200); + const { response } = await request.delete(`${nconf.get('url')}/api/v3/topics/${topicData.tid}`, adminApiOpts); + assert.strictEqual(response.statusCode, 200); }); it('should remove from topics:scheduled on purge', async () => { diff --git a/test/topics/thumbs.js b/test/topics/thumbs.js index 272d5cac32..2c396c7794 100644 --- a/test/topics/thumbs.js +++ b/test/topics/thumbs.js @@ -324,20 +324,14 @@ describe('Topic thumbs', () => { createFiles(); }); - it('should succeed with a valid tid', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - done(); - }); + it('should succeed with a valid tid', async () => { + const { response } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); + assert.strictEqual(response.statusCode, 200); }); - it('should succeed with a uuid', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - done(); - }); + it('should succeed with a uuid', async () => { + const { response } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); + assert.strictEqual(response.statusCode, 200); }); it('should succeed with uploader plugins', async () => { @@ -350,63 +344,49 @@ describe('Topic thumbs', () => { method: hookMethod, }); - await new Promise((resolve) => { - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - resolve(); - }); - }); + const { response } = await helpers.uploadFile( + `${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, + path.join(__dirname, '../files/test.png'), + {}, + adminJar, + adminCSRF + ); + assert.strictEqual(response.statusCode, 200); await plugins.hooks.unregister('test', 'filter:uploadFile', hookMethod); }); - it('should fail with a non-existant tid', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/4/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 404); - done(); - }); + it('should fail with a non-existant tid', async () => { + const { response } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/4/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); + assert.strictEqual(response.statusCode, 404); }); - it('should fail when garbage is passed in', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/abracadabra/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 404); - done(); - }); + it('should fail when garbage is passed in', async () => { + const { response } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/abracadabra/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); + assert.strictEqual(response.statusCode, 404); }); - it('should fail when calling user cannot edit the tid', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/2/thumbs`, path.join(__dirname, '../files/test.png'), {}, fooJar, fooCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 403); - done(); - }); + it('should fail when calling user cannot edit the tid', async () => { + const { response } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/2/thumbs`, path.join(__dirname, '../files/test.png'), {}, fooJar, fooCSRF); + assert.strictEqual(response.statusCode, 403); }); - it('should fail if thumbnails are not enabled', (done) => { + it('should fail if thumbnails are not enabled', async () => { meta.config.allowTopicsThumbnail = 0; - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 503); - assert(body && body.status); - assert.strictEqual(body.status.message, 'Topic thumbnails are disabled.'); - done(); - }); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); + assert.strictEqual(response.statusCode, 503); + assert(body && body.status); + assert.strictEqual(body.status.message, 'Topic thumbnails are disabled.'); }); - it('should fail if file is not image', (done) => { + it('should fail if file is not image', async () => { meta.config.allowTopicsThumbnail = 1; - helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/503.html'), {}, adminJar, adminCSRF, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 500); - assert(body && body.status); - assert.strictEqual(body.status.message, 'Invalid File'); - done(); - }); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/503.html'), {}, adminJar, adminCSRF); + assert.strictEqual(response.statusCode, 500); + assert(body && body.status); + assert.strictEqual(body.status.message, 'Invalid File'); }); }); diff --git a/test/uploads.js b/test/uploads.js index 7a00000737..5df32ba2fd 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -5,9 +5,6 @@ const assert = require('assert'); const nconf = require('nconf'); const path = require('path'); const fs = require('fs').promises; -const request = require('request'); -const requestAsync = require('request-promise-native'); -const util = require('util'); const db = require('./mocks/databasemock'); const categories = require('../src/categories'); @@ -21,6 +18,7 @@ const socketUser = require('../src/socket.io/user'); const helpers = require('./helpers'); const file = require('../src/file'); const image = require('../src/image'); +const request = require('../src/request'); const emptyUploadsFolder = async () => { const files = await fs.readdir(`${nconf.get('upload_path')}/files`); @@ -83,31 +81,25 @@ describe('Upload Controllers', () => { await privileges.global.give(['groups:upload:post:file'], 'registered-users'); }); - it('should fail if the user exceeds the upload rate limit threshold', (done) => { + it('should fail if the user exceeds the upload rate limit threshold', async () => { const oldValue = meta.config.allowedFileExtensions; meta.config.allowedFileExtensions = 'png,jpg,bmp,html'; require('../src/middleware/uploads').clearCache(); const times = meta.config.uploadRateLimitThreshold + 1; - async.timesSeries(times, (i, next) => { - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/503.html'), {}, jar, csrf_token, (err, res, body) => { - if (i + 1 >= times) { - assert.strictEqual(res.statusCode, 500); - assert.strictEqual(body.error, '[[error:upload-ratelimit-reached]]'); - } else { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body && body.status && body.response && body.response.images); - assert(Array.isArray(body.response.images)); - assert(body.response.images[0].url); - } - - next(err); - }); - }, (err) => { - meta.config.allowedFileExtensions = oldValue; - assert.ifError(err); - done(); - }); + for (let i = 0; i < times; i++) { + // eslint-disable-next-line no-await-in-loop + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/503.html'), {}, jar, csrf_token); + if (i + 1 >= times) { + assert.strictEqual(response.statusCode, 500); + assert.strictEqual(body.error, '[[error:upload-ratelimit-reached]]'); + } else { + assert.strictEqual(response.statusCode, 200); + assert(body && body.status && body.response && body.response.images); + assert(Array.isArray(body.response.images)); + assert(body.response.images[0].url); + } + } + meta.config.allowedFileExtensions = oldValue; }); }); @@ -121,34 +113,26 @@ describe('Upload Controllers', () => { await privileges.global.give(['groups:upload:post:file'], 'registered-users'); }); - it('should upload an image to a post', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body && body.status && body.response && body.response.images); - assert(Array.isArray(body.response.images)); - assert(body.response.images[0].url); - done(); - }); + it('should upload an image to a post', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token); + assert.equal(response.statusCode, 200); + assert(body && body.status && body.response && body.response.images); + assert(Array.isArray(body.response.images)); + assert(body.response.images[0].url); }); - it('should upload an image to a post and then delete the upload', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body && body.status && body.response && body.response.images); - assert(Array.isArray(body.response.images)); - assert(body.response.images[0].url); - const name = body.response.images[0].url.replace(`${nconf.get('relative_path') + nconf.get('upload_url')}/`, ''); - socketUser.deleteUpload({ uid: regularUid }, { uid: regularUid, name: name }, (err) => { - assert.ifError(err); - db.getSortedSetRange(`uid:${regularUid}:uploads`, 0, -1, (err, uploads) => { - assert.ifError(err); - assert.equal(uploads.includes(name), false); - done(); - }); - }); - }); + it('should upload an image to a post and then delete the upload', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token); + + assert.strictEqual(response.statusCode, 200); + assert(body && body.status && body.response && body.response.images); + assert(Array.isArray(body.response.images)); + assert(body.response.images[0].url); + const name = body.response.images[0].url.replace(`${nconf.get('relative_path') + nconf.get('upload_url')}/`, ''); + await socketUser.deleteUpload({ uid: regularUid }, { uid: regularUid, name: name }); + + const uploads = await db.getSortedSetRange(`uid:${regularUid}:uploads`, 0, -1); + assert.equal(uploads.includes(name), false); }); it('should not allow deleting if path is not correct', (done) => { @@ -165,55 +149,45 @@ describe('Upload Controllers', () => { }); }); - it('should resize and upload an image to a post', (done) => { + it('should resize and upload an image to a post', async () => { const oldValue = meta.config.resizeImageWidth; meta.config.resizeImageWidth = 10; meta.config.resizeImageWidthThreshold = 10; - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body && body.status && body.response && body.response.images); - assert(Array.isArray(body.response.images)); - assert(body.response.images[0].url); - assert(body.response.images[0].url.match(/\/assets\/uploads\/files\/\d+-test-resized\.png/)); - meta.config.resizeImageWidth = oldValue; - meta.config.resizeImageWidthThreshold = 1520; - done(); - }); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token); + + assert.equal(response.statusCode, 200); + assert(body && body.status && body.response && body.response.images); + assert(Array.isArray(body.response.images)); + assert(body.response.images[0].url); + assert(body.response.images[0].url.match(/\/assets\/uploads\/files\/\d+-test-resized\.png/)); + meta.config.resizeImageWidth = oldValue; + meta.config.resizeImageWidthThreshold = 1520; }); - it('should upload a file to a post', (done) => { + it('should upload a file to a post', async () => { const oldValue = meta.config.allowedFileExtensions; meta.config.allowedFileExtensions = 'png,jpg,bmp,html'; - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/503.html'), {}, jar, csrf_token, (err, res, body) => { - meta.config.allowedFileExtensions = oldValue; - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(body && body.status && body.response && body.response.images); - assert(Array.isArray(body.response.images)); - assert(body.response.images[0].url); - done(); - }); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/503.html'), {}, jar, csrf_token); + meta.config.allowedFileExtensions = oldValue; + + assert.strictEqual(response.statusCode, 200); + assert(body && body.status && body.response && body.response.images); + assert(Array.isArray(body.response.images)); + assert(body.response.images[0].url); }); - it('should fail to upload image to post if image dimensions are too big', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/toobig.png'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 500); - assert(body && body.status && body.status.message); - assert.strictEqual(body.status.message, 'Image dimensions are too big'); - done(); - }); + it('should fail to upload image to post if image dimensions are too big', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/toobig.png'), {}, jar, csrf_token); + assert.strictEqual(response.statusCode, 500); + assert(body && body.status && body.status.message); + assert.strictEqual(body.status.message, 'Image dimensions are too big'); }); - it('should fail to upload image to post if image is broken', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/brokenimage.png'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 500); - assert(body && body.status && body.status.message); - assert.strictEqual(body.status.message, 'Input file contains unsupported image format'); - done(); - }); + it('should fail to upload image to post if image is broken', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/brokenimage.png'), {}, jar, csrf_token); + assert.strictEqual(response.statusCode, 500); + assert(body && body.status && body.status.message); + assert.strictEqual(body.status.message, 'Input file contains unsupported image format'); }); it('should fail if file is not an image', (done) => { @@ -286,39 +260,22 @@ describe('Upload Controllers', () => { }); }); - it('should delete users uploads if account is deleted', (done) => { - let uid; - let url; + it('should delete users uploads if account is deleted', async () => { + const uid = await user.create({ username: 'uploader', password: 'barbar' }); const file = require('../src/file'); + const data = await helpers.loginUser('uploader', 'barbar'); + const { body } = await helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, data.jar, data.csrf_token); - async.waterfall([ - function (next) { - user.create({ username: 'uploader', password: 'barbar' }, next); - }, - function (_uid, next) { - uid = _uid; - helpers.loginUser('uploader', 'barbar', next); - }, - function (data, next) { - helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, data.jar, data.csrf_token, next); - }, - function (res, body, next) { - assert(body && body.status && body.response && body.response.images); - assert(Array.isArray(body.response.images)); - assert(body.response.images[0].url); - url = body.response.images[0].url; + assert(body && body.status && body.response && body.response.images); + assert(Array.isArray(body.response.images)); + assert(body.response.images[0].url); + const { url } = body.response.images[0]; - user.delete(1, uid, next); - }, - function (userData, next) { - const filePath = path.join(nconf.get('upload_path'), url.replace('/assets/uploads', '')); - file.exists(filePath, next); - }, - function (exists, next) { - assert(!exists); - done(); - }, - ], done); + await user.delete(1, uid); + + const filePath = path.join(nconf.get('upload_path'), url.replace('/assets/uploads', '')); + const exists = await file.exists(filePath); + assert(!exists); }); after(emptyUploadsFolder); @@ -337,173 +294,147 @@ describe('Upload Controllers', () => { regular_csrf_token = regularLogin.csrf_token; }); - it('should upload site logo', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadlogo`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - assert(Array.isArray(body)); - assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/system/site-logo.png`); - done(); - }); + it('should upload site logo', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadlogo`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token); + assert.strictEqual(response.statusCode, 200); + assert(Array.isArray(body)); + assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/system/site-logo.png`); }); - it('should fail to upload invalid file type', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/503.html'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 500); - assert.equal(body.error, '[[error:invalid-image-type, image/png&#44; image/jpeg&#44; image/pjpeg&#44; image/jpg&#44; image/gif&#44; image/svg+xml]]'); - done(); - }); + it('should fail to upload invalid file type', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/503.html'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token); + assert.strictEqual(response.statusCode, 500); + assert.equal(body.error, '[[error:invalid-image-type, image/png&#44; image/jpeg&#44; image/pjpeg&#44; image/jpg&#44; image/gif&#44; image/svg+xml]]'); }); - it('should fail to upload category image with invalid json params', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/test.png'), { params: 'invalid json' }, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 500); - assert.equal(body.error, '[[error:invalid-json]]'); - done(); - }); + it('should fail to upload category image with invalid json params', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/test.png'), { params: 'invalid json' }, jar, csrf_token); + assert.strictEqual(response.statusCode, 500); + assert.equal(body.error, '[[error:invalid-json]]'); }); - it('should upload category image', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/test.png'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(Array.isArray(body)); - assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.png`); - done(); - }); + it('should upload category image', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/test.png'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token); + assert.equal(response.statusCode, 200); + assert(Array.isArray(body)); + assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.png`); }); - it('should upload default avatar', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadDefaultAvatar`, path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/system/avatar-default.png`); - done(); - }); + it('should upload default avatar', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadDefaultAvatar`, path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token); + assert.equal(response.statusCode, 200); + assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/system/avatar-default.png`); }); - it('should upload og image', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadOgImage`, path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/system/og-image.png`); - done(); - }); + it('should upload og image', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadOgImage`, path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token); + assert.equal(response.statusCode, 200); + assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/system/og-image.png`); }); - it('should upload favicon', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadfavicon`, path.join(__dirname, '../test/files/favicon.ico'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(Array.isArray(body)); - assert.equal(body[0].url, '/assets/uploads/system/favicon.ico'); - done(); - }); + it('should upload favicon', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadfavicon`, path.join(__dirname, '../test/files/favicon.ico'), {}, jar, csrf_token); + assert.equal(response.statusCode, 200); + assert(Array.isArray(body)); + assert.equal(body[0].url, '/assets/uploads/system/favicon.ico'); }); - it('should upload touch icon', (done) => { + it('should upload touch icon', async () => { const touchiconAssetPath = '/assets/uploads/system/touchicon-orig.png'; - helpers.uploadFile(`${nconf.get('url')}/api/admin/uploadTouchIcon`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(Array.isArray(body)); - assert.equal(body[0].url, touchiconAssetPath); - meta.config['brand:touchIcon'] = touchiconAssetPath; - request(`${nconf.get('url')}/apple-touch-icon`, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); - }); + const { response, body } = await helpers.uploadFile( + `${nconf.get('url')}/api/admin/uploadTouchIcon`, + path.join(__dirname, '../test/files/test.png'), + {}, + jar, + csrf_token + ); + + assert.equal(response.statusCode, 200); + assert(Array.isArray(body)); + assert.equal(body[0].url, touchiconAssetPath); + meta.config['brand:touchIcon'] = touchiconAssetPath; + const { response: res1, body: body1 } = await request.get(`${nconf.get('url')}/apple-touch-icon`); + assert.equal(res1.statusCode, 200); + assert(body1); }); - it('should upload regular file', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/upload/file`, path.join(__dirname, '../test/files/test.png'), { + it('should upload regular file', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/upload/file`, path.join(__dirname, '../test/files/test.png'), { params: JSON.stringify({ folder: 'system', }), - }, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(Array.isArray(body)); - assert.equal(body[0].url, '/assets/uploads/system/test.png'); - assert(file.existsSync(path.join(nconf.get('upload_path'), 'system', 'test.png'))); - done(); - }); + }, jar, csrf_token); + + assert.equal(response.statusCode, 200); + assert(Array.isArray(body)); + assert.equal(body[0].url, '/assets/uploads/system/test.png'); + assert(file.existsSync(path.join(nconf.get('upload_path'), 'system', 'test.png'))); }); - it('should fail to upload regular file in wrong directory', (done) => { - helpers.uploadFile(`${nconf.get('url')}/api/admin/upload/file`, path.join(__dirname, '../test/files/test.png'), { + it('should fail to upload regular file in wrong directory', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/upload/file`, path.join(__dirname, '../test/files/test.png'), { params: JSON.stringify({ folder: '../../system', }), - }, jar, csrf_token, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 500); - assert.strictEqual(body.error, '[[error:invalid-path]]'); - done(); - }); + }, jar, csrf_token); + + assert.equal(response.statusCode, 500); + assert.strictEqual(body.error, '[[error:invalid-path]]'); }); describe('ACP uploads screen', () => { it('should create a folder', async () => { - const res = await helpers.createFolder('', 'myfolder', jar, csrf_token); - assert.strictEqual(res.statusCode, 200); + const { response } = await helpers.createFolder('', 'myfolder', jar, csrf_token); + assert.strictEqual(response.statusCode, 200); assert(file.existsSync(path.join(nconf.get('upload_path'), 'myfolder'))); }); it('should fail to create a folder if it already exists', async () => { - const res = await helpers.createFolder('', 'myfolder', jar, csrf_token); - assert.strictEqual(res.statusCode, 403); - assert.deepStrictEqual(res.body.status, { + const { response, body } = await helpers.createFolder('', 'myfolder', jar, csrf_token); + assert.strictEqual(response.statusCode, 403); + assert.deepStrictEqual(body.status, { code: 'forbidden', message: 'Folder exists', }); }); it('should fail to create a folder as a non-admin', async () => { - const res = await helpers.createFolder('', 'hisfolder', regularJar, regular_csrf_token); - assert.strictEqual(res.statusCode, 403); - assert.deepStrictEqual(res.body.status, { + const { response, body } = await helpers.createFolder('', 'hisfolder', regularJar, regular_csrf_token); + assert.strictEqual(response.statusCode, 403); + assert.deepStrictEqual(body.status, { code: 'forbidden', message: 'You are not authorised to make this call', }); }); it('should fail to create a folder in wrong directory', async () => { - const res = await helpers.createFolder('../traversing', 'unexpectedfolder', jar, csrf_token); - assert.strictEqual(res.statusCode, 403); - assert.deepStrictEqual(res.body.status, { + const { response, body } = await helpers.createFolder('../traversing', 'unexpectedfolder', jar, csrf_token); + assert.strictEqual(response.statusCode, 403); + assert.deepStrictEqual(body.status, { code: 'forbidden', message: 'Invalid path', }); }); it('should use basename of given folderName to create new folder', async () => { - const res = await helpers.createFolder('/myfolder', '../another folder', jar, csrf_token); - assert.strictEqual(res.statusCode, 200); + const { response } = await helpers.createFolder('/myfolder', '../another folder', jar, csrf_token); + assert.strictEqual(response.statusCode, 200); const slugifiedName = 'another-folder'; assert(file.existsSync(path.join(nconf.get('upload_path'), 'myfolder', slugifiedName))); }); it('should fail to delete a file as a non-admin', async () => { - const res = await requestAsync.delete(`${nconf.get('url')}/api/v3/files`, { + const { response, body } = await request.delete(`${nconf.get('url')}/api/v3/files`, { body: { path: '/system/test.png', }, jar: regularJar, - json: true, headers: { 'x-csrf-token': regular_csrf_token, }, - simple: false, - resolveWithFullResponse: true, }); - assert.strictEqual(res.statusCode, 403); - assert.deepStrictEqual(res.body.status, { + assert.strictEqual(response.statusCode, 403); + assert.deepStrictEqual(body.status, { code: 'forbidden', message: 'You are not authorised to make this call', }); diff --git a/test/user.js b/test/user.js index 907a43f388..421a8c2cf4 100644 --- a/test/user.js +++ b/test/user.js @@ -1,14 +1,12 @@ 'use strict'; const assert = require('assert'); -const async = require('async'); const fs = require('fs'); const path = require('path'); const nconf = require('nconf'); const validator = require('validator'); -const request = require('request'); -const requestAsync = require('request-promise-native'); const jwt = require('jsonwebtoken'); +const { setTimeout } = require('node:timers/promises'); const db = require('./mocks/databasemock'); const User = require('../src/user'); @@ -24,6 +22,7 @@ const socketUser = require('../src/socket.io/user'); const apiUser = require('../src/api/users'); const utils = require('../src/utils'); const privileges = require('../src/privileges'); +const request = require('../src/request'); describe('User', () => { let userData; @@ -174,7 +173,7 @@ describe('User', () => { }); describe('.uniqueUsername()', () => { - it('should deal with collisions', (done) => { + it('should deal with collisions', async () => { const users = []; for (let i = 0; i < 10; i += 1) { users.push({ @@ -182,25 +181,16 @@ describe('User', () => { email: `jane.doe${i}@example.com`, }); } + for (const user of users) { + // eslint-disable-next-line no-await-in-loop + await User.create(user); + } - async.series([ - function (next) { - async.eachSeries(users, (user, next) => { - User.create(user, next); - }, next); - }, - function (next) { - User.uniqueUsername({ - username: 'Jane Doe', - userslug: 'jane-doe', - }, (err, username) => { - assert.ifError(err); - - assert.strictEqual(username, 'Jane Doe 9'); - next(); - }); - }, - ], done); + const username = await User.uniqueUsername({ + username: 'Jane Doe', + userslug: 'jane-doe', + }); + assert.strictEqual(username, 'Jane Doe 9'); }); }); @@ -252,12 +242,10 @@ describe('User', () => { }); describe('.getModeratorUids()', () => { - before((done) => { - async.series([ - async.apply(groups.create, { name: 'testGroup' }), - async.apply(groups.join, 'cid:1:privileges:groups:moderate', 'testGroup'), - async.apply(groups.join, 'testGroup', 1), - ], done); + before(async () => { + await groups.create({ name: 'testGroup' }); + await groups.join('cid:1:privileges:groups:moderate', 'testGroup'); + await groups.join('testGroup', 1); }); it('should retrieve all users with moderator bit in category privilege', (done) => { @@ -269,38 +257,13 @@ describe('User', () => { }); }); - after((done) => { - async.series([ - async.apply(groups.leave, 'cid:1:privileges:groups:moderate', 'testGroup'), - async.apply(groups.destroy, 'testGroup'), - ], done); + after(async () => { + groups.leave('cid:1:privileges:groups:moderate', 'testGroup'); + groups.destroy('testGroup'); }); }); describe('.isReadyToPost()', () => { - it('should error when a user makes two posts in quick succession', (done) => { - meta.config = meta.config || {}; - meta.config.postDelay = '10'; - - async.series([ - async.apply(Topics.post, { - uid: testUid, - title: 'Topic 1', - content: 'lorem ipsum', - cid: testCid, - }), - async.apply(Topics.post, { - uid: testUid, - title: 'Topic 2', - content: 'lorem ipsum', - cid: testCid, - }), - ], (err) => { - assert(err); - done(); - }); - }); - it('should allow a post if the last post time is > 10 seconds', (done) => { User.setUserField(testUid, 'lastposttime', +new Date() - (11 * 1000), () => { Topics.post({ @@ -355,13 +318,12 @@ describe('User', () => { const titles = new Array(10).fill('topic title'); const res = await Promise.allSettled(titles.map(async (title) => { const { body } = await helpers.request('post', '/api/v3/topics', { - form: { + body: { cid: testCid, title: title, content: 'the content', }, jar: jar, - json: true, }); return body.status; })); @@ -486,32 +448,19 @@ describe('User', () => { assert.equal(data.users[0].username, 'ipsearch_filter'); }); - it('should sort results by username', (done) => { - async.waterfall([ - function (next) { - User.create({ username: 'brian' }, next); - }, - function (uid, next) { - User.create({ username: 'baris' }, next); - }, - function (uid, next) { - User.create({ username: 'bzari' }, next); - }, - function (uid, next) { - User.search({ - uid: testUid, - query: 'b', - sortBy: 'username', - paginate: false, - }, next); - }, - ], (err, data) => { - assert.ifError(err); - assert.equal(data.users[0].username, 'baris'); - assert.equal(data.users[1].username, 'brian'); - assert.equal(data.users[2].username, 'bzari'); - done(); + it('should sort results by username', async () => { + await User.create({ username: 'brian' }); + await User.create({ username: 'baris' }); + await User.create({ username: 'bzari' }); + const data = await User.search({ + uid: testUid, + query: 'b', + sortBy: 'username', + paginate: false, }); + assert.equal(data.users[0].username, 'baris'); + assert.equal(data.users[1].username, 'brian'); + assert.equal(data.users[2].username, 'bzari'); }); }); @@ -991,10 +940,8 @@ describe('User', () => { it('should let you set an external image', async () => { const token = await helpers.getCsrfToken(jar); - const body = await requestAsync(`${nconf.get('url')}/api/v3/users/${uid}/picture`, { + const { body } = await request.put(`${nconf.get('url')}/api/v3/users/${uid}/picture`, { jar, - method: 'put', - json: true, headers: { 'x-csrf-token': token, }, @@ -1193,46 +1140,34 @@ describe('User', () => { }); }); - it('should load profile page', (done) => { - request(`${nconf.get('url')}/api/user/updatedagain`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load profile page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/updatedagain`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); - it('should load settings page', (done) => { - request(`${nconf.get('url')}/api/user/updatedagain/settings`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body.settings); - assert(body.languages); - assert(body.homePageRoutes); - done(); - }); + it('should load settings page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/updatedagain/settings`, { jar }); + assert.equal(response.statusCode, 200); + assert(body.settings); + assert(body.languages); + assert(body.homePageRoutes); }); - it('should load edit page', (done) => { - request(`${nconf.get('url')}/api/user/updatedagain/edit`, { jar: jar, json: true }, (err, res, body) => { - assert.ifError(err); - assert.equal(res.statusCode, 200); - assert(body); - done(); - }); + it('should load edit page', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/api/user/updatedagain/edit`, { jar }); + assert.equal(response.statusCode, 200); + assert(body); }); it('should load edit/email page', async () => { - const res = await requestAsync(`${nconf.get('url')}/api/user/updatedagain/edit/email`, { jar: jar, json: true, resolveWithFullResponse: true }); - assert.strictEqual(res.statusCode, 200); - assert(res.body); + const { response, body } = await request.get(`${nconf.get('url')}/api/user/updatedagain/edit/email`, { jar }); + assert.strictEqual(response.statusCode, 200); + assert(body); // Accessing this page will mark the user's account as needing an updated email, below code undo's. - await requestAsync({ - uri: `${nconf.get('url')}/register/abort`, + await request.post(`${nconf.get('url')}/register/abort`, { jar, - method: 'POST', - simple: false, headers: { 'x-csrf-token': csrf_token, }, @@ -1246,7 +1181,7 @@ describe('User', () => { }); await groups.join('Test', uid); - const body = await requestAsync(`${nconf.get('url')}/api/user/updatedagain/groups`, { jar: jar, json: true }); + const { body } = await request.get(`${nconf.get('url')}/api/user/updatedagain/groups`, { jar }); assert(Array.isArray(body.groups)); assert.equal(body.groups[0].name, 'Test'); @@ -1279,30 +1214,12 @@ describe('User', () => { assert.equal(data[0].timestamp, now); }); - it('should return the correct ban reason', (done) => { - async.series([ - function (next) { - User.bans.ban(testUserUid, 0, '', (err) => { - assert.ifError(err); - next(err); - }); - }, - function (next) { - User.getModerationHistory(testUserUid, (err, data) => { - assert.ifError(err); - assert.equal(data.bans.length, 1, 'one ban'); - assert.equal(data.bans[0].reason, '[[user:info.banned-no-reason]]', 'no ban reason'); - - next(err); - }); - }, - ], (err) => { - assert.ifError(err); - User.bans.unban(testUserUid, (err) => { - assert.ifError(err); - done(); - }); - }); + it('should return the correct ban reason', async () => { + await User.bans.ban(testUserUid, 0, ''); + const data = await User.getModerationHistory(testUserUid); + assert.equal(data.bans.length, 1, 'one ban'); + assert.equal(data.bans[0].reason, '[[user:info.banned-no-reason]]', 'no ban reason'); + await User.bans.unban(testUserUid); }); it('should ban user permanently', (done) => { @@ -1316,22 +1233,14 @@ describe('User', () => { }); }); - it('should ban user temporarily', (done) => { - User.bans.ban(testUserUid, Date.now() + 2000, (err) => { - assert.ifError(err); - - User.bans.isBanned(testUserUid, (err, isBanned) => { - assert.ifError(err); - assert.equal(isBanned, true); - setTimeout(() => { - User.bans.isBanned(testUserUid, (err, isBanned) => { - assert.ifError(err); - assert.equal(isBanned, false); - User.bans.unban(testUserUid, done); - }); - }, 3000); - }); - }); + it('should ban user temporarily', async () => { + await User.bans.ban(testUserUid, Date.now() + 2000); + let isBanned = await User.bans.isBanned(testUserUid); + assert.equal(isBanned, true); + await setTimeout(3000); + isBanned = await User.bans.isBanned(testUserUid); + assert.equal(isBanned, false); + await User.bans.unban(testUserUid); }); it('should error if until is NaN', (done) => { @@ -1409,26 +1318,19 @@ describe('User', () => { describe('Digest.getSubscribers', () => { const uidIndex = {}; - before((done) => { + before(async () => { const testUsers = ['daysub', 'offsub', 'nullsub', 'weeksub']; - async.each(testUsers, (username, next) => { - async.waterfall([ - async.apply(User.create, { username: username, email: `${username}@example.com` }), - function (uid, next) { - if (username === 'nullsub') { - return setImmediate(next); - } + await Promise.all(testUsers.map(async (username) => { + const uid = await User.create({ username, email: `${username}@example.com` }); + if (username === 'nullsub') { + return; + } + uidIndex[username] = uid; - uidIndex[username] = uid; - - const sub = username.slice(0, -3); - async.parallel([ - async.apply(User.updateDigestSetting, uid, sub), - async.apply(User.setSetting, uid, 'dailyDigestFreq', sub), - ], next); - }, - ], next); - }, done); + const sub = username.slice(0, -3); + await User.updateDigestSetting(uid, sub); + await User.setSetting(uid, 'dailyDigestFreq', sub); + })); }); it('should accurately build digest list given ACP default "null" (not set)', (done) => { @@ -1440,71 +1342,38 @@ describe('User', () => { }); }); - it('should accurately build digest list given ACP default "day"', (done) => { - async.series([ - async.apply(meta.configs.set, 'dailyDigestFreq', 'day'), - function (next) { - User.digest.getSubscribers('day', (err, subs) => { - assert.ifError(err); - assert.strictEqual(subs.includes(uidIndex.daysub.toString()), true); // daysub does get emailed - assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), false); // weeksub does not get emailed - assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub doesn't get emailed + it('should accurately build digest list given ACP default "day"', async () => { + await meta.configs.set('dailyDigestFreq', 'day'); + const subs = await User.digest.getSubscribers('day'); - next(); - }); - }, - ], done); + assert.strictEqual(subs.includes(uidIndex.daysub.toString()), true); // daysub does get emailed + assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), false); // weeksub does not get emailed + assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub doesn't get emailed }); - it('should accurately build digest list given ACP default "week"', (done) => { - async.series([ - async.apply(meta.configs.set, 'dailyDigestFreq', 'week'), - function (next) { - User.digest.getSubscribers('week', (err, subs) => { - assert.ifError(err); - assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), true); // weeksub gets emailed - assert.strictEqual(subs.includes(uidIndex.daysub.toString()), false); // daysub gets emailed - assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub does not get emailed + it('should accurately build digest list given ACP default "week"', async () => { + await meta.configs.set('dailyDigestFreq', 'week'); + const subs = await User.digest.getSubscribers('week'); - next(); - }); - }, - ], done); + assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), true); // weeksub gets emailed + assert.strictEqual(subs.includes(uidIndex.daysub.toString()), false); // daysub gets emailed + assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub does not get emailed }); - it('should accurately build digest list given ACP default "off"', (done) => { - async.series([ - async.apply(meta.configs.set, 'dailyDigestFreq', 'off'), - function (next) { - User.digest.getSubscribers('day', (err, subs) => { - assert.ifError(err); - assert.strictEqual(subs.length, 1); - - next(); - }); - }, - ], done); + it('should accurately build digest list given ACP default "off"', async () => { + await meta.configs.set('dailyDigestFreq', 'off'); + const subs = await User.digest.getSubscribers('day'); + assert.strictEqual(subs.length, 1); }); }); describe('digests', () => { let uid; - before((done) => { - async.waterfall([ - function (next) { - User.create({ username: 'digestuser', email: 'test@example.com' }, next); - }, - function (_uid, next) { - uid = _uid; - User.updateDigestSetting(uid, 'day', next); - }, - function (next) { - User.setSetting(uid, 'dailyDigestFreq', 'day', next); - }, - function (next) { - User.setSetting(uid, 'notificationType_test', 'notificationemail', next); - }, - ], done); + before(async () => { + uid = await User.create({ username: 'digestuser', email: 'test@example.com' }); + await User.updateDigestSetting(uid, 'day'); + await User.setSetting(uid, 'dailyDigestFreq', 'day'); + await User.setSetting(uid, 'notificationType_test', 'notificationemail'); }); it('should send digests', async () => { @@ -1549,106 +1418,65 @@ describe('User', () => { }); describe('unsubscribe via POST', () => { - it('should unsubscribe from digest if one-click unsubscribe is POSTed', (done) => { + it('should unsubscribe from digest if one-click unsubscribe is POSTed', async () => { const token = jwt.sign({ template: 'digest', uid: uid, }, nconf.get('secret')); - request({ - method: 'post', - url: `${nconf.get('url')}/email/unsubscribe/${token}`, - }, (err, res) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); - - db.getObjectField(`user:${uid}:settings`, 'dailyDigestFreq', (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 'off'); - done(); - }); - }); + const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`); + assert.strictEqual(response.statusCode, 200); + const value = await db.getObjectField(`user:${uid}:settings`, 'dailyDigestFreq'); + assert.strictEqual(value, 'off'); }); - it('should unsubscribe from notifications if one-click unsubscribe is POSTed', (done) => { + it('should unsubscribe from notifications if one-click unsubscribe is POSTed', async () => { const token = jwt.sign({ template: 'notification', type: 'test', uid: uid, }, nconf.get('secret')); - request({ - method: 'post', - url: `${nconf.get('url')}/email/unsubscribe/${token}`, - }, (err, res) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 200); + const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`); + assert.strictEqual(response.statusCode, 200); - db.getObjectField(`user:${uid}:settings`, 'notificationType_test', (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 'notification'); - done(); - }); - }); + const value = await db.getObjectField(`user:${uid}:settings`, 'notificationType_test'); + assert.strictEqual(value, 'notification'); }); - it('should return errors on missing template in token', (done) => { + it('should return errors on missing template in token', async () => { const token = jwt.sign({ uid: uid, }, nconf.get('secret')); - request({ - method: 'post', - url: `${nconf.get('url')}/email/unsubscribe/${token}`, - }, (err, res) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 404); - done(); - }); + const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`); + assert.strictEqual(response.statusCode, 404); }); - it('should return errors on wrong template in token', (done) => { + it('should return errors on wrong template in token', async () => { const token = jwt.sign({ template: 'user', uid: uid, }, nconf.get('secret')); - request({ - method: 'post', - url: `${nconf.get('url')}/email/unsubscribe/${token}`, - }, (err, res) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 404); - done(); - }); + const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`); + assert.strictEqual(response.statusCode, 404); }); - it('should return errors on missing token', (done) => { - request({ - method: 'post', - url: `${nconf.get('url')}/email/unsubscribe/`, - }, (err, res) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 404); - done(); - }); + it('should return errors on missing token', async () => { + const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/`); + assert.strictEqual(response.statusCode, 404); }); - it('should return errors on token signed with wrong secret (verify-failure)', (done) => { + it('should return errors on token signed with wrong secret (verify-failure)', async () => { const token = jwt.sign({ template: 'notification', type: 'test', uid: uid, }, `${nconf.get('secret')}aababacaba`); - request({ - method: 'post', - url: `${nconf.get('url')}/email/unsubscribe/${token}`, - }, (err, res) => { - assert.ifError(err); - assert.strictEqual(res.statusCode, 403); - done(); - }); + const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`); + assert.strictEqual(response.statusCode, 403); }); }); }); @@ -1848,36 +1676,17 @@ describe('User', () => { } }); - it('should set moderation note', (done) => { - let adminUid; - async.waterfall([ - function (next) { - User.create({ username: 'noteadmin' }, next); - }, - function (_adminUid, next) { - adminUid = _adminUid; - groups.join('administrators', adminUid, next); - }, - function (next) { - socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'this is a test user' }, next); - }, - function (next) { - setTimeout(next, 50); - }, - function (next) { - socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: ' { - assert.ifError(err); - assert.equal(notes[0].note, '<svg/onload=alert(document.location);//'); - assert.equal(notes[0].uid, adminUid); - assert.equal(notes[1].note, 'this is a test user'); - assert(notes[0].timestamp); - done(); - }); + it('should set moderation note', async () => { + const adminUid = await User.create({ username: 'noteadmin' }); + await groups.join('administrators', adminUid); + await socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'this is a test user' }); + await setTimeout(50); + await socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: ' { @@ -1974,89 +1783,66 @@ describe('User', () => { gdpr_consent: true, }); const { jar } = await helpers.loginUser('admin', '123456'); - const { users } = await requestAsync(`${nconf.get('url')}/api/admin/manage/registration`, { jar, json: true }); + const { body: { users } } = await request.get(`${nconf.get('url')}/api/admin/manage/registration`, { jar }); assert.equal(users[0].username, 'rejectme'); assert.equal(users[0].email, '<script>alert("ok")<script>reject@me.com'); }); - it('should fail to add user to queue if username is taken', (done) => { - helpers.registerUser({ + it('should fail to add user to queue if username is taken', async () => { + const { body } = await helpers.registerUser({ username: 'rejectme', password: '123456', 'password-confirm': '123456', email: ' + Internal Server Error + + + + + + +
+
+

500

+

+ Internal server error. +

+

+ {message} +

+

+  Alright. You can stop clicking... it's not going to make the site come back sooner! +

+
+
+ + diff --git a/public/503.html b/public/503.html index 43d1e648d9..51d0e52d53 100644 --- a/public/503.html +++ b/public/503.html @@ -2,147 +2,12 @@ Excessive Load Warning - `, { jar }); + assert.strictEqual(body.filters.quick, '"<script>alert('foo');</script>'); + }); + it('should not allow flagging post in private category', async () => { const category = await Categories.create({ name: 'private category' }); @@ -1185,5 +1190,7 @@ describe('Flags', () => { } }); }); + + }); }); From 16504bad8100aa25327dd8b8b26483df9e087b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 12 May 2025 10:02:59 -0400 Subject: [PATCH 2988/4744] fix: sql injection in sortedSetScan --- src/database/postgres/sorted.js | 4 ++-- test/database/sorted.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 916425e0c1..9f59224497 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -707,9 +707,9 @@ SELECT z."value", ON o."_key" = z."_key" AND o."type" = z."type" WHERE o."_key" = $1::TEXT - AND z."value" LIKE '${match}' + AND z."value" LIKE $3 LIMIT $2::INTEGER`, - values: [params.key, params.limit], + values: [params.key, params.limit, match], }); if (!params.withScores) { return res.rows.map(r => r.value); diff --git a/test/database/sorted.js b/test/database/sorted.js index 33d3e4c4b5..b98d969730 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -78,6 +78,21 @@ describe('Sorted Set methods', () => { assert(data.includes('ddb')); assert(data.includes('adb')); }); + + it('should not error with invalid input', async () => { + const query = `-3217' +OR 1251=CAST((CHR(113)||CHR(98)||CHR(118)||CHR(98)||CHR(113))||(SELECT +(CASE WHEN (1251=1251) THEN 1 ELSE 0 +END))::text||(CHR(113)||CHR(113)||CHR(118)||CHR(98)||CHR(113)) AS +NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:read&states[]=watching&states[]=tracking&states[]=notwatching&showLinks=`; + const match = `*${query.toLowerCase()}*`; + const data = await db.getSortedSetScan({ + key: 'categories:name', + match: match, + limit: 500, + }); + assert.strictEqual(data.length, 0); + }); }); describe('sortedSetAdd()', () => { From dfa213298b7dce55845c18eee657ad4d32145ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 12 May 2025 10:28:26 -0400 Subject: [PATCH 2989/4744] refactor: call verify if request is POST --- src/middleware/activitypub.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index 490715dbcb..b38caa552a 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -33,10 +33,12 @@ middleware.verify = async function (req, res, next) { return next(); } - const verified = await activitypub.verify(req); - if (!verified && req.method === 'POST') { - activitypub.helpers.log('[middleware/activitypub] HTTP signature verification failed.'); - return res.sendStatus(400); + if (req.method === 'POST') { + const verified = await activitypub.verify(req); + if (!verified) { + activitypub.helpers.log('[middleware/activitypub] HTTP signature verification failed.'); + return res.sendStatus(400); + } } // Set calling user From 00668bdc342f22b1ef263ca2d6003b12bbb194af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 12 May 2025 10:29:32 -0400 Subject: [PATCH 2990/4744] refactor: wrap ap routes in try/catch --- src/routes/activitypub.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index b6a516b481..760287e102 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -25,28 +25,28 @@ module.exports = function (app, middleware, controllers) { middleware.activitypub.normalize, ]; - app.get('/actor', middlewares, controllers.activitypub.actors.application); - app.post('/inbox', [...middlewares, ...inboxMiddlewares], controllers.activitypub.postInbox); + app.get('/actor', middlewares, helpers.tryRoute(controllers.activitypub.actors.application)); + app.post('/inbox', [...middlewares, ...inboxMiddlewares], helpers.tryRoute(controllers.activitypub.postInbox)); - app.get('/uid/:uid', [...middlewares, middleware.assert.user], controllers.activitypub.actors.user); - app.get('/user/:userslug', [...middlewares, middleware.exposeUid, middleware.assert.user], controllers.activitypub.actors.userBySlug); - app.get('/uid/:uid/inbox', [...middlewares, middleware.assert.user], controllers.activitypub.getInbox); - app.post('/uid/:uid/inbox', [...middlewares, middleware.assert.user, ...inboxMiddlewares], controllers.activitypub.postInbox); - app.get('/uid/:uid/outbox', [...middlewares, middleware.assert.user], controllers.activitypub.getOutbox); - app.post('/uid/:uid/outbox', [...middlewares, middleware.assert.user], controllers.activitypub.postOutbox); - app.get('/uid/:uid/following', [...middlewares, middleware.assert.user], controllers.activitypub.getFollowing); - app.get('/uid/:uid/followers', [...middlewares, middleware.assert.user], controllers.activitypub.getFollowers); + app.get('/uid/:uid', [...middlewares, middleware.assert.user], helpers.tryRoute(controllers.activitypub.actors.user)); + app.get('/user/:userslug', [...middlewares, middleware.exposeUid, middleware.assert.user], helpers.tryRoute(controllers.activitypub.actors.userBySlug)); + app.get('/uid/:uid/inbox', [...middlewares, middleware.assert.user], helpers.tryRoute(controllers.activitypub.getInbox)); + app.post('/uid/:uid/inbox', [...middlewares, middleware.assert.user, ...inboxMiddlewares], helpers.tryRoute(controllers.activitypub.postInbox)); + app.get('/uid/:uid/outbox', [...middlewares, middleware.assert.user], helpers.tryRoute(controllers.activitypub.getOutbox)); + app.post('/uid/:uid/outbox', [...middlewares, middleware.assert.user], helpers.tryRoute(controllers.activitypub.postOutbox)); + app.get('/uid/:uid/following', [...middlewares, middleware.assert.user], helpers.tryRoute(controllers.activitypub.getFollowing)); + app.get('/uid/:uid/followers', [...middlewares, middleware.assert.user], helpers.tryRoute(controllers.activitypub.getFollowers)); - app.get('/post/:pid', [...middlewares, middleware.assert.post], controllers.activitypub.actors.note); - app.get('/post/:pid/replies', [...middlewares, middleware.assert.post], controllers.activitypub.actors.replies); + app.get('/post/:pid', [...middlewares, middleware.assert.post], helpers.tryRoute(controllers.activitypub.actors.note)); + app.get('/post/:pid/replies', [...middlewares, middleware.assert.post], helpers.tryRoute(controllers.activitypub.actors.replies)); - app.get('/topic/:tid/:slug?', [...middlewares, middleware.assert.topic], controllers.activitypub.actors.topic); + app.get('/topic/:tid/:slug?', [...middlewares, middleware.assert.topic], helpers.tryRoute(controllers.activitypub.actors.topic)); - app.get('/category/:cid/inbox', [...middlewares, middleware.assert.category], controllers.activitypub.getInbox); - app.post('/category/:cid/inbox', [...inboxMiddlewares, middleware.assert.category, ...inboxMiddlewares], controllers.activitypub.postInbox); - app.get('/category/:cid/outbox', [...middlewares, middleware.assert.category], controllers.activitypub.getCategoryOutbox); - app.post('/category/:cid/outbox', [...middlewares, middleware.assert.category], controllers.activitypub.postOutbox); - app.get('/category/:cid/:slug?', [...middlewares, middleware.assert.category], controllers.activitypub.actors.category); + app.get('/category/:cid/inbox', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.getInbox)); + app.post('/category/:cid/inbox', [...inboxMiddlewares, middleware.assert.category, ...inboxMiddlewares], helpers.tryRoute(controllers.activitypub).postInbox); + app.get('/category/:cid/outbox', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.getCategoryOutbox)); + app.post('/category/:cid/outbox', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.postOutbox)); + app.get('/category/:cid/:slug?', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.actors.category)); - app.get('/message/:mid', [...middlewares, middleware.assert.message], controllers.activitypub.actors.message); + app.get('/message/:mid', [...middlewares, middleware.assert.message], helpers.tryRoute(controllers.activitypub.actors.message)); }; From f60748906030c1e2f475ab5b8edcf951f99cc7b3 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 12 May 2025 14:53:39 +0000 Subject: [PATCH 2991/4744] chore: incrementing version number - v4.3.2 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index eca4d69ed8..10896920eb 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.3.1", + "version": "4.3.2", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 0aa9c187f71606ac0e395b51755b4c0394103193 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 12 May 2025 14:53:40 +0000 Subject: [PATCH 2992/4744] chore: update changelog for v4.3.2 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3228268f..540621ebaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +#### v4.3.2 (2025-05-12) + +##### Chores + +* up mentions (fcf9e8b7) +* incrementing version number - v4.3.1 (308e6b9f) +* update changelog for v4.3.1 (2310a7b8) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### Bug Fixes + +* sql injection in sortedSetScan (16504bad) +* escape flag filters (285d438c) +* #13407, don't restart user jobs (31be083e) +* closes #13405, catch errors in ap.verify (8174578c) +* send proper accept header for outgoing webfinger requests (20ab9069) +* wrap generateCollection calls in try..catch to send 404 if thrown (64fdf91b) +* #13397, null values in category sync list (26e6a222) +* #13392, regression from c6f2c87, unable to unfollow from pending follows (401ff797) +* #13397, update getCidByHandle to work with remote categories, fix sync with handles causing issues with null entries (a9a5ab5e) +* correct stage name in dev dockerfile (#13393) (10077d0f) + +##### Refactors + +* wrap ap routes in try/catch (00668bdc) +* call verify if request is POST (dfa21329) + #### v4.3.1 (2025-05-07) ##### Chores From 5802c7ddd9506a4e296f6dbdf2d9a32621c7f4ef Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 12 May 2025 14:59:57 -0400 Subject: [PATCH 2993/4744] fix: missing awaits, more comprehensive 1b12 tests --- src/activitypub/feps.js | 3 +- src/activitypub/inbox.js | 4 +- src/api/activitypub.js | 17 +- src/api/helpers.js | 4 +- test/activitypub/feps.js | 309 ++++++++++++++++++++++++++++-------- test/activitypub/helpers.js | 2 +- 6 files changed, 258 insertions(+), 81 deletions(-) diff --git a/src/activitypub/feps.js b/src/activitypub/feps.js index 1e2d96441e..7b7816516d 100644 --- a/src/activitypub/feps.js +++ b/src/activitypub/feps.js @@ -3,6 +3,7 @@ const nconf = require('nconf'); const posts = require('../posts'); +const utils = require('../utils'); const activitypub = module.parent.exports; const Feps = module.exports; @@ -13,7 +14,7 @@ Feps.announce = async function announce(id, activity) { ({ id: localId } = await activitypub.helpers.resolveLocalId(id)); } const cid = await posts.getCidByPid(localId || id); - if (cid === -1) { + if (cid === -1 || !utils.isNumber(cid)) { // local cids only return; } diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 19f149caec..10656a8d73 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -49,7 +49,7 @@ inbox.create = async (req) => { const asserted = await activitypub.notes.assert(0, object, { cid }); if (asserted) { - activitypub.feps.announce(object.id, req.body); + await activitypub.feps.announce(object.id, req.body); // api.activitypub.add(req, { pid: object.id }); } }; @@ -244,7 +244,7 @@ inbox.like = async (req) => { activitypub.helpers.log(`[activitypub/inbox/like] id ${id} via ${actor}`); const result = await posts.upvote(id, actor); - activitypub.feps.announce(object.id, req.body); + await activitypub.feps.announce(object.id, req.body); socketHelpers.upvote(result, 'notifications:upvoted-your-post-in'); }; diff --git a/src/api/activitypub.js b/src/api/activitypub.js index 43dad015b1..ce78f12d23 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -310,7 +310,15 @@ activitypubApi.delete.note = enabledCheck(async (caller, { pid }) => { activitypubApi.like = {}; activitypubApi.like.note = enabledCheck(async (caller, { pid }) => { - if (!activitypub.helpers.isUri(pid)) { // remote only + 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; } @@ -319,13 +327,6 @@ activitypubApi.like.note = enabledCheck(async (caller, { pid }) => { return; } - const payload = { - id: `${nconf.get('url')}/uid/${caller.uid}#activity/like/${encodeURIComponent(pid)}`, - type: 'Like', - actor: `${nconf.get('url')}/uid/${caller.uid}`, - object: pid, - }; - await Promise.all([ activitypub.send('uid', caller.uid, [uid], payload), activitypub.feps.announce(pid, payload), diff --git a/src/api/helpers.js b/src/api/helpers.js index e0d3bbc0bb..168e5539b6 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -137,12 +137,12 @@ async function executeCommand(caller, command, eventName, notification, data) { } if (result && command === 'upvote') { socketHelpers.upvote(result, notification); - api.activitypub.like.note(caller, { pid: data.pid }); + await api.activitypub.like.note(caller, { pid: 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); - api.activitypub.undo.like(caller, { pid: data.pid }); + await api.activitypub.undo.like(caller, { pid: data.pid }); } return result; } diff --git a/test/activitypub/feps.js b/test/activitypub/feps.js index a9b42644d8..65520f0f4e 100644 --- a/test/activitypub/feps.js +++ b/test/activitypub/feps.js @@ -12,6 +12,7 @@ const user = require('../../src/user'); const groups = require('../../src/groups'); const categories = require('../../src/categories'); const topics = require('../../src/topics'); +const posts = require('../../src/posts'); const api = require('../../src/api'); const helpers = require('./helpers'); @@ -47,84 +48,258 @@ describe('FEPs', () => { activitypub._sent.clear(); }); - it('should be called when a topic is moved from uncategorized to another category', async () => { - const { topicData, postData } = await topics.post({ - uid, - cid: -1, - title: utils.generateUUID(), - content: utils.generateUUID(), - }); + describe('local actions (create, reply, vote)', () => { + let topicData; - assert(topicData); - - await api.topics.move({ uid: adminUid }, { - tid: topicData.tid, - cid, - }); - - assert.strictEqual(activitypub._sent.size, 2); - - const key = Array.from(activitypub._sent.keys())[0]; - const activity = activitypub._sent.get(key); - - assert(activity && activity.object && typeof activity.object === 'object'); - assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${postData.pid}`); - }); - - it('should be called for a newly forked topic', async () => { - const { topicData } = await topics.post({ - uid, - cid: -1, - title: utils.generateUUID(), - content: utils.generateUUID(), - }); - const { tid } = topicData; - const { pid: reply1Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() }); - const { pid: reply2Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() }); - await topics.createTopicFromPosts( - adminUid, utils.generateUUID(), [reply1Pid, reply2Pid], tid, cid - ); - - assert.strictEqual(activitypub._sent.size, 2, activitypub._sent.keys()); - - const key = Array.from(activitypub._sent.keys())[0]; - const activity = activitypub._sent.get(key); - - assert(activity && activity.object && typeof activity.object === 'object'); - assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${reply1Pid}`); - }); - - it('should be called when a post is moved to another topic', async () => { - const [{ topicData: topic1 }, { topicData: topic2 }] = await Promise.all([ - topics.post({ - uid, + before(async () => { + topicData = await api.topics.create({ uid }, { cid, title: utils.generateUUID(), content: utils.generateUUID(), - }), - topics.post({ - uid, + }); + }); + + afterEach(() => { + activitypub._sent.clear(); + }); + + it('should have federated out both Announce(Create(Article)) and Announce(Article)', () => { + const activities = Array.from(activitypub._sent); + + const test1 = activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Create' && + activity.object.object && activity.object.object.type === 'Article'; + }); + + const test2 = activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Article'; + }); + + assert(test1 && test2); + }); + + it('should federate out Announce(Create(Note)) on local reply', async () => { + await api.topics.reply({ uid }, { + tid: topicData.tid, + content: utils.generateUUID(), + }); + + const activities = Array.from(activitypub._sent); + + assert(activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Create' && + activity.object.object && activity.object.object.type === 'Note'; + })); + }); + + it('should NOT federate out Announce(Note) on local reply', async () => { + await api.topics.reply({ uid }, { + tid: topicData.tid, + content: utils.generateUUID(), + }); + + const activities = Array.from(activitypub._sent); + + assert(activities.every((activity) => { + [, activity] = activity; + if (activity.type === 'Announce' && activity.object && activity.object.type === 'Note') { + return false; + } + + return true; + })); + }); + + it('should federate out Announce(Like) on local vote', async () => { + activitypub._sent.clear(); + await api.posts.upvote({ uid: adminUid }, { pid: topicData.mainPid, room_id: `topic_${topicData.tid}` }); + const activities = Array.from(activitypub._sent); + + assert(activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Like'; + })); + }); + }); + + describe('remote actions (create, reply, vote)', () => { + let activity; + let pid; + let topicData; + + before(async () => { + topicData = await api.topics.create({ uid }, { cid, title: utils.generateUUID(), content: utils.generateUUID(), - }), - ]); + }); + }); - assert(topic1 && topic2); + afterEach(() => { + activitypub._sent.clear(); + }); - // Create new reply and move it to topic 2 - const { pid } = await topics.reply({ uid, tid: topic1.tid, content: utils.generateUUID() }); - await api.posts.move({ uid: adminUid }, { pid, tid: topic2.tid }); + it('should have slotted the note into the test category', async () => { + const { id, note } = await helpers.mocks.note({ + cc: [`${nconf.get('url')}/category/${cid}`], + }); + pid = id; + ({ activity } = await helpers.mocks.create(note)); + await activitypub.inbox.create({ body: activity }); - assert.strictEqual(activitypub._sent.size, 1); - const activities = Array.from(activitypub._sent.keys()).map(key => activitypub._sent.get(key)); + const noteCid = await posts.getCidByPid(pid); + assert.strictEqual(noteCid, cid); + }); - const activity = activities.pop(); - assert.strictEqual(activity.type, 'Announce'); - assert(activity.object && activity.object.type); - assert.strictEqual(activity.object.type, 'Create'); - assert(activity.object.object && activity.object.object.type); - assert.strictEqual(activity.object.object.type, 'Note'); + it('should federate out an Announce(Create(Note)) and Announce(Note) on new topic', async () => { + const { id, note } = await helpers.mocks.note({ + cc: [`${nconf.get('url')}/category/${cid}`], + }); + pid = id; + ({ activity } = await helpers.mocks.create(note)); + await activitypub.inbox.create({ body: activity }); + + const activities = Array.from(activitypub._sent); + + const test1 = activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Create' && + activity.object.object && activity.object.object.type === 'Note'; + }); + + const test2 = activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Note'; + }); + + assert(test1 && test2); + }); + + it('should federate out an Announce(Create(Note)) on reply', async () => { + const { id, note } = await helpers.mocks.note({ + cc: [`${nconf.get('url')}/category/${cid}`], + inReplyTo: `${nconf.get('url')}/post/${topicData.mainPid}`, + }); + pid = id; + ({ activity } = await helpers.mocks.create(note)); + await activitypub.inbox.create({ body: activity }); + + const activities = Array.from(activitypub._sent); + + assert(activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Create' && + activity.object.object && activity.object.object.type === 'Note'; + })); + }); + + it('should federate out an Announce(Like) on vote', async () => { + const { activity } = await helpers.mocks.like({ + object: { + id: `${nconf.get('url')}/post/${topicData.mainPid}`, + }, + }); + await activitypub.inbox.like({ body: activity }); + + const activities = Array.from(activitypub._sent); + assert(activities.some((activity) => { + [, activity] = activity; + return activity.type === 'Announce' && + activity.object && activity.object.type === 'Like'; + })); + }); + }); + + describe('extended actions not explicitly specified in 1b12', () => { + it('should be called when a topic is moved from uncategorized to another category', async () => { + const { topicData, postData } = await topics.post({ + uid, + cid: -1, + title: utils.generateUUID(), + content: utils.generateUUID(), + }); + + assert(topicData); + + await api.topics.move({ uid: adminUid }, { + tid: topicData.tid, + cid, + }); + + assert.strictEqual(activitypub._sent.size, 2); + + const key = Array.from(activitypub._sent.keys())[0]; + const activity = activitypub._sent.get(key); + + assert(activity && activity.object && typeof activity.object === 'object'); + assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${postData.pid}`); + }); + + it('should be called for a newly forked topic', async () => { + const { topicData } = await topics.post({ + uid, + cid: -1, + title: utils.generateUUID(), + content: utils.generateUUID(), + }); + const { tid } = topicData; + const { pid: reply1Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() }); + const { pid: reply2Pid } = await topics.reply({ uid, tid, content: utils.generateUUID() }); + await topics.createTopicFromPosts( + adminUid, utils.generateUUID(), [reply1Pid, reply2Pid], tid, cid + ); + + assert.strictEqual(activitypub._sent.size, 2, activitypub._sent.keys()); + + const key = Array.from(activitypub._sent.keys())[0]; + const activity = activitypub._sent.get(key); + + assert(activity && activity.object && typeof activity.object === 'object'); + assert.strictEqual(activity.object.id, `${nconf.get('url')}/post/${reply1Pid}`); + }); + + it('should be called when a post is moved to another topic', async () => { + const [{ topicData: topic1 }, { topicData: topic2 }] = await Promise.all([ + topics.post({ + uid, + cid, + title: utils.generateUUID(), + content: utils.generateUUID(), + }), + topics.post({ + uid, + cid, + title: utils.generateUUID(), + content: utils.generateUUID(), + }), + ]); + + assert(topic1 && topic2); + + // Create new reply and move it to topic 2 + const { pid } = await topics.reply({ uid, tid: topic1.tid, content: utils.generateUUID() }); + await api.posts.move({ uid: adminUid }, { pid, tid: topic2.tid }); + + assert.strictEqual(activitypub._sent.size, 1); + const activities = Array.from(activitypub._sent.keys()).map(key => activitypub._sent.get(key)); + + const activity = activities.pop(); + assert.strictEqual(activity.type, 'Announce'); + assert(activity.object && activity.object.type); + assert.strictEqual(activity.object.type, 'Create'); + assert(activity.object.object && activity.object.object.type); + assert.strictEqual(activity.object.object.type, 'Note'); + }); }); }); }); diff --git a/test/activitypub/helpers.js b/test/activitypub/helpers.js index e4c3a4d689..57d1d1826f 100644 --- a/test/activitypub/helpers.js +++ b/test/activitypub/helpers.js @@ -144,7 +144,7 @@ Helpers.mocks.like = (override = {}) => { const activity = { '@context': 'https://www.w3.org/ns/activitystreams', - id: `${Helpers.mocks._baseUrl}/like/${encodeURIComponent(object)}`, + id: `${Helpers.mocks._baseUrl}/like/${encodeURIComponent(object.id)}`, type: 'Like', actor, object, From 1b0b1da6b98c5183bdfd645aba24ab5eafda3040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 12 May 2025 17:48:46 -0400 Subject: [PATCH 2994/4744] refactor: use a single until --- src/socket.io/admin/analytics.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/socket.io/admin/analytics.js b/src/socket.io/admin/analytics.js index bc084b14f5..8af8881873 100644 --- a/src/socket.io/admin/analytics.js +++ b/src/socket.io/admin/analytics.js @@ -18,14 +18,18 @@ Analytics.get = async function (socket, data) { data.amount = 24; } } - const getStats = data.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; + const getStats = data.units === 'days' ? + analytics.getDailyStatsForSet : + analytics.getHourlyStatsForSet; + if (data.graph === 'traffic') { + const until = data.until || Date.now(); const result = await utils.promiseParallel({ - uniqueVisitors: getStats('analytics:uniquevisitors', data.until || Date.now(), data.amount), - pageviews: getStats('analytics:pageviews', data.until || Date.now(), data.amount), - pageviewsRegistered: getStats('analytics:pageviews:registered', data.until || Date.now(), data.amount), - pageviewsGuest: getStats('analytics:pageviews:guest', data.until || Date.now(), data.amount), - pageviewsBot: getStats('analytics:pageviews:bot', data.until || Date.now(), data.amount), + uniqueVisitors: getStats('analytics:uniquevisitors', until, data.amount), + pageviews: getStats('analytics:pageviews', until, data.amount), + pageviewsRegistered: getStats('analytics:pageviews:registered', until, data.amount), + pageviewsGuest: getStats('analytics:pageviews:guest', until, data.amount), + pageviewsBot: getStats('analytics:pageviews:bot', until, data.amount), summary: analytics.getSummary(), }); result.pastDay = result.pageviews.reduce((a, b) => parseInt(a, 10) + parseInt(b, 10)); From fe13c75549ff2c9263635a8f1444eac1eedc5287 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 13 May 2025 13:59:34 -0400 Subject: [PATCH 2995/4744] fix: #13375, plus additional tests --- src/activitypub/inbox.js | 16 ++++--- src/activitypub/notes.js | 12 ++---- test/activitypub/notes.js | 91 ++++++++++++++++++++++++++------------- 3 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 10656a8d73..ab02cef6db 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -41,6 +41,7 @@ inbox.create = async (req) => { return await activitypub.notes.assertPrivate(object); } + // Category sync, remove when cross-posting available const { cids } = await activitypub.actors.getLocalFollowers(actor); let cid = null; if (cids.size > 0) { @@ -262,12 +263,19 @@ inbox.announce = async (req) => { let tid; let pid; + // Category sync, remove when cross-posting available const { cids } = await activitypub.actors.getLocalFollowers(actor); let cid = null; if (cids.size > 0) { cid = Array.from(cids)[0]; } + // 1b12 announce + const categoryActor = await categories.exists(actor); + if (categoryActor) { + cid = actor; + } + switch(true) { case object.type === 'Like': { const id = object.object.id || object.object; @@ -318,13 +326,7 @@ inbox.announce = async (req) => { } } - // Handle case where Announce(Create(Note-ish)) is received - if (object.type === 'Create' && activitypub._constants.acceptedPostTypes.includes(object.object.type)) { - pid = object.object.id; - } else { - pid = object.id; - } - + pid = object.id; pid = await activitypub.resolveId(0, pid); // in case wrong id is passed-in; unlikely, but still. if (!pid) { return; diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 571e038687..31ca249d3d 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -115,24 +115,18 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { if (hasTid) { mainPid = await topics.getTopicField(tid, 'mainPid'); } else { - // Check recipients/audience for category (local or remote) + // Check recipients/audience for local category const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']); await activitypub.actors.assert(Array.from(set)); - - // Local const resolved = await Promise.all(Array.from(set).map(async id => await activitypub.helpers.resolveLocalId(id))); const recipientCids = resolved .filter(Boolean) .filter(({ type }) => type === 'category') .map(obj => obj.id); - // Remote - const assertedGroups = await db.exists(Array.from(set).map(id => `categoryRemote:${id}`)); - const remoteCid = Array.from(set).filter((_, idx) => assertedGroups[idx]).shift(); - - if (remoteCid || recipientCids.length) { + if (recipientCids.length) { // Overrides passed-in value, respect addressing from main post over booster - options.cid = remoteCid || recipientCids.shift(); + options.cid = recipientCids.shift(); } // mainPid ok to leave as-is diff --git a/test/activitypub/notes.js b/test/activitypub/notes.js index 9b4aaf8653..f079076215 100644 --- a/test/activitypub/notes.js +++ b/test/activitypub/notes.js @@ -83,33 +83,6 @@ describe('Notes', () => { assert.strictEqual(topic.cid, cid); }); - it('should slot newly created topic in remote category if addressed', async () => { - const { id: cid, actor } = helpers.mocks.group(); - await activitypub.actors.assertGroup([cid]); - - const { id } = helpers.mocks.note({ - cc: [cid], - }); - - const assertion = await activitypub.notes.assert(0, id); - assert(assertion); - - const { tid, count } = assertion; - assert(tid); - assert.strictEqual(count, 1); - - const topic = await topics.getTopicData(tid); - assert.strictEqual(topic.cid, cid); - - const tids = await db.getSortedSetMembers(`cid:${cid}:tids`); - assert(tids.includes(tid)); - - const category = await categories.getCategoryData(cid); - ['topic_count', 'post_count', 'totalPostCount', 'totalTopicCount'].forEach((prop) => { - assert.strictEqual(category[prop], 1); - }); - }); - it('should add a remote category topic to a user\'s inbox if they are following the category', async () => { const { id: cid, actor } = helpers.mocks.group(); await activitypub.actors.assertGroup([cid]); @@ -120,7 +93,7 @@ describe('Notes', () => { const { id } = helpers.mocks.note({ cc: [cid], }); - const { tid } = await activitypub.notes.assert(0, id); + const { tid } = await activitypub.notes.assert(0, id, { cid }); const inInbox = await db.isSortedSetMember(`uid:${uid}:inbox`, tid); assert(inInbox); @@ -161,7 +134,7 @@ describe('Notes', () => { const { id } = helpers.mocks.note({ cc: [remoteCid], }); - const assertion = await activitypub.notes.assert(0, id); + const assertion = await activitypub.notes.assert(0, id, { cid: remoteCid }); assert(assertion); const unread = await topics.getTotalUnread(uid); @@ -180,7 +153,7 @@ describe('Notes', () => { const { id, note } = helpers.mocks.note({ cc: [remoteCid], }); - const assertion = await activitypub.notes.assert(0, id); + const assertion = await activitypub.notes.assert(0, id, { cid: remoteCid }); assert(assertion); const unread = await topics.getTotalUnread(uid); @@ -203,7 +176,7 @@ describe('Notes', () => { const { id, note } = helpers.mocks.note({ cc: [remoteCid], }); - const assertion = await activitypub.notes.assert(0, id); + const assertion = await activitypub.notes.assert(0, id, { cid: remoteCid }); assert(assertion); const unread = await topics.getTotalUnread(uid); @@ -457,6 +430,44 @@ describe('Notes', () => { }); }); + describe('Create', () => { + let uid; + + before(async () => { + uid = await user.create({ username: utils.generateUUID() }); + }); + + describe('(Note)', () => { + it('should create a new topic in cid -1', async () => { + const { note, id } = helpers.mocks.note(); + const { activity } = helpers.mocks.create(note); + + await db.sortedSetAdd(`followersRemote:${note.attributedTo}`, Date.now(), uid); + await activitypub.inbox.create({ body: activity }); + + assert(await posts.exists(id)); + + const cid = await posts.getCidByPid(id); + assert.strictEqual(cid, -1); + }); + + it('should create a new topic in cid -1 even if a remote category is addressed', async () => { + const { id: remoteCid } = helpers.mocks.group(); + const { note, id } = helpers.mocks.note({ + audience: [remoteCid], + }); + const { activity } = helpers.mocks.create(note); + + await activitypub.inbox.create({ body: activity }); + + assert(await posts.exists(id)); + + const cid = await posts.getCidByPid(id); + assert.strictEqual(cid, -1); + }); + }); + }); + describe('Announce', () => { let cid; @@ -464,6 +475,24 @@ describe('Notes', () => { ({ cid } = await categories.create({ name: utils.generateUUID().slice(0, 8) })); }); + describe('(Create)', () => { + it('should create a new topic in a remote category if addressed', async () => { + const { id: remoteCid } = helpers.mocks.group(); + const { id, note } = helpers.mocks.note({ + audience: [remoteCid], + }); + let { activity } = helpers.mocks.create(note); + ({ activity } = helpers.mocks.announce({ actor: remoteCid, object: activity })); + + await activitypub.inbox.announce({ body: activity }); + + assert(await posts.exists(id)); + + const cid = await posts.getCidByPid(id); + assert.strictEqual(cid, remoteCid); + }); + }); + describe('(Note)', () => { it('should create a new topic in cid -1 if category not addressed', async () => { const { note } = helpers.mocks.note(); From 7dc690a14ab1471ce6832e9ea84534cce903a3f7 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 14 May 2025 09:19:59 +0000 Subject: [PATCH 2996/4744] Latest translations and fallbacks --- public/language/he/global.json | 2 +- public/language/he/themes/harmony.json | 2 +- public/language/he/user.json | 4 +-- public/language/nb/topic.json | 36 +++++++++++++------------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/public/language/he/global.json b/public/language/he/global.json index dcbbbf0dbc..428cd81ac4 100644 --- a/public/language/he/global.json +++ b/public/language/he/global.json @@ -82,7 +82,7 @@ "downvoted": "הוצבע נגד", "views": "צפיות", "posters": "כותבים", - "watching": "במעקב", + "watching": "עוקבים", "reputation": "מוניטין", "lastpost": "פוסט אחרון", "firstpost": "פוסט ראשון", diff --git a/public/language/he/themes/harmony.json b/public/language/he/themes/harmony.json index f71d306f16..cace705973 100644 --- a/public/language/he/themes/harmony.json +++ b/public/language/he/themes/harmony.json @@ -14,7 +14,7 @@ "settings.stickyToolbar": "הצמד את סרגל הכלים בעת גלילה", "settings.stickyToolbar.help": "סרגל הכלים בדפי נושאים וקטגוריות ייצמד לראש העמוד בעת גלילה", "settings.topicSidebarTools": "כלי סרגל הצד", - "settings.topicSidebarTools.help": "אפשרות זו תעביר את כלי הנושא לסרגל הצד במחשבים שולחניים", + "settings.topicSidebarTools.help": "אפשרות זו תעביר את כלי הנושא לסרגל הצד במחשבים", "settings.autohideBottombar": "הסתרה אוטומטי של סרגל ניווט בנייד", "settings.autohideBottombar.help": "הסרגל בתצוגת הנייד יוסתר כאשר הדף ייגלל מטה", "settings.topMobilebar": "העברת סרגל הניווט בנייד לראש הדף", diff --git a/public/language/he/user.json b/public/language/he/user.json index 85c7a435d0..695f2f99c2 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -14,7 +14,7 @@ "account-info": "פרטי חשבון", "admin-actions-label": "פעולות ניהול", "ban-account": "הרחקת חשבון", - "ban-account-confirm": "האם אתהם בטוחים שאתם רוצים להרחיק משתמש זה?", + "ban-account-confirm": "האם להרחיק משתמש זה?", "unban-account": "ביטול הרחקת חשבון", "mute-account": "השתקת חשבון", "unmute-account": "ביטול השתקת חשבון", @@ -105,7 +105,7 @@ "show-email": "הצגת כתובת האימייל שלי", "show-fullname": "הצגת שמי המלא", "restrict-chats": "אפשר רק הודעות צ'אט ממשתמשים שאני עוקב אחריהם", - "disable-incoming-chats": "השבתת הודעות צ'אט נכנסות ", + "disable-incoming-chats": "השבתת הודעות צ'אט נכנסות ", "chat-allow-list": "אפשור הודעות צ'אט מהמשתמשים הבאים", "chat-deny-list": "דחיית הודעות צ'אט מהמשתמשים הבאים", "chat-list-add-user": "הוספת משתמש", diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 5321f0ec4b..48dc204ac2 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -1,10 +1,10 @@ { - "topic": "Emne", + "topic": "Innlegg", "title": "Tittel", - "no-topics-found": "Ingen tråder funnet!", - "no-posts-found": "Ingen innlegg funnet!", - "post-is-deleted": "Dette innlegget er slettet!", - "topic-is-deleted": "Denne tråden er slettet!", + "no-topics-found": "Ingen innlegg funnet!", + "no-posts-found": "Ingen poster funnet!", + "post-is-deleted": "Dette svaret er slettet!", + "topic-is-deleted": "Dette innlegget er slettet!", "profile": "Profil", "posted-by": "Opprettet av %1", "posted-by-guest": "Opprettet av Gjest", @@ -16,7 +16,7 @@ "one-reply-to-this-post": "1 svar", "last-reply-time": "Siste svar", "reply-options": "Alternativer for svar", - "reply-as-topic": "Svar som tråd", + "reply-as-topic": "Svar som innlegg", "guest-login-reply": "Logg inn for å besvare", "login-to-view": "🔒 Logg inn for å se", "edit": "Endre", @@ -58,7 +58,7 @@ "user-deleted-topic-ago": "%1 slett dette emnet %2", "user-deleted-topic-on": "%1 slett dette emnet på %2", "user-restored-topic-ago": "%1 gjenopprettet dette emnet %2", - "user-restored-topic-on": "%1 gjenopprettet dette menet på %2", + "user-restored-topic-on": "%1 gjenopprettet dette emnet på %2", "user-moved-topic-from-ago": "%1 flyttet dette emnet fra %2 %3", "user-moved-topic-from-on": "%1 flyttet dette emnet fra %2 på %3", "user-shared-topic-ago": "%1 delte dette emnet %2", @@ -66,21 +66,21 @@ "user-queued-post-ago": "%1 i kø post til godkjenning %3", "user-queued-post-on": "%1 i køpost til godkjenning %3", "user-referenced-topic-ago": "%1 refererte dette emnet %3", - "user-referenced-topic-on": "%1 refererte dette emnet dette emnet på %3", + "user-referenced-topic-on": "%1 refererte dette innlegget dette innlegget på %3", "user-forked-topic-ago": "%1 gaflet dette emnet %3", "user-forked-topic-on": "%1 gaflet dette emnet på %3", - "bookmark-instructions": "Klikk her for å gå tilbake til det siste innlegget i denne tråden.", - "flag-post": "Flagg denne posten", - "flag-user": "Flagg denne brukeren", - "already-flagged": "Allerede flagget", - "view-flag-report": "Vis flaggrapport", - "resolve-flag": "Løs flagg", + "bookmark-instructions": "Klikk her for å gå tilbake til det siste svaret i denne tråden.", + "flag-post": "Rapporter denne posten", + "flag-user": "Rapporter denne brukeren", + "already-flagged": "Allerede rapportert", + "view-flag-report": "Vis rapporteringsoversikt", + "resolve-flag": "Behandle rapport", "merged-message": "Dette emnet er slått sammen med %2", "forked-message": "This topic was forked from %2", - "deleted-message": "Denne tråden har blitt slettet. Bare brukere med trådhåndterings-privilegier kan se den.", - "following-topic.message": "Du vil nå motta varsler når noen skriver i denne tråden.", - "not-following-topic.message": "Du vil se denne tråden i trådlisten, men du vil ikke motta varslinger når noen skriver i den.", - "ignoring-topic.message": "Du vil ikke lenger se denne tråden blant de uleste trådene. Du vil få et varsel når du blir nevnt eller din tråd blir tilrådd.", + "deleted-message": "Denne innlegget har blitt slettet. ", + "following-topic.message": "Du vil nå motta varsler når noen svarer på dette innlegget.", + "not-following-topic.message": "Du vil se dette innlegget i oversikten over uleste innlegg, men du vil ikke motta varslinger når noen skriver et svar.", + "ignoring-topic.message": "Du vil ikke lenger se dette innlegget blant uleste innlegg. Du vil få et varsel når du blir nevnt eller innlegget blir anbefalt.", "login-to-subscribe": "Vennligst registrer deg eller logg inn for å abonnere på denne tråden.", "markAsUnreadForAll.success": "Tråd markert som ulest for alle.", "mark-unread": "Merk som ulest", From 9dc91f11a4ab57b8312d4c973894e0a4067c2249 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 14 May 2025 11:00:53 -0400 Subject: [PATCH 2997/4744] test: fix broken test due to adjusted note assertion relation logic --- test/activitypub/actors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/activitypub/actors.js b/test/activitypub/actors.js index 687bc92a44..37fc74280d 100644 --- a/test/activitypub/actors.js +++ b/test/activitypub/actors.js @@ -895,7 +895,7 @@ describe('Pruning', () => { const { id } = helpers.mocks.note({ cc: [cid], }); - await activitypub.notes.assert(0, id); + await activitypub.notes.assert(0, id, { cid }); const total = await db.sortedSetCard('usersRemote:lastCrawled'); const result = await activitypub.actors.prune(); From 5b118904c9544b8cfb53f38b9df0ed6aab888eec Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 14 May 2025 11:05:10 -0400 Subject: [PATCH 2998/4744] test: fix regression from 5802c7ddd9506a4e296f6dbdf2d9a32621c7f4ef --- test/activitypub/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/activitypub/helpers.js b/test/activitypub/helpers.js index 57d1d1826f..a995866838 100644 --- a/test/activitypub/helpers.js +++ b/test/activitypub/helpers.js @@ -144,7 +144,7 @@ Helpers.mocks.like = (override = {}) => { const activity = { '@context': 'https://www.w3.org/ns/activitystreams', - id: `${Helpers.mocks._baseUrl}/like/${encodeURIComponent(object.id)}`, + id: `${Helpers.mocks._baseUrl}/like/${encodeURIComponent(object.id || object)}`, type: 'Like', actor, object, From 61f6806b6a6939d56733e9c5ee9a3a1900e9aaa8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 14 May 2025 11:49:12 -0400 Subject: [PATCH 2999/4744] test: a few additional tests for announce handling --- src/posts/data.js | 2 +- test/activitypub/helpers.js | 3 ++ test/activitypub/notes.js | 77 +++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/posts/data.js b/src/posts/data.js index 688c131b8a..d74a22e69d 100644 --- a/src/posts/data.js +++ b/src/posts/data.js @@ -7,7 +7,7 @@ const utils = require('../utils'); const intFields = [ 'uid', 'pid', 'tid', 'deleted', 'timestamp', 'upvotes', 'downvotes', 'deleterUid', 'edited', - 'replies', 'bookmarks', + 'replies', 'bookmarks', 'announces', ]; module.exports = function (Posts) { diff --git a/test/activitypub/helpers.js b/test/activitypub/helpers.js index a995866838..2690910e06 100644 --- a/test/activitypub/helpers.js +++ b/test/activitypub/helpers.js @@ -162,6 +162,8 @@ Helpers.mocks.announce = (override = {}) => { if (!object) { ({ id: object } = Helpers.mocks.note()); } + delete override.actor; + delete override.object; const activity = { '@context': 'https://www.w3.org/ns/activitystreams', @@ -171,6 +173,7 @@ Helpers.mocks.announce = (override = {}) => { cc: [`${actor}/followers`], actor, object, + ...override, }; return { activity }; diff --git a/test/activitypub/notes.js b/test/activitypub/notes.js index f079076215..afd24081c5 100644 --- a/test/activitypub/notes.js +++ b/test/activitypub/notes.js @@ -493,6 +493,83 @@ describe('Notes', () => { }); }); + describe('(Create) or (Note) referencing local post', () => { + let uid; + let topicData; + let postData; + let localNote; + let announces = 0; + + before(async () => { + uid = await user.create({ username: utils.generateUUID().slice(0, 10) }); + ({ topicData, postData } = await topics.post({ + cid, + uid, + title: utils.generateUUID(), + content: utils.generateUUID(), + })); + localNote = await activitypub.mocks.notes.public(postData); + }); + + it('should increment announces counter when a remote user shares', async () => { + const { id } = helpers.mocks.person(); + const { activity } = helpers.mocks.announce({ + actor: id, + object: localNote, + cc: [`${nconf.get('url')}/uid/${topicData.uid}`], + }); + + await activitypub.inbox.announce({ body: activity }); + announces += 1; + + const count = await posts.getPostField(topicData.mainPid, 'announces'); + assert.strictEqual(count, announces); + }); + + it('should contain the remote user announcer id in the post announces zset', async () => { + const { id } = helpers.mocks.person(); + const { activity } = helpers.mocks.announce({ + actor: id, + object: localNote, + cc: [`${nconf.get('url')}/uid/${topicData.uid}`], + }); + + await activitypub.inbox.announce({ body: activity }); + announces += 1; + + const exists = await db.isSortedSetMember(`pid:${topicData.mainPid}:announces`, id); + assert(exists); + }); + + it('should NOT increment announces counter when a remote category shares', async () => { + const { id } = helpers.mocks.group(); + const { activity } = helpers.mocks.announce({ + actor: id, + object: localNote, + cc: [`${nconf.get('url')}/uid/${topicData.uid}`], + }); + + await activitypub.inbox.announce({ body: activity }); + + const count = await posts.getPostField(topicData.mainPid, 'announces'); + assert.strictEqual(count, announces); + }); + + it('should NOT contain the remote category announcer id in the post announces zset', async () => { + const { id } = helpers.mocks.group(); + const { activity } = helpers.mocks.announce({ + actor: id, + object: localNote, + cc: [`${nconf.get('url')}/uid/${topicData.uid}`], + }); + + await activitypub.inbox.announce({ body: activity }); + + const exists = await db.isSortedSetMember(`pid:${topicData.mainPid}:announces`, id); + assert(!exists); + }); + }); + describe('(Note)', () => { it('should create a new topic in cid -1 if category not addressed', async () => { const { note } = helpers.mocks.note(); From 0f576a42195b42a225eba94213299244e0fc1dfa Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 14 May 2025 12:16:06 -0400 Subject: [PATCH 3000/4744] fix: add `announces` to postdataobject schema --- public/openapi/components/schemas/PostObject.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 502ea8044d..bcb2f79e53 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -180,6 +180,8 @@ PostDataObject: type: number downvotes: type: number + announces: + type: number bookmarks: type: number deleterUid: From 383a7ce507b7650804520a98e9543ec5b2689b4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 12:38:00 -0400 Subject: [PATCH 3001/4744] fix(deps): update dependency nodebb-plugin-mentions to v4.7.6 (#13417) 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 4c9faa7c9c..2e9fd8a5bb 100644 --- a/install/package.json +++ b/install/package.json @@ -103,7 +103,7 @@ "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.0", - "nodebb-plugin-mentions": "4.7.5", + "nodebb-plugin-mentions": "4.7.6", "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.4", "nodebb-rewards-essentials": "1.0.2", From fbe97b4e914a31cdd1b66f90651ba77305e6a56f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 12:38:22 -0400 Subject: [PATCH 3002/4744] chore(deps): update redis docker tag to v8.0.1 (#13415) 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 39b577ca28..43fdf33611 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: - 5432:5432 redis: - image: 'redis:8.0.0' + image: 'redis:8.0.1' # 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 242bd04e9c..9011d0f92a 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.0.0-alpine + image: redis:8.0.1-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 e3da62c870..da31cd6886 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.0.0-alpine + image: redis:8.0.1-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 1bb783c771..637cecb0cd 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.0.0-alpine + image: redis:8.0.1-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 0825c569aa72d1e0a3ea526dfe2b3fad6a9404dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 12:40:24 -0400 Subject: [PATCH 3003/4744] fix(deps): update dependency pg to v8.16.0 (#13411) 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 2e9fd8a5bb..8360e2159e 100644 --- a/install/package.json +++ b/install/package.json @@ -117,7 +117,7 @@ "passport": "0.7.0", "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", - "pg": "8.15.6", + "pg": "8.16.0", "pg-cursor": "2.14.6", "postcss": "8.5.3", "postcss-clean": "1.2.0", From 366651d6e1651b0c5265ec21b1395463d3962f2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 12:40:46 -0400 Subject: [PATCH 3004/4744] fix(deps): update dependency semver to v7.7.2 (#13410) 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 8360e2159e..a09fb8bd8e 100644 --- a/install/package.json +++ b/install/package.json @@ -130,7 +130,7 @@ "sanitize-html": "2.16.0", "sass": "1.88.0", "satori": "0.12.2", - "semver": "7.7.1", + "semver": "7.7.2", "serve-favicon": "2.5.0", "sharp": "0.32.6", "sitemap": "8.0.0", From 84b8ecc7a0bcf20ceae6c5d30865843e952b3534 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 13:45:01 -0400 Subject: [PATCH 3005/4744] fix(deps): update dependency nodebb-plugin-markdown to v13.2.1 (#13416) 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 a09fb8bd8e..ffeeab1992 100644 --- a/install/package.json +++ b/install/package.json @@ -102,7 +102,7 @@ "nodebb-plugin-dbsearch": "6.2.16", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", - "nodebb-plugin-markdown": "13.2.0", + "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.4", From 7320a858968af80f508aeaeccf731d199f59112f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 13:45:10 -0400 Subject: [PATCH 3006/4744] fix(deps): update dependency pg-cursor to v2.15.0 (#13414) 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 ffeeab1992..ec4e60b1d5 100644 --- a/install/package.json +++ b/install/package.json @@ -118,7 +118,7 @@ "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", "pg": "8.16.0", - "pg-cursor": "2.14.6", + "pg-cursor": "2.15.0", "postcss": "8.5.3", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", From f176d6b2c5cc76ee8ee749c6ed6b3b1f1430b89e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 13:45:21 -0400 Subject: [PATCH 3007/4744] fix(deps): update dependency satori to v0.13.1 (#13408) 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 ec4e60b1d5..452ac8a79d 100644 --- a/install/package.json +++ b/install/package.json @@ -129,7 +129,7 @@ "rtlcss": "4.3.0", "sanitize-html": "2.16.0", "sass": "1.88.0", - "satori": "0.12.2", + "satori": "0.13.1", "semver": "7.7.2", "serve-favicon": "2.5.0", "sharp": "0.32.6", From d5865613e3ca132227150be1be81e9545259ecf3 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 14 May 2025 14:14:06 -0400 Subject: [PATCH 3008/4744] fix: #13081, don't add mention when you are replying to yourself --- public/src/client/topic/postTools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 42f10093a0..640d936f16 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -397,7 +397,7 @@ define('forum/topic/postTools', [ return; } const post = button.parents('[data-pid]'); - if (post.length) { + if (post.length && !post.hasClass('self-post')) { require(['slugify'], function (slugify) { slug = slugify(post.attr('data-username'), true); if (!slug) { From 3e18af1e2576ce4cbaef842674f80e15c60a9a4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 14:22:45 -0400 Subject: [PATCH 3009/4744] fix(deps): update dependency sanitize-html to v2.17.0 (#13418) 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 452ac8a79d..2bf994ee1d 100644 --- a/install/package.json +++ b/install/package.json @@ -127,7 +127,7 @@ "rimraf": "6.0.1", "rss": "1.2.2", "rtlcss": "4.3.0", - "sanitize-html": "2.16.0", + "sanitize-html": "2.17.0", "sass": "1.88.0", "satori": "0.13.1", "semver": "7.7.2", From 919d62ab4e3eb70808264e36f0afd42c62d8dd98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 14:23:24 -0400 Subject: [PATCH 3010/4744] fix(deps): update dependency diff to v8 (#13409) 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 2bf994ee1d..dde27f60ed 100644 --- a/install/package.json +++ b/install/package.json @@ -66,7 +66,7 @@ "cropperjs": "1.6.2", "csrf-sync": "4.2.1", "daemon": "1.1.0", - "diff": "7.0.0", + "diff": "8.0.1", "esbuild": "0.25.4", "express": "4.21.2", "express-session": "1.18.1", From 799b08db3a6f74bf32b4fc45fbb4e94441e3892d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 14 May 2025 15:22:58 -0400 Subject: [PATCH 3011/4744] fix: adjust Peertube-specific handling to shove mp4 into post attachments, #13324 --- src/activitypub/mocks.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index b569768f98..f2a8446d4a 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -42,7 +42,7 @@ const sanitizeConfig = { Mocks._normalize = async (object) => { // Normalized incoming AP objects into expected types for easier mocking - let { attributedTo, url, image, content, source } = object; + let { type, attributedTo, url, image, content, source, attachment } = object; switch (true) { // non-string attributedTo handling case Array.isArray(attributedTo): { @@ -102,6 +102,30 @@ Mocks._normalize = async (object) => { if (url) { // Handle url array if (Array.isArray(url)) { + // Special handling for Video type (from PeerTube specifically) + if (type === 'Video') { + const stream = url.reduce((memo, { type, mediaType, tag }) => { + if (!memo) { + if (type === 'Link' && mediaType === 'application/x-mpegURL') { + memo = tag.reduce((memo, { type, mediaType, href }) => { + if (!memo && (type === 'Link' && mediaType === 'video/mp4')) { + memo = { type, mediaType, href }; + } + + return memo; + }, null); + } + } + + return memo; + }, null); + + if (stream) { + attachment = attachment || []; + attachment.push(stream); + } + } + url = url.reduce((valid, cur) => { if (typeof cur === 'string') { valid.push(cur); @@ -126,6 +150,7 @@ Mocks._normalize = async (object) => { sourceContent, image, url, + attachment, }; }; @@ -332,7 +357,7 @@ Mocks.post = async (objects) => { attributedTo: uid, inReplyTo: toPid, published, updated, name, content, sourceContent, - type, to, cc, audience, attachment, tag, image, + to, cc, audience, attachment, tag, image, } = object; await activitypub.actors.assert(uid); @@ -346,11 +371,6 @@ Mocks.post = async (objects) => { let edited = new Date(updated); edited = Number.isNaN(edited.valueOf()) ? undefined : edited; - if (type === 'Video') { - attachment = attachment || []; - attachment.push({ url }); - } - const payload = { uid, pid, From b31d769d9c0171cd87281d3deec1198503de8998 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 14 May 2025 20:36:35 +0000 Subject: [PATCH 3012/4744] chore: incrementing version number - v4.4.0 --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index dde27f60ed..90f4f723ba 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.3.2", + "version": "4.4.0", "homepage": "https://www.nodebb.org", "repository": { "type": "git", @@ -200,4 +200,4 @@ "url": "https://github.com/barisusakli" } ] -} +} \ No newline at end of file From 09cc91d5a06b34d70187ac23857254e8705f4c9c Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 14 May 2025 20:36:36 +0000 Subject: [PATCH 3013/4744] chore: update changelog for v4.4.0 --- CHANGELOG.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540621ebaf..5728c63beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,113 @@ +#### v4.4.0 (2025-05-14) + +##### Breaking Changes + +* removal of deprecated privilege hooks (8ea377a4) +* removal of `filter:flags.getFilters` (547fb482) +* removal of `filter:user.verify.code` (7e25946c) +* removal of `filter:post.purge` (df5c1a93) +* removal of `filter:post.purge` (c84b72fb) +* removal of `filter:router.page` (9d8061ea) +* removal of `filter:email.send` (b73a8d3e) + +##### Chores + +* **deps:** + * update redis docker tag to v8.0.1 (#13415) (fbe97b4e) + * update redis docker tag to v8 (#13387) (1df7313c) + * update postgres docker tag to v17.5 (#13398) (d319b0aa) + * update dependency sass-embedded to v1.88.0 (#13402) (694c79bc) + * update dependency lint-staged to v16 (#13404) (9d877481) + * update commitlint monorepo to v19.8.1 (#13394) (7a7a4f0a) + * update dependency lint-staged to v15.5.2 (#13383) (96dc5c89) + * update dependency @eslint/js to v9.26.0 (#13371) (450ce3b8) + * update dependency mocha to v11.2.2 (#13366) (e958010f) +* incrementing version number - v4.3.2 (b92b5d80) +* update changelog for v4.3.2 (0aa9c187) +* incrementing version number - v4.3.1 (308e6b9f) +* remove unused require (15b6a2c1) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### Documentation Changes + +* remove since-removed `labels` property from api (860ac895) + +##### Bug Fixes + +* adjust Peertube-specific handling to shove mp4 into post attachments, #13324 (799b08db) +* #13081, don't add mention when you are replying to yourself (d5865613) +* add `announces` to postdataobject schema (0f576a42) +* #13375, plus additional tests (fe13c755) +* missing awaits, more comprehensive 1b12 tests (5802c7dd) +* another case (6bfe4e62) +* handle missing orderedItems property in followers route (e042201f) +* missing await (651ebaaf) +* handle missing orderedItems (53bb0bbc) +* extra `orderedItems` property in generated paginated OrderedCollection, #13153 (f83b1fbf) +* #13153, follower and following collections to use generateCollection helper (a2de7aae) +* #13374, updates to posts.edit to handle remote content updates better (b4338489) +* leftover `handle` var (625ce96f) +* AP inbox update handling for non-note objects (f8d012c8) +* 1b12 creates being dropped (9f80d10d) +* update AP api (un)follow ids to be url encoded id instead of handle (7cf61ab0) +* **deps:** + * update dependency diff to v8 (#13409) (919d62ab) + * update dependency sanitize-html to v2.17.0 (#13418) (3e18af1e) + * update dependency satori to v0.13.1 (#13408) (f176d6b2) + * update dependency pg-cursor to v2.15.0 (#13414) (7320a858) + * update dependency nodebb-plugin-markdown to v13.2.1 (#13416) (84b8ecc7) + * update dependency semver to v7.7.2 (#13410) (366651d6) + * update dependency pg to v8.16.0 (#13411) (0825c569) + * update dependency nodebb-plugin-mentions to v4.7.6 (#13417) (383a7ce5) + * update dependency lru-cache to v11 (#12685) (23374fd7) + * update dependency rimraf to v6 (#12686) (6a4ffe02) + * update dependency bootswatch to v5.3.6 (#13400) (7a7cf830) + * update dependency csrf-sync to v4.2.1 (#13401) (ecce9998) + * update dependency sass to v1.88.0 (#13403) (7ffba218) + * update dependency nodemailer to v7.0.3 (#13395) (af3afba0) + * update dependency nodemailer to v7 (#13381) (0b4d403c) + * update dependency csrf-sync to v4.2.0 (#13364) (4f0f67a4) + * update dependency webpack to v5.99.8 (#13390) (c7a164ae) + * update dependency bootstrap to v5.3.6 (#13384) (e6a19612) + * update dependency esbuild to v0.25.4 (#13385) (b6f4de5b) + * update dependency @fontsource/poppins to v5.2.6 (#13376) (e2a8cf98) + * update dependency nodebb-plugin-mentions to v4.7.5 (#13386) (2c0aba02) + * update dependency nodebb-widget-essentials to v7.0.38 (#13380) (7f757615) + * update dependency nodebb-theme-persona to v14.1.11 (#13379) (954aa541) + * update dependency nodebb-theme-peace to v2.2.42 (#13378) (2aa0bfc5) + * update dependency nodebb-theme-harmony to v2.1.12 (#13377) (72b3a215) + * update dependency ace-builds to v1.41.0 (#13372) (4b78710b) + * bump markdown (f3bd8590) + +##### Other Changes + +* //github.com/NodeBB/NodeBB/issues/13367 (39953ee1) + +##### Refactors + +* use a single until (1b0b1da6) +* Helpers.generateCollection so that total count and a bound function can be passed in, #13153 (7f59238d) + +##### Tests + +* a few additional tests for announce handling (61f6806b) +* fix regression from 5802c7ddd9506a4e296f6dbdf2d9a32621c7f4ef (5b118904) +* fix broken test due to adjusted note assertion relation logic (9dc91f11) +* update filter:router.page tests to response:router.page (a819d39c) +* adjustment for now-removed labels property (52df41b9) + #### v4.3.2 (2025-05-12) ##### Chores From ab6ed111554c617bd0a556794b7b0c64aaab0af8 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 15 May 2025 09:19:48 +0000 Subject: [PATCH 3014/4744] Latest translations and fallbacks --- public/language/he/admin/settings/user.json | 2 +- public/language/nb/category.json | 6 ++--- public/language/nb/global.json | 2 +- public/language/nb/modules.json | 4 +-- public/language/nb/notifications.json | 8 +++--- public/language/nb/tags.json | 4 +-- public/language/nb/topic.json | 22 ++++++++-------- public/language/nb/unread.json | 2 +- public/language/nb/user.json | 2 +- .../language/nn-NO/admin/settings/user.json | 2 +- public/language/nn-NO/category.json | 6 ++--- public/language/nn-NO/global.json | 2 +- public/language/nn-NO/modules.json | 4 +-- public/language/nn-NO/notifications.json | 10 +++---- public/language/nn-NO/tags.json | 4 +-- public/language/nn-NO/topic.json | 26 +++++++++---------- public/language/nn-NO/unread.json | 2 +- public/language/nn-NO/user.json | 2 +- 18 files changed, 55 insertions(+), 55 deletions(-) diff --git a/public/language/he/admin/settings/user.json b/public/language/he/admin/settings/user.json index 0c0d227b39..3f1b6958b6 100644 --- a/public/language/he/admin/settings/user.json +++ b/public/language/he/admin/settings/user.json @@ -67,7 +67,7 @@ "disable-incoming-chats": "השבתת הודעות צ'אט נכנסות", "outgoing-new-tab": "פתח קישורים חיצוניים בכרטיסייה חדשה", "topic-search": "הפעל חיפוש בתוך נושא", - "update-url-with-post-index": "עדכן את כתובת הURL עם מספר הפוסט הנוכחי בזמן גלישה בנושאים", + "update-url-with-post-index": "עדכן את כתובת ה-URL עם מספר הפוסט הנוכחי בזמן גלישה בנושאים", "digest-freq": "הרשם לקבלת תקציר", "digest-freq.off": "כבוי", "digest-freq.daily": "יומי", diff --git a/public/language/nb/category.json b/public/language/nb/category.json index 2f274001a6..e114c8a083 100644 --- a/public/language/nb/category.json +++ b/public/language/nb/category.json @@ -4,10 +4,10 @@ "uncategorized": "Uncategorized", "uncategorized.description": "Topics that do not strictly fit in with any existing categories", "handle.description": "This category can be followed from the open social web via the handle %1", - "new-topic-button": "Nytt emne", + "new-topic-button": "Nytt innlegg", "guest-login-post": "Logg inn for å publisere innlegg", "no-topics": "Denne kategorien er foreløpig tom.
Har du noe å dele? Opprett et innlegg her!", - "no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.", + "no-followers": "Ingen følger denne kategorien ennå. Følg den for å få oppdateringer om endringer.", "browsing": "leser", "no-replies": "Ingen har svart", "no-new-posts": "Ingen nye innlegg.", @@ -17,7 +17,7 @@ "tracking": "Følg", "not-watching": "Følger ikke", "ignoring": "Ignorerer", - "watching.description": "Varsle meg om nye emner.
Vis emner i ulest og nylig", + "watching.description": "Varsle meg om nye innlegg.
Vis innlegg i ulest og nylig", "tracking.description": "Følg emner i ulest og nylig", "not-watching.description": "Ikke vis emner i ulest, vis i nylig", "ignoring.description": "Ikke vis emner i ulest & nylig", diff --git a/public/language/nb/global.json b/public/language/nb/global.json index edb8c97735..fe49ecbeaf 100644 --- a/public/language/nb/global.json +++ b/public/language/nb/global.json @@ -82,7 +82,7 @@ "downvoted": "Nedstemt", "views": "Visninger", "posters": "Innlegg", - "watching": "Watching", + "watching": "Følger", "reputation": "Omdømme", "lastpost": "Siste innlegg", "firstpost": "Første innlegg", diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json index 08e42a051e..bd0436e435 100644 --- a/public/language/nb/modules.json +++ b/public/language/nb/modules.json @@ -8,7 +8,7 @@ "chat.usernames-and-x-others": "%1 & %2 andre", "chat.chat-with-usernames": "Chat med %1", "chat.chat-with-usernames-and-x-others": "Chat med %1 & %2 andre", - "chat.send": "Send", + "chat.send": "Publiser", "chat.no-active": "Du har ingen aktive chatter.", "chat.user-typing-1": "%1 skriver ...", "chat.user-typing-2": "%1 og %2 skriver ...", @@ -121,7 +121,7 @@ "bootbox.cancel": "Avbryt", "bootbox.confirm": "Bekreft", "bootbox.submit": "Send inn", - "bootbox.send": "Send", + "bootbox.send": "Publiser", "cover.dragging-title": "Posisjoner bilde", "cover.dragging-message": "Dra omslagsbildet til ønsket posisjon og klikk \"Lagre\"", "cover.saved": "Omslagsbilde og posisjon lagret", diff --git a/public/language/nb/notifications.json b/public/language/nb/notifications.json index 757e983c88..6c56f4b626 100644 --- a/public/language/nb/notifications.json +++ b/public/language/nb/notifications.json @@ -50,7 +50,7 @@ "user-posted-to-dual": "%1 og %2 har svart på innlegget ditt i %3", "user-posted-to-triple": "%1, %2 og %3 har svart til: %4", "user-posted-to-multiple": "%1, %2 og %3 andre har svart til: %4", - "user-posted-topic": "%1 har skrevet en ny tråd: %2", + "user-posted-topic": "%1 har skrevet et nytt innlegg: %2", "user-edited-post": "%1 har redigert et innlegg i %2", "user-posted-topic-with-tag": "%1 har publisert %2 (merket %3)", "user-posted-topic-with-tag-dual": "%1 har publisert %2 (merket %3 og %4)", @@ -83,9 +83,9 @@ "notificationType-upvote": "Når noen tilrår innlegget ditt", "notificationType-new-topic": "Når noen du følger legger ut et emne", "notificationType-new-topic-with-tag": "Når et emne publiseres med et stikkord du følger", - "notificationType-new-topic-in-category": "Når et emne er lagt ut i en kategori du ser på", - "notificationType-new-reply": "Når et nytt svar er lagt ut i et emne du overvåker", - "notificationType-post-edit": "Når et innlegg er redigert i et emne du overvåker", + "notificationType-new-topic-in-category": "Når et innlegg er lagt ut i en kategori du følger", + "notificationType-new-reply": "Når et nytt svar er lagt ut i innlegg du følger", + "notificationType-post-edit": "Når et svar er redigert i et innlegg du følger", "notificationType-follow": "Når noen starter å følge deg", "notificationType-new-chat": "Når du mottar en melding i chat", "notificationType-new-group-chat": "Når du mottar en gruppemelding i chat", diff --git a/public/language/nb/tags.json b/public/language/nb/tags.json index 300852b7ab..571ec1055c 100644 --- a/public/language/nb/tags.json +++ b/public/language/nb/tags.json @@ -10,8 +10,8 @@ "tag-whitelist": "Hviteliste for emneord", "watching": "Følger", "not-watching": "Følger ikke", - "watching.description": "Varsle meg om nye emner.", - "not-watching.description": "Ikke varsle meg om nye emner.", + "watching.description": "Varsle meg om nye innlegg", + "not-watching.description": "Ikke varsle meg om nye innlegg.", "following-tag.message": "Du vil nå motta varsler når noen legger ut et emne med dette emneordet.", "not-following-tag.message": "Du vil ikke motta varsler når noen legger ut et emne med dette emneordet." } \ No newline at end of file diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 48dc204ac2..3e8489d703 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -9,7 +9,7 @@ "posted-by": "Opprettet av %1", "posted-by-guest": "Opprettet av Gjest", "chat": "Chat", - "notify-me": "Bli varslet om nye svar i denne tråden", + "notify-me": "Varsle meg om nye svar på dette innlegget", "quote": "Siter", "reply": "Svar", "replies-to-this-post": "%1 svar", @@ -87,15 +87,15 @@ "mark-unread.success": "Tråd merket som ulest.", "watch": "Følg", "unwatch": "Ikke følg", - "watch.title": "Bli varslet om nye svar i denne tråden", + "watch.title": "Varlse meg om nye svar på dette innlegget", "unwatch.title": "Slutt å følge denne tråden", "share-this-post": "Del ditt innlegg", "watching": "Følger", "not-watching": "Følger ikke", "ignoring": "Ignorerer", "watching.description": "Varlse meg om nye svar.
Vis tråd i ulest.", - "not-watching.description": "Ikke varsle meg om nye svar.
Vis tråd i ulest hvis ikke kategori er ignorert.", - "ignoring.description": "Ikke varsle meg om nye svar.
Ikke vis tråd i ulest.", + "not-watching.description": "Ikke varsle meg om nye svar.
Vis innlegg i ulest hvis ikke kategori er ignorert.", + "ignoring.description": "Ikke varsle meg om nye svar.
Ikke vis innlegg i ulest.", "thread-tools.title": "Trådverktøy", "thread-tools.markAsUnreadForAll": "Merk som ulest for alle", "thread-tools.pin": "Fest tråd", @@ -144,8 +144,8 @@ "move-post": "Flytt innlegg", "post-moved": "Innlegg flyttet!", "fork-topic": "Forgren tråd", - "enter-new-topic-title": "Tast inn tittel på emne", - "fork-topic-instruction": "Klikk på innleggene du vil dele, skriv inn en tittel for det nye emnet og klikk på emnet", + "enter-new-topic-title": "Skriv tittel på innlegg", + "fork-topic-instruction": "Click the posts you want to fork, enter a title for the new topic and click fork topic", "fork-no-pids": "Ingen innlegg valgt!", "no-posts-selected": "Ingen innlegg valgt.", "x-posts-selected": "%1 innlegg valgt", @@ -157,7 +157,7 @@ "merge-topic-list-title": "Liste over emner som skal slås sammen", "merge-options": "Slå sammen alternativer", "merge-select-main-topic": "Velg hovedemne", - "merge-new-title-for-topic": "Ny tittel for emne", + "merge-new-title-for-topic": "Ny tittel for innlegg", "topic-id": "Emne ID", "move-posts-instruction": "Klikk på innleggene du vil flytte, og skriv deretter inn en emne-ID, eller gå til målemnet", "move-topic-instruction": "Velg målkategorien og klikk deretter flytt", @@ -172,7 +172,7 @@ "composer.post-later": "Publiser senere", "composer.schedule": "Timeplan", "composer.replying-to": "Svarer i %1", - "composer.new-topic": "Ny tråd", + "composer.new-topic": "Nytt innleg", "composer.editing-in": "Redigerer post i %1", "composer.uploading": "laster opp...", "composer.thumb-url-label": "Lim inn som tråd-minatyr URL", @@ -193,9 +193,9 @@ "most-votes": "Flest tilrådinger", "most-posts": "Flest innlegg", "most-views": "Flest visninger", - "stale.title": "Lag en ny tråd i stedet?", - "stale.warning": "Tråden du svarer på er ganske gammel. Vil du heller lage en ny tråd og referere til denne?", - "stale.create": "Lag en ny tråd", + "stale.title": "Opprett nytt innlegg i stedet?", + "stale.warning": "Innlegget du svarer på er ganske gammelt. Vil du heller lage et nytt innlegg og referere til dette?", + "stale.create": "Lag et nytt innlegg", "stale.reply-anyway": "Svar på denne tråden likevel", "link-back": "Sv: [%1](%2)", "diffs.title": "Redigeringshistorikk for innlegg", diff --git a/public/language/nb/unread.json b/public/language/nb/unread.json index 9ac5779198..64b5d53d47 100644 --- a/public/language/nb/unread.json +++ b/public/language/nb/unread.json @@ -9,7 +9,7 @@ "all-categories": "Alle kategorier", "topics-marked-as-read.success": "Emner merket som lest!", "all-topics": "Alle emner", - "new-topics": "Nye emner", + "new-topics": "Nye innlegg", "watched-topics": "Fulgte emner", "unreplied-topics": "Emner som ikke er svart på", "multiple-categories-selected": "Flere valg" diff --git a/public/language/nb/user.json b/public/language/nb/user.json index 590afb957a..efcbc0c1d7 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -110,7 +110,7 @@ "chat-deny-list": "Deny chat messages from the following users", "chat-list-add-user": "Add user", "digest-label": "Abonner på sammendrag", - "digest-description": "Abonner på e-post-oppdateringer for dette forumet (nye varsler og emner) i samsvar med valgte tidspunkt", + "digest-description": "Abonner på e-post-oppdateringer for dette forumet (nye varsler og innlegg) i samsvar med valgte tidspunkt", "digest-off": "Av", "digest-daily": "Daglig", "digest-weekly": "Ukentlig", diff --git a/public/language/nn-NO/admin/settings/user.json b/public/language/nn-NO/admin/settings/user.json index 5dc36d2d55..76c93d24c6 100644 --- a/public/language/nn-NO/admin/settings/user.json +++ b/public/language/nn-NO/admin/settings/user.json @@ -81,7 +81,7 @@ "default-notification-settings": "Standard varslingsinnstillingar", "categoryWatchState": "Kategori-overvåking", "categoryWatchState.tracking": "Sporing", - "categoryWatchState.notwatching": "Ikkje overvåking", + "categoryWatchState.notwatching": "Følgjer ikkje", "categoryWatchState.ignoring": "Ignorerer", "restrictions-new": "Nye restriksjonar", "restrictions.rep-threshold": "Omdømmegrense", diff --git a/public/language/nn-NO/category.json b/public/language/nn-NO/category.json index 22f6ffeaa3..152c69d345 100644 --- a/public/language/nn-NO/category.json +++ b/public/language/nn-NO/category.json @@ -4,10 +4,10 @@ "uncategorized": "Uncategorized", "uncategorized.description": "Topics that do not strictly fit in with any existing categories", "handle.description": "This category can be followed from the open social web via the handle %1", - "new-topic-button": "Nytt emne", + "new-topic-button": "Nytt innlegg", "guest-login-post": "Logg inn for å legge inn innlegg", "no-topics": "Denne kategorien er foreløpig tom.
Har du noko å dele? Opprett eit innlegg her!", - "no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.", + "no-followers": "Ingen følgjer denne kategorien enno. Følg den for å få oppdateringer.", "browsing": "blar gjennom", "no-replies": "Ingen har svart", "no-new-posts": "Ingen nye innlegg.", @@ -17,7 +17,7 @@ "tracking": "Følgjer med", "not-watching": "Følgjer ikkje", "ignoring": "Ignorerer", - "watching.description": "Varsle meg om nye emne.
Vis emne i Uleste og nye", + "watching.description": "Varsle meg om nye innlegg.
Vis innlegg i Uleste og nye", "tracking.description": "Viser emne som uleste og nye", "not-watching.description": "Vis ikkje emne som uleste, vis i nye", "ignoring.description": "Vis ikkje emne som uleste og nye", diff --git a/public/language/nn-NO/global.json b/public/language/nn-NO/global.json index 4f4ba51768..d6801a9670 100644 --- a/public/language/nn-NO/global.json +++ b/public/language/nn-NO/global.json @@ -82,7 +82,7 @@ "downvoted": "Stemte ned", "views": "Visningar", "posters": "Innleggsskrivarar", - "watching": "Watching", + "watching": "Følgjer", "reputation": "Omdømme", "lastpost": "Siste innlegg", "firstpost": "Fyrste innlegg", diff --git a/public/language/nn-NO/modules.json b/public/language/nn-NO/modules.json index 353020cc78..061d785238 100644 --- a/public/language/nn-NO/modules.json +++ b/public/language/nn-NO/modules.json @@ -8,7 +8,7 @@ "chat.usernames-and-x-others": "%1 og %2 andre", "chat.chat-with-usernames": "Chat med %1", "chat.chat-with-usernames-and-x-others": "Chat med %1 og %2 andre", - "chat.send": "Send", + "chat.send": "Publiser", "chat.no-active": "Du har ingen aktive chattar", "chat.user-typing-1": "%1 skriv ...", "chat.user-typing-2": "%1 og %2 skriv ...", @@ -121,7 +121,7 @@ "bootbox.cancel": "Avbryt", "bootbox.confirm": "Stadfest", "bootbox.submit": "Send inn", - "bootbox.send": "Send", + "bootbox.send": "Publiser", "cover.dragging-title": "Posisjonering av omslagsbilete", "cover.dragging-message": "Dra omslagsbiletet til ønska posisjon og klikk \"Lagre\"", "cover.saved": "Omslagsbilete og posisjon lagra", diff --git a/public/language/nn-NO/notifications.json b/public/language/nn-NO/notifications.json index b08b16179f..5677ae316f 100644 --- a/public/language/nn-NO/notifications.json +++ b/public/language/nn-NO/notifications.json @@ -50,13 +50,13 @@ "user-posted-to-dual": "%1 og %2 har posta svar til: %3", "user-posted-to-triple": "%1, %2 og %3 har posta svar til: %4", "user-posted-to-multiple": "%1, %2 og %3 andre har posta svar til: %4", - "user-posted-topic": "%1 har posta eit nytt emne: %2", + "user-posted-topic": "%1 har skrive eit nytt innlegg: %2", "user-edited-post": "%1 har redigert eit innlegg i %2", "user-posted-topic-with-tag": "%1 har posta %2 (merka %3)", "user-posted-topic-with-tag-dual": "%1 har posta %2 (merka %3 og %4)", "user-posted-topic-with-tag-triple": "%1 har posta %2 (merka %3, %4, og %5)", "user-posted-topic-with-tag-multiple": "%1 har posta %2 (merka %3)", - "user-posted-topic-in-category": "%1 har posta eit nytt emne i %2", + "user-posted-topic-in-category": "%1 har skrive eit nytt innlegg i %2", "user-started-following-you": "%1 starta å følgje deg.", "user-started-following-you-dual": "%1 og %2 starta å følgje deg.", "user-started-following-you-triple": "%1, %2 og %3 starta å følgje deg.", @@ -83,9 +83,9 @@ "notificationType-upvote": "Når nokon tilrår innlegget ditt", "notificationType-new-topic": "Når nokon du følgjer opprettar eit emne", "notificationType-new-topic-with-tag": "Når eit emne vert oppretta med eit emneord du følgjer", - "notificationType-new-topic-in-category": "Når eit emne vert oppretta i ein kategori du følgjer", - "notificationType-new-reply": "Når eit nytt svar vert posta i eit emne du følgjer", - "notificationType-post-edit": "Når eit innlegg vert redigert i eit emne du følgjer", + "notificationType-new-topic-in-category": "Når eit innlegg vert oppretta i ein kategori du følgjer", + "notificationType-new-reply": "Når eit nytt svar vert posta i eit innlegg du følgjer", + "notificationType-post-edit": "Når eit svar blir redigert i eit innlegg du følgjer", "notificationType-follow": "Når nokon startar å følgje deg", "notificationType-new-chat": "Når du mottek ei chatmelding", "notificationType-new-group-chat": "Når du mottek ei gruppemelding", diff --git a/public/language/nn-NO/tags.json b/public/language/nn-NO/tags.json index fd1087d24c..19f4a2b929 100644 --- a/public/language/nn-NO/tags.json +++ b/public/language/nn-NO/tags.json @@ -10,8 +10,8 @@ "tag-whitelist": "Kviteliste for emneord", "watching": "Følgjer", "not-watching": "Følgjer ikkje", - "watching.description": "Varsle meg om nye emne.", - "not-watching.description": "Ikkje varsle meg om nye emne.", + "watching.description": "Varsle meg om nye innlegg", + "not-watching.description": "Ikkje varsle meg om nye innlegg", "following-tag.message": "Du vil no motta varsel når nokon postar eit emne med dette emneordet.", "not-following-tag.message": "Du vil ikkje motta varsel når nokon postar eit emne med dette emneordet." } \ No newline at end of file diff --git a/public/language/nn-NO/topic.json b/public/language/nn-NO/topic.json index 48f429a550..ee67d56f48 100644 --- a/public/language/nn-NO/topic.json +++ b/public/language/nn-NO/topic.json @@ -9,7 +9,7 @@ "posted-by": "Posta av %1", "posted-by-guest": "Posta av gjest", "chat": "Chat", - "notify-me": "Varsle meg om nye svar i dette emnet", + "notify-me": "Varsle meg om nye svar på dette innlegget", "quote": "Sitat", "reply": "Svar", "replies-to-this-post": "%1 svar", @@ -87,15 +87,15 @@ "mark-unread.success": "Emne merka som ulest.", "watch": "Følg", "unwatch": "Ikkje følg", - "watch.title": "Varsle meg om nye svar i dette emnet", - "unwatch.title": "Slutt å følgje dette emnet", + "watch.title": "Varsle meg om nye svar på dette innlegget", + "unwatch.title": "Slutt å følgje dette innlegget", "share-this-post": "Del dette innlegget", "watching": "Følgjer", "not-watching": "Følgjer ikkje", "ignoring": "Ignorerer", - "watching.description": "Varsle meg om nye svar.
Vis emne som ulest.", - "not-watching.description": "Ikkje varsle meg om nye svar.
Vis emne som ulest om kategorien ikkje er ignorert.", - "ignoring.description": "Ikkje varsle meg om nye svar.
Vis ikkje emne som ulest.", + "watching.description": "Varsle meg om nye svar.
Vis innlegg som ulest.", + "not-watching.description": "Ikkje varsle meg om nye svar.
Vis innlegg i ulest om kategorien ikkje er ignorert.", + "ignoring.description": "Ikkje varsle meg om nye svar.
Ikkje vis innlegg i ulest.", "thread-tools.title": "Emneverktøy", "thread-tools.markAsUnreadForAll": "Merk som ulest for alle", "thread-tools.pin": "Fest emne", @@ -144,8 +144,8 @@ "move-post": "Flytt innlegg", "post-moved": "Innlegg flytta!", "fork-topic": "Kopier emne", - "enter-new-topic-title": "Skriv inn ny emnetittel", - "fork-topic-instruction": "Klikk på innlegga du vil kopiere, skriv inn ein tittel for det nye emnet, og klikk på kopier emne", + "enter-new-topic-title": "Skriv tittel på innlegg", + "fork-topic-instruction": "Click the posts you want to fork, enter a title for the new topic and click fork topic", "fork-no-pids": "Ingen innlegg vald!", "no-posts-selected": "Ingen innlegg vald!", "x-posts-selected": "%1 innlegg vald", @@ -157,7 +157,7 @@ "merge-topic-list-title": "Liste over emne som skal slåast saman", "merge-options": "Samanslåingsalternativ", "merge-select-main-topic": "Vel hovudemnet", - "merge-new-title-for-topic": "Ny tittel for emne", + "merge-new-title-for-topic": "Ny tittel for innlegg", "topic-id": "Emne-ID", "move-posts-instruction": "Klikk på innlegga du vil flytte, og skriv inn ein emne-ID eller gå til målemnet", "move-topic-instruction": "Vel mål-kategorien, og klikk deretter på flytt", @@ -172,7 +172,7 @@ "composer.post-later": "Post seinare", "composer.schedule": "Planlegg", "composer.replying-to": "Svarar til %1", - "composer.new-topic": "Nytt emne", + "composer.new-topic": "Nytt innlegg", "composer.editing-in": "Redigerer innlegg i %1", "composer.uploading": "laster opp...", "composer.thumb-url-label": "Lim inn ein emne-miniatyr-URL", @@ -193,9 +193,9 @@ "most-votes": "Fleire stemmer", "most-posts": "Fleire innlegg", "most-views": "Fleire visningar", - "stale.title": "Opprett nytt emne i staden?", - "stale.warning": "Emnet du svarar på er gammalt. Ønskjer du å opprette eit nytt emne i staden, og referere til dette i svaret ditt?", - "stale.create": "Opprett nytt emne", + "stale.title": "Opprett nytt innlegg i staden?", + "stale.warning": "Innlegget du svarer på er gammalt. Ønskjer du å opprette eit nytt innlegg i staden, og referere til dette i svaret ditt?", + "stale.create": "Opprett nytt innlegg", "stale.reply-anyway": "Svar i dette emnet likevel", "link-back": "Sv: [%1](%2)", "diffs.title": "Innleggsendringshistorikk", diff --git a/public/language/nn-NO/unread.json b/public/language/nn-NO/unread.json index 37583696e6..71e51f34dd 100644 --- a/public/language/nn-NO/unread.json +++ b/public/language/nn-NO/unread.json @@ -9,7 +9,7 @@ "all-categories": "Alle kategoriar", "topics-marked-as-read.success": "Emne merka som lest!", "all-topics": "Alle emne", - "new-topics": "Nye emne", + "new-topics": "Nye innlegg", "watched-topics": "Emne du følgjer", "unreplied-topics": "Emne utan svar", "multiple-categories-selected": "Fleire vald" diff --git a/public/language/nn-NO/user.json b/public/language/nn-NO/user.json index 9f36c196ea..cd615d95be 100644 --- a/public/language/nn-NO/user.json +++ b/public/language/nn-NO/user.json @@ -110,7 +110,7 @@ "chat-deny-list": "Deny chat messages from the following users", "chat-list-add-user": "Add user", "digest-label": "Abonner på oppsummering", - "digest-description": "Abonner på e-postoppdateringar for dette forumet (nye varsel og emne) etter ei fastsett tidsplan", + "digest-description": "Abonner på e-postoppdateringar for dette forumet (nye varsel og innlegg) etter ein fastsett tidsplan", "digest-off": "Av", "digest-daily": "Dagleg", "digest-weekly": "Kvar veke", From 3d96afb2d1bbb16c397e537133fd3fea21f9ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 15 May 2025 09:38:43 -0400 Subject: [PATCH 3015/4744] feat: use local date string for digest subject closes #13420 --- src/user/digest.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/user/digest.js b/src/user/digest.js index 61f4b2f12f..127bd550b1 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -89,12 +89,19 @@ Digest.send = async function (data) { } let errorLogged = false; await batch.processArray(data.subscribers, async (uids) => { - let userData = await user.getUsersFields(uids, ['uid', 'email', 'email:confirmed', 'username', 'userslug', 'lastonline']); - userData = userData.filter(u => u && u.email && (meta.config.includeUnverifiedEmails || u['email:confirmed'])); + let userData = await user.getUsersFields(uids, [ + 'uid', 'email', 'email:confirmed', 'username', 'userslug', 'lastonline', + ]); + userData = userData.filter( + u => u && u.email && (meta.config.includeUnverifiedEmails || u['email:confirmed']) + ); if (!userData.length) { return; } - await Promise.all(userData.map(async (userObj) => { + const userSettings = await user.getMultipleUserSettings(userData.map(u => u.uid)); + const date = new Date(); + await Promise.all(userData.map(async (userObj, index) => { + const userSetting = userSettings[index]; const [publicRooms, notifications, topics] = await Promise.all([ getUnreadPublicRooms(userObj.uid), user.notifications.getUnreadInterval(userObj.uid, data.interval), @@ -118,9 +125,8 @@ Digest.send = async function (data) { }); emailsSent += 1; - const now = new Date(); await emailer.send('digest', userObj.uid, { - subject: `[[email:digest.subject, ${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}]]`, + subject: `[[email:digest.subject, ${date.toLocaleDateString(userSetting.userLang)}]]`, username: userObj.username, userslug: userObj.userslug, notifications: unreadNotifs, From 6c3e2a8e2291dc15163dfd4ec2b2034df5d9d9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 15 May 2025 09:42:55 -0400 Subject: [PATCH 3016/4744] refactor: create date once per digest.send --- src/user/digest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user/digest.js b/src/user/digest.js index 127bd550b1..77bc2e93e5 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -88,6 +88,7 @@ Digest.send = async function (data) { return emailsSent; } let errorLogged = false; + const date = new Date(); await batch.processArray(data.subscribers, async (uids) => { let userData = await user.getUsersFields(uids, [ 'uid', 'email', 'email:confirmed', 'username', 'userslug', 'lastonline', @@ -99,7 +100,6 @@ Digest.send = async function (data) { return; } const userSettings = await user.getMultipleUserSettings(userData.map(u => u.uid)); - const date = new Date(); await Promise.all(userData.map(async (userObj, index) => { const userSetting = userSettings[index]; const [publicRooms, notifications, topics] = await Promise.all([ From 45a11d45fc2cd09943934b52bd8bef155306fa65 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 15 May 2025 12:01:45 -0400 Subject: [PATCH 3017/4744] fix: #13419, handle remote content with mediaType text/markdown --- src/activitypub/mocks.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index f2a8446d4a..fee85cd6bd 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -42,7 +42,7 @@ const sanitizeConfig = { Mocks._normalize = async (object) => { // Normalized incoming AP objects into expected types for easier mocking - let { type, attributedTo, url, image, content, source, attachment } = object; + let { type, attributedTo, url, image, mediaType, content, source, attachment } = object; switch (true) { // non-string attributedTo handling case Array.isArray(attributedTo): { @@ -70,6 +70,9 @@ Mocks._normalize = async (object) => { if (sourceContent) { content = null; sourceContent = await activitypub.helpers.remoteAnchorToLocalProfile(sourceContent, true); + } else if (mediaType === 'text/markdown') { + sourceContent = await activitypub.helpers.remoteAnchorToLocalProfile(content, true); + content = null; } else if (content && content.length) { content = sanitize(content, sanitizeConfig); content = await activitypub.helpers.remoteAnchorToLocalProfile(content); From 3674fa578346483dac1f4e5922bf7d545a689785 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 15 May 2025 13:56:31 -0400 Subject: [PATCH 3018/4744] feat: save width and height values into post attachment --- src/activitypub/mocks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index fee85cd6bd..76cc8af01b 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -110,9 +110,9 @@ Mocks._normalize = async (object) => { const stream = url.reduce((memo, { type, mediaType, tag }) => { if (!memo) { if (type === 'Link' && mediaType === 'application/x-mpegURL') { - memo = tag.reduce((memo, { type, mediaType, href }) => { + memo = tag.reduce((memo, { type, mediaType, href, width, height }) => { if (!memo && (type === 'Link' && mediaType === 'video/mp4')) { - memo = { type, mediaType, href }; + memo = { mediaType, href, width, height }; } return memo; From 8f933459cded56711406f5008cd812294188e38c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 15 May 2025 15:38:57 -0400 Subject: [PATCH 3019/4744] fix: bring back auto-categorization if group and object are same-origin, handle Peertube putting channel names in `attributedTo` --- src/activitypub/mocks.js | 7 ++++++- src/activitypub/notes.js | 21 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 76cc8af01b..e5a8e8e363 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -42,7 +42,7 @@ const sanitizeConfig = { Mocks._normalize = async (object) => { // Normalized incoming AP objects into expected types for easier mocking - let { type, attributedTo, url, image, mediaType, content, source, attachment } = object; + let { type, attributedTo, url, image, mediaType, content, source, attachment, cc } = object; switch (true) { // non-string attributedTo handling case Array.isArray(attributedTo): { @@ -52,6 +52,10 @@ Mocks._normalize = async (object) => { } else if (typeof cur === 'object') { if (cur.type === 'Person' && cur.id) { valid.push(cur.id); + } else if (cur.type === 'Group' && cur.id) { + // Add any groups found to cc where it is expected + cc = Array.isArray(cc) ? cc : [cc]; + cc.push(cur.id); } } @@ -148,6 +152,7 @@ Mocks._normalize = async (object) => { return { ...object, + cc, attributedTo, content, sourceContent, diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 31ca249d3d..616fc3a44f 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -115,18 +115,33 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { if (hasTid) { mainPid = await topics.getTopicField(tid, 'mainPid'); } else { - // Check recipients/audience for local category + // Check recipients/audience for category (local or remote) const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']); await activitypub.actors.assert(Array.from(set)); + + // Local const resolved = await Promise.all(Array.from(set).map(async id => await activitypub.helpers.resolveLocalId(id))); const recipientCids = resolved .filter(Boolean) .filter(({ type }) => type === 'category') .map(obj => obj.id); - if (recipientCids.length) { + // Remote + let remoteCid; + const assertedGroups = await categories.exists(Array.from(set)); + try { + const { hostname } = new URL(mainPid); + remoteCid = Array.from(set).filter((id, idx) => { + const { hostname: cidHostname } = new URL(id); + return assertedGroups[idx] && cidHostname === hostname; + }).shift(); + } catch (e) { + // noop + } + + if (remoteCid || recipientCids.length) { // Overrides passed-in value, respect addressing from main post over booster - options.cid = recipientCids.shift(); + options.cid = remoteCid || recipientCids.shift(); } // mainPid ok to leave as-is From a460a55064e1280f36a0021e0510c7c557251030 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 15 May 2025 15:38:57 -0400 Subject: [PATCH 3020/4744] fix: bring back auto-categorization if group and object are same-origin, handle Peertube putting channel names in `attributedTo` --- src/activitypub/mocks.js | 7 ++++++- src/activitypub/notes.js | 21 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 76cc8af01b..e5a8e8e363 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -42,7 +42,7 @@ const sanitizeConfig = { Mocks._normalize = async (object) => { // Normalized incoming AP objects into expected types for easier mocking - let { type, attributedTo, url, image, mediaType, content, source, attachment } = object; + let { type, attributedTo, url, image, mediaType, content, source, attachment, cc } = object; switch (true) { // non-string attributedTo handling case Array.isArray(attributedTo): { @@ -52,6 +52,10 @@ Mocks._normalize = async (object) => { } else if (typeof cur === 'object') { if (cur.type === 'Person' && cur.id) { valid.push(cur.id); + } else if (cur.type === 'Group' && cur.id) { + // Add any groups found to cc where it is expected + cc = Array.isArray(cc) ? cc : [cc]; + cc.push(cur.id); } } @@ -148,6 +152,7 @@ Mocks._normalize = async (object) => { return { ...object, + cc, attributedTo, content, sourceContent, diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 31ca249d3d..616fc3a44f 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -115,18 +115,33 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { if (hasTid) { mainPid = await topics.getTopicField(tid, 'mainPid'); } else { - // Check recipients/audience for local category + // Check recipients/audience for category (local or remote) const set = activitypub.helpers.makeSet(_activitypub, ['to', 'cc', 'audience']); await activitypub.actors.assert(Array.from(set)); + + // Local const resolved = await Promise.all(Array.from(set).map(async id => await activitypub.helpers.resolveLocalId(id))); const recipientCids = resolved .filter(Boolean) .filter(({ type }) => type === 'category') .map(obj => obj.id); - if (recipientCids.length) { + // Remote + let remoteCid; + const assertedGroups = await categories.exists(Array.from(set)); + try { + const { hostname } = new URL(mainPid); + remoteCid = Array.from(set).filter((id, idx) => { + const { hostname: cidHostname } = new URL(id); + return assertedGroups[idx] && cidHostname === hostname; + }).shift(); + } catch (e) { + // noop + } + + if (remoteCid || recipientCids.length) { // Overrides passed-in value, respect addressing from main post over booster - options.cid = recipientCids.shift(); + options.cid = remoteCid || recipientCids.shift(); } // mainPid ok to leave as-is From 8f9f377121d38a1bb4aff121d35236b12bf75ebc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 15 May 2025 16:57:05 -0400 Subject: [PATCH 3021/4744] fix: add attachments to getpostsummaries call in search, #13324 --- src/search.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/search.js b/src/search.js index 8e9d947dae..baf4d3c340 100644 --- a/src/search.js +++ b/src/search.js @@ -146,7 +146,9 @@ async function searchInContent(data) { metadata.pids = metadata.pids.slice(start, start + itemsPerPage); } - returnData.posts = await posts.getPostSummaryByPids(metadata.pids, data.uid, {}); + returnData.posts = await posts.getPostSummaryByPids(metadata.pids, data.uid, { + extraFields: ['attachments'], + }); await plugins.hooks.fire('filter:search.contentGetResult', { result: returnData, data: data }); delete metadata.pids; delete metadata.data; From 0a574d72404f885e46e2a2783a24f129f5bfd3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 15 May 2025 18:23:38 -0400 Subject: [PATCH 3022/4744] fix: group edit url --- src/views/admin/partials/manage_user_groups.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/admin/partials/manage_user_groups.tpl b/src/views/admin/partials/manage_user_groups.tpl index 5d686d730f..03804d8be9 100644 --- a/src/views/admin/partials/manage_user_groups.tpl +++ b/src/views/admin/partials/manage_user_groups.tpl @@ -4,7 +4,7 @@ From 61a63851d4f26386494756c65c4385e09da61815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 15 May 2025 18:25:10 -0400 Subject: [PATCH 3023/4744] 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 90f4f723ba..e6500e7908 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.4", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.12", + "nodebb-theme-harmony": "2.1.13", "nodebb-theme-lavender": "7.1.19", - "nodebb-theme-peace": "2.2.42", - "nodebb-theme-persona": "14.1.11", + "nodebb-theme-peace": "2.2.43", + "nodebb-theme-persona": "14.1.12", "nodebb-widget-essentials": "7.0.38", "nodemailer": "7.0.3", "nprogress": "0.2.0", From 4602b6b7c8f58993c1b22d6ce174910587d91a31 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 16 May 2025 09:20:24 +0000 Subject: [PATCH 3024/4744] Latest translations and fallbacks --- public/language/ru/topic.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index 86c8306ecf..af877fb070 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -61,8 +61,8 @@ "user-restored-topic-on": "%1 восстановил эту тему в %2", "user-moved-topic-from-ago": "%1 переместил эту тему из %2 %3", "user-moved-topic-from-on": "%1 переместил эту тему из %2 в %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 поделился этой темой %2", + "user-shared-topic-on": "%1 поделился этой темой на %2", "user-queued-post-ago": "%1 добавил запись для одобрения %3", "user-queued-post-on": "%1 добавил запись для одобрения в %3", "user-referenced-topic-ago": "%1 сослался на эту тему %3", From ce5ef1ab6e98c2c8e91735beab5eb6ee9fec6ca5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 16 May 2025 10:04:39 -0400 Subject: [PATCH 3025/4744] fix: openapi schema to handle additional `attachments` field in postsobject --- public/openapi/components/schemas/PostsObject.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/public/openapi/components/schemas/PostsObject.yaml b/public/openapi/components/schemas/PostsObject.yaml index b43d965888..5a079a08d9 100644 --- a/public/openapi/components/schemas/PostsObject.yaml +++ b/public/openapi/components/schemas/PostsObject.yaml @@ -2,4 +2,15 @@ PostsObject: description: One of the objects in the array returned from `Posts.getPostSummaryByPids` type: array items: - $ref: ./PostObject.yaml#/PostObject \ No newline at end of file + allOf: + - $ref: ./PostObject.yaml#/PostObject + - type: object + description: Optional properties that may or may not be present (except for `pid`, which is always present, and is only here as a hack to pass validation) + properties: + pid: + type: number + description: A post identifier + attachments: + type: array + required: + - pid \ No newline at end of file From 948bfe46f1c0fdb5c9d7e56dbcf26f40586ce330 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 16 May 2025 11:43:26 -0400 Subject: [PATCH 3026/4744] test: fix tests to account for a460a55064e1280f36a0021e0510c7c557251030 --- test/activitypub/notes.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/activitypub/notes.js b/test/activitypub/notes.js index afd24081c5..fbbf0c59ec 100644 --- a/test/activitypub/notes.js +++ b/test/activitypub/notes.js @@ -451,7 +451,7 @@ describe('Notes', () => { assert.strictEqual(cid, -1); }); - it('should create a new topic in cid -1 even if a remote category is addressed', async () => { + it('should create a new topic in a remote category if addressed (category same-origin)', async () => { const { id: remoteCid } = helpers.mocks.group(); const { note, id } = helpers.mocks.note({ audience: [remoteCid], @@ -462,6 +462,23 @@ describe('Notes', () => { assert(await posts.exists(id)); + const cid = await posts.getCidByPid(id); + assert.strictEqual(cid, remoteCid); + }); + + it('should create a new topic in cid -1 if a non-same origin remote category is addressed', async () => { + const { id: remoteCid } = helpers.mocks.group({ + id: `https://example.com/${utils.generateUUID()}`, + }); + const { note, id } = helpers.mocks.note({ + audience: [remoteCid], + }); + const { activity } = helpers.mocks.create(note); + + await activitypub.inbox.create({ body: activity }); + + assert(await posts.exists(id)); + const cid = await posts.getCidByPid(id); assert.strictEqual(cid, -1); }); From 672dcc5d142e34f36cb9621dca1e05c7d41ee1ea Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 16 May 2025 16:37:49 +0000 Subject: [PATCH 3027/4744] chore: incrementing version number - v4.4.1 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index e6500e7908..abdbfcc2c0 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.4.0", + "version": "4.4.1", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From a686cf20624dfebf2077ddfb86054252deef7061 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 16 May 2025 16:37:49 +0000 Subject: [PATCH 3028/4744] chore: update changelog for v4.4.1 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5728c63beb..45ae952b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +#### v4.4.1 (2025-05-16) + +##### Chores + +* up themes (61a63851) +* incrementing version number - v4.4.0 (0a75eee3) +* update changelog for v4.4.0 (09cc91d5) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### New Features + +* save width and height values into post attachment (3674fa57) +* use local date string for digest subject (3d96afb2) + +##### Bug Fixes + +* openapi schema to handle additional `attachments` field in postsobject (ce5ef1ab) +* group edit url (0a574d72) +* add attachments to getpostsummaries call in search, #13324 (8f9f3771) +* bring back auto-categorization if group and object are same-origin, handle Peertube putting channel names in `attributedTo` (a460a550) +* #13419, handle remote content with mediaType text/markdown (45a11d45) + +##### Refactors + +* create date once per digest.send (6c3e2a8e) + +##### Tests + +* fix tests to account for a460a55064e1280f36a0021e0510c7c557251030 (948bfe46) + #### v4.4.0 (2025-05-14) ##### Breaking Changes From 0fe1e53cf9f52b4060359d44ed215597c3fa1cdf Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 18 May 2025 09:19:19 +0000 Subject: [PATCH 3029/4744] Latest translations and fallbacks --- public/language/nb/topic.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 3e8489d703..15db4e3016 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -67,8 +67,8 @@ "user-queued-post-on": "%1 i køpost til godkjenning %3", "user-referenced-topic-ago": "%1 refererte dette emnet %3", "user-referenced-topic-on": "%1 refererte dette innlegget dette innlegget på %3", - "user-forked-topic-ago": "%1 gaflet dette emnet %3", - "user-forked-topic-on": "%1 gaflet dette emnet på %3", + "user-forked-topic-ago": "%1 forgrenet dette emnet %3", + "user-forked-topic-on": "%1 forgrenet dette emnet på %3", "bookmark-instructions": "Klikk her for å gå tilbake til det siste svaret i denne tråden.", "flag-post": "Rapporter denne posten", "flag-user": "Rapporter denne brukeren", @@ -145,7 +145,7 @@ "post-moved": "Innlegg flyttet!", "fork-topic": "Forgren tråd", "enter-new-topic-title": "Skriv tittel på innlegg", - "fork-topic-instruction": "Click the posts you want to fork, enter a title for the new topic and click fork topic", + "fork-topic-instruction": "Velg innleggene du vil flytte til ny forgrenet emne, skriv tittel for det nye emnet og klikk forgren", "fork-no-pids": "Ingen innlegg valgt!", "no-posts-selected": "Ingen innlegg valgt.", "x-posts-selected": "%1 innlegg valgt", From 475b0704b9f5b950a435a74eb1b3dc0d15d249d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 13:14:48 -0400 Subject: [PATCH 3030/4744] chore(deps): update dependency @eslint/js to v9.27.0 (#13429) 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 e6500e7908..cf4bc84c5d 100644 --- a/install/package.json +++ b/install/package.json @@ -161,7 +161,7 @@ "@commitlint/cli": "19.8.1", "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", - "@eslint/js": "9.26.0", + "@eslint/js": "9.27.0", "@stylistic/eslint-plugin-js": "4.2.0", "eslint-config-nodebb": "1.1.4", "eslint-plugin-import": "2.31.0", From 2417a79b5fa8acc07f605db44f53a50078cbd024 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 13:15:06 -0400 Subject: [PATCH 3031/4744] fix(deps): update dependency sass to v1.89.0 (#13427) 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 cf4bc84c5d..1f813463aa 100644 --- a/install/package.json +++ b/install/package.json @@ -128,7 +128,7 @@ "rss": "1.2.2", "rtlcss": "4.3.0", "sanitize-html": "2.17.0", - "sass": "1.88.0", + "sass": "1.89.0", "satori": "0.13.1", "semver": "7.7.2", "serve-favicon": "2.5.0", From 650eeac9087c24117bf0ca98514d0a50a57a8126 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 13:15:23 -0400 Subject: [PATCH 3032/4744] chore(deps): update dependency mocha to v11.3.0 (#13426) 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 1f813463aa..6e6f15ce63 100644 --- a/install/package.json +++ b/install/package.json @@ -170,7 +170,7 @@ "husky": "8.0.3", "jsdom": "26.1.0", "lint-staged": "16.0.0", - "mocha": "11.2.2", + "mocha": "11.3.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", From 42f16da501dcfd74c5f7874e4bc73e11b9dedf4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 13:43:21 -0400 Subject: [PATCH 3033/4744] fix(deps): update dependency nodebb-plugin-dbsearch to v6.2.17 (#13432) 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 6e6f15ce63..a87883043a 100644 --- a/install/package.json +++ b/install/package.json @@ -99,7 +99,7 @@ "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.50", - "nodebb-plugin-dbsearch": "6.2.16", + "nodebb-plugin-dbsearch": "6.2.17", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.1", From 5d017710bda84ca268c9ddb93b44dcdb46de43af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 17:10:26 -0400 Subject: [PATCH 3034/4744] chore(deps): update dependency mocha to v11.4.0 (#13435) 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 a87883043a..f54667b509 100644 --- a/install/package.json +++ b/install/package.json @@ -170,7 +170,7 @@ "husky": "8.0.3", "jsdom": "26.1.0", "lint-staged": "16.0.0", - "mocha": "11.3.0", + "mocha": "11.4.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", From aa9772822afb260df4fa9cb22274e61dcebc412e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 17:11:40 -0400 Subject: [PATCH 3035/4744] chore(deps): update dependency sass-embedded to v1.89.0 (#13425) 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 f54667b509..95438ff373 100644 --- a/install/package.json +++ b/install/package.json @@ -177,7 +177,7 @@ "smtp-server": "3.13.6" }, "optionalDependencies": { - "sass-embedded": "1.88.0" + "sass-embedded": "1.89.0" }, "resolutions": { "*/jquery": "3.7.1" From ee8e223f2032fd3d0aa788615c69bd88e4ffb9a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 17:11:51 -0400 Subject: [PATCH 3036/4744] fix(deps): update dependency connect-redis to v8.1.0 (#13433) 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 95438ff373..6695ff083e 100644 --- a/install/package.json +++ b/install/package.json @@ -60,7 +60,7 @@ "connect-mongo": "5.1.0", "connect-multiparty": "2.2.0", "connect-pg-simple": "10.0.0", - "connect-redis": "8.0.3", + "connect-redis": "8.1.0", "cookie-parser": "1.4.7", "cron": "4.3.0", "cropperjs": "1.6.2", From 2e02d3f6730ebfb1a891ebf9a468fa407d8fffe2 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 20 May 2025 09:19:53 +0000 Subject: [PATCH 3037/4744] Latest translations and fallbacks --- public/language/nb/topic.json | 4 ++-- public/language/nn-NO/topic.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 15db4e3016..54fc6d6007 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -76,7 +76,7 @@ "view-flag-report": "Vis rapporteringsoversikt", "resolve-flag": "Behandle rapport", "merged-message": "Dette emnet er slått sammen med %2", - "forked-message": "This topic was forked from %2", + "forked-message": "Dette emnet vart forgrenet fra %2", "deleted-message": "Denne innlegget har blitt slettet. ", "following-topic.message": "Du vil nå motta varsler når noen svarer på dette innlegget.", "not-following-topic.message": "Du vil se dette innlegget i oversikten over uleste innlegg, men du vil ikke motta varslinger når noen skriver et svar.", @@ -145,7 +145,7 @@ "post-moved": "Innlegg flyttet!", "fork-topic": "Forgren tråd", "enter-new-topic-title": "Skriv tittel på innlegg", - "fork-topic-instruction": "Velg innleggene du vil flytte til ny forgrenet emne, skriv tittel for det nye emnet og klikk forgren", + "fork-topic-instruction": "Velg innleggene du vil flytte til nytt forgrenet emne, skriv tittel for det nye emnet og klikk forgren", "fork-no-pids": "Ingen innlegg valgt!", "no-posts-selected": "Ingen innlegg valgt.", "x-posts-selected": "%1 innlegg valgt", diff --git a/public/language/nn-NO/topic.json b/public/language/nn-NO/topic.json index ee67d56f48..4e6c94845e 100644 --- a/public/language/nn-NO/topic.json +++ b/public/language/nn-NO/topic.json @@ -145,7 +145,7 @@ "post-moved": "Innlegg flytta!", "fork-topic": "Kopier emne", "enter-new-topic-title": "Skriv tittel på innlegg", - "fork-topic-instruction": "Click the posts you want to fork, enter a title for the new topic and click fork topic", + "fork-topic-instruction": "Klikk på innlegga du vil forgreine, skriv tittel for det nye emnet og velg forgrein emne", "fork-no-pids": "Ingen innlegg vald!", "no-posts-selected": "Ingen innlegg vald!", "x-posts-selected": "%1 innlegg vald", From 385f4f12be4dbd41832bebbe043288ee524a1bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Tue, 20 May 2025 10:45:56 -0400 Subject: [PATCH 3038/4744] replace connect-multiparty with Multer (#13439) * post upload route * more multer changes keep name and type fields in file objects so we dont break all plugins using these * remove log * fix: thumbs delete * test: add array check --- install/package.json | 3 +-- src/controllers/accounts/edit.js | 2 +- src/controllers/admin/uploads.js | 16 +++++++------ src/controllers/uploads.js | 11 ++++----- src/middleware/index.js | 41 ++++++++++++++++---------------- src/middleware/uploads.js | 2 +- src/routes/admin.js | 12 +++++++--- src/routes/api.js | 8 ++++--- src/routes/authentication.js | 12 +++++++--- src/routes/helpers.js | 6 +++-- src/routes/write/topics.js | 11 +++++---- test/helpers/index.js | 2 +- 12 files changed, 73 insertions(+), 53 deletions(-) diff --git a/install/package.json b/install/package.json index b3dca2f562..e56c9e056c 100644 --- a/install/package.json +++ b/install/package.json @@ -58,7 +58,6 @@ "compression": "1.8.0", "connect-flash": "0.1.1", "connect-mongo": "5.1.0", - "connect-multiparty": "2.2.0", "connect-pg-simple": "10.0.0", "connect-redis": "8.1.0", "cookie-parser": "1.4.7", @@ -95,7 +94,7 @@ "mongodb": "6.16.0", "morgan": "1.10.0", "mousetrap": "1.6.5", - "multiparty": "4.2.3", + "multer": "2.0.0", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.50", diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js index 61a13399a0..1192687a5f 100644 --- a/src/controllers/accounts/edit.js +++ b/src/controllers/accounts/edit.js @@ -143,7 +143,7 @@ async function renderRoute(name, req, res) { } editController.uploadPicture = async function (req, res, next) { - const userPhoto = req.files.files[0]; + const userPhoto = req.files[0]; try { const updateUid = await user.getUidByUserslug(req.params.userslug); const isAllowed = await privileges.users.canEdit(req.uid, updateUid); diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 56d64674cf..433edd07ce 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -13,7 +13,9 @@ const image = require('../../image'); const plugins = require('../../plugins'); const pagination = require('../../pagination'); -const allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; +const allowedImageTypes = [ + 'image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml', +]; const uploadsController = module.exports; @@ -147,7 +149,7 @@ async function getFileData(currentDir, file) { } uploadsController.uploadCategoryPicture = async function (req, res, next) { - const uploadedFile = req.files.files[0]; + const uploadedFile = req.files[0]; let params = null; try { @@ -202,7 +204,7 @@ async function sanitizeSvg(filePath) { } uploadsController.uploadFavicon = async function (req, res, next) { - const uploadedFile = req.files.files[0]; + const uploadedFile = req.files[0]; const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; await validateUpload(uploadedFile, allowedTypes); @@ -217,7 +219,7 @@ uploadsController.uploadFavicon = async function (req, res, next) { }; uploadsController.uploadTouchIcon = async function (req, res, next) { - const uploadedFile = req.files.files[0]; + const uploadedFile = req.files[0]; const allowedTypes = ['image/png']; const sizes = [36, 48, 72, 96, 144, 192, 512]; @@ -244,7 +246,7 @@ uploadsController.uploadTouchIcon = async function (req, res, next) { uploadsController.uploadMaskableIcon = async function (req, res, next) { - const uploadedFile = req.files.files[0]; + const uploadedFile = req.files[0]; const allowedTypes = ['image/png']; await validateUpload(uploadedFile, allowedTypes); @@ -263,7 +265,7 @@ uploadsController.uploadLogo = async function (req, res, next) { }; uploadsController.uploadFile = async function (req, res, next) { - const uploadedFile = req.files.files[0]; + const uploadedFile = req.files[0]; let params; try { params = JSON.parse(req.body.params); @@ -294,7 +296,7 @@ uploadsController.uploadOgImage = async function (req, res, next) { }; async function upload(name, req, res, next) { - const uploadedFile = req.files.files[0]; + const uploadedFile = req.files[0]; await validateUpload(uploadedFile, allowedImageTypes); const filename = name + path.extname(uploadedFile.name); diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index d2e392b5d3..d2cfb48107 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -18,7 +18,7 @@ const uploadsController = module.exports; uploadsController.upload = async function (req, res, filesIterator) { let files; try { - files = req.files.files; + files = req.files; } catch (e) { return helpers.formatApiResponse(400, res); } @@ -27,9 +27,6 @@ uploadsController.upload = async function (req, res, filesIterator) { if (!Array.isArray(files)) { return helpers.formatApiResponse(500, res, new Error('[[error:invalid-file]]')); } - if (Array.isArray(files[0])) { - files = files[0]; - } try { const images = []; @@ -126,7 +123,7 @@ async function resizeImage(fileObj) { uploadsController.uploadThumb = async function (req, res) { if (!meta.config.allowTopicsThumbnail) { - deleteTempFiles(req.files.files); + deleteTempFiles(req.files); return helpers.formatApiResponse(503, res, new Error('[[error:topic-thumbnails-are-disabled]]')); } @@ -201,7 +198,9 @@ async function saveFileToLocal(uid, folder, uploadedFile) { } function deleteTempFiles(files) { - files.forEach(fileObj => file.delete(fileObj.path)); + if (Array.isArray(files)) { + files.forEach(fileObj => file.delete(fileObj.path)); + } } require('../promisify')(uploadsController, ['upload', 'uploadPost', 'uploadThumb']); diff --git a/src/middleware/index.js b/src/middleware/index.js index 5a0e69842f..417d8309bc 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -5,7 +5,6 @@ const validator = require('validator'); const nconf = require('nconf'); const toobusy = require('toobusy-js'); const util = require('util'); -const multipart = require('connect-multiparty'); const { csrfSynchronisedProtection } = require('./csrf'); const plugins = require('../plugins'); @@ -27,7 +26,7 @@ const delayCache = cacheCreate({ ttl: 1000 * 60, max: 200, }); -const multipartMiddleware = multipart(); + const middleware = module.exports; @@ -101,17 +100,30 @@ middleware.pluginHooks = helpers.try(async (req, res, next) => { }); middleware.validateFiles = function validateFiles(req, res, next) { - if (!req.files.files) { + if (!req.files) { return next(new Error(['[[error:invalid-files]]'])); } - - if (Array.isArray(req.files.files) && req.files.files.length) { - return next(); + function makeFilesCompatible(files) { + if (Array.isArray(files)) { + // multer uses originalname and mimetype, but we use name and type + files.forEach((file) => { + if (file.originalname) { + file.name = file.originalname; + } + if (file.mimetype) { + file.type = file.mimetype; + } + }); + } + next(); + } + if (Array.isArray(req.files) && req.files.length) { + return makeFilesCompatible(req.files); } - if (typeof req.files.files === 'object') { - req.files.files = [req.files.files]; - return next(); + if (typeof req.files === 'object') { + req.files = [req.files]; + return makeFilesCompatible(req.files); } return next(new Error(['[[error:invalid-files]]'])); @@ -291,14 +303,3 @@ middleware.checkRequired = function (fields, req, res, next) { controllers.helpers.formatApiResponse(400, res, new Error(`[[error:required-parameters-missing, ${missing.join(' ')}]]`)); }; - -middleware.handleMultipart = (req, res, next) => { - // Applies multipart handler on applicable content-type - const { 'content-type': contentType } = req.headers; - - if (contentType && !contentType.startsWith('multipart/form-data')) { - return next(); - } - - multipartMiddleware(req, res, next); -}; diff --git a/src/middleware/uploads.js b/src/middleware/uploads.js index d1ce5b09b2..9b434b3286 100644 --- a/src/middleware/uploads.js +++ b/src/middleware/uploads.js @@ -23,7 +23,7 @@ exports.ratelimit = helpers.try(async (req, res, next) => { ttl: meta.config.uploadRateLimitCooldown * 1000, }); } - const count = (cache.get(`${req.ip}:uploaded_file_count`) || 0) + req.files.files.length; + const count = (cache.get(`${req.ip}:uploaded_file_count`) || 0) + req.files.length; if (count > meta.config.uploadRateLimitThreshold) { return next(new Error(['[[error:upload-ratelimit-reached]]'])); } diff --git a/src/routes/admin.js b/src/routes/admin.js index 89a3050ee6..b7e751695c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -82,10 +82,16 @@ function apiRoutes(router, name, middleware, controllers) { router.get(`/api/${name}/analytics`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.dashboard.getAnalytics)); router.get(`/api/${name}/advanced/cache/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.cache.dump)); - const multipart = require('connect-multiparty'); - const multipartMiddleware = multipart(); + const multer = require('multer'); + const storage = multer.diskStorage({}); + const upload = multer({ storage }); - const middlewares = [multipartMiddleware, middleware.validateFiles, middleware.applyCSRF, middleware.ensureLoggedIn]; + const middlewares = [ + upload.array('files[]', 20), + middleware.validateFiles, + middleware.applyCSRF, + middleware.ensureLoggedIn, + ]; router.post(`/api/${name}/category/uploadpicture`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadCategoryPicture)); router.post(`/api/${name}/uploadfavicon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadFavicon)); diff --git a/src/routes/api.js b/src/routes/api.js index 0fe575a326..e374e242a4 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -23,11 +23,13 @@ module.exports = function (app, middleware, controllers) { router.get('/topic/teaser/:topic_id', [...middlewares], helpers.tryRoute(controllers.topics.teaser)); router.get('/topic/pagination/:topic_id', [...middlewares], helpers.tryRoute(controllers.topics.pagination)); - const multipart = require('connect-multiparty'); - const multipartMiddleware = multipart(); + const multer = require('multer'); + const storage = multer.diskStorage({}); + const upload = multer({ storage }); + const postMiddlewares = [ middleware.maintenanceMode, - multipartMiddleware, + upload.array('files[]', 20), middleware.validateFiles, middleware.uploads.ratelimit, middleware.applyCSRF, diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 9d89df90e1..720675b29d 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -154,9 +154,15 @@ Auth.reloadRoutes = async function (params) { }); }); - const multipart = require('connect-multiparty'); - const multipartMiddleware = multipart(); - const middlewares = [multipartMiddleware, Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist]; + + const multer = require('multer'); + const storage = multer.diskStorage({}); + const upload = multer({ storage }); + const middlewares = [ + upload.any(), + Auth.middleware.applyCSRF, + Auth.middleware.applyBlacklist, + ]; router.post('/register', middlewares, controllers.authentication.register); router.post('/register/complete', middlewares, controllers.authentication.registerComplete); diff --git a/src/routes/helpers.js b/src/routes/helpers.js index 34a455076e..2109a0bd9c 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -54,7 +54,9 @@ helpers.setupApiRoute = function (...args) { const [router, verb, name] = args; let middlewares = args.length > 4 ? args[args.length - 2] : []; const controller = args[args.length - 1]; - + const multer = require('multer'); + const storage = multer.diskStorage({}); + const upload = multer({ storage }); middlewares = [ middleware.autoLocale, middleware.applyBlacklist, @@ -63,7 +65,7 @@ helpers.setupApiRoute = function (...args) { middleware.registrationComplete, middleware.pluginHooks, middleware.logApiUsage, - middleware.handleMultipart, + upload.any(), ...middlewares, ]; diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index df10f66633..eb56cdaf42 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -10,9 +10,6 @@ const { setupApiRoute } = routeHelpers; module.exports = function () { const middlewares = [middleware.ensureLoggedIn]; - const multipart = require('connect-multiparty'); - const multipartMiddleware = multipart(); - setupApiRoute(router, 'post', '/', [middleware.checkRequired.bind(null, ['cid', 'title', 'content'])], controllers.write.topics.create); setupApiRoute(router, 'get', '/:tid', [], controllers.write.topics.get); setupApiRoute(router, 'post', '/:tid', [middleware.checkRequired.bind(null, ['content']), middleware.assert.topic], controllers.write.topics.reply); @@ -37,7 +34,13 @@ module.exports = function () { setupApiRoute(router, 'delete', '/:tid/tags', [...middlewares, middleware.assert.topic], controllers.write.topics.deleteTags); setupApiRoute(router, 'get', '/:tid/thumbs', [], controllers.write.topics.getThumbs); - setupApiRoute(router, 'post', '/:tid/thumbs', [multipartMiddleware, middleware.validateFiles, middleware.uploads.ratelimit, ...middlewares], controllers.write.topics.addThumb); + + setupApiRoute(router, 'post', '/:tid/thumbs', [ + middleware.validateFiles, + middleware.uploads.ratelimit, + ...middlewares, + ], controllers.write.topics.addThumb); + setupApiRoute(router, 'put', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['tid'])], controllers.write.topics.migrateThumbs); setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['path'])], controllers.write.topics.deleteThumb); setupApiRoute(router, 'put', '/:tid/thumbs/order', [...middlewares, middleware.checkRequired.bind(null, ['path', 'order'])], controllers.write.topics.reorderThumbs); diff --git a/test/helpers/index.js b/test/helpers/index.js index e71a05edaa..89de878ce8 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -95,7 +95,7 @@ helpers.uploadFile = async function (uploadEndPoint, filePath, data, jar, csrf_t const file = await fs.promises.readFile(filePath); const blob = new Blob([file], { type: mime.getType(filePath) }); - form.append('files', blob, path.basename(filePath)); + form.append('files[]', blob, path.basename(filePath)); if (data && data.params) { form.append('params', data.params); From 314a4ff047cdf1a3e1e2b681f35719ec95c41bcc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 10:56:59 -0400 Subject: [PATCH 3039/4744] fix(deps): update dependency webpack to v5.99.9 (#13438) 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 e56c9e056c..2f51844f91 100644 --- a/install/package.json +++ b/install/package.json @@ -146,7 +146,7 @@ "toobusy-js": "0.5.1", "tough-cookie": "5.1.2", "validator": "13.15.0", - "webpack": "5.99.8", + "webpack": "5.99.9", "webpack-merge": "6.0.1", "winston": "3.17.0", "workerpool": "9.2.0", From 136e88140f689a4133894fdd2771f911e4e212fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 10:57:09 -0400 Subject: [PATCH 3040/4744] chore(deps): update dependency smtp-server to v3.13.7 (#13437) 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 2f51844f91..5ea7e1476b 100644 --- a/install/package.json +++ b/install/package.json @@ -173,7 +173,7 @@ "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", - "smtp-server": "3.13.6" + "smtp-server": "3.13.7" }, "optionalDependencies": { "sass-embedded": "1.89.0" From 1d624aadbe8aefe54badaf48602f2a09956259fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 10:57:23 -0400 Subject: [PATCH 3041/4744] fix(deps): update dependency commander to v14 (#13434) 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 5ea7e1476b..24e7f42ec7 100644 --- a/install/package.json +++ b/install/package.json @@ -53,7 +53,7 @@ "chart.js": "4.4.9", "cli-graph": "3.2.2", "clipboard": "2.0.11", - "commander": "13.1.0", + "commander": "14.0.0", "compare-versions": "6.1.1", "compression": "1.8.0", "connect-flash": "0.1.1", From a16bc7382cee1fe5c278ca05bd1c014203de8ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 22 May 2025 11:01:05 -0400 Subject: [PATCH 3042/4744] 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 abdbfcc2c0..cce2fc7c1e 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.4", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.13", + "nodebb-theme-harmony": "2.1.14", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.43", "nodebb-theme-persona": "14.1.12", From 99234b3f97af00c136d126fd748e8c6cbb1ff989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 22 May 2025 11:16:14 -0400 Subject: [PATCH 3043/4744] 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 cce2fc7c1e..331013f700 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.4", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.14", + "nodebb-theme-harmony": "2.1.15", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.43", "nodebb-theme-persona": "14.1.12", From 76a624b9ca2004f76f101300922ca6dfa17f4fee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 11:31:52 -0400 Subject: [PATCH 3044/4744] fix(deps): update dependency diff to v8.0.2 (#13440) 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 8236c389f8..35d41c8db3 100644 --- a/install/package.json +++ b/install/package.json @@ -65,7 +65,7 @@ "cropperjs": "1.6.2", "csrf-sync": "4.2.1", "daemon": "1.1.0", - "diff": "8.0.1", + "diff": "8.0.2", "esbuild": "0.25.4", "express": "4.21.2", "express-session": "1.18.1", From e70e990a1aa932a3f994b2205bacc2f35730aa01 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 22 May 2025 14:13:41 -0400 Subject: [PATCH 3045/4744] feat: restrict access to ap.probe method to registered users, add rate limiting protection --- src/activitypub/index.js | 22 ++++++++++++++++++++++ src/controllers/activitypub/index.js | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index c87a0654f3..4067405080 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -27,6 +27,9 @@ const probeCache = ttl({ max: 500, ttl: 1000 * 60 * 60, // 1 hour }); +const probeRateLimit = ttl({ + ttl: 1000 * 3, // 3 seconds +}); const ActivityPub = module.exports; @@ -506,6 +509,13 @@ ActivityPub.probe = async ({ uid, url }) => { * - Returns a relative path if already available, true if not, and false otherwise. */ + // Disable on config setting; restrict lookups to HTTPS-enabled URLs only + const { activitypubProbe } = meta.config; + const { protocol } = new URL(url); + if (!activitypubProbe || protocol !== 'https:') { + return false; + } + // Known resources const [isNote, isMessage, isActor, isActorUrl] = await Promise.all([ posts.exists(url), @@ -541,6 +551,17 @@ ActivityPub.probe = async ({ uid, url }) => { } } + // Guests not allowed to use expensive logic path + if (!uid) { + return false; + } + + // One request allowed every 3 seconds (configured at top) + const limited = probeRateLimit.get(uid); + if (limited) { + return false; + } + // Cached result if (probeCache.has(url)) { return probeCache.get(url); @@ -572,6 +593,7 @@ ActivityPub.probe = async ({ uid, url }) => { return false; } try { + probeRateLimit.set(uid, true); return await checkHeader(meta.config.activitypubProbeTimeout || 2000); } catch (e) { if (e.name === 'TimeoutError') { diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 36054d6616..0e7112ddfd 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -31,7 +31,7 @@ Controller.fetch = async (req, res, next) => { if (typeof result === 'string') { return helpers.redirect(res, result); } else if (result) { - const { id, type } = await activitypub.get('uid', req.uid || 0, url.href); + const { id, type } = await activitypub.get('uid', req.uid, url.href); switch (true) { case activitypub._constants.acceptedPostTypes.includes(type): { return helpers.redirect(res, `/post/${encodeURIComponent(id)}`); From 777ecdf2c14b7882f550cb9e4140b6ccda566c00 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 23 May 2025 09:20:20 +0000 Subject: [PATCH 3046/4744] Latest translations and fallbacks --- public/language/bg/modules.json | 2 +- public/language/he/global.json | 6 +++--- public/language/ru/admin/advanced/events.json | 8 ++++---- public/language/ru/category.json | 2 +- public/language/ru/error.json | 2 +- public/language/ru/global.json | 10 +++++----- public/language/ru/user.json | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index ba3da12344..f6485e5213 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -120,7 +120,7 @@ "bootbox.ok": "Добре", "bootbox.cancel": "Отказ", "bootbox.confirm": "Потвърждаване", - "bootbox.submit": "Публикуване", + "bootbox.submit": "Изпращане", "bootbox.send": "Изпращане", "cover.dragging-title": "Наместване на снимката", "cover.dragging-message": "Преместете снимката на желаното положение и натиснете „Запазване“", diff --git a/public/language/he/global.json b/public/language/he/global.json index 428cd81ac4..7ec0571727 100644 --- a/public/language/he/global.json +++ b/public/language/he/global.json @@ -32,7 +32,7 @@ "pagination.enter-index": "עבור למיקום פוסט", "pagination.go-to-page": "ניווט לדף", "pagination.page-x": "עמוד %1", - "header.brand-logo": "לוגו מותג", + "header.brand-logo": "לוגו אתר", "header.admin": "ניהול", "header.categories": "קטגוריות", "header.recent": "פוסטים אחרונים", @@ -134,8 +134,8 @@ "upload": "העלאה", "uploads": "העלאות", "allowed-file-types": "פורמטי הקבצים המורשים הם %1", - "unsaved-changes": "יש לכם שינויים שלא נשמרו. האם הנכם בטוחים שברצונכם להמשיך?", - "reconnecting-message": "החיבור ל-%1 אבד, אנא המתינו בזמן שאנו מנסים לחבר אתכם מחדש", + "unsaved-changes": "יש שינויים שלא נשמרו. האם אתם בטוחים שברצונכם להמשיך?", + "reconnecting-message": "החיבור ל-%1 אבד, נא להמתין בזמן שאנו מנסים לחבר אתכם מחדש", "play": "נגן", "cookies.message": "אתר זה משתמש ב cookies על מנת לשפר את חוויות המשתמש.", "cookies.accept": "קיבלתי!", diff --git a/public/language/ru/admin/advanced/events.json b/public/language/ru/admin/advanced/events.json index f1d1c69dea..9ec80306ff 100644 --- a/public/language/ru/admin/advanced/events.json +++ b/public/language/ru/admin/advanced/events.json @@ -9,9 +9,9 @@ "filter-type": "Тип события", "filter-start": "Дата начала", "filter-end": "Дата окончания", - "filter-user": "Filter by User", - "filter-user.placeholder": "Type user name to filter...", - "filter-group": "Filter by Group", - "filter-group.placeholder": "Type group name to filter...", + "filter-user": "Фильтровать по пользователю", + "filter-user.placeholder": "Введите имя пользователя для фильтрации…", + "filter-group": "Фильтровать по группе", + "filter-group.placeholder": "Введите название группы для фильтрации…", "filter-per-page": "Записей на страницу" } \ No newline at end of file diff --git a/public/language/ru/category.json b/public/language/ru/category.json index 4739a2510f..79ef86d73a 100644 --- a/public/language/ru/category.json +++ b/public/language/ru/category.json @@ -7,7 +7,7 @@ "new-topic-button": "Создать тему", "guest-login-post": "Авторизуйтесь, чтобы написать сообщение", "no-topics": "В этой категории еще нет тем.
Почему бы вам не создать первую?", - "no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.", + "no-followers": "Никто на этом сайте не отслеживает эту категорию. Начните отслеживать или наблюдать за этой категорией, чтобы получать обновления.", "browsing": "просматривают", "no-replies": "Нет ответов", "no-new-posts": "Нет новых сообщений", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index c419fcb3c8..89555f0367 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -38,7 +38,7 @@ "email-not-confirmed": "Вы не сможете отправлять сообщения, пока ваш адрес электронной почты не подтверждён. Пожалуйста, нажмите здесь, чтобы подтвердить его.", "email-not-confirmed-chat": "Вы не можете оставлять сообщения, пока ваша электронная почта не подтверждена. Отправить письмо с кодом подтверждения повторно.", "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", - "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "no-email-to-confirm": "В вашей учётной записи не указан адрес электронной почты. Он необходим для восстановления доступа, а также может потребоваться для общения и публикации в некоторых разделах. Пожалуйста, нажмите здесь, чтобы указать адрес электронной почты.", "user-doesnt-have-email": "У пользователя %1 не задана электронная почта.", "email-confirm-failed": "По техническим причинам мы не можем подтвердить ваш адрес электронной почты. Приносим вам наши извинения, пожалуйста, попробуйте позже.", "confirm-email-already-sent": "Сообщение для подтверждения регистрации уже выслано на ваш адрес электронной почты. Повторная отправка возможна через %1 мин.", diff --git a/public/language/ru/global.json b/public/language/ru/global.json index 01765d3374..d4381e5558 100644 --- a/public/language/ru/global.json +++ b/public/language/ru/global.json @@ -24,14 +24,14 @@ "cancel": "Cancel", "close": "Закрыть", "pagination": "Разбивка на страницы", - "pagination.previouspage": "Previous Page", - "pagination.nextpage": "Next Page", - "pagination.firstpage": "First Page", - "pagination.lastpage": "Last Page", + "pagination.previouspage": "Предыдущая страница", + "pagination.nextpage": "Следующая страница", + "pagination.firstpage": "Первая страница", + "pagination.lastpage": "Последняя страница", "pagination.out-of": "%1 из %2", "pagination.enter-index": "Go to post index", "pagination.go-to-page": "Go to page", - "pagination.page-x": "Page %1", + "pagination.page-x": "Страница %1", "header.brand-logo": "Brand Logo", "header.admin": "Админка", "header.categories": "Категории", diff --git a/public/language/ru/user.json b/public/language/ru/user.json index 60982acf7a..2942cb492a 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -108,7 +108,7 @@ "disable-incoming-chats": "Disable incoming chat messages ", "chat-allow-list": "Allow chat messages from the following users", "chat-deny-list": "Deny chat messages from the following users", - "chat-list-add-user": "Add user", + "chat-list-add-user": "Добавить участника", "digest-label": "Подписка на дайджест", "digest-description": "Подписаться на рассылку уведомлений о событиях и новых темах на форуме с указанной периодичностью", "digest-off": "Отключена", From c18464757885aed98ccf481808b84c8c6b59dc39 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 19:51:45 -0400 Subject: [PATCH 3047/4744] chore(deps): update dependency mocha to v11.5.0 (#13442) 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 35d41c8db3..d98b9d2cbf 100644 --- a/install/package.json +++ b/install/package.json @@ -169,7 +169,7 @@ "husky": "8.0.3", "jsdom": "26.1.0", "lint-staged": "16.0.0", - "mocha": "11.4.0", + "mocha": "11.5.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", From e3a7fb5ccb05b95cc82eaee209e0b35f57a9c146 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 06:11:41 -0400 Subject: [PATCH 3048/4744] fix(deps): update dependency bootbox to v6.0.4 (#13443) 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 d98b9d2cbf..2b96813e70 100644 --- a/install/package.json +++ b/install/package.json @@ -46,7 +46,7 @@ "bcryptjs": "3.0.2", "benchpressjs": "2.5.5", "body-parser": "2.2.0", - "bootbox": "6.0.3", + "bootbox": "6.0.4", "bootstrap": "5.3.6", "bootswatch": "5.3.6", "chalk": "4.1.2", From 30aa0fe6d25c0b5aac9df422818911e909c178aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 24 May 2025 11:49:49 -0400 Subject: [PATCH 3049/4744] 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 331013f700..b47fba2015 100644 --- a/install/package.json +++ b/install/package.json @@ -99,7 +99,7 @@ "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.50", - "nodebb-plugin-dbsearch": "6.2.16", + "nodebb-plugin-dbsearch": "6.2.18", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.1", From 3ca6a9bcfa89a36069ea506857f1d3e41b38400c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 11:54:27 -0400 Subject: [PATCH 3050/4744] fix(deps): update dependency nodebb-plugin-dbsearch to v6.2.18 (#13445) 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 2b96813e70..5df8baa302 100644 --- a/install/package.json +++ b/install/package.json @@ -98,7 +98,7 @@ "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.50", - "nodebb-plugin-dbsearch": "6.2.17", + "nodebb-plugin-dbsearch": "6.2.18", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.1", From e2de0ec212bda29ae419f81fc92cad122da99e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 24 May 2025 16:50:53 -0400 Subject: [PATCH 3051/4744] 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 b47fba2015..c8298570fd 100644 --- a/install/package.json +++ b/install/package.json @@ -99,7 +99,7 @@ "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.50", - "nodebb-plugin-dbsearch": "6.2.18", + "nodebb-plugin-dbsearch": "6.2.19", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.1", From aeeda7c3be86c0941b94323df274d6af64ee2041 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 25 May 2025 09:19:33 +0000 Subject: [PATCH 3052/4744] Latest translations and fallbacks --- public/language/vi/category.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/vi/category.json b/public/language/vi/category.json index 4abc703c44..ccddbfda2b 100644 --- a/public/language/vi/category.json +++ b/public/language/vi/category.json @@ -1,7 +1,7 @@ { "category": "Danh mục", "subcategories": "Danh mục phụ", - "uncategorized": "Chưa có danh mục", + "uncategorized": "Chưa phân loại", "uncategorized.description": "Các chủ đề không phù hợp với bất kỳ danh mục hiện có nào", "handle.description": "Có thể theo dõi danh mục này từ mạng xã hội mở thông qua xử lý %1", "new-topic-button": "Chủ Đề Mới", From fd2ae7261e0e8378c10e7b8863c368ce864d5edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 25 May 2025 19:04:01 -0400 Subject: [PATCH 3053/4744] chore: up eslint stylistic --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index c8298570fd..29c2898f4f 100644 --- a/install/package.json +++ b/install/package.json @@ -162,8 +162,8 @@ "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", "@eslint/js": "9.26.0", - "@stylistic/eslint-plugin-js": "4.2.0", - "eslint-config-nodebb": "1.1.4", + "@stylistic/eslint-plugin-js": "4.4.0", + "eslint-config-nodebb": "1.1.5", "eslint-plugin-import": "2.31.0", "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", From a888b868c70361b2298c2fa1eb4770085c2c6689 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 26 May 2025 14:49:48 -0400 Subject: [PATCH 3054/4744] fix: additional tests for remote privileges, enforcing privileges for remote edits and deletes --- src/activitypub/inbox.js | 14 ++- test/activitypub/helpers.js | 23 ++++ test/activitypub/privileges.js | 204 +++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 test/activitypub/privileges.js diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index ab02cef6db..0369b11248 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -95,6 +95,12 @@ inbox.update = async (req) => { try { switch (true) { case isNote: { + const cid = await posts.getCidByPid(object.id); + const allowed = await privileges.categories.can('posts:edit', cid, activitypub._constants.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + const postData = await activitypub.mocks.post(object); postData.tags = await activitypub.notes._normalizeTags(postData._activitypub.tag, postData.cid); await posts.edit(postData); @@ -200,7 +206,7 @@ inbox.delete = async (req) => { const objectHostname = new URL(pid).hostname; if (actorHostname !== objectHostname) { - throw new Error('[[error:activitypub.origin-mismatch]]'); + return reject('Delete', object, actor); } const [isNote/* , isActor */] = await Promise.all([ @@ -210,6 +216,12 @@ inbox.delete = async (req) => { switch (true) { case isNote: { + const cid = await posts.getCidByPid(pid); + 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 }); diff --git a/test/activitypub/helpers.js b/test/activitypub/helpers.js index 2690910e06..3bad2f7002 100644 --- a/test/activitypub/helpers.js +++ b/test/activitypub/helpers.js @@ -202,3 +202,26 @@ Helpers.mocks.update = (override = {}) => { return { activity }; }; +Helpers.mocks.delete = (override = {}) => { + let actor = override.actor; + let object = override.object; + if (!actor) { + ({ id: actor } = Helpers.mocks.person()); + } + if (!object) { + ({ id: object } = Helpers.mocks.note()); + } + + const activity = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: `${Helpers.mocks._baseUrl}/delete/${encodeURIComponent(object.id || object)}`, + type: 'Delete', + to: [activitypub._constants.publicAddress], + cc: [`${actor}/followers`], + actor, + object, + }; + + return { activity }; +}; + diff --git a/test/activitypub/privileges.js b/test/activitypub/privileges.js new file mode 100644 index 0000000000..2284b7203d --- /dev/null +++ b/test/activitypub/privileges.js @@ -0,0 +1,204 @@ +'use strict'; + +const assert = require('assert'); +const nconf = require('nconf'); + +const db = require('../mocks/databasemock'); +const user = require('../../src/user'); +const topics = require('../../src/topics'); +const posts = require('../../src/posts'); +const categories = require('../../src/categories'); +const privileges = require('../../src/privileges'); +const meta = require('../../src/meta'); +const install = require('../../src/install'); +const utils = require('../../src/utils'); +const activitypub = require('../../src/activitypub'); + +const helpers = require('./helpers'); + +describe('Privilege logic for remote users/content (ActivityPub)', () => { + before(async () => { + meta.config.activitypubEnabled = 1; + // await install.giveWorldPrivileges(); + }); + + describe('"fediverse" pseudo-user', () => { + describe('no privileges given', () => { + let uid; + let cid; + let topicData; + let postData; + let mainPid; + let handle; + + before(async () => { + uid = await user.create({ username: utils.generateUUID() }); + ({ cid } = await categories.create({ name: utils.generateUUID() })); + ({ topicData, postData } = await topics.post({ + cid, + uid, + title: utils.generateUUID(), + content: utils.generateUUID(), + })); + handle = await categories.getCategoryField(cid, 'handle'); + const privsToRemove = await privileges.categories.getGroupPrivilegeList(); + await privileges.categories.rescind(privsToRemove, cid, ['fediverse']); + }); + + describe('incoming requests', () => { + it('should not respond to a webfinger request to a category\'s handle', async () => { + const response = await activitypub.helpers.query(`${handle}@${nconf.get('url_parsed').hostname}`); + assert.strictEqual(response, false); + }); + + it('should not respond to a request for the category actor', async () => { + await assert.rejects( + activitypub.get('uid', uid, `${nconf.get('url')}/category/${cid}`), + { message: '[[error:activitypub.get-failed]]' } + ); + }); + + it('should not respond to a request for a topic collection', async () => { + await assert.rejects( + activitypub.get('uid', uid, `${nconf.get('url')}/topic/${topicData.tid}`), + { message: '[[error:activitypub.get-failed]]' } + ); + }); + + it('should not respond to a request for a post', async () => { + await assert.rejects( + activitypub.get('uid', uid, `${nconf.get('url')}/post/${topicData.mainPid}`), + { message: '[[error:activitypub.get-failed]]' } + ); + }); + }); + + describe('incoming activities', () => { + describe('Create(Note)', () => { + let note; + let activity; + + before(async () => { + ({ note } = helpers.mocks.note({ + cc: [`${nconf.get('url')}/category/${cid}`], + })); + ({ activity } = helpers.mocks.create(note)); + await activitypub.inbox.create({ body: activity }); + }); + + it('should not assert the note', async () => { + const exists = await posts.exists(note.id); + assert.strictEqual(exists, false); + }); + }); + + describe('Update(Note)', () => { + let note; + let activity; + + before(async () => { + ({ note } = helpers.mocks.note({ + cc: [`${nconf.get('url')}/category/${cid}`], + })); + ({ activity } = helpers.mocks.create(note)); + await privileges.categories.give(['groups:topics:create'], cid, ['fediverse']); + await activitypub.inbox.create({ body: activity }); + }); + + after(async () => { + await privileges.categories.rescind(['groups:topics:create'], cid, ['fediverse']); + }); + + it('should assert the note', async () => { + const exists = await posts.exists(note.id); + assert.strictEqual(exists, true); + }); + + it('should not allow edits to the note', async () => { + const oldContent = note.content; + note.content = 'new content'; + ({ activity } = helpers.mocks.update({ + object: note, + })); + + await activitypub.inbox.update({ body: activity }); + + const postData = await posts.getPostData(note.id); + assert.strictEqual(postData.content, oldContent); + assert.strictEqual(postData.edited, 0); + }); + }); + + describe('Delete(Note)', () => { + let note; + let activity; + + before(async () => { + ({ note } = helpers.mocks.note({ + cc: [`${nconf.get('url')}/category/${cid}`], + })); + ({ activity } = helpers.mocks.create(note)); + await privileges.categories.give(['groups:topics:create'], cid, ['fediverse']); + await activitypub.inbox.create({ body: activity }); + }); + + after(async () => { + await privileges.categories.rescind(['groups:topics:create'], cid, ['fediverse']); + }); + + it('should assert the note', async () => { + const exists = await posts.exists(note.id); + assert.strictEqual(exists, true); + }); + + it('should ignore remote deletion of said note', async () => { + ({ activity } = helpers.mocks.delete({ object: note })); + await activitypub.inbox.delete({ body: activity }); + + const exists = await posts.exists(note.id); + assert.strictEqual(exists, true); + }); + }); + }); + + describe('outgoing requests', () => { + it('should not federate out a new post', async () => { + + }); + + it('should not federate out a post edit', async () => { + + }); + + it('should not federate out a post deletion', async () => { + + }); + + it('should not federate out a post announce', async () => { + + }); + }); + }); + + describe('regular privilege set', () => { + let cid; + let handle; + + before(async () => { + ({ cid } = await categories.create({ name: utils.generateUUID() })); + handle = await categories.getCategoryField(cid, 'handle'); + const privsToRemove = await privileges.categories.getGroupPrivilegeList(); + }); + + describe('groups:find', () => { + it('should return webfinger response to a category\'s handle', async () => { + const response = await activitypub.helpers.query(`${handle}@${nconf.get('url_parsed').hostname}`); + + assert(response); + assert.strictEqual(response.subject, `acct:${handle}@${nconf.get('url_parsed').hostname}`); + assert.strictEqual(response.hostname, nconf.get('url_parsed').hostname); + }); + }); + }); + }); +}); \ No newline at end of file From 6a5bbe9204092434ba446521cbde74ae0f12cef1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 09:09:21 -0400 Subject: [PATCH 3055/4744] fix(deps): update dependency esbuild to v0.25.5 (#13447) 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 580f3c992b..7ad282fd29 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.4", + "esbuild": "0.25.5", "express": "4.21.2", "express-session": "1.18.1", "express-useragent": "1.0.15", From 6efe3fdd02f9359d77c97eddf86179737b5792d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 17:36:42 -0400 Subject: [PATCH 3056/4744] chore(deps): update dependency lint-staged to v16.1.0 (#13449) 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 7ad282fd29..e5bebd24ef 100644 --- a/install/package.json +++ b/install/package.json @@ -168,7 +168,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "26.1.0", - "lint-staged": "16.0.0", + "lint-staged": "16.1.0", "mocha": "11.5.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From 36f0cf250fc2c5e04d13a4709d2131e5232e3878 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 09:04:04 -0400 Subject: [PATCH 3057/4744] fix(deps): update dependency validator to v13.15.15 (#13451) 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 e5bebd24ef..ecd3950cb8 100644 --- a/install/package.json +++ b/install/package.json @@ -145,7 +145,7 @@ "tinycon": "0.6.8", "toobusy-js": "0.5.1", "tough-cookie": "5.1.2", - "validator": "13.15.0", + "validator": "13.15.15", "webpack": "5.99.9", "webpack-merge": "6.0.1", "winston": "3.17.0", From b20a6ed0d700f0a02cdf5d9e42d6f0fd42b6f4e0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 28 May 2025 12:31:53 -0400 Subject: [PATCH 3058/4744] fix: missed handling zset on ap unfollow --- src/api/activitypub.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/activitypub.js b/src/api/activitypub.js index ce78f12d23..57a9bfe287 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -126,6 +126,7 @@ activitypubApi.unfollow = enabledCheck(async (caller, { type, id, actor }) => { await Promise.all([ db.sortedSetRemove(`followingRemote:${id}`, actor), db.sortedSetRemove(`followRequests:uid.${id}`, actor), + db.sortedSetRemove(`followersRemote:${actor}`, id), db.decrObjectField(`user:${id}`, 'followingRemoteCount'), ]); } else if (type === 'cid') { From 49b5268e529a403ca929797361f853c3c40f301d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 28 May 2025 14:53:32 -0400 Subject: [PATCH 3059/4744] fix: send actor in undo(follow) --- src/api/activitypub.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/activitypub.js b/src/api/activitypub.js index 57a9bfe287..1f074f6776 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -119,6 +119,7 @@ activitypubApi.unfollow = enabledCheck(async (caller, { type, id, actor }) => { await activitypub.send(type, id, [actor], { id: `${nconf.get('url')}/${type}/${id}#activity/undo:follow/${encodeURIComponent(actor)}/${timestamp}`, type: 'Undo', + actor: object.actor, object, }); From 72417d82bd05c26dd8e38220fd9dc05792a07fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 29 May 2025 11:36:46 -0400 Subject: [PATCH 3060/4744] fix: closes #13454, align dropdowns to opposite side on rtl --- public/scss/generics.scss | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/scss/generics.scss b/public/scss/generics.scss index 808d427ba5..0938613ec9 100644 --- a/public/scss/generics.scss +++ b/public/scss/generics.scss @@ -23,11 +23,13 @@ display: block; } } -.dropdown-left { - .dropdown-menu { --bs-position: start; } +html[data-dir="ltr"] { + .dropdown-left .dropdown-menu { --bs-position: start; } + .dropdown-right .dropdown-menu { --bs-position: end; } } -.dropdown-right { - .dropdown-menu { --bs-position: end; } +html[data-dir="rtl"] { + .dropdown-left .dropdown-menu { --bs-position: end; } + .dropdown-right .dropdown-menu { --bs-position: start; } } .category-dropdown-menu { From 0c1a61839efc6bfb57b945e223584c5c70c69177 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 29 May 2025 12:49:56 -0400 Subject: [PATCH 3061/4744] test: fix groups:find webfinger test --- test/activitypub/privileges.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/activitypub/privileges.js b/test/activitypub/privileges.js index 2284b7203d..1c8d2ec7f2 100644 --- a/test/activitypub/privileges.js +++ b/test/activitypub/privileges.js @@ -4,6 +4,7 @@ const assert = require('assert'); const nconf = require('nconf'); const db = require('../mocks/databasemock'); +const request = require('../../src/request'); const user = require('../../src/user'); const topics = require('../../src/topics'); const posts = require('../../src/posts'); @@ -192,11 +193,12 @@ describe('Privilege logic for remote users/content (ActivityPub)', () => { describe('groups:find', () => { it('should return webfinger response to a category\'s handle', async () => { - const response = await activitypub.helpers.query(`${handle}@${nconf.get('url_parsed').hostname}`); + const { response, body } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${handle}@${nconf.get('url_parsed').host}`); assert(response); - assert.strictEqual(response.subject, `acct:${handle}@${nconf.get('url_parsed').hostname}`); - assert.strictEqual(response.hostname, nconf.get('url_parsed').hostname); + assert.strictEqual(response.statusCode, 200); + assert(body.links && body.links.length); + assert.strictEqual(body.subject, `acct:${handle}@${nconf.get('url_parsed').host}`); }); }); }); From a80edfa1f11823bd6e0047b7b51ebbc0493d83c3 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 29 May 2025 15:15:06 -0400 Subject: [PATCH 3062/4744] fix: patch ap .probe() so that it does not execute on requests for its own resources --- src/activitypub/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 4067405080..80ec4a40f5 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -511,8 +511,8 @@ ActivityPub.probe = async ({ uid, url }) => { // Disable on config setting; restrict lookups to HTTPS-enabled URLs only const { activitypubProbe } = meta.config; - const { protocol } = new URL(url); - if (!activitypubProbe || protocol !== 'https:') { + const { protocol, host } = new URL(url); + if (!activitypubProbe || protocol !== 'https:' || host === nconf.get('url_parsed').host) { return false; } From 78de8c6da12919e60660142bff46ebde5ad23b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 30 May 2025 09:22:06 -0400 Subject: [PATCH 3063/4744] fix: allow guests to load topic tools if they have privilege to view them display errors from topics.loadTopicTools --- public/src/client/topic/threadTools.js | 2 +- src/socket.io/topics/tools.js | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index a83a9d86b5..7a2519b069 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -225,7 +225,7 @@ define('forum/topic/threadTools', [ return; } dropdownMenu.html(helpers.generatePlaceholderWave([8, 8, 8])); - const data = await socket.emit('topics.loadTopicTools', { tid: ajaxify.data.tid, cid: ajaxify.data.cid }); + const data = await socket.emit('topics.loadTopicTools', { tid: ajaxify.data.tid, cid: ajaxify.data.cid }).catch(alerts.error); const html = await app.parseAndTranslate('partials/topic/topic-menu-list', data); $(dropdownMenu).attr('data-loaded', 'true').html(html); hooks.fire('action:topic.tools.load', { diff --git a/src/socket.io/topics/tools.js b/src/socket.io/topics/tools.js index 80bab41662..e4044a5272 100644 --- a/src/socket.io/topics/tools.js +++ b/src/socket.io/topics/tools.js @@ -6,9 +6,6 @@ const plugins = require('../../plugins'); module.exports = function (SocketTopics) { SocketTopics.loadTopicTools = async function (socket, data) { - if (!socket.uid) { - throw new Error('[[error:no-privileges]]'); - } if (!data) { throw new Error('[[error:invalid-data]]'); } @@ -21,7 +18,7 @@ module.exports = function (SocketTopics) { if (!topicData) { throw new Error('[[error:no-topic]]'); } - if (!userPrivileges['topics:read']) { + if (!userPrivileges['topics:read'] || !userPrivileges.view_thread_tools) { throw new Error('[[error:no-privileges]]'); } topicData.privileges = userPrivileges; From 390f6428506f98458045fe07b6f594508141cc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 30 May 2025 11:00:08 -0400 Subject: [PATCH 3064/4744] fix: browser title translation --- src/middleware/render.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/middleware/render.js b/src/middleware/render.js index e01110936f..9741585428 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -150,6 +150,7 @@ module.exports = function (middleware) { async function loadClientHeaderFooterData(req, res, options) { const registrationType = meta.config.registrationType || 'normal'; res.locals.config = res.locals.config || {}; + const userLang = res.locals.config.userLang || meta.config.userLang || 'en-GB'; const templateValues = { title: meta.config.title || '', 'title:url': meta.config['title:url'] || '', @@ -180,9 +181,9 @@ module.exports = function (middleware) { blocks: user.blocks.list(req.uid), user: user.getUserData(req.uid), isEmailConfirmSent: req.uid <= 0 ? false : await user.email.isValidationPending(req.uid), - languageDirection: translator.translate('[[language:dir]]', res.locals.config.userLang), - timeagoCode: languages.userTimeagoCode(res.locals.config.userLang), - browserTitle: translator.translate(controllersHelpers.buildTitle(title)), + languageDirection: translator.translate('[[language:dir]]', userLang), + timeagoCode: languages.userTimeagoCode(userLang), + browserTitle: translator.translate(controllersHelpers.buildTitle(title), userLang), navigation: navigation.get(req.uid), roomIds: req.uid > 0 ? db.getSortedSetRevRange(`uid:${req.uid}:chat:rooms`, 0, 0) : [], }); From 28c021a01bf86abaf8404261a9d161ad99328a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 30 May 2025 11:11:45 -0400 Subject: [PATCH 3065/4744] fix: remove null categories --- src/categories/search.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/categories/search.js b/src/categories/search.js index bd6a96435c..9e3d869974 100644 --- a/src/categories/search.js +++ b/src/categories/search.js @@ -44,7 +44,8 @@ module.exports = function (Categories) { const childrenCids = await getChildrenCids(cids, uid); const uniqCids = _.uniq(cids.concat(childrenCids)); - const categoryData = await Categories.getCategories(uniqCids); + let categoryData = await Categories.getCategories(uniqCids); + categoryData = categoryData.filter(Boolean); Categories.getTree(categoryData, 0); await Categories.getRecentTopicReplies(categoryData, uid, data.qs); From 57a5de26827693f86b5804b2e389930dac16c0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 30 May 2025 11:15:02 -0400 Subject: [PATCH 3066/4744] refactor: use strings for cids --- src/categories/search.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/categories/search.js b/src/categories/search.js index 9e3d869974..50521d882a 100644 --- a/src/categories/search.js +++ b/src/categories/search.js @@ -5,7 +5,6 @@ const _ = require('lodash'); const privileges = require('../privileges'); const activitypub = require('../activitypub'); const plugins = require('../plugins'); -const utils = require('../utils'); const db = require('../database'); module.exports = function (Categories) { @@ -65,7 +64,7 @@ module.exports = function (Categories) { return c1.order - c2.order; }); searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2); - searchResult.categories = categoryData.filter(c => cids.includes(c.cid)); + searchResult.categories = categoryData.filter(c => cids.includes(String(c.cid))); return searchResult; }; @@ -82,7 +81,7 @@ module.exports = function (Categories) { const split = data.split(':'); split.shift(); const cid = split.join(':'); - return utils.isNumber(cid) ? parseInt(cid, 10) : cid; + return cid; }); } From ebb88c1277945887062c1e669f5e4a93cbffd2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 30 May 2025 11:45:04 -0400 Subject: [PATCH 3067/4744] feat: add action:post-queue.save fires after a post is added to the post queue --- src/posts/queue.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/posts/queue.js b/src/posts/queue.js index cc3b1078c8..9f6b21636d 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -188,13 +188,16 @@ module.exports = function (Posts) { data: data, }; payload = await plugins.hooks.fire('filter:post-queue.save', payload); - payload.data = JSON.stringify(data); await db.sortedSetAdd('post:queue', now, id); - await db.setObject(`post:queue:${id}`, payload); + await db.setObject(`post:queue:${id}`, { + ...payload, + data: JSON.stringify(payload.data), + }); await user.setUserField(data.uid, 'lastqueuetime', now); cache.del('post-queue'); + await plugins.hooks.fire('action:post-queue.save', payload); const cid = await getCid(type, data); const uids = await getNotificationUids(cid); const bodyLong = await parseBodyLong(cid, type, data); From 629eec7b5b17c55984dd690b281224d9139a57d4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 30 May 2025 16:49:15 -0400 Subject: [PATCH 3068/4744] =?UTF-8?q?fix:=20add=20try..catch=20wrapper=20a?= =?UTF-8?q?round=20Announce(Like)=20call=20to=20internal=20method=20so=20a?= =?UTF-8?q?s=20to=20not=20return=20a=20500=20=E2=80=94=20just=20drop=20the?= =?UTF-8?q?=20Like=20activity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/activitypub/inbox.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 0369b11248..9b0f5c6987 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -294,9 +294,13 @@ inbox.announce = async (req) => { const { id: localId } = await activitypub.helpers.resolveLocalId(id); const exists = await posts.exists(localId || id); if (exists) { - const result = await posts.upvote(localId || id, object.actor); - if (localId) { - socketHelpers.upvote(result, 'notifications:upvoted-your-post-in'); + try { + const result = await posts.upvote(localId || id, object.actor); + if (localId) { + socketHelpers.upvote(result, 'notifications:upvoted-your-post-in'); + } + } catch (e) { + // vote denied due to local limitations (frequency, privilege, etc.); noop. } } From 0d595008b0bc08dfe52449fe43015257c0f71fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 30 May 2025 17:12:54 -0400 Subject: [PATCH 3069/4744] chore: eslint config --- eslint.config.mjs | 2 +- install/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index dd39fc1544..47cfa158f5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,7 @@ import publicConfig from 'eslint-config-nodebb/public'; import commonRules from 'eslint-config-nodebb/common'; import { defineConfig } from 'eslint/config'; -import stylisticJs from '@stylistic/eslint-plugin-js' +import stylisticJs from '@stylistic/eslint-plugin' import js from '@eslint/js'; import globals from 'globals'; diff --git a/install/package.json b/install/package.json index ecd3950cb8..1126e4fbc7 100644 --- a/install/package.json +++ b/install/package.json @@ -161,8 +161,8 @@ "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", "@eslint/js": "9.27.0", - "@stylistic/eslint-plugin-js": "4.4.0", - "eslint-config-nodebb": "1.1.5", + "@stylistic/eslint-plugin": "4.4.0", + "eslint-config-nodebb": "1.1.6", "eslint-plugin-import": "2.31.0", "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", From 83a55f6adcd246920ba08415dcdf46505503c4a4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sat, 31 May 2025 22:46:47 -0400 Subject: [PATCH 3070/4744] fix: don't throw on unknown post on Undo(Like) --- src/activitypub/inbox.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index 9b0f5c6987..ec2f9d7fb0 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -546,7 +546,8 @@ inbox.undo = async (req) => { case 'Like': { const exists = await posts.exists(id); if (localType !== 'post' || !exists) { - throw new Error('[[error:invalid-pid]]'); + reject('Like', object, actor); + break; } const allowed = await privileges.posts.can('posts:upvote', id, activitypub._constants.uid); From cc9270262074b154fd6d3a5df7d1f354f3b4cb37 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 1 Jun 2025 00:31:58 -0400 Subject: [PATCH 3071/4744] fix: add try..catch around topics.post in note assertion logic --- src/activitypub/notes.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 616fc3a44f..64c6dcd53f 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -196,8 +196,8 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { const { to, cc, attachment } = mainPost._activitypub; const tags = await Notes._normalizeTags(mainPost._activitypub.tag || []); - await Promise.all([ - topics.post({ + try { + await topics.post({ tid, uid: authorId, cid: options.cid || cid, @@ -208,13 +208,16 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { content: mainPost.content, sourceContent: mainPost.sourceContent, _activitypub: mainPost._activitypub, - }), - Notes.updateLocalRecipients(mainPid, { to, cc }), - ]); - unprocessed.shift(); + }); + unprocessed.shift(); + } catch (e) { + activitypub.helpers.log(`[activitypub/notes.assert] Could not post topic (${mainPost.pid}): ${e.message}`); + return null; + } // These must come after topic is posted await Promise.all([ + Notes.updateLocalRecipients(mainPid, { to, cc }), mainPost._activitypub.image ? topics.thumbs.associate({ id: tid, path: mainPost._activitypub.image, From ff00829b3f9cad8ee726eab84b231e9dcc10c953 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 1 Jun 2025 09:19:27 +0000 Subject: [PATCH 3072/4744] Latest translations and fallbacks --- public/language/hr/category.json | 2 +- public/language/hr/themes/harmony.json | 2 +- public/language/hr/topic.json | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/language/hr/category.json b/public/language/hr/category.json index ff5b1cb9c5..a02822d8d7 100644 --- a/public/language/hr/category.json +++ b/public/language/hr/category.json @@ -5,7 +5,7 @@ "uncategorized.description": "Topics that do not strictly fit in with any existing categories", "handle.description": "This category can be followed from the open social web via the handle %1", "new-topic-button": "Nova Tema", - "guest-login-post": "Prijavi se za objavu", + "guest-login-post": "Prijavite se da biste objavili", "no-topics": "Nema tema u ovoj kategoriji.
Zašto ne probate napisati novu?", "no-followers": "Nobody on this website is tracking or watching this category. Track or watch this category in order to begin receiving updates.", "browsing": "pregledavanje", diff --git a/public/language/hr/themes/harmony.json b/public/language/hr/themes/harmony.json index 727a1b0553..3d91fe5a4f 100644 --- a/public/language/hr/themes/harmony.json +++ b/public/language/hr/themes/harmony.json @@ -6,7 +6,7 @@ "sidebar-toggle": "Sidebar Toggle", "login-register-to-search": "Login or register to search.", "settings.title": "Theme settings", - "settings.enableQuickReply": "Enable quick reply", + "settings.enableQuickReply": "Omogući brzi odgovor", "settings.enableBreadcrumbs": "Show breadcrumbs in Category and Topic pages", "settings.enableBreadcrumbs.why": "Breadcrumbs are visible in most pages for ease-of-navigation. The base design of the category and topic pages has alternative means to link back to parent pages, but the breadcrumb can be toggled off to reduce clutter.", "settings.centerHeaderElements": "Center header elements", diff --git a/public/language/hr/topic.json b/public/language/hr/topic.json index 3b5ca2abc8..fdf37e5614 100644 --- a/public/language/hr/topic.json +++ b/public/language/hr/topic.json @@ -17,8 +17,8 @@ "last-reply-time": "Zadnji odgovor", "reply-options": "Reply options", "reply-as-topic": "Odgovori kao temu", - "guest-login-reply": "Prijavi se za objavu", - "login-to-view": "🔒 Log in to view", + "guest-login-reply": "Prijavite se kako bi odgovorili", + "login-to-view": "🔒 Prijavite se kako bi vidjeli sadržaj", "edit": "Uredi", "delete": "Obriši", "delete-event": "Delete Event", @@ -215,7 +215,7 @@ "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", - "post-quick-reply": "Quick reply", + "post-quick-reply": " Brzi odgovor", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", From fcb3bfbc35221f207a83b197766edc06d0f05cdb Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 1 Jun 2025 12:40:37 -0400 Subject: [PATCH 3073/4744] fix: return 200 for non-implemented activities instead of 501 --- src/controllers/activitypub/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 0e7112ddfd..de478a6021 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -145,7 +145,7 @@ Controller.postInbox = async (req, res) => { const method = String(req.body.type).toLowerCase(); if (!activitypub.inbox.hasOwnProperty(method)) { winston.warn(`[activitypub/inbox] Received Activity of type ${method} but unable to handle. Ignoring.`); - return res.sendStatus(501); + return res.sendStatus(200); } try { From b1022566da98b5b58f08a1efc76daba345eac232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 2 Jun 2025 09:55:20 -0400 Subject: [PATCH 3074/4744] fix: closes #13458, check if plugin is system plugin before activate/deactive/install/uninstall --- public/language/en-GB/error.json | 1 + src/plugins/install.js | 10 ++++++++++ src/socket.io/admin/plugins.js | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 82a623ea32..ec51a88ead 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -271,6 +271,7 @@ "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/src/plugins/install.js b/src/plugins/install.js index 21d993226d..f3da1c3fb8 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -156,6 +156,16 @@ module.exports = function (Plugins) { } }; + Plugins.isSystemPlugin = async function (id) { + const pluginDir = path.join(paths.nodeModules, id, 'plugin.json'); + try { + const pluginData = JSON.parse(await fs.readFile(pluginDir, 'utf8')); + return pluginData && pluginData.system === true; + } catch (err) { + return false; + } + }; + Plugins.isActive = async function (id) { if (nconf.get('plugins:active')) { return nconf.get('plugins:active').includes(id); diff --git a/src/socket.io/admin/plugins.js b/src/socket.io/admin/plugins.js index 4489be42bd..32f5ff61d0 100644 --- a/src/socket.io/admin/plugins.js +++ b/src/socket.io/admin/plugins.js @@ -11,6 +11,9 @@ const { pluginNamePattern } = require('../../constants'); const Plugins = module.exports; Plugins.toggleActive = async function (socket, plugin_id) { + if (await plugins.isSystemPlugin(plugin_id)) { + throw new Error('[[error:cannot-toggle-system-plugin]]'); + } postsCache.reset(); const data = await plugins.toggleActive(plugin_id); await events.log({ @@ -22,6 +25,9 @@ Plugins.toggleActive = async function (socket, plugin_id) { }; Plugins.toggleInstall = async function (socket, data) { + if (await plugins.isSystemPlugin(data.id)) { + throw new Error('[[error:cannot-toggle-system-plugin]]'); + } const isInstalled = await plugins.isInstalled(data.id); const isStarterPlan = nconf.get('saas_plan') === 'starter'; if ((isStarterPlan || nconf.get('acpPluginInstallDisabled')) && !isInstalled) { From 524a1e8bfe403fa240e804f076943871264caf2f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 1 Jun 2025 12:40:37 -0400 Subject: [PATCH 3075/4744] fix: return 200 for non-implemented activities instead of 501 --- src/controllers/activitypub/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 0e7112ddfd..de478a6021 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -145,7 +145,7 @@ Controller.postInbox = async (req, res) => { const method = String(req.body.type).toLowerCase(); if (!activitypub.inbox.hasOwnProperty(method)) { winston.warn(`[activitypub/inbox] Received Activity of type ${method} but unable to handle. Ignoring.`); - return res.sendStatus(501); + return res.sendStatus(200); } try { From 9d3b8c3abcd60aa5a6d85ff804008e7d8345a95b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 22 May 2025 14:14:53 -0400 Subject: [PATCH 3076/4744] feat: add protection mechanism to request lib so that network requests to reserved IP ranges throw an error --- public/language/en-GB/error.json | 2 ++ src/request.js | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 82a623ea32..2ca10a839f 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -4,6 +4,8 @@ "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "not-logged-in": "You don't seem to be logged in.", "account-locked": "Your account has been locked temporarily", "search-requires-login": "Searching requires an account - please login or register.", diff --git a/src/request.js b/src/request.js index b84b198914..4806eabc41 100644 --- a/src/request.js +++ b/src/request.js @@ -1,10 +1,18 @@ 'use strict'; +const dns = require('dns').promises; + const nconf = require('nconf'); +const ipaddr = require('ipaddr.js'); const { CookieJar } = require('tough-cookie'); const fetchCookie = require('fetch-cookie').default; const { version } = require('../package.json'); +const ttl = require('./cache/ttl'); +const checkCache = ttl({ + ttl: 1000 * 60 * 60, // 1 hour +}); + exports.jar = function () { return new CookieJar(); }; @@ -13,6 +21,11 @@ const userAgent = `NodeBB/${version.split('.').shift()}.x (${nconf.get('url')})` // Initialize fetch - somewhat hacky, but it's required for globalDispatcher to be available async function call(url, method, { body, timeout, jar, ...config } = {}) { + const ok = await check(url); + if (!ok) { + throw new Error('[[error:reserved-ip-address]]'); + } + let fetchImpl = fetch; if (jar) { fetchImpl = fetchCookie(fetch, jar); @@ -75,6 +88,40 @@ async function call(url, method, { body, timeout, jar, ...config } = {}) { }; } +// Checks url to ensure it is not in reserved IP range (private, etc.) +async function check(url) { + const cached = checkCache.get(url); + if (cached) { + return cached; + } + + const addresses = new Set(); + if (ipaddr.isValid(url)) { + addresses.add(url); + } else { + const { host } = new URL(url); + const [v4, v6] = await Promise.all([ + dns.resolve4(host), + dns.resolve6(host), + ]); + v4.forEach((ip) => { + addresses.add(ip); + }); + v6.forEach((ip) => { + addresses.add(ip); + }); + } + + // Every IP address that the host resolves to should be a unicast address + const ok = Array.from(addresses).every((ip) => { + const parsed = ipaddr.parse(ip); + return parsed.range() === 'unicast'; + }); + + checkCache.set(url, ok); + return ok; +} + /* const { body, response } = await request.get('someurl?foo=1&baz=2') */ From df36021628c47f584d94b88f69dbcd6e3fdba29a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 22 May 2025 15:36:22 -0400 Subject: [PATCH 3077/4744] fix: simplify dns to use .lookup instead of .resolve4 and .resolve6, automatically allow requests to own hostname --- src/request.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/request.js b/src/request.js index 4806eabc41..86ad8c5fc7 100644 --- a/src/request.js +++ b/src/request.js @@ -90,6 +90,11 @@ async function call(url, method, { body, timeout, jar, ...config } = {}) { // Checks url to ensure it is not in reserved IP range (private, etc.) async function check(url) { + const { host } = new URL(url); + if (host === nconf.get('url_parsed').host) { + return true; + } + const cached = checkCache.get(url); if (cached) { return cached; @@ -99,16 +104,9 @@ async function check(url) { if (ipaddr.isValid(url)) { addresses.add(url); } else { - const { host } = new URL(url); - const [v4, v6] = await Promise.all([ - dns.resolve4(host), - dns.resolve6(host), - ]); - v4.forEach((ip) => { - addresses.add(ip); - }); - v6.forEach((ip) => { - addresses.add(ip); + const lookup = await dns.lookup(host, { all: true }); + lookup.forEach(({ address }) => { + addresses.add(address); }); } @@ -118,7 +116,7 @@ async function check(url) { return parsed.range() === 'unicast'; }); - checkCache.set(url, ok); + checkCache.set(host, ok); return ok; } From 70c04f0cb25e15f150084b1d87d3d8af3efb44a1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 23 May 2025 13:57:25 -0400 Subject: [PATCH 3078/4744] fix: undefined check, allow plugins to append to allow list --- install/package.json | 1 + src/request.js | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 1126e4fbc7..5d454bf7ed 100644 --- a/install/package.json +++ b/install/package.json @@ -145,6 +145,7 @@ "tinycon": "0.6.8", "toobusy-js": "0.5.1", "tough-cookie": "5.1.2", + "undici": "^7.10.0", "validator": "13.15.15", "webpack": "5.99.9", "webpack-merge": "6.0.1", diff --git a/src/request.js b/src/request.js index 86ad8c5fc7..3709aba2f6 100644 --- a/src/request.js +++ b/src/request.js @@ -8,10 +8,13 @@ const { CookieJar } = require('tough-cookie'); const fetchCookie = require('fetch-cookie').default; const { version } = require('../package.json'); +const plugins = require('./plugins'); const ttl = require('./cache/ttl'); const checkCache = ttl({ ttl: 1000 * 60 * 60, // 1 hour }); +let allowList = new Set(); +let initialized = false; exports.jar = function () { return new CookieJar(); @@ -19,6 +22,19 @@ exports.jar = function () { const userAgent = `NodeBB/${version.split('.').shift()}.x (${nconf.get('url')})`; +async function init() { + if (initialized) { + return; + } + + allowList.add(nconf.get('url_parsed').host); + const { allowed } = await plugins.hooks.fire('filter:request.init', { allowed: allowList }); + if (allowed instanceof Set) { + allowList = allowed; + } + initialized = true; +} + // Initialize fetch - somewhat hacky, but it's required for globalDispatcher to be available async function call(url, method, { body, timeout, jar, ...config } = {}) { const ok = await check(url); @@ -90,13 +106,15 @@ async function call(url, method, { body, timeout, jar, ...config } = {}) { // Checks url to ensure it is not in reserved IP range (private, etc.) async function check(url) { + await init(); + const { host } = new URL(url); - if (host === nconf.get('url_parsed').host) { + if (allowList.has(host)) { return true; } const cached = checkCache.get(url); - if (cached) { + if (cached !== undefined) { return cached; } From a8e613e13ac6f3eb8cc048bc085beb31f2594270 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sat, 24 May 2025 22:12:48 -0400 Subject: [PATCH 3079/4744] fix: further guard against DNS rebinding attack --- src/request.js | 60 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/src/request.js b/src/request.js index 3709aba2f6..aaf0d55b32 100644 --- a/src/request.js +++ b/src/request.js @@ -1,6 +1,7 @@ 'use strict'; const dns = require('dns').promises; +require('undici'); // keep this here, needed for SSRF (see `lookup()`) const nconf = require('nconf'); const ipaddr = require('ipaddr.js'); @@ -35,9 +36,39 @@ async function init() { initialized = true; } +/** + * This method (alongside `check()`) guards against SSRF via DNS rebinding. + * + * - `check()` does a DNS lookup and ensures that all returned IPs do not belong to a reserved IP address space + * - `lookup()` provides additional logic that uses the cached DNS result from `check()` + * instead of doing another lookup (which is where DNS rebinding comes into play.) + * - For whatever reason `undici` needs to be required so that lookup can be overwritten properly. + */ +function lookup(hostname, options, callback) { + const { ok, lookup } = checkCache.get(hostname); + if (!ok) { + throw new Error('lookup-failed'); + } + + if (!lookup) { + // trusted, do regular lookup + dns.lookup(hostname, options).then((addresses) => { + callback(null, addresses); + }); + return; + } + + if (options.all === true) { + callback(null, lookup); + } else { + const { address, family } = lookup.shift(); + callback(null, address, family); + } +} + // Initialize fetch - somewhat hacky, but it's required for globalDispatcher to be available async function call(url, method, { body, timeout, jar, ...config } = {}) { - const ok = await check(url); + const { ok } = await check(url); if (!ok) { throw new Error('[[error:reserved-ip-address]]'); } @@ -75,7 +106,9 @@ async function call(url, method, { body, timeout, jar, ...config } = {}) { return super.dispatch(opts, handler); } } - opts.dispatcher = new FetchAgent(); + opts.dispatcher = new FetchAgent({ + connect: { lookup }, + }); } const response = await fetchImpl(url, opts); @@ -109,33 +142,36 @@ async function check(url) { await init(); const { host } = new URL(url); - if (allowList.has(host)) { - return true; - } - const cached = checkCache.get(url); if (cached !== undefined) { return cached; } + if (allowList.has(host)) { + const payload = { ok: true }; + checkCache.set(host, payload); + return payload; + } const addresses = new Set(); + let lookup; if (ipaddr.isValid(url)) { addresses.add(url); } else { - const lookup = await dns.lookup(host, { all: true }); - lookup.forEach(({ address }) => { - addresses.add(address); + lookup = await dns.lookup(host, { all: true }); + lookup.forEach(({ address, family }) => { + addresses.add({ address, family }); }); } // Every IP address that the host resolves to should be a unicast address - const ok = Array.from(addresses).every((ip) => { + const ok = Array.from(addresses).every(({ address: ip }) => { const parsed = ipaddr.parse(ip); return parsed.range() === 'unicast'; }); - checkCache.set(host, ok); - return ok; + const payload = { ok, lookup }; + checkCache.set(host, payload); + return payload; } /* From e1eb76feba9ff1aba8ba2a032651c5de9e79e371 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 2 Jun 2025 15:06:01 +0000 Subject: [PATCH 3080/4744] chore(i18n): fallback strings for new resources: nodebb.error --- 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/vi/error.json | 1 + public/language/zh-CN/error.json | 1 + public/language/zh-TW/error.json | 1 + 48 files changed, 48 insertions(+) diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 4c9d77e775..7c7c5263bd 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "لم تقم بتسجيل الدخول", "account-locked": "تم حظر حسابك مؤقتًا.", "search-requires-login": "البحث في المنتدى يتطلب حساب - الرجاء تسجيل الدخول أو التسجيل", diff --git a/public/language/az/error.json b/public/language/az/error.json index 4c364364fb..b166b6697d 100644 --- a/public/language/az/error.json +++ b/public/language/az/error.json @@ -3,6 +3,7 @@ "invalid-json": "Yanlış JSON", "wrong-parameter-type": "`%1` mülkiyyəti üçün %3 növünün dəyəri gözlənilən idi, lakin bunun əvəzinə %2 alındı", "required-parameters-missing": "Bu API çağırışında tələb olunan parametrlər yoxdur: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Siz hesaba daxil olmamısınız.", "account-locked": "Hesabınız müvəqqəti olaraq bloklanıb", "search-requires-login": "Axtarış üçün hesab tələb olunur - zəhmət olmasa daxil olun və ya qeydiyyatdan keçin.", diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 78566e8bf1..ef2e08eb2e 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -3,6 +3,7 @@ "invalid-json": "Неправилен JSON", "wrong-parameter-type": "За свойството `%1` се очакваше стойност от тип %3, но вместо това беше получено %2", "required-parameters-missing": "Липсват задължителни параметри от това извикване към ППИ: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Изглежда не сте се вписали в системата.", "account-locked": "Вашият акаунт беше заключен временно", "search-requires-login": "Търсенето изисква регистриран акаунт! Моля, впишете се или се регистрирайте!", diff --git a/public/language/bn/error.json b/public/language/bn/error.json index 19157e3724..20d1ba7460 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "আপনি লগিন করেননি", "account-locked": "আপনার অ্যাকাউন্ট সাময়িকভাবে লক করা হয়েছে", "search-requires-login": "Searching requires an account - please login or register.", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index be1a799f5b..20e0f99402 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -3,6 +3,7 @@ "invalid-json": "Neplatný JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Zdá se, že nejste přihlášen/a", "account-locked": "Váš účet byl dočasně uzamknut", "search-requires-login": "Pro hledání je vyžadován účet – přihlaste se nebo zaregistrujte.", diff --git a/public/language/da/error.json b/public/language/da/error.json index 657c4a479d..74c2493e70 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Det ser ikke ud til at du er logget ind.", "account-locked": "Din konto er blevet blokeret midlertidigt.", "search-requires-login": "Du skal have en konto for at søge - log venligst ind eller registrer dig.", diff --git a/public/language/de/error.json b/public/language/de/error.json index 8c44aceccd..df536bf3b8 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -3,6 +3,7 @@ "invalid-json": "Ungültiges JSON", "wrong-parameter-type": "Für die Eigenschaft „%1“ wurde ein Wert vom Typ %3 erwartet, aber stattdessen wurde %2 empfangen", "required-parameters-missing": "Bei diesem API-Aufruf fehlten erforderliche Parameter: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Du bist nicht angemeldet.", "account-locked": "Dein Konto wurde vorübergehend gesperrt.", "search-requires-login": "Die Suche erfordert ein Konto, bitte einloggen oder registrieren.", diff --git a/public/language/el/error.json b/public/language/el/error.json index 9043105827..eacb718b80 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Φαίνεται πως δεν είσαι συνδεδεμένος/η.", "account-locked": "Ο λογαριασμός σου έχει κλειδωθεί προσωρινά", "search-requires-login": "Searching requires an account - please login or register.", diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index 535a568d1e..ac1d640df8 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "You don't seem to be logged in.", "account-locked": "Your account has been locked temporarily", "search-requires-login": "Searching requires an account - please login or register.", diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json index 535a568d1e..ac1d640df8 100644 --- a/public/language/en-x-pirate/error.json +++ b/public/language/en-x-pirate/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "You don't seem to be logged in.", "account-locked": "Your account has been locked temporarily", "search-requires-login": "Searching requires an account - please login or register.", diff --git a/public/language/es/error.json b/public/language/es/error.json index b1e53c4474..0eb92764ec 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON no válido", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "No has iniciado sesión.", "account-locked": "Tu cuenta ha sido bloqueada temporalmente.", "search-requires-login": "¡Buscar requiere estar registrado! Por favor, entra o regístrate.", diff --git a/public/language/et/error.json b/public/language/et/error.json index caffd57a57..fed9ae8c6a 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Sa ei ole sisse logitud", "account-locked": "Su kasutaja on ajutiselt lukustatud", "search-requires-login": "Otsing nõuab kasutajat - palun registreeruge või logige sisse.", diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json index 827290a155..c3a0d69b51 100644 --- a/public/language/fa-IR/error.json +++ b/public/language/fa-IR/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON نامعتبر", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "وارد حساب کاربری نشده‌اید.", "account-locked": "حساب کاربری شما موقتاً مسدود شده است.", "search-requires-login": "استفاده از جستجو نیازمند ورود با نام‌کاربری و رمز‌عبور است. لطفا ابتدا وارد شوید.", diff --git a/public/language/fi/error.json b/public/language/fi/error.json index 905a03415a..bf7ac10acb 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Et taida olla kirjautuneena sisään.", "account-locked": "Käyttäjätilisi on lukittu väliaikaisesti", "search-requires-login": "Haku vaatii tunnukset. Kirjaudu sisään tai luo tunnus.", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index 94ea74f7ab..6850f40cfa 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON invalide", "wrong-parameter-type": "Une valeur de type %3 était attendue pour la propriété `%1`, mais %2 a été reçu à la place", "required-parameters-missing": "Les paramètres requis étaient manquants dans cet appel d'API : %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Vous ne semblez pas être connecté.", "account-locked": "Votre compte a été temporairement suspendu", "search-requires-login": "Rechercher nécessite d'avoir un compte. Veuillez vous identifier ou vous enregistrer.", diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 40b34fe8be..90a746455a 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Parece que estás desconectado.", "account-locked": "A túa conta foi bloqueada temporalmente.", "search-requires-login": "As buscas requiren unha conta. Por favor inicia sesión ou rexístrate.", diff --git a/public/language/he/error.json b/public/language/he/error.json index 82af4336b3..8e8701f593 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -3,6 +3,7 @@ "invalid-json": "אובייקט JSON לא תקין", "wrong-parameter-type": "ערך מסוג %3 היה צפוי למאפיין `%1`, אבל %2 התקבל במקום זאת", "required-parameters-missing": "פרמטרים נדרשים היו חסרים בקריאת API זו: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "נראה שאינכם מחוברים למערכת.", "account-locked": "חשבונכם נחסם באופן זמני", "search-requires-login": "חיפוש מצריך חשבון - אנא הירשמו או התחברו.", diff --git a/public/language/hr/error.json b/public/language/hr/error.json index 9160c18653..64e727f74e 100644 --- a/public/language/hr/error.json +++ b/public/language/hr/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Izgleda da niste prijavljeni.", "account-locked": "Vaš račun je privremeno blokiran", "search-requires-login": "Pretraga zahtijeva prijavu - prijavite se ili se registrirajte.", diff --git a/public/language/hu/error.json b/public/language/hu/error.json index 63e9d7f465..d57301a8f8 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -3,6 +3,7 @@ "invalid-json": "Érvénytelen JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Úgy tűnik, nem vagy bejelentkezve.", "account-locked": "A fiókod ideiglenesen zárolva lett.", "search-requires-login": "A kereséshez fiók szükséges - kérlek, lépj be vagy regisztrálj.", diff --git a/public/language/hy/error.json b/public/language/hy/error.json index 553a5f00b4..9f7ada1b52 100644 --- a/public/language/hy/error.json +++ b/public/language/hy/error.json @@ -3,6 +3,7 @@ "invalid-json": "Անվավեր JSON", "wrong-parameter-type": "«%1» հատկության համար սպասվում էր %3 տիպի արժեք, բայց փոխարենը ստացվեց %2", "required-parameters-missing": "Պահանջվող պարամետրերը բացակայում էին այս API զանգից՝ %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Դուք, կարծես, մուտք չեք գործել:", "account-locked": "Ձեր հաշիվը ժամանակավորապես արգելափակվել է", "search-requires-login": "Որոնումը պահանջում է հաշիվ. խնդրում ենք մուտք գործել կամ գրանցվել:", diff --git a/public/language/id/error.json b/public/language/id/error.json index 7cc7b20821..b531cf59a7 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Kamu terlihat belum login", "account-locked": "Akun kamu dikunci sementara", "search-requires-login": "Searching requires an account - please login or register.", diff --git a/public/language/it/error.json b/public/language/it/error.json index 1b862ddede..b71b169fd9 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON non valido", "wrong-parameter-type": "Era previsto un valore di tipo %3 per la proprietà '%1', ma invece è stato ricevuto %2", "required-parameters-missing": "I parametri richiesti sono mancanti in questa chiamata API: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Non sembra che tu abbia effettuato l'accesso.", "account-locked": "Il tuo account è stato bloccato temporaneamente", "search-requires-login": "La ricerca richiede un account! Si prega di effettuare l'accesso o registrarsi!", diff --git a/public/language/ja/error.json b/public/language/ja/error.json index ce848d8b34..711ba3f8d7 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -3,6 +3,7 @@ "invalid-json": "無効なJSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "ログインしていません。", "account-locked": "あなたのアカウントは一時的にロックされています", "search-requires-login": "検索するにはアカウントが必要です - ログインするかアカウントを作成してください。", diff --git a/public/language/ko/error.json b/public/language/ko/error.json index b81d681962..4efa32a96d 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -3,6 +3,7 @@ "invalid-json": "잘못된 JSON", "wrong-parameter-type": "속성 `%1`에 대해 %3 유형의 값이 예상되었지만 대신 %2가 수신되었습니다", "required-parameters-missing": "이 API 호출에서 필수 매개변수가 누락되었습니다: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "로그인되지 않았습니다.", "account-locked": "계정이 일시적으로 잠겼습니다.", "search-requires-login": "검색에는 계정이 필요합니다. 로그인하거나 등록하세요.", diff --git a/public/language/lt/error.json b/public/language/lt/error.json index 1b1c92380d..0c12b909ff 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -3,6 +3,7 @@ "invalid-json": "Nevalidus JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Atrodo, kad jūs neesate prisijungęs.", "account-locked": "Jūsų paskyra buvo laikinai užrakinta", "search-requires-login": "Paieška reikalauja vartotojo - prašome prisijungti arba užsiregistruoti", diff --git a/public/language/lv/error.json b/public/language/lv/error.json index e902e1e6b7..e61d791583 100644 --- a/public/language/lv/error.json +++ b/public/language/lv/error.json @@ -3,6 +3,7 @@ "invalid-json": "Nederīgs JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Šķiet, ka neesi ielogojies.", "account-locked": "Tavs konts ir uz laiku bloķēts", "search-requires-login": "Meklēšanai nepieciešams konts - lūdzu, ielogojies vai reģistrējies.", diff --git a/public/language/ms/error.json b/public/language/ms/error.json index a1c4660876..7a62ec9697 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Anda tidak log masuk.", "account-locked": "Akaun anda telah dikunci untuk seketika", "search-requires-login": "Fungsi Carian perlukan akaun - sila log masuk atau daftar.", diff --git a/public/language/nb/error.json b/public/language/nb/error.json index f7e2f4a8f6..ce8b49bedc 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -3,6 +3,7 @@ "invalid-json": "Ugyldig JSON", "wrong-parameter-type": "En verdi av typen %3 var forventet for egenskapen `%1`, men %2 ble mottatt i stedet", "required-parameters-missing": "Nødvendige parametere manglet fra dette API-kallet: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Du ser ikke ut til å være logget inn.", "account-locked": "Kontoen din har blitt midlertidig låst", "search-requires-login": "Søking krever en konto - vennligst logg inn eller registrer deg.", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 83670b19ba..38fc553a01 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -3,6 +3,7 @@ "invalid-json": "Ongeldige JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Het lijkt erop dat je niet ingelogd bent.", "account-locked": "Je account is tijdelijk vergrendeld", "search-requires-login": "Zoeken vereist een account - meld je aan of registreer je om te zoeken.", diff --git a/public/language/nn-NO/error.json b/public/language/nn-NO/error.json index 3325de8889..2f8a10bbec 100644 --- a/public/language/nn-NO/error.json +++ b/public/language/nn-NO/error.json @@ -3,6 +3,7 @@ "invalid-json": "Ugyldig JSON", "wrong-parameter-type": "Ein verdi av typen %3 var venta for eigenskapen `%1`, men %2 vart mottatt i staden", "required-parameters-missing": "Naudsynt parameter mangla i denne API-kallinga: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Du ser ikkje ut til å vere logga inn.", "account-locked": "Kontoen din har blitt midlertidig låst", "search-requires-login": "Søk krev ein konto - ver venleg å logge inn eller registrer deg.", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 135608bb95..5f7f39d7e7 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -3,6 +3,7 @@ "invalid-json": "Niewłaściwy JSON", "wrong-parameter-type": "Wartość typu %3 była oczekiwania dla właściwości `%1`, ale %2 został dostarczony", "required-parameters-missing": "Brakowało wymaganych parametrów w tym żądaniu API: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Nie jesteś zalogowany(-a).", "account-locked": "Twoje konto zostało tymczasowo zablokowane", "search-requires-login": "Wyszukiwanie wymaga konta - zaloguj się lub zarejestruj.", diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json index 111b323f42..7c3299664d 100644 --- a/public/language/pt-BR/error.json +++ b/public/language/pt-BR/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON Inválido", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Você não parece estar logado.", "account-locked": "Sua conta foi temporariamente bloqueada", "search-requires-login": "É necessário ter uma conta para pesquisar - por favor efetue o login ou cadastre-se.", diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json index 95fa2edfa4..cef7efc5e3 100644 --- a/public/language/pt-PT/error.json +++ b/public/language/pt-PT/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON inválido", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Não tens sessão iniciada.", "account-locked": "A sua conta foi bloqueada temporariamente", "search-requires-login": "A pesquisa requer uma conta de utilizador - por favor inicia sessão ou cria uma conta.", diff --git a/public/language/ro/error.json b/public/language/ro/error.json index 472aa0fa53..be141aba93 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Se pare ca nu ești logat.", "account-locked": "Contul tău a fost blocat temporar", "search-requires-login": "Pentru a cauta ai nevoie de un cont. Logheaza-te sau autentifica-te.", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index 89555f0367..4b428d82b9 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -3,6 +3,7 @@ "invalid-json": "Некорректный JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Обязательные параметры отсутствуют в API запросе: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Вы не вошли на сайт.", "account-locked": "Учётная запись временно заблокирована", "search-requires-login": "Поиск доступен только для зарегистрированных участников. Пожалуйста, войдите или зарегистрируйтесь.", diff --git a/public/language/rw/error.json b/public/language/rw/error.json index 7c0dd10704..a086230d94 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Biragaragara ko utinjiyemo.", "account-locked": "Konte yawe yabaye ifunze", "search-requires-login": "Gushaka ikintu bisaba kuba ufite konte - Injiramo cyangwa wiyandike.", diff --git a/public/language/sc/error.json b/public/language/sc/error.json index 535a568d1e..ac1d640df8 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "You don't seem to be logged in.", "account-locked": "Your account has been locked temporarily", "search-requires-login": "Searching requires an account - please login or register.", diff --git a/public/language/sk/error.json b/public/language/sk/error.json index 8015c6c2b6..90c552fff9 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -3,6 +3,7 @@ "invalid-json": "Neplatné JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Zdá sa že nie ste prihlásený/á.", "account-locked": "Váš účet bol dočasne uzamknutý", "search-requires-login": "K vyhľadávaniu je vyžadovaný účet - prosím prihláste sa alebo zaregistrujte.", diff --git a/public/language/sl/error.json b/public/language/sl/error.json index d87f7af441..1f793c9618 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -3,6 +3,7 @@ "invalid-json": "Invalid JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Niste prijavljeni.", "account-locked": "Vaš račun je bil začasno zaklenjen.", "search-requires-login": "Iskanje zahteva uporabniški račun - prosimo, da se prijavite ali registrirate.", diff --git a/public/language/sq-AL/error.json b/public/language/sq-AL/error.json index 28c75032ae..7bff3fb57a 100644 --- a/public/language/sq-AL/error.json +++ b/public/language/sq-AL/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON i pavlefshëm", "wrong-parameter-type": "Pritej një vlerë e tipit %3 për vetinë '%1', por në vend të saj u mor %2", "required-parameters-missing": "Parametrat e kërkuar mungonin në këtë API: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Mesa duket nuk jeni identifikuar.", "account-locked": "Llogaria juaj është bllokuar përkohësisht", "search-requires-login": "Për të kërkuar ju duhet të keni një llogari - ju lutemi identifikohuni ose regjistrohuni.", diff --git a/public/language/sr/error.json b/public/language/sr/error.json index 8200be23b1..3bec40bc5b 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -3,6 +3,7 @@ "invalid-json": "Неважећи JSON", "wrong-parameter-type": "Очекивана је вредност типа %3 за својство %1, али је уместо тога примљен %2", "required-parameters-missing": "Недостајали су обавезни параметри у овом API позиву: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Изгледа да нисте пријављени.", "account-locked": "Ваш налог је привремено закључан", "search-requires-login": "Претраживање захтева налог — пријавите се или се региструјте.", diff --git a/public/language/sv/error.json b/public/language/sv/error.json index 6aad430f33..0fdd062e90 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -3,6 +3,7 @@ "invalid-json": "Ogiltig JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Du verkar inte vara inloggad.", "account-locked": "Ditt konto har tillfälligt blivit låst", "search-requires-login": "Sökning kräver ett konto, var god logga in eller registrera dig.", diff --git a/public/language/th/error.json b/public/language/th/error.json index 16672afaef..062d51afa9 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -3,6 +3,7 @@ "invalid-json": "รูปแบบ JSON ไม่ถูกต้อง", "wrong-parameter-type": "ต้องการข้อมูลประเภท %3 สำหรับค่า `%1` แต่ได้รับค่า %2 แทน", "required-parameters-missing": "ขาดพารามิเตอร์ที่จำเป็นต่อการเรียก API นี้: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "คุณยังไม่ได้เข้าสู่ระบบ", "account-locked": "บัญชีของคุณถูกระงับการใช้งานชั่วคราว", "search-requires-login": "\"ฟังก์ชั่นการค้นหา\" ต้องการบัญชีผู้ใช้ กรุณาเข้าสู่ระบบหรือสมัครสมาชิก", diff --git a/public/language/tr/error.json b/public/language/tr/error.json index a31b16828a..1b4093991a 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -3,6 +3,7 @@ "invalid-json": "Geçersiz JSON", "wrong-parameter-type": "\"%1\" özelliği için %3 türünde bir değer bekleniyordu, ancak bunun yerine %2 alındı", "required-parameters-missing": "Bu API çağrısında gerekli parametreler eksikti: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Giriş yapmamış görünüyorsunuz.", "account-locked": "Hesabınız geçici olarak kilitlendi", "search-requires-login": "Arama yapmak için üyelik hesabı gerekiyor. Lütfen giriş yapın ya da kaydolun.", diff --git a/public/language/uk/error.json b/public/language/uk/error.json index f5ead51c58..33ea4560f3 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -3,6 +3,7 @@ "invalid-json": "Некоректний формат JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Не схоже, що ви увійшли в систему.", "account-locked": "Ваш акаунт тимчасово заблоковано", "search-requires-login": "Для пошуку потрібен акаунт — будь ласка, увійдіть чи зареєструйтесь.", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index c95f985908..5d84e951e9 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -3,6 +3,7 @@ "invalid-json": "JSON không hợp lệ", "wrong-parameter-type": "Giá trị của loại %3 được mong đợi cho thuộc tính `%1`, nhưng thay vào đó, %2 đã được nhận", "required-parameters-missing": "Các thông số bắt buộc bị thiếu trong lệnh gọi API này: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "Có vẻ như bạn chưa đăng nhập.", "account-locked": "Tài khoản của bạn tạm thời bị khóa", "search-requires-login": "Tìm kiếm yêu cầu một tài khoản - vui lòng đăng nhập hoặc đăng ký.", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index dd760f4ad1..186940d8a8 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -3,6 +3,7 @@ "invalid-json": "无效 JSON", "wrong-parameter-type": "属性 `%1` 要求是类型 %3 的值,却收到了 %2", "required-parameters-missing": "此 API 调用必需参数缺少了:%1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "您还没有登录。", "account-locked": "您的帐号已被临时锁定", "search-requires-login": "搜索功能仅限会员使用 - 请先登录或者注册。", diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json index f7b64e8001..3b78a51fbb 100644 --- a/public/language/zh-TW/error.json +++ b/public/language/zh-TW/error.json @@ -3,6 +3,7 @@ "invalid-json": "無效 JSON", "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", "required-parameters-missing": "Required parameters were missing from this API call: %1", + "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", "not-logged-in": "您還沒有登入。", "account-locked": "您的帳戶已被暫時鎖定", "search-requires-login": "搜尋功能僅限成員使用 - 請先登入或者註冊。", From 9c7cbbe2e4f5043ed99036ccf39576eed070657b Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 2 Jun 2025 15:06:29 +0000 Subject: [PATCH 3081/4744] chore: incrementing version number - v4.4.2 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 29c2898f4f..d892077725 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.4.1", + "version": "4.4.2", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 6d40a2118c7244ff8e2cb7f4aad8b52341aab0b9 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 2 Jun 2025 15:06:29 +0000 Subject: [PATCH 3082/4744] chore: update changelog for v4.4.2 --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ae952b8f..e8883eacb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +#### v4.4.2 (2025-06-02) + +##### Chores + +* up eslint stylistic (fd2ae726) +* up dbsearch (e2de0ec2) +* up dbsearch (30aa0fe6) +* up harmony (99234b3f) +* up harmony (a16bc738) +* incrementing version number - v4.4.1 (5ae79b4e) +* update changelog for v4.4.1 (a686cf20) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### New Features + +* add action:post-queue.save (ebb88c12) +* restrict access to ap.probe method to registered users, add rate limiting protection (e70e990a) + +##### Bug Fixes + +* return 200 for non-implemented activities instead of 501 (524a1e8b) +* closes #13458, check if plugin is system (b1022566) +* add try..catch around topics.post in note assertion logic (cc927026) +* don't throw on unknown post on Undo(Like) (83a55f6a) +* add try..catch wrapper around Announce(Like) call to internal method so as to not return a 500 — just drop the Like activity (629eec7b) +* browser title translation (390f6428) +* allow guests to load topic tools if they have privilege to view them (78de8c6d) +* closes #13454, align dropdowns to opposite side on rtl (72417d82) +* send actor in undo(follow) (49b5268e) +* missed handling zset on ap unfollow (b20a6ed0) +* additional tests for remote privileges, enforcing privileges for remote edits and deletes (a888b868) + +##### Tests + +* fix groups:find webfinger test (0c1a6183) + #### v4.4.1 (2025-05-16) ##### Chores From 6411c19765050f12864bfb04678adb8fbe271be1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 2 Jun 2025 11:58:54 -0400 Subject: [PATCH 3083/4744] fix: #13459, unread indicators for remote categories --- src/topics/unread.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/topics/unread.js b/src/topics/unread.js index db0e9c0f9e..4dd5a94435 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -351,8 +351,23 @@ module.exports = function (Topics) { if (!(parseInt(uid, 10) > 0)) { return tids.map(() => false); } - const [topicScores, userScores, tids_unread, blockedUids] = await Promise.all([ + + // Remote tids do not get slotted into topics:recent; separate calculation follows + async function getRemoteTopicScores(tids) { + let cids = await Topics.getTopicsFields(tids, ['cid']); + cids = cids.map(({ cid }) => cid); + return await Promise.all(tids.map(async (tid, idx) => { + const cid = cids[idx]; + if (utils.isNumber(tid) || !cid) { + return null; + } + return await db.sortedSetScore(`cid:${cid}:tids`, tid); + })); + } + + const [topicScores, remoteTopicScores, userScores, tids_unread, blockedUids] = await Promise.all([ db.sortedSetScores('topics:recent', tids), + getRemoteTopicScores(tids), db.sortedSetScores(`uid:${uid}:tids_read`, tids), db.sortedSetScores(`uid:${uid}:tids_unread`, tids), user.blocks.list(uid), @@ -361,7 +376,7 @@ module.exports = function (Topics) { const cutoff = await Topics.unreadCutoff(uid); const result = tids.map((tid, index) => { const read = !tids_unread[index] && - (topicScores[index] < cutoff || + ((topicScores[index] || remoteTopicScores[index]) < cutoff || !!(userScores[index] && userScores[index] >= topicScores[index])); return { tid: tid, read: read, index: index }; }); From 0ccfe1dfe9a5c11bbfac6156ec67adee781fdead Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 3 Jun 2025 09:20:10 +0000 Subject: [PATCH 3084/4744] Latest translations and fallbacks --- public/language/bg/error.json | 2 +- public/language/pl/error.json | 2 +- public/language/vi/error.json | 2 +- public/language/zh-CN/error.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/language/bg/error.json b/public/language/bg/error.json index ef2e08eb2e..ab85561c34 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -3,7 +3,7 @@ "invalid-json": "Неправилен JSON", "wrong-parameter-type": "За свойството `%1` се очакваше стойност от тип %3, но вместо това беше получено %2", "required-parameters-missing": "Липсват задължителни параметри от това извикване към ППИ: %1", - "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "reserved-ip-address": "Мрежовите заявки до IP адреси от резервирани области не са позволени.", "not-logged-in": "Изглежда не сте се вписали в системата.", "account-locked": "Вашият акаунт беше заключен временно", "search-requires-login": "Търсенето изисква регистриран акаунт! Моля, впишете се или се регистрирайте!", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 5f7f39d7e7..4e9c0cc58a 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -3,7 +3,7 @@ "invalid-json": "Niewłaściwy JSON", "wrong-parameter-type": "Wartość typu %3 była oczekiwania dla właściwości `%1`, ale %2 został dostarczony", "required-parameters-missing": "Brakowało wymaganych parametrów w tym żądaniu API: %1", - "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "reserved-ip-address": "Wywołania sieciowe do zarezerwowanych zakresów IP nie są dozwolone.", "not-logged-in": "Nie jesteś zalogowany(-a).", "account-locked": "Twoje konto zostało tymczasowo zablokowane", "search-requires-login": "Wyszukiwanie wymaga konta - zaloguj się lub zarejestruj.", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index 5d84e951e9..cbd7c3798a 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -3,7 +3,7 @@ "invalid-json": "JSON không hợp lệ", "wrong-parameter-type": "Giá trị của loại %3 được mong đợi cho thuộc tính `%1`, nhưng thay vào đó, %2 đã được nhận", "required-parameters-missing": "Các thông số bắt buộc bị thiếu trong lệnh gọi API này: %1", - "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "reserved-ip-address": "Không được phép yêu cầu mạng đến phạm vi IP dành riêng.", "not-logged-in": "Có vẻ như bạn chưa đăng nhập.", "account-locked": "Tài khoản của bạn tạm thời bị khóa", "search-requires-login": "Tìm kiếm yêu cầu một tài khoản - vui lòng đăng nhập hoặc đăng ký.", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index 186940d8a8..42a7901772 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -3,7 +3,7 @@ "invalid-json": "无效 JSON", "wrong-parameter-type": "属性 `%1` 要求是类型 %3 的值,却收到了 %2", "required-parameters-missing": "此 API 调用必需参数缺少了:%1", - "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "reserved-ip-address": "不允许向保留 IP 地址发送网络请求。", "not-logged-in": "您还没有登录。", "account-locked": "您的帐号已被临时锁定", "search-requires-login": "搜索功能仅限会员使用 - 请先登录或者注册。", From ea91dc00cd7359a0c81ed7af29e990ada4a6b989 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 4 Jun 2025 09:20:16 +0000 Subject: [PATCH 3085/4744] Latest translations and fallbacks --- public/language/it/error.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/it/error.json b/public/language/it/error.json index b71b169fd9..abf63e8df8 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -3,7 +3,7 @@ "invalid-json": "JSON non valido", "wrong-parameter-type": "Era previsto un valore di tipo %3 per la proprietà '%1', ma invece è stato ricevuto %2", "required-parameters-missing": "I parametri richiesti sono mancanti in questa chiamata API: %1", - "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "reserved-ip-address": "Le richieste di rete agli intervalli IP riservati non sono consentite.", "not-logged-in": "Non sembra che tu abbia effettuato l'accesso.", "account-locked": "Il tuo account è stato bloccato temporaneamente", "search-requires-login": "La ricerca richiede un account! Si prega di effettuare l'accesso o registrarsi!", From 010113a9a00280663822b17696c56f3f8d1227ec Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 4 Jun 2025 13:19:29 -0400 Subject: [PATCH 3086/4744] fix: wrap cached returns for dns lookups in nextTick --- src/request.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/request.js b/src/request.js index aaf0d55b32..fe8de851c1 100644 --- a/src/request.js +++ b/src/request.js @@ -58,12 +58,15 @@ function lookup(hostname, options, callback) { return; } - if (options.all === true) { - callback(null, lookup); - } else { - const { address, family } = lookup.shift(); - callback(null, address, family); - } + // Lookup needs to behave asynchronously — https://github.com/nodejs/node/issues/28664 + process.nextTick(() => { + if (options.all === true) { + callback(null, lookup); + } else { + const { address, family } = lookup.shift(); + callback(null, address, family); + } + }); } // Initialize fetch - somewhat hacky, but it's required for globalDispatcher to be available From 3694f6555bca1ce0f16c75096eaced0260f83e33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:54:37 -0400 Subject: [PATCH 3087/4744] fix(deps): update dependency cron to v4.3.1 (#13457) 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 5d454bf7ed..ddd5520400 100644 --- a/install/package.json +++ b/install/package.json @@ -61,7 +61,7 @@ "connect-pg-simple": "10.0.0", "connect-redis": "8.1.0", "cookie-parser": "1.4.7", - "cron": "4.3.0", + "cron": "4.3.1", "cropperjs": "1.6.2", "csrf-sync": "4.2.1", "daemon": "1.1.0", From 4fbcfae8b15e4ce5d132c408bca69ebb9cf146ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Thu, 5 Jun 2025 07:15:45 -0400 Subject: [PATCH 3088/4744] Post queue write api (#13473) * move post queue from socket.io to rest api * move harmony post-queue to core add canEdit, allow users to edit their queued posts * fix: openapi spec * lint: whitespace --- public/openapi/read/post-queue.yaml | 4 +- public/openapi/write.yaml | 4 + public/openapi/write/posts/queue/id.yaml | 92 ++++++ public/openapi/write/posts/queue/notify.yaml | 36 +++ public/src/client/post-queue.js | 82 ++--- src/api/posts.js | 90 ++++++ src/controllers/mods.js | 1 + src/controllers/write/posts.js | 21 ++ src/posts/queue.js | 2 + src/routes/write/posts.js | 6 + src/socket.io/posts.js | 91 +----- src/views/post-queue.tpl | 299 ++++++++++++------- test/posts.js | 52 ++-- 13 files changed, 521 insertions(+), 259 deletions(-) create mode 100644 public/openapi/write/posts/queue/id.yaml create mode 100644 public/openapi/write/posts/queue/notify.yaml diff --git a/public/openapi/read/post-queue.yaml b/public/openapi/read/post-queue.yaml index 0ecb95500c..9bd93903c4 100644 --- a/public/openapi/read/post-queue.yaml +++ b/public/openapi/read/post-queue.yaml @@ -1,7 +1,7 @@ get: tags: - admin - summary: Get flag data + summary: Get post queue responses: "200": description: "" @@ -42,6 +42,8 @@ get: description: A user identifier type: type: string + canEdit: + type: boolean data: type: object properties: diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index f7012e77a8..5f58bdfbf1 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -202,6 +202,10 @@ paths: $ref: 'write/posts/pid/diffs/timestamp.yaml' /posts/{pid}/replies: $ref: 'write/posts/pid/replies.yaml' + /posts/queue/{id}: + $ref: 'write/posts/queue/id.yaml' + /posts/queue/{id}/notify: + $ref: 'write/posts/queue/notify.yaml' /chats/: $ref: 'write/chats.yaml' /chats/unread: diff --git a/public/openapi/write/posts/queue/id.yaml b/public/openapi/write/posts/queue/id.yaml new file mode 100644 index 0000000000..00bc01d303 --- /dev/null +++ b/public/openapi/write/posts/queue/id.yaml @@ -0,0 +1,92 @@ +post: + summary: Accept a queued post + tags: + - QueuedPosts + parameters: + - in: path + name: id + schema: + type: string + required: true + description: a valid queued post id + example: 2 + responses: + '200': + description: post successfully accepted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + type: + type: string + pid: + type: number + tid: + type: number + '400': + description: Bad request, invalid post id +delete: + summary: Remove a queued post + tags: + - QueuedPosts + parameters: + - name: id + in: path + required: true + schema: + type: string + example: 'topic-12345' + responses: + '200': + description: Post removed successfully + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + '400': + description: Bad request, invalid post id +put: + summary: Edit a queued post + tags: + - QueuedPosts + parameters: + - name: id + in: path + required: true + schema: + type: string + example: 'topic-12345' + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + example: This is a test reply + cid: + type: number + description: Category ID to which the post belongs + title: + type: string + description: Updated Post Title + responses: + '200': + description: Post edited successfully + '400': + description: Bad request, invalid post id + + diff --git a/public/openapi/write/posts/queue/notify.yaml b/public/openapi/write/posts/queue/notify.yaml new file mode 100644 index 0000000000..8569d9b232 --- /dev/null +++ b/public/openapi/write/posts/queue/notify.yaml @@ -0,0 +1,36 @@ +post: + summary: Notify the owner of a queued post + tags: + - QueuedPosts + parameters: + - in: path + name: id + schema: + type: string + required: true + description: a valid queued post id + example: 2 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: body of the notification message + responses: + '200': + description: post successfully accepted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + '400': + description: Bad request, invalid post id \ No newline at end of file diff --git a/public/src/client/post-queue.js b/public/src/client/post-queue.js index ff5fa931d7..5cdf120517 100644 --- a/public/src/client/post-queue.js +++ b/public/src/client/post-queue.js @@ -69,14 +69,10 @@ define('forum/post-queue', [ const id = textarea.parents('[data-id]').attr('data-id'); const titleEdit = triggerClass === '[data-action="editTitle"]'; - socket.emit('posts.editQueuedContent', { - id: id, + api.put(`/posts/queue/${id}`, { title: titleEdit ? textarea.val() : undefined, content: titleEdit ? undefined : textarea.val(), - }, function (err, data) { - if (err) { - return alerts.error(err); - } + }).then((data) => { if (titleEdit) { preview.find('.title-text').text(data.postData.title); } else { @@ -85,7 +81,7 @@ define('forum/post-queue', [ textarea.parent().addClass('hidden'); preview.removeClass('hidden'); - }); + }).catch(alerts.error); }); } @@ -96,8 +92,7 @@ define('forum/post-queue', [ onSubmit: function (selectedCategory) { Promise.all([ api.get(`/categories/${selectedCategory.cid}`, {}), - socket.emit('posts.editQueuedContent', { - id: id, + api.put(`/posts/queue/${id}`, { cid: selectedCategory.cid, }), ]).then(function (result) { @@ -174,6 +169,35 @@ define('forum/post-queue', [ async function handleQueueActions() { // accept, reject, notify + + const parent = $(this).parents('[data-id]'); + const action = $(this).attr('data-action'); + const id = parent.attr('data-id'); + const listContainer = parent.get(0).parentNode; + + if ((!['accept', 'reject', 'notify'].includes(action)) || + (action === 'reject' && !await confirmReject(ajaxify.data.canAccept ? '[[post-queue:confirm-reject]]' : '[[post-queue:confirm-remove]]'))) { + return; + } + + doAction(action, id).then(function () { + if (action === 'accept' || action === 'reject') { + parent.remove(); + } + + if (listContainer.childElementCount === 0) { + if (ajaxify.data.singlePost) { + ajaxify.go('/post-queue' + window.location.search); + } else { + ajaxify.refresh(); + } + } + }).catch(alerts.error); + + return false; + } + + async function doAction(action, id) { function getMessage() { return new Promise((resolve) => { const modal = bootbox.dialog({ @@ -194,36 +218,16 @@ define('forum/post-queue', [ }); } - const parent = $(this).parents('[data-id]'); - const action = $(this).attr('data-action'); - const id = parent.attr('data-id'); - const listContainer = parent.get(0).parentNode; - - if ((!['accept', 'reject', 'notify'].includes(action)) || - (action === 'reject' && !await confirmReject(ajaxify.data.canAccept ? '[[post-queue:confirm-reject]]' : '[[post-queue:confirm-remove]]'))) { - return; + const actionsMap = { + accept: () => api.post(`/posts/queue/${id}`, {}), + reject: () => api.del(`/posts/queue/${id}`, {}), + notify: async () => api.post(`/posts/queue/${id}/notify`, { message: await getMessage() }), + }; + if (actionsMap[action]) { + const result = actionsMap[action](); + return (result instanceof Promise ? result : Promise.resolve(result)); } - - socket.emit('posts.' + action, { - id: id, - message: action === 'notify' ? await getMessage() : undefined, - }, function (err) { - if (err) { - return alerts.error(err); - } - if (action === 'accept' || action === 'reject') { - parent.remove(); - } - - if (listContainer.childElementCount === 0) { - if (ajaxify.data.singlePost) { - ajaxify.go('/post-queue' + window.location.search); - } else { - ajaxify.refresh(); - } - } - }); - return false; + throw new Error(`Unknown action: ${action}`); } function handleBulkActions() { @@ -244,7 +248,7 @@ define('forum/post-queue', [ return; } const action = bulkAction.split('-')[0]; - const promises = ids.map(id => socket.emit('posts.' + action, { id: id })); + const promises = ids.map(id => doAction(action, id)); Promise.allSettled(promises).then(function (results) { const fulfilled = results.filter(res => res.status === 'fulfilled').length; diff --git a/src/api/posts.js b/src/api/posts.js index a7111e0c22..b39c173eb6 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -17,6 +17,8 @@ const activitypub = require('../activitypub'); const apiHelpers = require('./helpers'); const websockets = require('../socket.io'); const socketHelpers = require('../socket.io/helpers'); +const translator = require('../translator'); +const notifications = require('../notifications'); const postsAPI = module.exports; @@ -574,3 +576,91 @@ postsAPI.getReplies = async (caller, { pid }) => { return postData; }; + +postsAPI.acceptQueuedPost = async (caller, data) => { + await canEditQueue(caller.uid, data, 'accept'); + const result = await posts.submitFromQueue(data.id); + if (result && caller.uid !== parseInt(result.uid, 10)) { + await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`); + } + await logQueueEvent(caller, result, 'accept'); + return { type: result.type, pid: result.pid, tid: result.tid }; +}; + +postsAPI.removeQueuedPost = async (caller, data) => { + await canEditQueue(caller.uid, data, 'reject'); + const result = await posts.removeFromQueue(data.id); + if (result && caller.uid !== parseInt(result.uid, 10)) { + await sendQueueNotification('post-queue-rejected', result.uid, '/'); + } + await logQueueEvent(caller, result, 'reject'); +}; + +postsAPI.editQueuedPost = async (caller, data) => { + if (!data || !data.id || (!data.content && !data.title && !data.cid)) { + throw new Error('[[error:invalid-data]]'); + } + await posts.editQueuedContent(caller.uid, data); + if (data.content) { + return await plugins.hooks.fire('filter:parse.post', { postData: data }); + } + return { postData: data }; +}; + +postsAPI.notifyQueuedPostOwner = async (caller, data) => { + await canEditQueue(caller.uid, data, 'notify'); + const result = await posts.getFromQueue(data.id); + if (result) { + await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message))); + } +}; + +async function canEditQueue(uid, data, action) { + const [canEditQueue, queuedPost] = await Promise.all([ + posts.canEditQueue(uid, data, action), + posts.getFromQueue(data.id), + ]); + if (!queuedPost) { + throw new Error('[[error:no-post]]'); + } + if (!canEditQueue) { + throw new Error('[[error:no-privileges]]'); + } +} + +async function logQueueEvent(caller, result, type) { + const eventData = { + type: `post-queue-${result.type}-${type}`, + uid: caller.uid, + ip: caller.ip, + content: result.data.content, + targetUid: result.uid, + }; + if (result.type === 'topic') { + eventData.cid = result.data.cid; + eventData.title = result.data.title; + } else { + eventData.tid = result.data.tid; + } + if (result.pid) { + eventData.pid = result.pid; + } + await events.log(eventData); +} + +async function sendQueueNotification(type, targetUid, path, notificationText) { + const bodyShort = notificationText ? + translator.compile(`notifications:${type}`, notificationText) : + translator.compile(`notifications:${type}`); + const notifData = { + type: type, + nid: `${type}-${targetUid}-${path}`, + bodyShort: bodyShort, + path: path, + }; + if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) { + notifData.from = meta.config.postQueueNotificationUid; + } + const notifObj = await notifications.create(notifData); + await notifications.push(notifObj, [targetUid]); +} diff --git a/src/controllers/mods.js b/src/controllers/mods.js index c0abc18fe8..2726e600d4 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -237,6 +237,7 @@ modsController.postQueue = async function (req, res, next) { .map((post) => { const isSelf = post.user.uid === req.uid; post.canAccept = !isSelf && (isAdmin || isGlobalMod || !!moderatedCids.length); + post.canEdit = isSelf || isAdmin || isGlobalMod; return post; }); diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 884517c126..5828b44704 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -189,3 +189,24 @@ Posts.getReplies = async (req, res) => { helpers.formatApiResponse(200, res, { replies }); }; + +Posts.acceptQueuedPost = async (req, res) => { + const post = await api.posts.acceptQueuedPost(req, { id: req.params.id }); + helpers.formatApiResponse(200, res, { post }); +}; + +Posts.removeQueuedPost = async (req, res) => { + await api.posts.removeQueuedPost(req, { id: req.params.id }); + helpers.formatApiResponse(200, res); +}; + +Posts.editQueuedPost = async (req, res) => { + const result = await api.posts.editQueuedPost(req, { id: req.params.id, ...req.body }); + helpers.formatApiResponse(200, res, result); +}; + +Posts.notifyQueuedPostOwner = async (req, res) => { + const { id } = req.params; + await api.posts.notifyQueuedPostOwner(req, { id, message: req.body.message }); + helpers.formatApiResponse(200, res); +}; \ No newline at end of file diff --git a/src/posts/queue.js b/src/posts/queue.js index 9f6b21636d..8c1bbf90d0 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -307,9 +307,11 @@ module.exports = function (Posts) { if (data.type === 'topic') { const result = await createTopic(data.data); data.pid = result.postData.pid; + data.tid = result.topicData.tid; } else if (data.type === 'reply') { const result = await createReply(data.data); data.pid = result.pid; + data.tid = result.tid; } await removeFromQueue(id); plugins.hooks.fire('action:post-queue:submitFromQueue', { data: data }); diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index 829dd56df9..2c9a54be64 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -41,6 +41,12 @@ module.exports = function () { setupApiRoute(router, 'get', '/:pid/replies', [middleware.assert.post], controllers.write.posts.getReplies); + setupApiRoute(router, 'post', '/queue/:id', controllers.write.posts.acceptQueuedPost); + setupApiRoute(router, 'delete', '/queue/:id', controllers.write.posts.removeQueuedPost); + setupApiRoute(router, 'put', '/queue/:id', controllers.write.posts.editQueuedPost); + setupApiRoute(router, 'post', '/queue/:id/notify', [middleware.checkRequired.bind(null, ['message'])], controllers.write.posts.notifyQueuedPostOwner); + + // Shorthand route to access post routes by topic index router.all('/+byIndex/:index*?', [middleware.checkRequired.bind(null, ['tid'])], controllers.write.posts.redirectByIndex); diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index d6833e363a..b20fbe9b97 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -1,18 +1,10 @@ 'use strict'; -const validator = require('validator'); - const db = require('../database'); const posts = require('../posts'); const privileges = require('../privileges'); -const plugins = require('../plugins'); -const meta = require('../meta'); const topics = require('../topics'); -const notifications = require('../notifications'); const utils = require('../utils'); -const events = require('../events'); -const translator = require('../translator'); - const api = require('../api'); const sockets = require('.'); @@ -99,90 +91,23 @@ SocketPosts.getReplies = async function (socket, pid) { }; SocketPosts.accept = async function (socket, data) { - await canEditQueue(socket, data, 'accept'); - const result = await posts.submitFromQueue(data.id); - if (result && socket.uid !== parseInt(result.uid, 10)) { - await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`); - } - await logQueueEvent(socket, result, 'accept'); + sockets.warnDeprecated(socket, 'POST /api/v3/posts/queue/:id'); + await api.posts.acceptQueuedPost(socket, data); }; SocketPosts.reject = async function (socket, data) { - await canEditQueue(socket, data, 'reject'); - const result = await posts.removeFromQueue(data.id); - if (result && socket.uid !== parseInt(result.uid, 10)) { - await sendQueueNotification('post-queue-rejected', result.uid, '/'); - } - await logQueueEvent(socket, result, 'reject'); + sockets.warnDeprecated(socket, 'DELETE /api/v3/posts/queue/:id'); + await api.posts.removeQueuedPost(socket, data); }; -async function logQueueEvent(socket, result, type) { - const eventData = { - type: `post-queue-${result.type}-${type}`, - uid: socket.uid, - ip: socket.ip, - content: result.data.content, - targetUid: result.uid, - }; - if (result.type === 'topic') { - eventData.cid = result.data.cid; - eventData.title = result.data.title; - } else { - eventData.tid = result.data.tid; - } - if (result.pid) { - eventData.pid = result.pid; - } - await events.log(eventData); -} - SocketPosts.notify = async function (socket, data) { - await canEditQueue(socket, data, 'notify'); - const result = await posts.getFromQueue(data.id); - if (result) { - await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message))); - } + sockets.warnDeprecated(socket, 'POST /api/v3/posts/queue/:id/notify'); + await api.posts.notifyQueuedPostOwner(socket, data); }; -async function canEditQueue(socket, data, action) { - const [canEditQueue, queuedPost] = await Promise.all([ - posts.canEditQueue(socket.uid, data, action), - posts.getFromQueue(data.id), - ]); - if (!queuedPost) { - throw new Error('[[error:no-post]]'); - } - if (!canEditQueue) { - throw new Error('[[error:no-privileges]]'); - } -} - -async function sendQueueNotification(type, targetUid, path, notificationText) { - const bodyShort = notificationText ? - translator.compile(`notifications:${type}`, notificationText) : - translator.compile(`notifications:${type}`); - const notifData = { - type: type, - nid: `${type}-${targetUid}-${path}`, - bodyShort: bodyShort, - path: path, - }; - if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) { - notifData.from = meta.config.postQueueNotificationUid; - } - const notifObj = await notifications.create(notifData); - await notifications.push(notifObj, [targetUid]); -} - SocketPosts.editQueuedContent = async function (socket, data) { - if (!data || !data.id || (!data.content && !data.title && !data.cid)) { - throw new Error('[[error:invalid-data]]'); - } - await posts.editQueuedContent(socket.uid, data); - if (data.content) { - return await plugins.hooks.fire('filter:parse.post', { postData: data }); - } - return { postData: data }; + sockets.warnDeprecated(socket, 'PUT /api/v3/posts/queue/:id'); + return await api.posts.editQueuedPost(socket, data); }; require('../promisify')(SocketPosts); diff --git a/src/views/post-queue.tpl b/src/views/post-queue.tpl index 040fc7a9d5..113e8791a6 100644 --- a/src/views/post-queue.tpl +++ b/src/views/post-queue.tpl @@ -1,92 +1,158 @@ -{{{ if isAdmin }}} -{{{ if !enabled }}} -
- [[post-queue:enabling-help, {config.relative_path}/admin/settings/post#post-queue]] -
-{{{ end }}} -{{{ else }}} -
-

[[post-queue:public-intro]]

-

[[post-queue:public-description]]

-
-
-{{{ end }}} - -{{{ if (!singlePost && posts.length) }}} -
-
- +
+ {{{ if isAdmin }}} + {{{ if !enabled }}} +
+ [[post-queue:enabling-help, {config.relative_path}/admin/settings/post#post-queue]]
-
- - + {{{ end }}} + {{{ else }}} +
+

[[post-queue:public-intro]]

+

[[post-queue:public-description]]

+
-
+ {{{ end }}} -
-{{{ end }}} + {{{ if (!singlePost && posts.length) }}} +
+
+ +
-
-
-
- {{{ if !posts.length }}} - {{{ if !singlePost }}} -
-
-
- -
- [[post-queue:no-queued-posts]] -
-
+
+ + +
+
+ {{{ end }}} - {{{ each posts }}} -
-
- {{{ if !singlePost }}} - - {{{ end }}} - {{{ if posts.data.tid }}}[[post-queue:reply]]{{{ else }}}[[post-queue:topic]]{{{ end }}} - +
+ {{{ if !posts.length }}} + {{{ if !singlePost }}} +
+
+
+ +
+ [[post-queue:no-queued-posts]]
-
-
-
- [[post-queue:user]] -
+
+ {{{ else }}} +
+

[[post-queue:no-single-post]]

+ +
+ {{{ end }}} + {{{ end }}} + + {{{ each posts }}} +
+
+
+
-
- [[post-queue:category]]{{{ if posts.data.cid}}} {{{ end }}} -
+
+ + {humanReadableNumber(posts.user.postcount)} + [[global:posts]] + + + {humanReadableNumber(posts.user.reputation)} + [[global:reputation]] + + + [[user:joined]] + + +
+ +
  • +
    [[post-queue:when]]
    + +
  • +
  • +
    + {{{ if posts.data.tid }}}[[post-queue:topic]]{{{ else }}}[[post-queue:title]]{{{ end }}} +
    + + {{{ if posts.data.tid }}} +
    + {posts.topic.title} + + [[global:lastpost]] + + +
    + {{{ end }}} + {posts.data.title} +
    + {{{if !posts.data.tid}}} + + {{{end}}} +
  • +
  • +
    + [[post-queue:category]] +
    + -
  • -
    - {{{ if posts.data.tid }}}[[post-queue:topic]]{{{ else }}}[[post-queue:title]] {{{ end }}} -
    - {{{ if posts.data.tid }}} - {posts.topic.title} + +
  • +
    + {{{ if ./canAccept }}} +
    + +
    +
    + +
    + {{{ end }}} + {{{ if ./canEdit}}} + {{{ if !posts.data.tid }}} +
    + +
    + {{{ end }}} +
    + +
    + {{{if posts.data.cid}}} +
    + +
    + {{{ end }}} + {{{ end }}} + {{{ if ./canAccept }}} +
    + +
    + {{{ else }}} +
    + +
    {{{ end }}} - {posts.data.title}
    - {{{if !posts.data.tid}}} - - {{{end}}} -
  • -
    -
    -
    - [[post-queue:content]] -
    {posts.data.content}
    - -
    + +
    - - - + {{{ end }}}
    + +
    \ No newline at end of file diff --git a/test/posts.js b/test/posts.js index 2ec2faf24c..2fc22904fc 100644 --- a/test/posts.js +++ b/test/posts.js @@ -985,15 +985,15 @@ describe('Post\'s', () => { assert.equal(posts[1].data.content, 'this is a queued reply'); }); - it('should error if data is invalid', (done) => { - socketPosts.editQueuedContent({ uid: globalModUid }, null, (err) => { - assert.equal(err.message, '[[error:invalid-data]]'); - done(); - }); + it('should error if data is invalid', async () => { + await assert.rejects( + apiPosts.editQueuedPost({ uid: globalModUid }, null), + { message: '[[error:invalid-data]]' }, + ); }); it('should edit post in queue', async () => { - await socketPosts.editQueuedContent({ uid: globalModUid }, { id: queueId, content: 'newContent' }); + await apiPosts.editQueuedPost({ uid: globalModUid }, { id: queueId, content: 'newContent' }); const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar }); const { posts } = body; assert.equal(posts[1].type, 'reply'); @@ -1001,7 +1001,7 @@ describe('Post\'s', () => { }); it('should edit topic title in queue', async () => { - await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, title: 'new topic title' }); + await apiPosts.editQueuedPost({ uid: globalModUid }, { id: topicQueueId, title: 'new topic title' }); const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar }); const { posts } = body; assert.equal(posts[0].type, 'topic'); @@ -1009,39 +1009,39 @@ describe('Post\'s', () => { }); it('should edit topic category in queue', async () => { - await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: 2 }); + await apiPosts.editQueuedPost({ uid: globalModUid }, { id: topicQueueId, cid: 2 }); const { body } = await request.get(`${nconf.get('url')}/api/post-queue`, { jar }); const { posts } = body; assert.equal(posts[0].type, 'topic'); assert.equal(posts[0].data.cid, 2); - await socketPosts.editQueuedContent({ uid: globalModUid }, { id: topicQueueId, cid: cid }); + await apiPosts.editQueuedPost({ uid: globalModUid }, { id: topicQueueId, cid: cid }); }); - it('should prevent regular users from approving posts', (done) => { - socketPosts.accept({ uid: uid }, { id: queueId }, (err) => { - assert.equal(err.message, '[[error:no-privileges]]'); - done(); - }); + it('should prevent regular users from approving posts', async () => { + await assert.rejects( + apiPosts.acceptQueuedPost({ uid: uid }, { id: queueId }), + { message: '[[error:no-privileges]]' }, + ); }); - it('should prevent regular users from approving non existing posts', (done) => { - socketPosts.accept({ uid: uid }, { id: 123123 }, (err) => { - assert.equal(err.message, '[[error:no-post]]'); - done(); - }); + it('should prevent regular users from approving non existing posts', async () => { + await assert.rejects( + apiPosts.acceptQueuedPost({ uid: uid }, { id: 123123 }), + { message: '[[error:no-post]]' }, + ); }); it('should accept queued posts and submit', async () => { const ids = await db.getSortedSetRange('post:queue', 0, -1); - await socketPosts.accept({ uid: globalModUid }, { id: ids[0] }); - await socketPosts.accept({ uid: globalModUid }, { id: ids[1] }); + await apiPosts.acceptQueuedPost({ uid: globalModUid }, { id: ids[0] }); + await apiPosts.acceptQueuedPost({ uid: globalModUid }, { id: ids[1] }); }); - it('should not crash if id does not exist', (done) => { - socketPosts.reject({ uid: globalModUid }, { id: '123123123' }, (err) => { - assert.equal(err.message, '[[error:no-post]]'); - done(); - }); + it('should not crash if id does not exist', async () => { + await assert.rejects( + apiPosts.removeQueuedPost({ uid: globalModUid }, { id: '123123123' }), + { message: '[[error:no-post]]' }, + ); }); it('should bypass post queue if user is in exempt group', async () => { From efb14ead1d4d2e2ba7370eea179b044ee192686f Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 5 Jun 2025 11:16:26 +0000 Subject: [PATCH 3089/4744] chore(i18n): fallback strings for new resources: nodebb.error --- 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/vi/error.json | 1 + public/language/zh-CN/error.json | 1 + public/language/zh-TW/error.json | 1 + 48 files changed, 48 insertions(+) diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 7c7c5263bd..4d2ea94cba 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/az/error.json b/public/language/az/error.json index b166b6697d..62ca9934ff 100644 --- a/public/language/az/error.json +++ b/public/language/az/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Hazırda serverə daxil olmaq mümkün deyil. Yenidən cəhd etmək üçün bura klikləyin və ya daha sonra yenidən cəhd edin", "invalid-plugin-id": "Yanlış plagin identifikatoru", "plugin-not-whitelisted": "Plugini quraşdırmaq mümkün deyil – yalnız NodeBB Paket Meneceri tərəfindən ağ siyahıya alınmış plaginlər ACP vasitəsilə quraşdırıla bilər", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "ACP vasitəsilə plagin quraşdırılması deaktiv edilib", "plugins-set-in-configuration": "Sizə plagin vəziyyətini dəyişdirmək icazəsi verilmir, çünki onlar icra zamanı təyin olunur (config.json, ətraf mühit dəyişənləri və ya terminal arqumentləri), lütfən, bunun əvəzinə konfiqurasiyanı dəyişdirin.", "theme-not-set-in-configuration": "Konfiqurasiyada aktiv plaginləri təyin edərkən, mövzuların dəyişdirilməsi ACP-də yeniləmədən əvvəl yeni mövzunun aktiv plaginlərin siyahısına əlavə edilməsini tələb edir.", diff --git a/public/language/bg/error.json b/public/language/bg/error.json index ab85561c34..122239e27c 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "В момента сървърът е недостъпен. Натиснете тук, за да опитате отново, или опитайте пак по-късно.", "invalid-plugin-id": "Грешен идентификатор на добавка", "plugin-not-whitelisted": "Добавката не може да бъде инсталирана – само добавки, одобрени от пакетния мениджър на NodeBB могат да бъдат инсталирани чрез ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Инсталирането на добавки чрез ACP е изключено", "plugins-set-in-configuration": "Не можете да променяте състоянието на добавката, тъй като то се определя по време на работата ѝ (чрез config.json, променливи на средата или аргументи при изпълнение). Вместо това може да промените конфигурацията.", "theme-not-set-in-configuration": "Когато определяте активните добавки в конфигурацията, промяната на темите изисква да се добави новата тема към активните добавки, преди актуализирането ѝ в ACP", diff --git a/public/language/bn/error.json b/public/language/bn/error.json index 20d1ba7460..3dfb852227 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 20e0f99402..4295b7d97a 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/da/error.json b/public/language/da/error.json index 74c2493e70..9418e8663c 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/de/error.json b/public/language/de/error.json index df536bf3b8..dfb5a8da31 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Der Server kann zurzeit nicht erreicht werden. Klicken Sie hier, um es erneut zu versuchen, oder versuchen Sie es später erneut", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Plugin kann nicht installiert werden – nur Plugins, die vom NodeBB Package Manager in die Whitelist aufgenommen wurden, können über den ACP installiert werden", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "Du darfst den Status der Plugins nicht ändern, da sie zur Laufzeit definiert werden (config.json, Umgebungsvariablen oder Terminalargumente). Bitte ändere stattdessen die Konfiguration.", "theme-not-set-in-configuration": "Wenn in der Konfiguration aktive Plugins definiert werden, muss bei einem Themenwechsel das neue Thema zur Liste der aktiven Plugins hinzugefügt werden, bevor es im ACP aktualisiert wird.", diff --git a/public/language/el/error.json b/public/language/el/error.json index eacb718b80..70bedd30c8 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index ac1d640df8..c3bb2dc892 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json index ac1d640df8..c3bb2dc892 100644 --- a/public/language/en-x-pirate/error.json +++ b/public/language/en-x-pirate/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/es/error.json b/public/language/es/error.json index 0eb92764ec..27a8776929 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "ID de plugin inválido", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Instalación de extensiones vía ACP está deshabilitada", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/et/error.json b/public/language/et/error.json index fed9ae8c6a..7fd4027cec 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json index c3a0d69b51..7660c66770 100644 --- a/public/language/fa-IR/error.json +++ b/public/language/fa-IR/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/fi/error.json b/public/language/fi/error.json index bf7ac10acb..d7d1af89ea 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index 6850f40cfa..d22c59af08 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Serveur inaccessible pour le moment. Cliquez ici pour réessayer ou réessayez plus tard", "invalid-plugin-id": "ID de plugin invalide", "plugin-not-whitelisted": "Impossible d'installer le plugin, seuls les plugins mis en liste blanche dans le gestionnaire de packages NodeBB peuvent être installés via l'ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "Vous n'êtes pas autorisé à modifier l'état des plugins car ils sont définis au moment de l'exécution (config.json, variables d'environnement ou arguments de terminal), veuillez plutôt modifier la configuration.", "theme-not-set-in-configuration": "Lors de la définition des plugins actifs, le changement de thème nécessite d'ajouter le nouveau thème à la liste des plugins actifs avant de le mettre à jour dans l'ACP", diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 90a746455a..7669c41b8f 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/he/error.json b/public/language/he/error.json index 8e8701f593..a8e7a433d5 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "לא ניתן להגיע לשרת בשלב זה. לחצו כאן כדי לנסות שוב, או נסו שוב במועד מאוחר יותר", "invalid-plugin-id": "מזהה תוסף לא תקין", "plugin-not-whitelisted": "לא ניתן להתקין את התוסף – ניתן להתקין דרך הניהול רק תוספים שנמצאים ברשימה הלבנה של מנהל החבילות של NodeBB.", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "התקנת תוסף באמצעות ACP מושבתת", "plugins-set-in-configuration": "לא ניתן לשנות את מצב התוסף כפי שהם מוגדרים בזמן ריצה (config.json, משתני סביבה או ארגומנטים של מסוף), שנו את התצורה במקום זאת.", "theme-not-set-in-configuration": "כאשר מגדירים תוספים פעילים בתצורה, שינוי ערכות נושא מחייב הוספת ערכת הנושא החדשה לרשימת התוספים הפעילים לפני עדכון שלו ב-ACP", diff --git a/public/language/hr/error.json b/public/language/hr/error.json index 64e727f74e..4e2d576d5c 100644 --- a/public/language/hr/error.json +++ b/public/language/hr/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/hu/error.json b/public/language/hu/error.json index d57301a8f8..e337533e57 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Nem lehet elérni a szervert. Kattints ide az újra próbáláshoz vagy várj egy kicsit", "invalid-plugin-id": "Érvénytelen plugin ID", "plugin-not-whitelisted": "Ez a bővítmény nem telepíthető – csak olyan bővítmények telepíthetőek amiket a NodeBB Package Manager az ACP-n keresztül tud telepíteni", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/hy/error.json b/public/language/hy/error.json index 9f7ada1b52..e6c4a3c18b 100644 --- a/public/language/hy/error.json +++ b/public/language/hy/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Այս պահին հնարավոր չէ միանալ սերվերին: Սեղմեք այստեղ՝ նորից փորձելու համար, կամ ավելի ուշ նորից փորձեք", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Հնարավոր չէ տեղադրել plugin – ACP-ի միջոցով կարող են տեղադրվել միայն NodeBB Package Manager-ի կողմից սպիտակ ցուցակում ներառված պլագինները", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "Ձեզ չի թույլատրվում փոխել plugin-ի վիճակը, քանի որ դրանք սահմանված են գործարկման ժամանակ (config.json, շրջակա միջավայրի փոփոխականներ կամ տերմինալի արգումենտներ), փոխարենը փոխեք կազմաձևը:", "theme-not-set-in-configuration": "Կազմաձևում ակտիվ պլագիններ սահմանելիս, թեմաները փոխելիս անհրաժեշտ է ավելացնել նոր թեման ակտիվ հավելումների ցանկում՝ նախքան այն թարմացնելը ACP-ում:", diff --git a/public/language/id/error.json b/public/language/id/error.json index b531cf59a7..21d344ce72 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/it/error.json b/public/language/it/error.json index abf63e8df8..bddf939887 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Impossibile raggiungere il server al momento. Clicca qui per riprovare o riprova in un secondo momento", "invalid-plugin-id": "ID plugin non valido", "plugin-not-whitelisted": "Impossibile installare il plug-in & solo i plugin nella whitelist del Gestione Pacchetti di NodeBB possono essere installati tramite ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "L'installazione dei plugin tramite ACP è disabilitata", "plugins-set-in-configuration": "Non è possibile modificare lo stato dei plugin, poiché sono definiti in fase di esecuzione. (config.json, variabili ambientali o argomenti del terminale); modificare invece la configurazione.", "theme-not-set-in-configuration": "Quando si definiscono i plugin attivi nella configurazione, la modifica dei temi richiede l'aggiunta del nuovo tema all'elenco dei plugin attivi prima di aggiornarlo nell'ACP", diff --git a/public/language/ja/error.json b/public/language/ja/error.json index 711ba3f8d7..be0b4396d1 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/ko/error.json b/public/language/ko/error.json index 4efa32a96d..80ee9f08e5 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "현재 서버에 연결할 수 없습니다. 여기를 클릭 후 다시 시도하거나 나중에 다시 시도하세요", "invalid-plugin-id": "잘못된 플러그인 ID", "plugin-not-whitelisted": "플러그인을 설치할 수 없습니다 - NodeBB 패키지 관리자에서 허용목록에 등록된 플러그인만 ACP를 통해 설치할 수 있습니다", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "실행 중에 정의된 플러그인 상태를 변경할 수 없습니다 (config.json, 환경 변수 또는 터미널 인수). 대신 구성을 수정하세요.", "theme-not-set-in-configuration": "구성에서 활성 플러그인을 정의할 때 새 테마를 추가하기 전에 ACP에서 테마를 업데이트해야 합니다", diff --git a/public/language/lt/error.json b/public/language/lt/error.json index 0c12b909ff..5fbf1188c4 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/lv/error.json b/public/language/lv/error.json index e61d791583..6195541875 100644 --- a/public/language/lv/error.json +++ b/public/language/lv/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/ms/error.json b/public/language/ms/error.json index 7a62ec9697..26e1b5a310 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/nb/error.json b/public/language/nb/error.json index ce8b49bedc..74d4ffbdac 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Får ikke tilgang til serveren for øyeblikket. Klikk her for å prøve igjen, eller prøv igjen senere", "invalid-plugin-id": "Ugyldig innstikk-ID", "plugin-not-whitelisted": "Ute av stand til å installere tillegget – bare tillegg som er hvitelistet av NodeBB sin pakkebehandler kan bli installert via administratorkontrollpanelet", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "Du har ikke tillatelse til å endre plugin-status da de er definert under kjøring (config.json, miljøvariabler eller terminalargumenter)Vennligst endre konfigurasjonen i stedet.", "theme-not-set-in-configuration": "Når aktive plugins er definert i konfigurasjonen, krever endring av tema at det nye temaet legges til i listen over aktive plugins før det oppdateres i ACP.", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 38fc553a01..662a161197 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Kan plugin niet installeren – alleen plugins toegestaan door de NodeBB Package Manager kunnen via de ACP geinstalleerd worden", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/nn-NO/error.json b/public/language/nn-NO/error.json index 2f8a10bbec..1268223a30 100644 --- a/public/language/nn-NO/error.json +++ b/public/language/nn-NO/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Kan ikkje nå serveren for augneblinken. Klikk her for å prøve igjen, eller prøv seinare", "invalid-plugin-id": "Ugyldig plugin-ID", "plugin-not-whitelisted": "Kan ikkje installere plugin – berre pluginar som er kvitelistet av NodeBB Package Manager kan installerast via ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "Du har ikkje løyve til å endre plugin-status sidan dei er definert ved oppstart (config.json, miljøvariablar eller terminalargument), ver venleg å endre konfigurasjonen i staden.", "theme-not-set-in-configuration": "Når ein definerer aktive pluginar i konfigurasjonen, krev endring av tema at det nye temaet vert lagt til i lista over aktive pluginar før det oppdaterast i ACP", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 4e9c0cc58a..acaf834286 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "W tej chwili nie można połączyć się z serwerem. Kliknij tutaj, aby spróbować ponownie, lub spróbuj ponownie później", "invalid-plugin-id": "Niepoprawny identyfikator wtyczki", "plugin-not-whitelisted": "Nie da się zainstalować tej wtyczki – tylko wtyczki z białej listy menadżera pakietów NodeBB mogą być instalowane przez ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Instalacja wtyczek przez ACP jest wyłączona", "plugins-set-in-configuration": "Nie możesz zmienić stanu wtyczki, bo został on zdefiniowany przy uruchamianiu (config.json, zmienne środowiskowe lub argumenty z terminala). Zamiast tego zmień konfigurację.", "theme-not-set-in-configuration": "Pamiętaj o zależności między aktywnymi wtyczkami a wystrojem, który ma z nimi współpracować.", diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json index 7c3299664d..2fc1039862 100644 --- a/public/language/pt-BR/error.json +++ b/public/language/pt-BR/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Não foi possível acessar o servidor neste momento. Clique aqui para tentar novamente ou tente novamente mais tarde", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Não foi possível instalar o plugin - apenas os plug-ins permitidos pelo NodeBB Package Manager podem ser instalados através do ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json index cef7efc5e3..fa0418c41c 100644 --- a/public/language/pt-PT/error.json +++ b/public/language/pt-PT/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/ro/error.json b/public/language/ro/error.json index be141aba93..abc63f3b06 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index 4b428d82b9..d741a098a1 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "В настоящее время невозможно связаться с сервером. Нажмите здесь, чтобы повторить попытку, или сделайте это позднее", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Не удалось установить плагин – только плагины, внесенные в белый список диспетчером пакетов NodeBB, могут быть установлены через ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/rw/error.json b/public/language/rw/error.json index a086230d94..0452b4fc11 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/sc/error.json b/public/language/sc/error.json index ac1d640df8..c3bb2dc892 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/sk/error.json b/public/language/sk/error.json index 90c552fff9..cd21ec05a3 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/sl/error.json b/public/language/sl/error.json index 1f793c9618..ac9775d30b 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/sq-AL/error.json b/public/language/sq-AL/error.json index 7bff3fb57a..b1aab456a0 100644 --- a/public/language/sq-AL/error.json +++ b/public/language/sq-AL/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Nuk mund të arrihet serveri në këtë moment. Kliko këtu për të provuar përsëri, ose provo më vonë", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Nuk mund të instalohet plugin – vetëm shtojcat e listuara në listën e bardhë nga Menaxheri i Paketave të NodeBB mund të instalohen nëpërmjet ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/sr/error.json b/public/language/sr/error.json index 3bec40bc5b..ec8ef67b20 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Тренутно није могуће приступити серверу. Кликните овде да бисте покушали поново или покушајте поново касније", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Инсталација додатне компоненте &ndash није могућа; преко ACP-а могу се инсталирати само додатне компоненте које је на белој листи ставио NodeBB Package Manager", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "Није вам дозвољено да мењате стање додатне компоненте онако како је дефинисано у време извршавања (config.json, променљиве окружења или аргументи терминала), уместо тога измените конфигурацију.", "theme-not-set-in-configuration": "Приликом дефинисања активних додатних компоненти у конфигурацији, промена тема захтева додавање нове теме на листу активних додатних компоненти пре ажурирања у ACP", diff --git a/public/language/sv/error.json b/public/language/sv/error.json index 0fdd062e90..19a3580440 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/th/error.json b/public/language/th/error.json index 062d51afa9..529a74d404 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "ไม่สามารถติดต่อกับเซิร์ฟเวอร์ในขณะนี้ คลิกที่นี่เพื่อลองใหม่ หรือลองอีกครั้งภายหลัง", "invalid-plugin-id": "รหัสปลั๊กอินไม่ถูกต้อง", "plugin-not-whitelisted": "ไม่สามารถติดตั้งปลั๊กอิน – เฉพาะปลั๊กอินที่ได้รับอนุญาตจาก NodeBB Package Manager ถึงจะติดตั้งผ่านแผงควบคุมผู้ดูแลระบบได้", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "คุณไม่สามารถเปลี่ยนสถานะของปลั๊กอินเนื่องจากถูกกำหนดตอนรัน (ไฟล์ config.json, ตัวแปร environmental หรือระบุตอนสั่งในบรรทัดคำสั่ง) โปรดปรับที่การตั้งค่าแทน", "theme-not-set-in-configuration": "เมื่อกำหนดปลั๊กอันที่กำลังทำงานในส่วนตั้งค่า การเปลี่ยนธีมต้องเพิ่มทีมในรายการปลั๊กอินที่กำลังใช้งานก่อนที่จะเปลี่ยนในแผงควบคุมผู้ดูแล", diff --git a/public/language/tr/error.json b/public/language/tr/error.json index 1b4093991a..ea93e9abe6 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Şu anda sunucuya ulaşılamıyor. Tekrar denemek için buraya tıklayın, veya daha sonra tekrar deneyin.", "invalid-plugin-id": "Geçersiz Eklenti ID", "plugin-not-whitelisted": "– eklentisi yüklenemedi, sadece NodeBB Paket Yöneticisi tarafından onaylanan eklentiler kontrol panelinden kurulabilir", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 33ea4560f3..002b6a8471 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "Invalid plugin ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index cbd7c3798a..a60add62f9 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Không thể truy cập máy chủ vào lúc này. Nhấp vào đây để thử lại hoặc thử lại sau", "invalid-plugin-id": "ID plugin không hợp lệ", "plugin-not-whitelisted": "Không thể cài đặt plugin – chỉ có plugin được Quản Lý Gói NodeBB đưa vào danh sách trắng mới có thể được cài đặt qua ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Cài đặt plugin qua ACP bị tắt", "plugins-set-in-configuration": "Bạn không được phép thay đổi trạng thái plugin vì chúng được xác định trong thời gian chạy (config.json, biến môi trường hoặc đối số đầu cuối), thay vào đó hãy sửa đổi cấu hình.", "theme-not-set-in-configuration": "Khi xác định các plugin hoạt động trong cấu hình, thay đổi giao diện buộc phải thêm giao diện mới vào danh sách các plugin hoạt động trước khi cập nhật nó trong ACP", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index 42a7901772..48103d6188 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "目前无法连接到服务器。请点击这里重试,或稍后再试", "invalid-plugin-id": "无效插件ID", "plugin-not-whitelisted": "无法安装插件 – 只有被NodeBB包管理器列入白名单的插件才能通过ACP安装。", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "ACP 安装插件已被禁用", "plugins-set-in-configuration": "您不能修改插件状态因为它们在运行时中被定义(config.json,环境变量或终端选项),请转而修改配置。", "theme-not-set-in-configuration": "在配置中定义活跃的插件时,需要先将新主题加入活跃插件的列表,才能在管理员控制面板中修改主题", diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json index 3b78a51fbb..14a9bcdbbb 100644 --- a/public/language/zh-TW/error.json +++ b/public/language/zh-TW/error.json @@ -237,6 +237,7 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "invalid-plugin-id": "無效的插件 ID", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", "plugin-installation-via-acp-disabled": "Plugin installation via ACP is disabled", "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", From a3cc99a2f07de51d33231dff75ded8504b833f89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:17:11 -0400 Subject: [PATCH 3090/4744] fix(deps): update dependency mongodb to v6.17.0 (#13471) 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 3a6dc38e22..70a231b0e4 100644 --- a/install/package.json +++ b/install/package.json @@ -91,7 +91,7 @@ "lru-cache": "11.1.0", "mime": "3.0.0", "mkdirp": "3.0.1", - "mongodb": "6.16.0", + "mongodb": "6.17.0", "morgan": "1.10.0", "mousetrap": "1.6.5", "multer": "2.0.0", From c363b84e90bbad19ae33356dfbc119a20a5d857d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:17:34 -0400 Subject: [PATCH 3091/4744] fix(deps): update dependency ace-builds to v1.42.0 (#13470) 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 70a231b0e4..c326eb69c7 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.41.0", + "ace-builds": "1.42.0", "archiver": "7.0.1", "async": "3.2.6", "autoprefixer": "10.4.21", From 602417d0f91526e41d9d15625db09c41b45ba4a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:17:56 -0400 Subject: [PATCH 3092/4744] fix(deps): update dependency sass to v1.89.1 (#13467) 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 c326eb69c7..14516f7473 100644 --- a/install/package.json +++ b/install/package.json @@ -127,7 +127,7 @@ "rss": "1.2.2", "rtlcss": "4.3.0", "sanitize-html": "2.17.0", - "sass": "1.89.0", + "sass": "1.89.1", "satori": "0.13.1", "semver": "7.7.2", "serve-favicon": "2.5.0", From d0060e5d7119f53ecb5600a50e61ec8cfe425e83 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:19:13 -0400 Subject: [PATCH 3093/4744] fix(deps): update dependency multer to v2.0.1 (#13466) 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 14516f7473..8c196c6567 100644 --- a/install/package.json +++ b/install/package.json @@ -94,7 +94,7 @@ "mongodb": "6.17.0", "morgan": "1.10.0", "mousetrap": "1.6.5", - "multer": "2.0.0", + "multer": "2.0.1", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.50", From 1c432925cdd83476e2e196f401a56cac31ef161c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:19:30 -0400 Subject: [PATCH 3094/4744] fix(deps): update dependency postcss to v8.5.4 (#13453) 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 8c196c6567..b248cbe0c5 100644 --- a/install/package.json +++ b/install/package.json @@ -118,7 +118,7 @@ "passport-local": "1.0.0", "pg": "8.16.0", "pg-cursor": "2.15.0", - "postcss": "8.5.3", + "postcss": "8.5.4", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", From 32f13162dc65cf1e49378ca9413a9b8a114012f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:19:56 -0400 Subject: [PATCH 3095/4744] chore(deps): update dependency sass-embedded to v1.89.1 (#13463) 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 b248cbe0c5..17c2ecec06 100644 --- a/install/package.json +++ b/install/package.json @@ -177,7 +177,7 @@ "smtp-server": "3.13.7" }, "optionalDependencies": { - "sass-embedded": "1.89.0" + "sass-embedded": "1.89.1" }, "resolutions": { "*/jquery": "3.7.1" From 6478532bf5e8e02050f31147f9f4665dd0e882d5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 5 Jun 2025 11:28:47 -0400 Subject: [PATCH 3096/4744] fix: ensure check returns false if no addresses are looked up, fix bug where cached value got changed accidentally --- src/request.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/request.js b/src/request.js index fe8de851c1..ce5e95d5bb 100644 --- a/src/request.js +++ b/src/request.js @@ -45,7 +45,8 @@ async function init() { * - For whatever reason `undici` needs to be required so that lookup can be overwritten properly. */ function lookup(hostname, options, callback) { - const { ok, lookup } = checkCache.get(hostname); + let { ok, lookup } = checkCache.get(hostname); + lookup = [...lookup]; if (!ok) { throw new Error('lookup-failed'); } @@ -166,6 +167,10 @@ async function check(url) { }); } + if (addresses.size < 1) { + return { ok: false }; + } + // Every IP address that the host resolves to should be a unicast address const ok = Array.from(addresses).every(({ address: ip }) => { const parsed = ipaddr.parse(ip); From 806e54bf5a0d6a6cefb2f9e0ba57346e77aef8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 5 Jun 2025 11:42:29 -0400 Subject: [PATCH 3097/4744] fix: closes #13475, don't store escaped username when updating profile --- src/api/users.js | 8 ++++---- test/user.js | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/api/users.js b/src/api/users.js index d3f9d8f7a3..4fb8155734 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -57,7 +57,7 @@ usersAPI.update = async function (caller, data) { throw new Error('[[error:invalid-data]]'); } - const oldUserData = await user.getUserFields(data.uid, ['email', 'username']); + const oldUserData = await db.getObjectFields(`user:${data.uid}`, ['email', 'username']); if (!oldUserData || !oldUserData.username) { throw new Error('[[error:invalid-data]]'); } @@ -86,14 +86,14 @@ usersAPI.update = async function (caller, data) { await user.updateProfile(caller.uid, data); const userData = await user.getUserData(data.uid); - - if (userData.username !== oldUserData.username) { + const oldUsernameEscaped = validator.escape(String(oldUserData.username)); + if (userData.username !== oldUsernameEscaped) { await events.log({ type: 'username-change', uid: caller.uid, targetUid: data.uid, ip: caller.ip, - oldUsername: oldUserData.username, + oldUsername: oldUsernameEscaped, newUsername: userData.username, }); } diff --git a/test/user.js b/test/user.js index 3fb1592af7..16f0919366 100644 --- a/test/user.js +++ b/test/user.js @@ -748,7 +748,9 @@ describe('User', () => { signature: 'nodebb is good', password: '123456', }; - const result = await apiUser.update({ uid: uid }, { ...data, password: '123456', invalid: 'field' }); + const result = await apiUser.update({ uid: uid }, { + ...data, password: '123456', invalid: 'field', + }); assert.equal(result.username, 'updatedUserName'); assert.equal(result.userslug, 'updatedusername'); assert.equal(result.fullname, 'updatedFullname'); @@ -767,6 +769,27 @@ describe('User', () => { assert.strictEqual(userData.invalid, undefined); }); + it('should not change the username to escaped version', async () => { + const uid = await User.create({ + username: 'ex\'ample_user', email: '13475@test.com', password: '123456', + }); + await User.setUserField(uid, 'email', '13475@test.com'); + await User.email.confirmByUid(uid); + + const data = { + uid: uid, + username: 'ex\'ample_user', + password: '123456', + }; + const result = await apiUser.update({ uid: uid }, { + ...data, password: '123456', invalid: 'field', + }); + const storedUsername = await db.getObjectField(`user:${uid}`, 'username'); + assert.equal(result.username, 'ex'ample_user'); + assert.equal(storedUsername, 'ex\'ample_user'); + assert.equal(result.userslug, 'ex-ample_user'); + }); + it('should also generate an email confirmation code for the changed email', async () => { const confirmSent = await User.email.isValidationPending(uid, 'updatedemail@me.com'); assert.strictEqual(confirmSent, true); From 44d1a17bc59a15eb74d35055b85b3a1774e16995 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:46:25 -0400 Subject: [PATCH 3098/4744] fix(deps): update dependency satori to v0.13.2 (#13468) 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 17c2ecec06..0ae491693d 100644 --- a/install/package.json +++ b/install/package.json @@ -128,7 +128,7 @@ "rtlcss": "4.3.0", "sanitize-html": "2.17.0", "sass": "1.89.1", - "satori": "0.13.1", + "satori": "0.13.2", "semver": "7.7.2", "serve-favicon": "2.5.0", "sharp": "0.32.6", From 01b10170aafd03961e9c6a5b950587901f7e00aa Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 6 Jun 2025 09:20:17 +0000 Subject: [PATCH 3099/4744] Latest translations and fallbacks --- public/language/bg/error.json | 2 +- public/language/it/error.json | 2 +- public/language/pl/error.json | 2 +- public/language/zh-CN/error.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 122239e27c..94d12a36b1 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -237,7 +237,7 @@ "socket-reconnect-failed": "В момента сървърът е недостъпен. Натиснете тук, за да опитате отново, или опитайте пак по-късно.", "invalid-plugin-id": "Грешен идентификатор на добавка", "plugin-not-whitelisted": "Добавката не може да бъде инсталирана – само добавки, одобрени от пакетния мениджър на NodeBB могат да бъдат инсталирани чрез ACP", - "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", + "cannot-toggle-system-plugin": "Не можете да превключите състоянието на системна добавка", "plugin-installation-via-acp-disabled": "Инсталирането на добавки чрез ACP е изключено", "plugins-set-in-configuration": "Не можете да променяте състоянието на добавката, тъй като то се определя по време на работата ѝ (чрез config.json, променливи на средата или аргументи при изпълнение). Вместо това може да промените конфигурацията.", "theme-not-set-in-configuration": "Когато определяте активните добавки в конфигурацията, промяната на темите изисква да се добави новата тема към активните добавки, преди актуализирането ѝ в ACP", diff --git a/public/language/it/error.json b/public/language/it/error.json index bddf939887..0fb310b196 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -237,7 +237,7 @@ "socket-reconnect-failed": "Impossibile raggiungere il server al momento. Clicca qui per riprovare o riprova in un secondo momento", "invalid-plugin-id": "ID plugin non valido", "plugin-not-whitelisted": "Impossibile installare il plug-in & solo i plugin nella whitelist del Gestione Pacchetti di NodeBB possono essere installati tramite ACP", - "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", + "cannot-toggle-system-plugin": "Non puoi attivare/disattivare lo stato di un plugin di sistema.", "plugin-installation-via-acp-disabled": "L'installazione dei plugin tramite ACP è disabilitata", "plugins-set-in-configuration": "Non è possibile modificare lo stato dei plugin, poiché sono definiti in fase di esecuzione. (config.json, variabili ambientali o argomenti del terminale); modificare invece la configurazione.", "theme-not-set-in-configuration": "Quando si definiscono i plugin attivi nella configurazione, la modifica dei temi richiede l'aggiunta del nuovo tema all'elenco dei plugin attivi prima di aggiornarlo nell'ACP", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index acaf834286..27cb6c9d42 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -237,7 +237,7 @@ "socket-reconnect-failed": "W tej chwili nie można połączyć się z serwerem. Kliknij tutaj, aby spróbować ponownie, lub spróbuj ponownie później", "invalid-plugin-id": "Niepoprawny identyfikator wtyczki", "plugin-not-whitelisted": "Nie da się zainstalować tej wtyczki – tylko wtyczki z białej listy menadżera pakietów NodeBB mogą być instalowane przez ACP", - "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", + "cannot-toggle-system-plugin": "Nie można zmienić nastawu dla wtyczki systemowej", "plugin-installation-via-acp-disabled": "Instalacja wtyczek przez ACP jest wyłączona", "plugins-set-in-configuration": "Nie możesz zmienić stanu wtyczki, bo został on zdefiniowany przy uruchamianiu (config.json, zmienne środowiskowe lub argumenty z terminala). Zamiast tego zmień konfigurację.", "theme-not-set-in-configuration": "Pamiętaj o zależności między aktywnymi wtyczkami a wystrojem, który ma z nimi współpracować.", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index 48103d6188..e91848b78d 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -237,7 +237,7 @@ "socket-reconnect-failed": "目前无法连接到服务器。请点击这里重试,或稍后再试", "invalid-plugin-id": "无效插件ID", "plugin-not-whitelisted": "无法安装插件 – 只有被NodeBB包管理器列入白名单的插件才能通过ACP安装。", - "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", + "cannot-toggle-system-plugin": "您不能切换系统插件的状态", "plugin-installation-via-acp-disabled": "ACP 安装插件已被禁用", "plugins-set-in-configuration": "您不能修改插件状态因为它们在运行时中被定义(config.json,环境变量或终端选项),请转而修改配置。", "theme-not-set-in-configuration": "在配置中定义活跃的插件时,需要先将新主题加入活跃插件的列表,才能在管理员控制面板中修改主题", From b3170c9c8ba4172f2401839179731050ebb7b503 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:08:13 -0400 Subject: [PATCH 3100/4744] chore(deps): update dependency @eslint/js to v9.28.0 (#13469) 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 0ae491693d..fd02304c27 100644 --- a/install/package.json +++ b/install/package.json @@ -161,7 +161,7 @@ "@commitlint/cli": "19.8.1", "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.28.0", "@stylistic/eslint-plugin": "4.4.0", "eslint-config-nodebb": "1.1.6", "eslint-plugin-import": "2.31.0", From 166aaa7ab9379fc2ac2fb9ad6b908e21aceacaa4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:08:25 -0400 Subject: [PATCH 3101/4744] chore(deps): update redis docker tag to v8.0.2 (#13465) 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 43fdf33611..cf72066b92 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: - 5432:5432 redis: - image: 'redis:8.0.1' + image: 'redis:8.0.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 9011d0f92a..8a67d5964d 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.0.1-alpine + image: redis:8.0.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 da31cd6886..d8855ccac4 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.0.1-alpine + image: redis:8.0.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 637cecb0cd..a53acdfef2 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.0.1-alpine + image: redis:8.0.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 6b33b1f457a06416881437a814c1df2999daa080 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:08:44 -0400 Subject: [PATCH 3102/4744] fix(deps): update dependency workerpool to v9.3.2 (#13452) 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 fd02304c27..bcab3b1d66 100644 --- a/install/package.json +++ b/install/package.json @@ -150,7 +150,7 @@ "webpack": "5.99.9", "webpack-merge": "6.0.1", "winston": "3.17.0", - "workerpool": "9.2.0", + "workerpool": "9.3.2", "xml": "1.0.1", "xregexp": "5.1.2", "yargs": "17.7.2", From d239125f438e6ba8a514524f0278898896002fc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:08:54 -0400 Subject: [PATCH 3103/4744] chore(deps): update dependency smtp-server to v3.13.8 (#13464) 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 bcab3b1d66..e9a67cd2c0 100644 --- a/install/package.json +++ b/install/package.json @@ -174,7 +174,7 @@ "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", - "smtp-server": "3.13.7" + "smtp-server": "3.13.8" }, "optionalDependencies": { "sass-embedded": "1.89.1" From 536ae9d6a577926aef93c3bf601c4914f6f9f3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 6 Jun 2025 11:26:02 -0400 Subject: [PATCH 3104/4744] chore: up eslint --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index e9a67cd2c0..117a6d013d 100644 --- a/install/package.json +++ b/install/package.json @@ -162,8 +162,8 @@ "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", "@eslint/js": "9.28.0", - "@stylistic/eslint-plugin": "4.4.0", - "eslint-config-nodebb": "1.1.6", + "@stylistic/eslint-plugin": "4.4.1", + "eslint-config-nodebb": "1.1.7", "eslint-plugin-import": "2.31.0", "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", From 29afcd36b51946f6267f4ba3a1e5f49c89bf3a39 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:18:57 -0400 Subject: [PATCH 3105/4744] fix(deps): update dependency satori to v0.14.0 (#13476) 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 117a6d013d..58d09b18cf 100644 --- a/install/package.json +++ b/install/package.json @@ -128,7 +128,7 @@ "rtlcss": "4.3.0", "sanitize-html": "2.17.0", "sass": "1.89.1", - "satori": "0.13.2", + "satori": "0.14.0", "semver": "7.7.2", "serve-favicon": "2.5.0", "sharp": "0.32.6", From f157cfa7e8225dd6fd904cd1c2c6ff4160a3dae1 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 8 Jun 2025 09:19:19 +0000 Subject: [PATCH 3106/4744] Latest translations and fallbacks --- public/language/vi/error.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/vi/error.json b/public/language/vi/error.json index a60add62f9..467b78742c 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -237,7 +237,7 @@ "socket-reconnect-failed": "Không thể truy cập máy chủ vào lúc này. Nhấp vào đây để thử lại hoặc thử lại sau", "invalid-plugin-id": "ID plugin không hợp lệ", "plugin-not-whitelisted": "Không thể cài đặt plugin – chỉ có plugin được Quản Lý Gói NodeBB đưa vào danh sách trắng mới có thể được cài đặt qua ACP", - "cannot-toggle-system-plugin": "You cannot toggle the state of a system plugin", + "cannot-toggle-system-plugin": "Bạn không thể chuyển đổi trạng thái của một plugin hệ thống", "plugin-installation-via-acp-disabled": "Cài đặt plugin qua ACP bị tắt", "plugins-set-in-configuration": "Bạn không được phép thay đổi trạng thái plugin vì chúng được xác định trong thời gian chạy (config.json, biến môi trường hoặc đối số đầu cuối), thay vào đó hãy sửa đổi cấu hình.", "theme-not-set-in-configuration": "Khi xác định các plugin hoạt động trong cấu hình, thay đổi giao diện buộc phải thêm giao diện mới vào danh sách các plugin hoạt động trước khi cập nhật nó trong ACP", From b02eb57d067105c4aedae09cf1eea5413db566e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 9 Jun 2025 10:23:00 -0400 Subject: [PATCH 3107/4744] fix: escape, query params --- src/controllers/admin/events.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js index 9f3321276a..72d9b4c3e1 100644 --- a/src/controllers/admin/events.js +++ b/src/controllers/admin/events.js @@ -1,5 +1,6 @@ 'use strict'; +const validator = require('validator'); const db = require('../../database'); const events = require('../../events'); const pagination = require('../../pagination'); @@ -58,6 +59,12 @@ eventsController.get = async function (req, res) { events: eventData, pagination: pagination.create(page, pageCount, req.query), types: types, - query: req.query, + query: { + start: validator.escape(String(req.query.start)), + end: validator.escape(String(req.query.end)), + username: validator.escape(String(req.query.username)), + group: validator.escape(String(req.query.group)), + perPage: validator.escape(String(req.query.perPage)), + }, }); }; From 9b4082dcfbec77e72437975e76b2ba2de4e3572b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 10:54:01 -0400 Subject: [PATCH 3108/4744] chore(deps): update dependency mocha to v11.6.0 (#13479) 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 58d09b18cf..a1b3026c7b 100644 --- a/install/package.json +++ b/install/package.json @@ -170,7 +170,7 @@ "husky": "8.0.3", "jsdom": "26.1.0", "lint-staged": "16.1.0", - "mocha": "11.5.0", + "mocha": "11.6.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", From 5f51dfc4356a70493a789d419dec41866b8003f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 9 Jun 2025 11:10:07 -0400 Subject: [PATCH 3109/4744] chore: up composer --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d892077725..d4f1154359 100644 --- a/install/package.json +++ b/install/package.json @@ -98,7 +98,7 @@ "multiparty": "4.2.3", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", - "nodebb-plugin-composer-default": "10.2.50", + "nodebb-plugin-composer-default": "10.2.51", "nodebb-plugin-dbsearch": "6.2.19", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", From 3d88cb8696cc9a5d206575646ae952352e4d06f1 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 9 Jun 2025 15:26:58 +0000 Subject: [PATCH 3110/4744] chore: incrementing version number - v4.4.3 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d4f1154359..fae65ae63f 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.4.2", + "version": "4.4.3", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 0c9297f81cd1e62df4c6cecaf36fe66fc4016472 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 9 Jun 2025 15:26:59 +0000 Subject: [PATCH 3111/4744] chore: update changelog for v4.4.3 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8883eacb3..ec4faa3658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +#### v4.4.3 (2025-06-09) + +##### Chores + +* up composer (5f51dfc4) +* incrementing version number - v4.4.2 (55c510ae) +* update changelog for v4.4.2 (6d40a211) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### Bug Fixes + +* escape, query params (b02eb57d) +* closes #13475, don't store escaped username (806e54bf) + #### v4.4.2 (2025-06-02) ##### Chores From 78ebe2988bb6ed76c95120116f2319ea986469e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 11:55:37 -0400 Subject: [PATCH 3112/4744] fix(deps): update dependency satori to v0.15.2 (#13481) 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 897109b8e2..1e31e33e2e 100644 --- a/install/package.json +++ b/install/package.json @@ -128,7 +128,7 @@ "rtlcss": "4.3.0", "sanitize-html": "2.17.0", "sass": "1.89.1", - "satori": "0.14.0", + "satori": "0.15.2", "semver": "7.7.2", "serve-favicon": "2.5.0", "sharp": "0.32.6", From 14e30c4bf8b0c3064cd1b3789badf32f50e22cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 10:47:14 -0400 Subject: [PATCH 3113/4744] feat: closes #13484, post preview changes don't close preview when mouse leaves the anchor close preview on click outside close preview when mouseleaves preview open the preview to the top if there isn't enough space add scrollbar to post preview --- public/src/client/topic.js | 28 ++++++++++++++++++++--- src/views/partials/topic/post-preview.tpl | 15 +++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 1919638f01..b790560fa1 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -312,12 +312,27 @@ define('forum/topic', [ const postCache = {}; function destroyTooltip() { clearTimeout(timeoutId); + timeoutId = 0; $('#post-tooltip').remove(); destroyed = true; } + + function onClickOutside(ev) { + // If the click is outside the tooltip, destroy it + if (!$(ev.target).closest('#post-tooltip').length) { + destroyTooltip(); + } + } + $(window).one('action:ajaxify.start', destroyTooltip); - $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/content"] a, [component="topic/event"] a', async function () { + $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { const link = $(this); + link.one('mouseleave', function() { + if (timeoutId > 0) { + clearTimeout(timeoutId); + timeoutId = 0; + } + }); destroyed = false; async function renderPost(pid) { @@ -335,8 +350,13 @@ define('forum/topic', [ const postRect = postContent.offset(); const postWidth = postContent.width(); const linkRect = link.offset(); + const { top } = link.get(0).getBoundingClientRect(); + const dropup = top > window.innerHeight / 2; + + tooltip.one('mouseleave', destroyTooltip); + $(window).off('click', onClickOutside).one('click', onClickOutside); tooltip.css({ - top: linkRect.top + 30, + top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, left: postRect.left, width: postWidth, }); @@ -357,16 +377,18 @@ define('forum/topic', [ } timeoutId = setTimeout(async () => { + timeoutId = 0; renderPost(pid); }, 300); } else if (topicMatch) { timeoutId = setTimeout(async () => { + timeoutId = 0; const tid = topicMatch[1]; const topicData = await api.get('/topics/' + tid, {}); renderPost(topicData.mainPid); }, 300); } - }).on('mouseleave', '[component="post"] a, [component="topic/event"] a', destroyTooltip); + }); } function setupQuickReply() { diff --git a/src/views/partials/topic/post-preview.tpl b/src/views/partials/topic/post-preview.tpl index 107075eef3..c307e14f81 100644 --- a/src/views/partials/topic/post-preview.tpl +++ b/src/views/partials/topic/post-preview.tpl @@ -1,11 +1,14 @@
    From 8ab034d8f05c90e03fef0f719c2110061d0de01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 10:52:55 -0400 Subject: [PATCH 3114/4744] lint: fix lint --- public/src/client/topic.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index b790560fa1..0480d3844c 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -327,7 +327,7 @@ define('forum/topic', [ $(window).one('action:ajaxify.start', destroyTooltip); $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { const link = $(this); - link.one('mouseleave', function() { + link.one('mouseleave', function () { if (timeoutId > 0) { clearTimeout(timeoutId); timeoutId = 0; @@ -356,7 +356,7 @@ define('forum/topic', [ tooltip.one('mouseleave', destroyTooltip); $(window).off('click', onClickOutside).one('click', onClickOutside); tooltip.css({ - top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, + top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, left: postRect.left, width: postWidth, }); From 0ebb31fe87750e6b50a2cccc389c0170543af493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 12:39:49 -0400 Subject: [PATCH 3115/4744] fix: #13484, clear tooltip if cursor leaves link and doesn't enter tooltip --- public/src/client/topic.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 0480d3844c..3bb50a5da5 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -309,12 +309,14 @@ define('forum/topic', [ } let timeoutId = 0; let destroyed = false; + let cursorOnPreviewTooltip = false; const postCache = {}; function destroyTooltip() { clearTimeout(timeoutId); timeoutId = 0; $('#post-tooltip').remove(); destroyed = true; + cursorOnPreviewTooltip = false; } function onClickOutside(ev) { @@ -328,6 +330,14 @@ define('forum/topic', [ $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { const link = $(this); link.one('mouseleave', function () { + setTimeout(() => { + // If the mouse leaves the link and it's not on a tooltip, destroy the tooltip after a short delay + if (!cursorOnPreviewTooltip) { + destroyTooltip(); + } + }, 300); + + // if mouse leaves the link before the tooltip is rendered, clear the timeout if (timeoutId > 0) { clearTimeout(timeoutId); timeoutId = 0; @@ -352,7 +362,9 @@ define('forum/topic', [ const linkRect = link.offset(); const { top } = link.get(0).getBoundingClientRect(); const dropup = top > window.innerHeight / 2; - + tooltip.on('mouseenter', function () { + onPreviewTooltip = true; + }); tooltip.one('mouseleave', destroyTooltip); $(window).off('click', onClickOutside).one('click', onClickOutside); tooltip.css({ From 2280ea88f24ff82a8d1ec1347dade0c525b41f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 12:46:07 -0400 Subject: [PATCH 3116/4744] fix: typo --- public/src/client/topic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 3bb50a5da5..f332a807be 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -363,7 +363,7 @@ define('forum/topic', [ const { top } = link.get(0).getBoundingClientRect(); const dropup = top > window.innerHeight / 2; tooltip.on('mouseenter', function () { - onPreviewTooltip = true; + cursorOnPreviewTooltip = true; }); tooltip.one('mouseleave', destroyTooltip); $(window).off('click', onClickOutside).one('click', onClickOutside); From 32faaba0e5a53da8c6ba4eb9192c6dd157a252ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 10 Jun 2025 13:36:23 -0400 Subject: [PATCH 3117/4744] fix: more edge cases --- public/src/client/topic.js | 119 +++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 63 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 3bb50a5da5..43a64a9fa3 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -307,16 +307,16 @@ define('forum/topic', [ if (!ajaxify.data.showPostPreviewsOnHover || utils.isMobile()) { return; } - let timeoutId = 0; + let renderTimeout = 0; let destroyed = false; - let cursorOnPreviewTooltip = false; + let link = null; + const postCache = {}; function destroyTooltip() { - clearTimeout(timeoutId); - timeoutId = 0; + clearTimeout(renderTimeout); + renderTimeout = 0; $('#post-tooltip').remove(); destroyed = true; - cursorOnPreviewTooltip = false; } function onClickOutside(ev) { @@ -327,79 +327,72 @@ define('forum/topic', [ } $(window).one('action:ajaxify.start', destroyTooltip); + $('[component="topic"]').on('mouseenter', 'a[component="post/parent"], [component="post/parent/content"] a,[component="post/content"] a, [component="topic/event"] a', async function () { - const link = $(this); + link = $(this); + link.removeAttr('over-tooltip'); link.one('mouseleave', function () { + clearTimeout(renderTimeout); + renderTimeout = 0; setTimeout(() => { - // If the mouse leaves the link and it's not on a tooltip, destroy the tooltip after a short delay - if (!cursorOnPreviewTooltip) { + if (!link.attr('over-tooltip') && !renderTimeout) { destroyTooltip(); } - }, 300); - - // if mouse leaves the link before the tooltip is rendered, clear the timeout - if (timeoutId > 0) { - clearTimeout(timeoutId); - timeoutId = 0; - } + }, 100); }); + clearTimeout(renderTimeout); destroyed = false; - async function renderPost(pid) { - const postData = postCache[pid] || await api.get(`/posts/${encodeURIComponent(pid)}/summary`); - $('#post-tooltip').remove(); - if (postData && ajaxify.data.template.topic) { - postCache[pid] = postData; - const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData }); - if (destroyed) { - return; + renderTimeout = setTimeout(async () => { + async function renderPost(pid) { + const postData = postCache[pid] || await api.get(`/posts/${encodeURIComponent(pid)}/summary`); + $('#post-tooltip').remove(); + if (postData && ajaxify.data.template.topic) { + postCache[pid] = postData; + const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData }); + if (destroyed) { + return; + } + tooltip.hide().find('.timeago').timeago(); + tooltip.appendTo($('body')).fadeIn(300); + const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first(); + const postRect = postContent.offset(); + const postWidth = postContent.width(); + const linkRect = link.offset(); + const { top } = link.get(0).getBoundingClientRect(); + const dropup = top > window.innerHeight / 2; + tooltip.on('mouseenter', function () { + link.attr('over-tooltip', 1); + }); + tooltip.one('mouseleave', destroyTooltip); + $(window).off('click', onClickOutside).one('click', onClickOutside); + tooltip.css({ + top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, + left: postRect.left, + width: postWidth, + }); } - tooltip.hide().find('.timeago').timeago(); - tooltip.appendTo($('body')).fadeIn(300); - const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first(); - const postRect = postContent.offset(); - const postWidth = postContent.width(); - const linkRect = link.offset(); - const { top } = link.get(0).getBoundingClientRect(); - const dropup = top > window.innerHeight / 2; - tooltip.on('mouseenter', function () { - onPreviewTooltip = true; - }); - tooltip.one('mouseleave', destroyTooltip); - $(window).off('click', onClickOutside).one('click', onClickOutside); - tooltip.css({ - top: dropup ? linkRect.top - tooltip.outerHeight() : linkRect.top + 30, - left: postRect.left, - width: postWidth, - }); - } - } - - const href = link.attr('href'); - const location = utils.urlToLocation(href); - const pathname = location.pathname; - const validHref = href && href !== '#' && window.location.hostname === location.hostname; - $('#post-tooltip').remove(); - const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+|(?:[\w_.~!$&'()*+,;=:@-]|%[\dA-F]{2})+)/); - const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\da-z-]+)/); - if (postMatch) { - const pid = postMatch[1]; - if (encodeURIComponent(link.parents('[component="post"]').attr('data-pid')) === encodeURIComponent(pid)) { - return; // dont render self post } - timeoutId = setTimeout(async () => { - timeoutId = 0; + const href = link.attr('href'); + const location = utils.urlToLocation(href); + const pathname = location.pathname; + const validHref = href && href !== '#' && window.location.hostname === location.hostname; + $('#post-tooltip').remove(); + const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+|(?:[\w_.~!$&'()*+,;=:@-]|%[\dA-F]{2})+)/); + const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\da-z-]+)/); + if (postMatch) { + const pid = postMatch[1]; + if (encodeURIComponent(link.parents('[component="post"]').attr('data-pid')) === encodeURIComponent(pid)) { + return; // dont render self post + } renderPost(pid); - }, 300); - } else if (topicMatch) { - timeoutId = setTimeout(async () => { - timeoutId = 0; + } else if (topicMatch) { const tid = topicMatch[1]; const topicData = await api.get('/topics/' + tid, {}); renderPost(topicData.mainPid); - }, 300); - } + } + }, 300); }); } From 95ae8b5f1a109996f2122ae7a51a9fbadd670279 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 11 Jun 2025 09:19:40 +0000 Subject: [PATCH 3118/4744] Latest translations and fallbacks --- public/language/nb/category.json | 4 ++-- public/language/nb/error.json | 6 +++--- public/language/nb/user.json | 10 +++++----- public/language/nb/world.json | 14 +++++++------- public/language/nn-NO/category.json | 4 ++-- public/language/nn-NO/user.json | 4 ++-- public/language/nn-NO/world.json | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/public/language/nb/category.json b/public/language/nb/category.json index e114c8a083..5da2e6af1f 100644 --- a/public/language/nb/category.json +++ b/public/language/nb/category.json @@ -1,8 +1,8 @@ { "category": "Kategori", "subcategories": "Underkategorier", - "uncategorized": "Uncategorized", - "uncategorized.description": "Topics that do not strictly fit in with any existing categories", + "uncategorized": "Ukategorisert", + "uncategorized.description": "Innlegg som ikke passer inn i noen av de eksisterende kategoriene.", "handle.description": "This category can be followed from the open social web via the handle %1", "new-topic-button": "Nytt innlegg", "guest-login-post": "Logg inn for å publisere innlegg", diff --git a/public/language/nb/error.json b/public/language/nb/error.json index 74d4ffbdac..e7fbac1aaa 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -3,7 +3,7 @@ "invalid-json": "Ugyldig JSON", "wrong-parameter-type": "En verdi av typen %3 var forventet for egenskapen `%1`, men %2 ble mottatt i stedet", "required-parameters-missing": "Nødvendige parametere manglet fra dette API-kallet: %1", - "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "reserved-ip-address": "Nettverksforespørsler til reservert IP-område er ikke tillatt.", "not-logged-in": "Du ser ikke ut til å være logget inn.", "account-locked": "Kontoen din har blitt midlertidig låst", "search-requires-login": "Søking krever en konto - vennligst logg inn eller registrer deg.", @@ -68,8 +68,8 @@ "no-chat-room": "Chatten eksisterer ikke", "no-privileges": "Du har ikke nok rettigheter til å utføre denne handlingen.", "category-disabled": "Kategori deaktivert", - "post-deleted": "Post deleted", - "topic-locked": "Topic locked", + "post-deleted": "Innlegget er slettet.", + "topic-locked": "Innlegget er låst", "post-edit-duration-expired": "Du har bare lov til å redigere innlegg i %1 sekund(er) etter at det er sendt", "post-edit-duration-expired-minutes": "Du har bare lov til å redigere innlegg i %1 sekund(er) etter at det er sendt", "post-edit-duration-expired-minutes-seconds": "Du har bare lov til å redigere innlegg i %1 minutt(er), %2 sekund(er) etter at det er sendt", diff --git a/public/language/nb/user.json b/public/language/nb/user.json index efcbc0c1d7..10f16695ae 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -59,7 +59,7 @@ "chat": "Chat", "chat-with": "Fortsett å chatte med %1", "new-chat-with": "Start ny chat med %1", - "view-remote": "View Original", + "view-remote": "Vis opprinnelig versjon", "flag-profile": "Rapporter profil", "profile-flagged": "Allerede flagget", "follow": "Følg", @@ -105,10 +105,10 @@ "show-email": "Vis min e-post", "show-fullname": "Vis mitt fulle navn", "restrict-chats": "Bare tillat chat-meldinger fra brukere jeg følger", - "disable-incoming-chats": "Disable incoming chat messages ", - "chat-allow-list": "Allow chat messages from the following users", - "chat-deny-list": "Deny chat messages from the following users", - "chat-list-add-user": "Add user", + "disable-incoming-chats": "Slå av meldinger fra chat ", + "chat-allow-list": "Bare tillat chat-meldinger fra følgende brukere", + "chat-deny-list": "Ikke tillat chat-meldinger fra følgende brukere", + "chat-list-add-user": "Legg til bruker", "digest-label": "Abonner på sammendrag", "digest-description": "Abonner på e-post-oppdateringer for dette forumet (nye varsler og innlegg) i samsvar med valgte tidspunkt", "digest-off": "Av", diff --git a/public/language/nb/world.json b/public/language/nb/world.json index d61c766c85..c2d9bb4bc2 100644 --- a/public/language/nb/world.json +++ b/public/language/nb/world.json @@ -7,15 +7,15 @@ "help.title": "Hva er denne siden?", "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.build": "Det kan hende det ikke er så mange innlegg her i starten, det er helt normalt. Du vil begynne å se mer innhold her etter hvert som du begynner å følge andre brukere.", "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.next-generation": "Dette er neste generasjon sosiale medier, begynn å bidra i dag!", "onboard.title": "Ditt vindu til fødiverset...", - "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.what": "Dette er din personlige kategori, som kun består av innhold funnet utenfor dette forumet. Om noe vises på denne siden, avhenger av om du følger dem, eller om innlegget ble delt av noen du følger.", + "onboard.why": "Det skjer mye utenfor dette forumet, og ikke alt er relevant for dine interesser. Derfor er det å følge folk den beste måten å vise at du vil se mer fra noen.", + "onboard.how": "I mellomtiden kan du klikke på snarveisknappene øverst for å se hva annet dette forumet inneholder, og begynne å oppdage nytt innhold!", - "show-categories": "Show categories", - "hide-categories": "Hide categories" + "show-categories": "Vis kategorier", + "hide-categories": "Skjul kategorier" } \ No newline at end of file diff --git a/public/language/nn-NO/category.json b/public/language/nn-NO/category.json index 152c69d345..2ef1b7bff5 100644 --- a/public/language/nn-NO/category.json +++ b/public/language/nn-NO/category.json @@ -1,8 +1,8 @@ { "category": "Kategori", "subcategories": "Underkategoriar", - "uncategorized": "Uncategorized", - "uncategorized.description": "Topics that do not strictly fit in with any existing categories", + "uncategorized": "Ukategorisert", + "uncategorized.description": "Innlegg som ikkje passar helt inn i nokon av dei eksisterande kategoriane.", "handle.description": "This category can be followed from the open social web via the handle %1", "new-topic-button": "Nytt innlegg", "guest-login-post": "Logg inn for å legge inn innlegg", diff --git a/public/language/nn-NO/user.json b/public/language/nn-NO/user.json index cd615d95be..702ba8b92a 100644 --- a/public/language/nn-NO/user.json +++ b/public/language/nn-NO/user.json @@ -59,7 +59,7 @@ "chat": "Chat", "chat-with": "Fortset chat med %1", "new-chat-with": "Start ny chat med %1", - "view-remote": "View Original", + "view-remote": "Vis opphavleg versjon", "flag-profile": "Rapporter profil", "profile-flagged": "Allerede flagga", "follow": "Følg", @@ -105,7 +105,7 @@ "show-email": "Vis e-posten min", "show-fullname": "Vis fullt namn", "restrict-chats": "Tillat berre chatmeldingar frå brukarar eg følgjer", - "disable-incoming-chats": "Disable incoming chat messages ", + "disable-incoming-chats": "Slå av meldingar frå chat ", "chat-allow-list": "Allow chat messages from the following users", "chat-deny-list": "Deny chat messages from the following users", "chat-list-add-user": "Add user", diff --git a/public/language/nn-NO/world.json b/public/language/nn-NO/world.json index 71be5a0749..978f5841bf 100644 --- a/public/language/nn-NO/world.json +++ b/public/language/nn-NO/world.json @@ -14,7 +14,7 @@ "onboard.title": "Ditt vindauge til fødiverset...", "onboard.what": "Dette er din personlege kategori som berre består av innhald funne utanfor dette forumet. Om noko blir vist på denne sida, avheng av om du følgjer dei, eller om innlegget blei delt av nokon du følgjer.", "onboard.why": "Det skjer mykje utanfor dette forumet, og ikkje alt er relevant for interessene dine. Difor er det å følgje folk den beste måten å signalisere at du ønskjer å sjå meir frå nokon.", - "onboard.how": "I mellomtida kan du klikke på snarvegsknappane øvst for å sjå kva anna dette forumet kjenner til, og begynne å oppdage nytt innhald!", + "onboard.how": "I mellomtida kan du klikke på snarvegsknappane øvst for å sjå kva anna dette forumet inneheld, og begynne å oppdage nytt innhald!", "show-categories": "Show categories", "hide-categories": "Hide categories" From 6c5b22684bdb30598dd7166f8fd4104ab7bd177b Mon Sep 17 00:00:00 2001 From: cliffmccarthy <16453869+cliffmccarthy@users.noreply.github.com> Date: Wed, 11 Jun 2025 08:52:36 -0500 Subject: [PATCH 3119/4744] fix: Revise package hash check in Docker entrypoint.sh (#13483) - In the build_forum() function, the file install_hash.md5 is intended to track the content of package.json and detect changes that imply the need to run 'nodebb upgrade'. - The check to compare the current checksum of package.json to the one saved in install_hash.md5 is reversed. The "package.json was updated" branch is taken when the hashes are the same, not when they are different. - When install_hash.md5 does not exist, the comparison value becomes the null string, which never matches the checksum of package.json. As a result, the code always takes the "No changes in package.json" branch and returns from the function without creating install_hash.md5. As a result, install_hash.md5 never gets created on a new installation. - Revised build_forum() to use "not equals" when comparing the two checksums. This causes it to run 'nodebb upgrade' when the checksums are different, and also when install_hash.md5 does not yet exist. If the checksum saved in install_hash.md5 matches the current package.json checksum, it proceeds to either the "Build before start" case or the "No changes" case. --- install/docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/docker/entrypoint.sh b/install/docker/entrypoint.sh index dd17d707e7..db2a637ee0 100755 --- a/install/docker/entrypoint.sh +++ b/install/docker/entrypoint.sh @@ -103,7 +103,7 @@ build_forum() { local config="$1" local start_build="$2" local package_hash=$(md5sum install/package.json | head -c 32) - if [ "$package_hash" = "$(cat $CONFIG_DIR/install_hash.md5 || true)" ]; then + if [ "$package_hash" != "$(cat $CONFIG_DIR/install_hash.md5 || true)" ]; then echo "package.json was updated. Upgrading..." /usr/src/app/nodebb upgrade --config="$config" || { echo "Failed to build NodeBB. Exiting..." From f56517878288cac09b12f27c354e4a4bf2002c97 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:37:52 -0400 Subject: [PATCH 3120/4744] chore(deps): update dependency sass-embedded to v1.89.2 (#13482) 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 1e31e33e2e..a17eb36a4c 100644 --- a/install/package.json +++ b/install/package.json @@ -177,7 +177,7 @@ "smtp-server": "3.13.8" }, "optionalDependencies": { - "sass-embedded": "1.89.1" + "sass-embedded": "1.89.2" }, "resolutions": { "*/jquery": "3.7.1" From c04bd7cc6e6569440b82f86218e1f3b3ff559248 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:38:12 -0400 Subject: [PATCH 3121/4744] fix(deps): update dependency @fontsource/inter to v5.2.6 (#13477) 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 a17eb36a4c..c8f0dca264 100644 --- a/install/package.json +++ b/install/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@adactive/bootstrap-tagsinput": "0.8.2", - "@fontsource/inter": "5.2.5", + "@fontsource/inter": "5.2.6", "@fontsource/poppins": "5.2.6", "@fortawesome/fontawesome-free": "6.7.2", "@isaacs/ttlcache": "1.4.1", From d2a7eecb28ec267b6ec9abb2dd150328d98ad202 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:02:25 -0400 Subject: [PATCH 3122/4744] fix(deps): update dependency serve-favicon to v2.5.1 (#13488) 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 c8f0dca264..17e51cfc99 100644 --- a/install/package.json +++ b/install/package.json @@ -130,7 +130,7 @@ "sass": "1.89.1", "satori": "0.15.2", "semver": "7.7.2", - "serve-favicon": "2.5.0", + "serve-favicon": "2.5.1", "sharp": "0.32.6", "sitemap": "8.0.0", "socket.io": "4.8.1", From efcbbf29d161290ab4f14b40ecac34279eb94500 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:02:43 -0400 Subject: [PATCH 3123/4744] fix(deps): update dependency nodebb-plugin-emoji to v6.0.3 (#13486) 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 17e51cfc99..5df2e774ab 100644 --- a/install/package.json +++ b/install/package.json @@ -99,7 +99,7 @@ "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.51", "nodebb-plugin-dbsearch": "6.2.19", - "nodebb-plugin-emoji": "6.0.2", + "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.1", "nodebb-plugin-mentions": "4.7.6", From 442c6e71c0d0fb9c0b575be6bf6a6c4e721b1f96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:04:53 -0400 Subject: [PATCH 3124/4744] fix(deps): update dependency sass to v1.89.2 (#13487) 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 5df2e774ab..1ec373b8a9 100644 --- a/install/package.json +++ b/install/package.json @@ -127,7 +127,7 @@ "rss": "1.2.2", "rtlcss": "4.3.0", "sanitize-html": "2.17.0", - "sass": "1.89.1", + "sass": "1.89.2", "satori": "0.15.2", "semver": "7.7.2", "serve-favicon": "2.5.1", From 84d99a0fc775f01e1dc28b4c80ad78b58d539263 Mon Sep 17 00:00:00 2001 From: Eli Sheinfeld Date: Wed, 11 Jun 2025 20:13:23 +0300 Subject: [PATCH 3125/4744] feat: Add live reload functionality with Grunt watch and Socket.IO (#13489) - Added livereload event to Grunt watch tasks for instant browser refresh - Integrated Socket.IO WebSocket communication for real-time updates - Enhanced development workflow with immediate file change detection - Improved developer experience with automatic browser reload on file changes Changes: - Gruntfile.js: Send livereload message when files change - src/start.js: Handle livereload events and broadcast via Socket.IO --- Gruntfile.js | 2 ++ src/start.js | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index dcfa831cd6..53a4b7e06f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -174,6 +174,8 @@ module.exports = function (grunt) { } if (worker) { worker.send({ compiling: compiling }); + // Send livereload event via Socket.IO for instant browser refresh + worker.send({ livereload: true }); } }); }); diff --git a/src/start.js b/src/start.js index 99f3b662c5..a15aa44c6a 100644 --- a/src/start.js +++ b/src/start.js @@ -115,6 +115,13 @@ function addProcessHandlers() { const translator = require('./translator'); translator.flush(); } + } else if (msg && msg.livereload) { + // Send livereload event to all connected clients via Socket.IO + const websockets = require('./socket.io'); + if (websockets.server) { + websockets.server.emit('event:livereload'); + winston.info('[livereload] Sent reload event to all clients'); + } } }); } From dc37789b5dd4888332c6ef4c3ede65fedcdd2452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 11 Jun 2025 13:16:52 -0400 Subject: [PATCH 3126/4744] refactor: send single message --- Gruntfile.js | 7 ++++--- src/start.js | 30 +++++++++++++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 53a4b7e06f..60d8f8b23e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -173,9 +173,10 @@ module.exports = function (grunt) { winston.error(err.stack); } if (worker) { - worker.send({ compiling: compiling }); - // Send livereload event via Socket.IO for instant browser refresh - worker.send({ livereload: true }); + worker.send({ + compiling: compiling, + livereload: true, // Send livereload event via Socket.IO for instant browser refresh + }); } }); }); diff --git a/src/start.js b/src/start.js index a15aa44c6a..c4dd925aad 100644 --- a/src/start.js +++ b/src/start.js @@ -107,20 +107,24 @@ function addProcessHandlers() { shutdown(1); }); process.on('message', (msg) => { - if (msg && Array.isArray(msg.compiling)) { - if (msg.compiling.includes('tpl')) { - const benchpressjs = require('benchpressjs'); - benchpressjs.flush(); - } else if (msg.compiling.includes('lang')) { - const translator = require('./translator'); - translator.flush(); + if (msg) { + if (Array.isArray(msg.compiling)) { + if (msg.compiling.includes('tpl')) { + const benchpressjs = require('benchpressjs'); + benchpressjs.flush(); + } else if (msg.compiling.includes('lang')) { + const translator = require('./translator'); + translator.flush(); + } } - } else if (msg && msg.livereload) { - // Send livereload event to all connected clients via Socket.IO - const websockets = require('./socket.io'); - if (websockets.server) { - websockets.server.emit('event:livereload'); - winston.info('[livereload] Sent reload event to all clients'); + + if (msg.livereload) { + // Send livereload event to all connected clients via Socket.IO + const websockets = require('./socket.io'); + if (websockets.server) { + websockets.server.emit('event:livereload'); + winston.info('[livereload] Sent reload event to all clients'); + } } } }); From da2597f81ce5c7df5e02a90d5215e2da087d1f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 11 Jun 2025 17:13:56 -0400 Subject: [PATCH 3127/4744] fix: sanitize svg when uploading site-logo, default avatar and og:image --- src/controllers/admin/uploads.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 56d64674cf..ccd4261b36 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -258,10 +258,6 @@ uploadsController.uploadMaskableIcon = async function (req, res, next) { } }; -uploadsController.uploadLogo = async function (req, res, next) { - await upload('site-logo', req, res, next); -}; - uploadsController.uploadFile = async function (req, res, next) { const uploadedFile = req.files.files[0]; let params; @@ -285,6 +281,10 @@ uploadsController.uploadFile = async function (req, res, next) { } }; +uploadsController.uploadLogo = async function (req, res, next) { + await upload('site-logo', req, res, next); +}; + uploadsController.uploadDefaultAvatar = async function (req, res, next) { await upload('avatar-default', req, res, next); }; @@ -296,6 +296,10 @@ uploadsController.uploadOgImage = async function (req, res, next) { async function upload(name, req, res, next) { const uploadedFile = req.files.files[0]; + if (uploadedFile.path.endsWith('.svg')) { + await sanitizeSvg(uploadedFile.path); + } + await validateUpload(uploadedFile, allowedImageTypes); const filename = name + path.extname(uploadedFile.name); await uploadImage(filename, 'system', uploadedFile, req, res, next); From c101d0d5afa70cab727f76f31a3f0c0faf678901 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:59:25 -0400 Subject: [PATCH 3128/4744] fix(deps): update dependency postcss to v8.5.5 (#13490) 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 1ec373b8a9..3c4d7c11e1 100644 --- a/install/package.json +++ b/install/package.json @@ -118,7 +118,7 @@ "passport-local": "1.0.0", "pg": "8.16.0", "pg-cursor": "2.15.0", - "postcss": "8.5.4", + "postcss": "8.5.5", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", From 703fcbbf36bac82f0cc0e2ef8a92fce92a445a5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:02:18 -0400 Subject: [PATCH 3129/4744] fix(deps): update dependency postcss to v8.5.6 (#13494) 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 3c4d7c11e1..d6eb0b1466 100644 --- a/install/package.json +++ b/install/package.json @@ -118,7 +118,7 @@ "passport-local": "1.0.0", "pg": "8.16.0", "pg-cursor": "2.15.0", - "postcss": "8.5.5", + "postcss": "8.5.6", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", From d6ba79302df38990b258c0aa9b734dc95cbc8177 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:09:05 -0400 Subject: [PATCH 3130/4744] chore(deps): update dependency lint-staged to v16.1.2 (#13492) 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 d6eb0b1466..43f7637970 100644 --- a/install/package.json +++ b/install/package.json @@ -169,7 +169,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "26.1.0", - "lint-staged": "16.1.0", + "lint-staged": "16.1.2", "mocha": "11.6.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From f36a5ac8928f1dbf33ca8aa86cb56a24f144607b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:10:55 -0400 Subject: [PATCH 3131/4744] fix(deps): update dependency chart.js to v4.5.0 (#13495) 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 43f7637970..df6c0fd5fb 100644 --- a/install/package.json +++ b/install/package.json @@ -50,7 +50,7 @@ "bootstrap": "5.3.6", "bootswatch": "5.3.6", "chalk": "4.1.2", - "chart.js": "4.4.9", + "chart.js": "4.5.0", "cli-graph": "3.2.2", "clipboard": "2.0.11", "commander": "14.0.0", From 8c69c6a0c4399debac837469dbbb1a142d220679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Jun 2025 09:17:57 -0400 Subject: [PATCH 3132/4744] feat: link to post in preview timestamp --- src/views/partials/topic/post-preview.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/partials/topic/post-preview.tpl b/src/views/partials/topic/post-preview.tpl index c307e14f81..d3436bf6ba 100644 --- a/src/views/partials/topic/post-preview.tpl +++ b/src/views/partials/topic/post-preview.tpl @@ -6,7 +6,7 @@ {post.user.username}
    - +
    {post.content}
    From a3fed408e571b4f4be0dfcf75539dd77f95ae60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Jun 2025 09:21:00 -0400 Subject: [PATCH 3133/4744] change default to perma ban --- src/views/modals/temporary-ban.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/modals/temporary-ban.tpl b/src/views/modals/temporary-ban.tpl index d84bc28331..33bd5c8b47 100644 --- a/src/views/modals/temporary-ban.tpl +++ b/src/views/modals/temporary-ban.tpl @@ -12,7 +12,7 @@
    - +
    From 2046ca724ad3617e27fcf68d8cecb576a0309335 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:36:26 -0400 Subject: [PATCH 3134/4744] chore(deps): update dependency @eslint/js to v9.29.0 (#13491) 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 df6c0fd5fb..d817989349 100644 --- a/install/package.json +++ b/install/package.json @@ -161,7 +161,7 @@ "@commitlint/cli": "19.8.1", "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", - "@eslint/js": "9.28.0", + "@eslint/js": "9.29.0", "@stylistic/eslint-plugin": "4.4.1", "eslint-config-nodebb": "1.1.7", "eslint-plugin-import": "2.31.0", From 2490c312c97926d0b5975c15780db593b8d7ade6 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 18 Jun 2025 14:20:41 +0000 Subject: [PATCH 3135/4744] chore: incrementing version number - v4.4.4 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index fae65ae63f..4033fe1a27 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.4.3", + "version": "4.4.4", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 7b14e2677544938f3a121363783b06071e15a4f5 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 18 Jun 2025 14:20:41 +0000 Subject: [PATCH 3136/4744] chore: update changelog for v4.4.4 --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4faa3658..9010b97b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,49 @@ +#### v4.4.4 (2025-06-18) + +##### Chores + +* incrementing version number - v4.4.3 (d354c2eb) +* update changelog for v4.4.3 (0c9297f8) +* incrementing version number - v4.4.2 (55c510ae) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### New Features + +* link to post in preview timestamp (8c69c6a0) +* Add live reload functionality with Grunt watch and Socket.IO (#13489) (84d99a0f) +* closes #13484, post preview changes (14e30c4b) + +##### Bug Fixes + +* sanitize svg when uploading site-logo, default avatar and og:image (da2597f8) +* Revise package hash check in Docker entrypoint.sh (#13483) (6c5b2268) +* more edge cases (32faaba0) +* #13484, clear tooltip if cursor leaves link (0ebb31fe) + +##### Other Changes + +* fix lint (8ab034d8) + +##### Refactors + +* send single message (dc37789b) + #### v4.4.3 (2025-06-09) ##### Chores From 14043ab0fd6efc3070cdca0b2c4f5cbfef3d7f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 13:04:57 -0400 Subject: [PATCH 3137/4744] Node redis (#13500) * refactor: start migrating to node-redis * few more zset fixes * fix: db.scan * fix: list methods * fix set methods * fix: hash methods * use hasOwn, remove cloning * sorted set fixes * fix: so data is converted to strings before saving otherwise node-redis throws below error TypeError: "arguments[2]" must be of type "string | Buffer", got number instead. * chore: remove comments * fix: zrank string param * use new close * chore: up dbsearch * test: add log * test: more log * test: log failing test * test: catch errors in formatApiResponse add await so exception goes to catch * tetst: add log * fix: dont set null/undefined values * test: more fixes --- install/package.json | 4 +- src/activitypub/index.js | 1 - src/controllers/activitypub/index.js | 5 +- src/controllers/helpers.js | 4 + src/database/redis.js | 2 +- src/database/redis/connection.js | 39 ++-- src/database/redis/hash.js | 48 +++-- src/database/redis/helpers.js | 40 +++-- src/database/redis/list.js | 22 ++- src/database/redis/main.js | 22 +-- src/database/redis/sets.js | 24 +-- src/database/redis/sorted.js | 239 ++++++++++++------------- src/database/redis/sorted/add.js | 24 +-- src/database/redis/sorted/intersect.js | 30 ++-- src/database/redis/sorted/remove.js | 8 +- src/database/redis/sorted/union.js | 26 +-- src/topics/posts.js | 2 +- test/activitypub/actors.js | 3 +- test/activitypub/notes.js | 4 +- test/database/sorted.js | 64 +++---- test/mocks/databasemock.js | 1 - 21 files changed, 307 insertions(+), 305 deletions(-) diff --git a/install/package.json b/install/package.json index d817989349..a42eb52182 100644 --- a/install/package.json +++ b/install/package.json @@ -98,7 +98,7 @@ "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.2.51", - "nodebb-plugin-dbsearch": "6.2.19", + "nodebb-plugin-dbsearch": "6.3.0", "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.1", @@ -122,7 +122,7 @@ "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", - "ioredis": "5.6.1", + "redis": "5.5.6", "rimraf": "6.0.1", "rss": "1.2.2", "rtlcss": "4.3.0", diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 80ec4a40f5..78cb3f26f6 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -132,7 +132,6 @@ ActivityPub.resolveInboxes = async (ids) => { }, [[], []]); const categoryData = await categories.getCategoriesFields(cids, ['inbox', 'sharedInbox']); const userData = await user.getUsersFields(uids, ['inbox', 'sharedInbox']); - currentIds.forEach((id) => { if (cids.includes(id)) { const data = categoryData[cids.indexOf(id)]; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index de478a6021..6751cb30cd 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -145,14 +145,15 @@ Controller.postInbox = async (req, res) => { const method = String(req.body.type).toLowerCase(); if (!activitypub.inbox.hasOwnProperty(method)) { winston.warn(`[activitypub/inbox] Received Activity of type ${method} but unable to handle. Ignoring.`); + console.log('[activitypub/inbox] method not found', method, req.body); return res.sendStatus(200); } try { await activitypub.inbox[method](req); await activitypub.record(req.body); - helpers.formatApiResponse(202, res); + await helpers.formatApiResponse(202, res); } catch (e) { - helpers.formatApiResponse(500, res, e); + helpers.formatApiResponse(500, res, e).catch(err => winston.error(err.stack)); } }; diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index a6ade8c73b..e11692867e 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -448,6 +448,10 @@ helpers.getHomePageRoutes = async function (uid) { }; helpers.formatApiResponse = async (statusCode, res, payload) => { + if (!res.hasOwnProperty('req')) { + console.log('formatApiResponse', statusCode, payload); + } + if (res.req.method === 'HEAD') { return res.sendStatus(statusCode); } diff --git a/src/database/redis.js b/src/database/redis.js index f73ee79313..472dae8de6 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -61,7 +61,7 @@ redisModule.checkCompatibilityVersion = function (version, callback) { }; redisModule.close = async function () { - await redisModule.client.quit(); + await redisModule.client.close(); if (redisModule.objectCache) { redisModule.objectCache.reset(); } diff --git a/src/database/redis/connection.js b/src/database/redis/connection.js index a4ba757ef6..2a38cf1a79 100644 --- a/src/database/redis/connection.js +++ b/src/database/redis/connection.js @@ -1,7 +1,7 @@ 'use strict'; const nconf = require('nconf'); -const Redis = require('ioredis'); +const { createClient, createCluster, createSentinel } = require('redis'); const winston = require('winston'); const connection = module.exports; @@ -13,28 +13,40 @@ connection.connect = async function (options) { let cxn; if (options.cluster) { - cxn = new Redis.Cluster(options.cluster, options.options); - } else if (options.sentinels) { - cxn = new Redis({ - sentinels: options.sentinels, + const rootNodes = options.cluster.map(node => ({ url : `redis://${node.host}:${node.port}` })); + cxn = createCluster({ ...options.options, + rootNodes: rootNodes, + }); + } else if (options.sentinels) { + const sentinelRootNodes = options.sentinels.map(sentinel => ({ host: sentinel.host, port: sentinel.port })); + cxn = createSentinel({ + ...options.options, + name: 'sentinel-db', + sentinelRootNodes, }); } else if (redis_socket_or_host && String(redis_socket_or_host).indexOf('/') >= 0) { // If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock - cxn = new Redis({ + cxn = createClient({ ...options.options, - path: redis_socket_or_host, password: options.password, - db: options.database, + database: options.database, + socket: { + path: redis_socket_or_host, + reconnectStrategy: 3000, + }, }); } else { // Else, connect over tcp/ip - cxn = new Redis({ + cxn = createClient({ ...options.options, host: redis_socket_or_host, port: options.port, password: options.password, - db: options.database, + database: options.database, + socket: { + reconnectStrategy: 3000, + }, }); } @@ -49,9 +61,14 @@ connection.connect = async function (options) { }); cxn.on('ready', () => { // back-compat with node_redis - cxn.batch = cxn.pipeline; + cxn.batch = cxn.multi; resolve(cxn); }); + cxn.connect().then(() => { + winston.info('Connected to Redis successfully'); + }).catch((err) => { + winston.error('Error connecting to Redis:', err); + }); if (options.password) { cxn.auth(options.password); diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js index 4c6e7b374f..c03bff055b 100644 --- a/src/database/redis/hash.js +++ b/src/database/redis/hash.js @@ -25,12 +25,13 @@ module.exports = function (module) { if (!Object.keys(data).length) { return; } + const strObj = helpers.objectFieldsToString(data); if (Array.isArray(key)) { const batch = module.client.batch(); - key.forEach(k => batch.hmset(k, data)); + key.forEach(k => batch.hSet(k, strObj)); await helpers.execBatch(batch); } else { - await module.client.hmset(key, data); + await module.client.hSet(key, strObj); } cache.del(key); @@ -49,10 +50,16 @@ module.exports = function (module) { const batch = module.client.batch(); data.forEach((item) => { + Object.keys(item[1]).forEach((key) => { + if (item[1][key] === undefined || item[1][key] === null) { + delete item[1][key]; + } + }); if (Object.keys(item[1]).length) { - batch.hmset(item[0], item[1]); + batch.hSet(item[0], helpers.objectFieldsToString(item[1])); } }); + await helpers.execBatch(batch); cache.del(data.map(item => item[0])); }; @@ -61,12 +68,15 @@ module.exports = function (module) { if (!field) { return; } + if (value === null || value === undefined) { + return; + } if (Array.isArray(key)) { const batch = module.client.batch(); - key.forEach(k => batch.hset(k, field, value)); + key.forEach(k => batch.hSet(k, field, String(value))); await helpers.execBatch(batch); } else { - await module.client.hset(key, field, value); + await module.client.hSet(key, field, String(value)); } cache.del(key); @@ -92,9 +102,9 @@ module.exports = function (module) { const cachedData = {}; cache.getUnCachedKeys([key], cachedData); if (cachedData[key]) { - return cachedData[key].hasOwnProperty(field) ? cachedData[key][field] : null; + return Object.hasOwn(cachedData[key], field) ? cachedData[key][field] : null; } - return await module.client.hget(key, String(field)); + return await module.client.hGet(key, String(field)); }; module.getObjectFields = async function (key, fields) { @@ -116,10 +126,10 @@ module.exports = function (module) { let data = []; if (unCachedKeys.length > 1) { const batch = module.client.batch(); - unCachedKeys.forEach(k => batch.hgetall(k)); + unCachedKeys.forEach(k => batch.hGetAll(k)); data = await helpers.execBatch(batch); } else if (unCachedKeys.length === 1) { - data = [await module.client.hgetall(unCachedKeys[0])]; + data = [await module.client.hGetAll(unCachedKeys[0])]; } // convert empty objects into null for back-compat with node_redis @@ -149,21 +159,21 @@ module.exports = function (module) { }; module.getObjectKeys = async function (key) { - return await module.client.hkeys(key); + return await module.client.hKeys(key); }; module.getObjectValues = async function (key) { - return await module.client.hvals(key); + return await module.client.hVals(key); }; module.isObjectField = async function (key, field) { - const exists = await module.client.hexists(key, field); + const exists = await module.client.hExists(key, String(field)); return exists === 1; }; module.isObjectFields = async function (key, fields) { const batch = module.client.batch(); - fields.forEach(f => batch.hexists(String(key), String(f))); + fields.forEach(f => batch.hExists(String(key), String(f))); const results = await helpers.execBatch(batch); return Array.isArray(results) ? helpers.resultsToBool(results) : null; }; @@ -174,7 +184,7 @@ module.exports = function (module) { } field = field.toString(); if (field) { - await module.client.hdel(key, field); + await module.client.hDel(key, field); cache.del(key); } }; @@ -189,10 +199,10 @@ module.exports = function (module) { } if (Array.isArray(key)) { const batch = module.client.batch(); - key.forEach(k => batch.hdel(k, fields)); + key.forEach(k => batch.hDel(k, fields)); await helpers.execBatch(batch); } else { - await module.client.hdel(key, fields); + await module.client.hDel(key, fields); } cache.del(key); @@ -214,10 +224,10 @@ module.exports = function (module) { let result; if (Array.isArray(key)) { const batch = module.client.batch(); - key.forEach(k => batch.hincrby(k, field, value)); + key.forEach(k => batch.hIncrBy(k, field, value)); result = await helpers.execBatch(batch); } else { - result = await module.client.hincrby(key, field, value); + result = await module.client.hIncrBy(key, field, value); } cache.del(key); return Array.isArray(result) ? result.map(value => parseInt(value, 10)) : parseInt(result, 10); @@ -231,7 +241,7 @@ module.exports = function (module) { const batch = module.client.batch(); data.forEach((item) => { for (const [field, value] of Object.entries(item[1])) { - batch.hincrby(item[0], field, value); + batch.hIncrBy(item[0], field, value); } }); await helpers.execBatch(batch); diff --git a/src/database/redis/helpers.js b/src/database/redis/helpers.js index 8961da8255..39585b1f88 100644 --- a/src/database/redis/helpers.js +++ b/src/database/redis/helpers.js @@ -5,13 +5,8 @@ const helpers = module.exports; helpers.noop = function () {}; helpers.execBatch = async function (batch) { - const results = await batch.exec(); - return results.map(([err, res]) => { - if (err) { - throw err; - } - return res; - }); + const results = await batch.execAsPipeline(); + return results; }; helpers.resultsToBool = function (results) { @@ -21,10 +16,29 @@ helpers.resultsToBool = function (results) { return results; }; -helpers.zsetToObjectArray = function (data) { - const objects = new Array(data.length / 2); - for (let i = 0, k = 0; i < objects.length; i += 1, k += 2) { - objects[i] = { value: data[k], score: parseFloat(data[k + 1]) }; - } - return objects; +helpers.objectFieldsToString = function (obj) { + const stringified = Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, String(value)]) + ); + return stringified; +}; + +helpers.normalizeLexRange = function (min, max, reverse) { + let minmin; + let maxmax; + if (reverse) { + minmin = '+'; + maxmax = '-'; + } else { + minmin = '-'; + maxmax = '+'; + } + + if (min !== minmin && !min.match(/^[[(]/)) { + min = `[${min}`; + } + if (max !== maxmax && !max.match(/^[[(]/)) { + max = `[${max}`; + } + return { lmin: min, lmax: max }; }; diff --git a/src/database/redis/list.js b/src/database/redis/list.js index 101ef178e3..229069b6ed 100644 --- a/src/database/redis/list.js +++ b/src/database/redis/list.js @@ -1,27 +1,25 @@ 'use strict'; module.exports = function (module) { - const helpers = require('./helpers'); - module.listPrepend = async function (key, value) { if (!key) { return; } - await module.client.lpush(key, value); + await module.client.lPush(key, Array.isArray(value) ? value.map(String) : String(value)); }; module.listAppend = async function (key, value) { if (!key) { return; } - await module.client.rpush(key, value); + await module.client.rPush(key, Array.isArray(value) ? value.map(String) : String(value)); }; module.listRemoveLast = async function (key) { if (!key) { return; } - return await module.client.rpop(key); + return await module.client.rPop(key); }; module.listRemoveAll = async function (key, value) { @@ -29,11 +27,11 @@ module.exports = function (module) { return; } if (Array.isArray(value)) { - const batch = module.client.batch(); - value.forEach(value => batch.lrem(key, 0, value)); - await helpers.execBatch(batch); + const batch = module.client.multi(); + value.forEach(value => batch.lRem(key, 0, value)); + await batch.execAsPipeline(); } else { - await module.client.lrem(key, 0, value); + await module.client.lRem(key, 0, value); } }; @@ -41,17 +39,17 @@ module.exports = function (module) { if (!key) { return; } - await module.client.ltrim(key, start, stop); + await module.client.lTrim(key, start, stop); }; module.getListRange = async function (key, start, stop) { if (!key) { return; } - return await module.client.lrange(key, start, stop); + return await module.client.lRange(key, start, stop); }; module.listLength = async function (key) { - return await module.client.llen(key); + return await module.client.lLen(key); }; }; diff --git a/src/database/redis/main.js b/src/database/redis/main.js index b849361a8e..a6506ba701 100644 --- a/src/database/redis/main.js +++ b/src/database/redis/main.js @@ -4,7 +4,7 @@ module.exports = function (module) { const helpers = require('./helpers'); module.flushdb = async function () { - await module.client.send_command('flushdb', []); + await module.client.sendCommand(['FLUSHDB']); }; module.emptydb = async function () { @@ -32,9 +32,9 @@ module.exports = function (module) { const seen = Object.create(null); do { /* eslint-disable no-await-in-loop */ - const res = await module.client.scan(cursor, 'MATCH', params.match, 'COUNT', 10000); - cursor = res[0]; - const values = res[1].filter((value) => { + const res = await module.client.scan(cursor, { MATCH: params.match, COUNT: 10000 }); + cursor = res.cursor; + const values = res.keys.filter((value) => { const isSeen = !!seen[value]; if (!isSeen) { seen[value] = 1; @@ -67,7 +67,7 @@ module.exports = function (module) { if (!keys || !Array.isArray(keys) || !keys.length) { return []; } - return await module.client.mget(keys); + return await module.client.mGet(keys); }; module.set = async function (key, value) { @@ -96,26 +96,26 @@ module.exports = function (module) { }; module.expire = async function (key, seconds) { - await module.client.expire(key, seconds); + await module.client.EXPIRE(key, seconds); }; module.expireAt = async function (key, timestamp) { - await module.client.expireat(key, timestamp); + await module.client.EXPIREAT(key, timestamp); }; module.pexpire = async function (key, ms) { - await module.client.pexpire(key, ms); + await module.client.PEXPIRE(key, ms); }; module.pexpireAt = async function (key, timestamp) { - await module.client.pexpireat(key, timestamp); + await module.client.PEXPIREAT(key, timestamp); }; module.ttl = async function (key) { - return await module.client.ttl(key); + return await module.client.TTL(key); }; module.pttl = async function (key) { - return await module.client.pttl(key); + return await module.client.PTTL(key); }; }; diff --git a/src/database/redis/sets.js b/src/database/redis/sets.js index b2b390598b..dd7e484325 100644 --- a/src/database/redis/sets.js +++ b/src/database/redis/sets.js @@ -10,7 +10,7 @@ module.exports = function (module) { if (!value.length) { return; } - await module.client.sadd(key, value); + await module.client.sAdd(key, value.map(String)); }; module.setsAdd = async function (keys, value) { @@ -18,7 +18,7 @@ module.exports = function (module) { return; } const batch = module.client.batch(); - keys.forEach(k => batch.sadd(String(k), String(value))); + keys.forEach(k => batch.sAdd(String(k), String(value))); await helpers.execBatch(batch); }; @@ -34,57 +34,57 @@ module.exports = function (module) { } const batch = module.client.batch(); - key.forEach(k => batch.srem(String(k), value)); + key.forEach(k => batch.sRem(String(k), value.map(String))); await helpers.execBatch(batch); }; module.setsRemove = async function (keys, value) { const batch = module.client.batch(); - keys.forEach(k => batch.srem(String(k), value)); + keys.forEach(k => batch.sRem(String(k), String(value))); await helpers.execBatch(batch); }; module.isSetMember = async function (key, value) { - const result = await module.client.sismember(key, value); + const result = await module.client.sIsMember(key, String(value)); return result === 1; }; module.isSetMembers = async function (key, values) { const batch = module.client.batch(); - values.forEach(v => batch.sismember(String(key), String(v))); + values.forEach(v => batch.sIsMember(String(key), String(v))); const results = await helpers.execBatch(batch); return results ? helpers.resultsToBool(results) : null; }; module.isMemberOfSets = async function (sets, value) { const batch = module.client.batch(); - sets.forEach(s => batch.sismember(String(s), String(value))); + sets.forEach(s => batch.sIsMember(String(s), String(value))); const results = await helpers.execBatch(batch); return results ? helpers.resultsToBool(results) : null; }; module.getSetMembers = async function (key) { - return await module.client.smembers(key); + return await module.client.sMembers(key); }; module.getSetsMembers = async function (keys) { const batch = module.client.batch(); - keys.forEach(k => batch.smembers(String(k))); + keys.forEach(k => batch.sMembers(String(k))); return await helpers.execBatch(batch); }; module.setCount = async function (key) { - return await module.client.scard(key); + return await module.client.sCard(key); }; module.setsCount = async function (keys) { const batch = module.client.batch(); - keys.forEach(k => batch.scard(String(k))); + keys.forEach(k => batch.sCard(String(k))); return await helpers.execBatch(batch); }; module.setRemoveRandom = async function (key) { - return await module.client.spop(key); + return await module.client.sPop(key); }; return module; diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index 013477da5a..d8613a3a68 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -11,34 +11,74 @@ module.exports = function (module) { require('./sorted/intersect')(module); module.getSortedSetRange = async function (key, start, stop) { - return await sortedSetRange('zrange', key, start, stop, '-inf', '+inf', false); + return await sortedSetRange(key, start, stop, '-inf', '+inf', false, false, false); }; module.getSortedSetRevRange = async function (key, start, stop) { - return await sortedSetRange('zrevrange', key, start, stop, '-inf', '+inf', false); + return await sortedSetRange(key, start, stop, '-inf', '+inf', false, true, false); }; module.getSortedSetRangeWithScores = async function (key, start, stop) { - return await sortedSetRange('zrange', key, start, stop, '-inf', '+inf', true); + return await sortedSetRange(key, start, stop, '-inf', '+inf', true, false, false); }; module.getSortedSetRevRangeWithScores = async function (key, start, stop) { - return await sortedSetRange('zrevrange', key, start, stop, '-inf', '+inf', true); + return await sortedSetRange(key, start, stop, '-inf', '+inf', true, true, false); }; - async function sortedSetRange(method, key, start, stop, min, max, withScores) { + module.getSortedSetRangeByScore = async function (key, start, count, min, max) { + return await sortedSetRangeByScore(key, start, count, min, max, false, false); + }; + + module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { + return await sortedSetRangeByScore(key, start, count, max, min, false, true); + }; + + module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { + return await sortedSetRangeByScore(key, start, count, min, max, true, false); + }; + + module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { + return await sortedSetRangeByScore(key, start, count, max, min, true, true); + }; + + async function sortedSetRangeByScore(key, start, count, min, max, withScores, rev) { + if (parseInt(count, 10) === 0) { + return []; + } + const stop = (parseInt(count, 10) === -1) ? -1 : (start + count - 1); + return await sortedSetRange(key, start, stop, min, max, withScores, rev, true); + } + + async function sortedSetRange(key, start, stop, min, max, withScores, rev, byScore) { + const opts = {}; + const cmd = withScores ? 'zRangeWithScores' : 'zRange'; + if (byScore) { + opts.BY = 'SCORE'; + opts.LIMIT = { offset: start, count: stop !== -1 ? stop + 1 : stop }; + } + if (rev) { + opts.REV = true; + } + if (Array.isArray(key)) { if (!key.length) { return []; } const batch = module.client.batch(); - key.forEach(key => batch[method](genParams(method, key, 0, stop, min, max, true))); + + if (byScore) { + key.forEach(key => batch.zRangeWithScores(key, min, max, { + ...opts, + LIMIT: { offset: 0, count: stop !== -1 ? stop + 1 : stop }, + })); + } else { + key.forEach(key => batch.zRangeWithScores(key, 0, stop, { ...opts })); + } + const data = await helpers.execBatch(batch); - - const batchData = data.map(setData => helpers.zsetToObjectArray(setData)); - - let objects = dbHelpers.mergeBatch(batchData, 0, stop, method === 'zrange' ? 1 : -1); - + const batchData = data; + let objects = dbHelpers.mergeBatch(batchData, 0, stop, rev ? -1 : 1); if (start > 0) { objects = objects.slice(start, stop !== -1 ? stop + 1 : undefined); } @@ -48,63 +88,25 @@ module.exports = function (module) { return objects; } - const params = genParams(method, key, start, stop, min, max, withScores); - const data = await module.client[method](params); + let data; + if (byScore) { + data = await module.client[cmd](key, min, max, opts); + } else { + data = await module.client[cmd](key, start, stop, opts); + } + if (!withScores) { return data; } - const objects = helpers.zsetToObjectArray(data); - return objects; - } - - function genParams(method, key, start, stop, min, max, withScores) { - const params = { - zrevrange: [key, start, stop], - zrange: [key, start, stop], - zrangebyscore: [key, min, max], - zrevrangebyscore: [key, max, min], - }; - if (withScores) { - params[method].push('WITHSCORES'); - } - - if (method === 'zrangebyscore' || method === 'zrevrangebyscore') { - const count = stop !== -1 ? stop - start + 1 : stop; - params[method].push('LIMIT', start, count); - } - return params[method]; - } - - module.getSortedSetRangeByScore = async function (key, start, count, min, max) { - return await sortedSetRangeByScore('zrangebyscore', key, start, count, min, max, false); - }; - - module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { - return await sortedSetRangeByScore('zrevrangebyscore', key, start, count, min, max, false); - }; - - module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { - return await sortedSetRangeByScore('zrangebyscore', key, start, count, min, max, true); - }; - - module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { - return await sortedSetRangeByScore('zrevrangebyscore', key, start, count, min, max, true); - }; - - async function sortedSetRangeByScore(method, key, start, count, min, max, withScores) { - if (parseInt(count, 10) === 0) { - return []; - } - const stop = (parseInt(count, 10) === -1) ? -1 : (start + count - 1); - return await sortedSetRange(method, key, start, stop, min, max, withScores); + return data; } module.sortedSetCount = async function (key, min, max) { - return await module.client.zcount(key, min, max); + return await module.client.zCount(key, min, max); }; module.sortedSetCard = async function (key) { - return await module.client.zcard(key); + return await module.client.zCard(key); }; module.sortedSetsCard = async function (keys) { @@ -112,7 +114,7 @@ module.exports = function (module) { return []; } const batch = module.client.batch(); - keys.forEach(k => batch.zcard(String(k))); + keys.forEach(k => batch.zCard(String(k))); return await helpers.execBatch(batch); }; @@ -125,26 +127,26 @@ module.exports = function (module) { } const batch = module.client.batch(); if (min !== '-inf' || max !== '+inf') { - keys.forEach(k => batch.zcount(String(k), min, max)); + keys.forEach(k => batch.zCount(String(k), min, max)); } else { - keys.forEach(k => batch.zcard(String(k))); + keys.forEach(k => batch.zCard(String(k))); } const counts = await helpers.execBatch(batch); return counts.reduce((acc, val) => acc + val, 0); }; module.sortedSetRank = async function (key, value) { - return await module.client.zrank(key, value); + return await module.client.zRank(key, String(value)); }; module.sortedSetRevRank = async function (key, value) { - return await module.client.zrevrank(key, value); + return await module.client.zRevRank(key, String(value)); }; module.sortedSetsRanks = async function (keys, values) { const batch = module.client.batch(); for (let i = 0; i < values.length; i += 1) { - batch.zrank(keys[i], String(values[i])); + batch.zRank(keys[i], String(values[i])); } return await helpers.execBatch(batch); }; @@ -152,7 +154,7 @@ module.exports = function (module) { module.sortedSetsRevRanks = async function (keys, values) { const batch = module.client.batch(); for (let i = 0; i < values.length; i += 1) { - batch.zrevrank(keys[i], String(values[i])); + batch.zRevRank(keys[i], String(values[i])); } return await helpers.execBatch(batch); }; @@ -160,7 +162,7 @@ module.exports = function (module) { module.sortedSetRanks = async function (key, values) { const batch = module.client.batch(); for (let i = 0; i < values.length; i += 1) { - batch.zrank(key, String(values[i])); + batch.zRank(key, String(values[i])); } return await helpers.execBatch(batch); }; @@ -168,7 +170,7 @@ module.exports = function (module) { module.sortedSetRevRanks = async function (key, values) { const batch = module.client.batch(); for (let i = 0; i < values.length; i += 1) { - batch.zrevrank(key, String(values[i])); + batch.zRevRank(key, String(values[i])); } return await helpers.execBatch(batch); }; @@ -177,8 +179,7 @@ module.exports = function (module) { if (!key || value === undefined) { return null; } - - const score = await module.client.zscore(key, value); + const score = await module.client.zScore(key, String(value)); return score === null ? score : parseFloat(score); }; @@ -187,7 +188,7 @@ module.exports = function (module) { return []; } const batch = module.client.batch(); - keys.forEach(key => batch.zscore(String(key), String(value))); + keys.forEach(key => batch.zScore(String(key), String(value))); const scores = await helpers.execBatch(batch); return scores.map(d => (d === null ? d : parseFloat(d))); }; @@ -197,7 +198,7 @@ module.exports = function (module) { return []; } const batch = module.client.batch(); - values.forEach(value => batch.zscore(String(key), String(value))); + values.forEach(value => batch.zScore(String(key), String(value))); const scores = await helpers.execBatch(batch); return scores.map(d => (d === null ? d : parseFloat(d))); }; @@ -211,9 +212,9 @@ module.exports = function (module) { if (!values.length) { return []; } - const batch = module.client.batch(); - values.forEach(v => batch.zscore(key, String(v))); - const results = await helpers.execBatch(batch); + const batch = module.client.multi(); + values.forEach(v => batch.zScore(key, String(v))); + const results = await batch.execAsPipeline(); return results.map(utils.isNumber); }; @@ -221,20 +222,18 @@ module.exports = function (module) { if (!Array.isArray(keys) || !keys.length) { return []; } - const batch = module.client.batch(); - keys.forEach(k => batch.zscore(k, String(value))); - const results = await helpers.execBatch(batch); + const batch = module.client.multi(); + keys.forEach(k => batch.zScore(k, String(value))); + const results = await batch.execAsPipeline(); return results.map(utils.isNumber); }; module.getSortedSetMembers = async function (key) { - return await module.client.zrange(key, 0, -1); + return await module.client.zRange(key, 0, -1); }; module.getSortedSetMembersWithScores = async function (key) { - return helpers.zsetToObjectArray( - await module.client.zrange(key, 0, -1, 'WITHSCORES') - ); + return await module.client.zRangeWithScores(key, 0, -1); }; module.getSortedSetsMembers = async function (keys) { @@ -242,7 +241,7 @@ module.exports = function (module) { return []; } const batch = module.client.batch(); - keys.forEach(k => batch.zrange(k, 0, -1)); + keys.forEach(k => batch.zRange(k, 0, -1)); return await helpers.execBatch(batch); }; @@ -251,65 +250,52 @@ module.exports = function (module) { return []; } const batch = module.client.batch(); - keys.forEach(k => batch.zrange(k, 0, -1, 'WITHSCORES')); + keys.forEach(k => batch.zRangeWithScores(k, 0, -1)); const res = await helpers.execBatch(batch); - return res.map(helpers.zsetToObjectArray); + return res; }; module.sortedSetIncrBy = async function (key, increment, value) { - const newValue = await module.client.zincrby(key, increment, value); + const newValue = await module.client.zIncrBy(key, increment, String(value)); return parseFloat(newValue); }; module.sortedSetIncrByBulk = async function (data) { const multi = module.client.multi(); data.forEach((item) => { - multi.zincrby(item[0], item[1], item[2]); + multi.zIncrBy(item[0], item[1], String(item[2])); }); const result = await multi.exec(); - return result.map(item => item && parseFloat(item[1])); + return result; }; - module.getSortedSetRangeByLex = async function (key, min, max, start, count) { - return await sortedSetLex('zrangebylex', false, key, min, max, start, count); + module.getSortedSetRangeByLex = async function (key, min, max, start = 0, count = -1) { + const { lmin, lmax } = helpers.normalizeLexRange(min, max, false); + return await module.client.zRange(key, lmin, lmax, { + BY: 'LEX', + LIMIT: { offset: start, count: count }, + }); }; - module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) { - return await sortedSetLex('zrevrangebylex', true, key, max, min, start, count); + module.getSortedSetRevRangeByLex = async function (key, max, min, start = 0, count = -1) { + const { lmin, lmax } = helpers.normalizeLexRange(max, min, true); + return await module.client.zRange(key, lmin, lmax, { + REV: true, + BY: 'LEX', + LIMIT: { offset: start, count: count }, + }); }; module.sortedSetRemoveRangeByLex = async function (key, min, max) { - await sortedSetLex('zremrangebylex', false, key, min, max); + const { lmin, lmax } = helpers.normalizeLexRange(min, max, false); + await module.client.zRemRangeByLex(key, lmin, lmax); }; module.sortedSetLexCount = async function (key, min, max) { - return await sortedSetLex('zlexcount', false, key, min, max); + const { lmin, lmax } = helpers.normalizeLexRange(min, max, false); + return await module.client.zLexCount(key, lmin, lmax); }; - async function sortedSetLex(method, reverse, key, min, max, start, count) { - let minmin; - let maxmax; - if (reverse) { - minmin = '+'; - maxmax = '-'; - } else { - minmin = '-'; - maxmax = '+'; - } - - if (min !== minmin && !min.match(/^[[(]/)) { - min = `[${min}`; - } - if (max !== maxmax && !max.match(/^[[(]/)) { - max = `[${max}`; - } - const args = [key, min, max]; - if (count) { - args.push('LIMIT', start, count); - } - return await module.client[method](args); - } - module.getSortedSetScan = async function (params) { let cursor = '0'; @@ -318,20 +304,19 @@ module.exports = function (module) { const seen = Object.create(null); do { /* eslint-disable no-await-in-loop */ - const res = await module.client.zscan(params.key, cursor, 'MATCH', params.match, 'COUNT', 5000); - cursor = res[0]; + const res = await module.client.zScan(params.key, cursor, { MATCH: params.match, COUNT: 5000 }); + cursor = res.cursor; done = cursor === '0'; - const data = res[1]; - for (let i = 0; i < data.length; i += 2) { - const value = data[i]; - if (!seen[value]) { - seen[value] = 1; + for (let i = 0; i < res.members.length; i ++) { + const item = res.members[i]; + if (!seen[item.value]) { + seen[item.value] = 1; if (params.withScores) { - returnData.push({ value: value, score: parseFloat(data[i + 1]) }); + returnData.push({ value: item.value, score: parseFloat(item.score) }); } else { - returnData.push(value); + returnData.push(item.value); } if (params.limit && returnData.length >= params.limit) { done = true; diff --git a/src/database/redis/sorted/add.js b/src/database/redis/sorted/add.js index 660618b8a4..264c877845 100644 --- a/src/database/redis/sorted/add.js +++ b/src/database/redis/sorted/add.js @@ -1,7 +1,6 @@ 'use strict'; module.exports = function (module) { - const helpers = require('../helpers'); const utils = require('../../../utils'); module.sortedSetAdd = async function (key, score, value) { @@ -14,7 +13,8 @@ module.exports = function (module) { if (!utils.isNumber(score)) { throw new Error(`[[error:invalid-score, ${score}]]`); } - await module.client.zadd(key, score, String(value)); + + await module.client.zAdd(key, { score, value: String(value) }); }; async function sortedSetAddMulti(key, scores, values) { @@ -30,11 +30,8 @@ module.exports = function (module) { throw new Error(`[[error:invalid-score, ${scores[i]}]]`); } } - const args = [key]; - for (let i = 0; i < scores.length; i += 1) { - args.push(scores[i], String(values[i])); - } - await module.client.zadd(args); + const members = scores.map((score, i) => ({ score, value: String(values[i])})); + await module.client.zAdd(key, members); } module.sortedSetsAdd = async function (keys, scores, value) { @@ -51,13 +48,16 @@ module.exports = function (module) { throw new Error('[[error:invalid-data]]'); } - const batch = module.client.batch(); + const batch = module.client.multi(); for (let i = 0; i < keys.length; i += 1) { if (keys[i]) { - batch.zadd(keys[i], isArrayOfScores ? scores[i] : scores, String(value)); + batch.zAdd(keys[i], { + score: isArrayOfScores ? scores[i] : scores, + value: String(value), + }); } } - await helpers.execBatch(batch); + await batch.execAsPipeline(); }; module.sortedSetAddBulk = async function (data) { @@ -69,8 +69,8 @@ module.exports = function (module) { if (!utils.isNumber(item[1])) { throw new Error(`[[error:invalid-score, ${item[1]}]]`); } - batch.zadd(item[0], item[1], item[2]); + batch.zAdd(item[0], { score: item[1], value: String(item[2]) }); }); - await helpers.execBatch(batch); + await batch.execAsPipeline(); }; }; diff --git a/src/database/redis/sorted/intersect.js b/src/database/redis/sorted/intersect.js index 2b2ed1fe90..983c11abc4 100644 --- a/src/database/redis/sorted/intersect.js +++ b/src/database/redis/sorted/intersect.js @@ -8,52 +8,46 @@ module.exports = function (module) { return 0; } const tempSetName = `temp_${Date.now()}`; - - const interParams = [tempSetName, keys.length].concat(keys); - const multi = module.client.multi(); - multi.zinterstore(interParams); - multi.zcard(tempSetName); + multi.zInterStore(tempSetName, keys); + multi.zCard(tempSetName); multi.del(tempSetName); const results = await helpers.execBatch(multi); return results[1] || 0; }; module.getSortedSetIntersect = async function (params) { - params.method = 'zrange'; + params.reverse = false; return await getSortedSetRevIntersect(params); }; module.getSortedSetRevIntersect = async function (params) { - params.method = 'zrevrange'; + params.reverse = true; return await getSortedSetRevIntersect(params); }; async function getSortedSetRevIntersect(params) { - const { sets } = params; + let { sets } = params; const start = params.hasOwnProperty('start') ? params.start : 0; const stop = params.hasOwnProperty('stop') ? params.stop : -1; const weights = params.weights || []; const tempSetName = `temp_${Date.now()}`; - let interParams = [tempSetName, sets.length].concat(sets); + const interParams = {}; if (weights.length) { - interParams = interParams.concat(['WEIGHTS'].concat(weights)); + sets = sets.map((set, index) => ({ key: set, weight: weights[index] })); } if (params.aggregate) { - interParams = interParams.concat(['AGGREGATE', params.aggregate]); + interParams['AGGREGATE'] = params.aggregate.toUpperCase(); } - const rangeParams = [tempSetName, start, stop]; - if (params.withScores) { - rangeParams.push('WITHSCORES'); - } + const rangeCmd = params.withScores ? 'zRangeWithScores' : 'zRange'; const multi = module.client.multi(); - multi.zinterstore(interParams); - multi[params.method](rangeParams); + multi.zInterStore(tempSetName, sets, interParams); + multi[rangeCmd](tempSetName, start, stop, { REV: params.reverse}); multi.del(tempSetName); let results = await helpers.execBatch(multi); @@ -61,6 +55,6 @@ module.exports = function (module) { return results ? results[1] : null; } results = results[1] || []; - return helpers.zsetToObjectArray(results); + return results; } }; diff --git a/src/database/redis/sorted/remove.js b/src/database/redis/sorted/remove.js index 0c2b0164b0..df4c980b11 100644 --- a/src/database/redis/sorted/remove.js +++ b/src/database/redis/sorted/remove.js @@ -18,10 +18,10 @@ module.exports = function (module) { if (Array.isArray(key)) { const batch = module.client.batch(); - key.forEach(k => batch.zrem(k, value)); + key.forEach(k => batch.zRem(k, value.map(String))); await helpers.execBatch(batch); } else { - await module.client.zrem(key, value); + await module.client.zRem(key, value.map(String)); } }; @@ -31,7 +31,7 @@ module.exports = function (module) { module.sortedSetsRemoveRangeByScore = async function (keys, min, max) { const batch = module.client.batch(); - keys.forEach(k => batch.zremrangebyscore(k, min, max)); + keys.forEach(k => batch.zRemRangeByScore(k, min, max)); await helpers.execBatch(batch); }; @@ -40,7 +40,7 @@ module.exports = function (module) { return; } const batch = module.client.batch(); - data.forEach(item => batch.zrem(item[0], item[1])); + data.forEach(item => batch.zRem(item[0], String(item[1]))); await helpers.execBatch(batch); }; }; diff --git a/src/database/redis/sorted/union.js b/src/database/redis/sorted/union.js index acd57c2db0..329c5f125f 100644 --- a/src/database/redis/sorted/union.js +++ b/src/database/redis/sorted/union.js @@ -4,25 +4,20 @@ module.exports = function (module) { const helpers = require('../helpers'); module.sortedSetUnionCard = async function (keys) { - const tempSetName = `temp_${Date.now()}`; if (!keys.length) { return 0; } - const multi = module.client.multi(); - multi.zunionstore([tempSetName, keys.length].concat(keys)); - multi.zcard(tempSetName); - multi.del(tempSetName); - const results = await helpers.execBatch(multi); - return Array.isArray(results) && results.length ? results[1] : 0; + const results = await module.client.zUnion(keys); + return results ? results.length : 0; }; module.getSortedSetUnion = async function (params) { - params.method = 'zrange'; + params.reverse = false; return await module.sortedSetUnion(params); }; module.getSortedSetRevUnion = async function (params) { - params.method = 'zrevrange'; + params.reverse = true; return await module.sortedSetUnion(params); }; @@ -32,21 +27,16 @@ module.exports = function (module) { } const tempSetName = `temp_${Date.now()}`; - - const rangeParams = [tempSetName, params.start, params.stop]; - if (params.withScores) { - rangeParams.push('WITHSCORES'); - } - + const rangeCmd = params.withScores ? 'zRangeWithScores' : 'zRange'; const multi = module.client.multi(); - multi.zunionstore([tempSetName, params.sets.length].concat(params.sets)); - multi[params.method](rangeParams); + multi.zUnionStore(tempSetName, params.sets); + multi[rangeCmd](tempSetName, params.start, params.stop, { REV: params.reverse }); multi.del(tempSetName); let results = await helpers.execBatch(multi); if (!params.withScores) { return results ? results[1] : null; } results = results[1] || []; - return helpers.zsetToObjectArray(results); + return results; }; }; diff --git a/src/topics/posts.js b/src/topics/posts.js index e32c18e727..8201bcad02 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -442,7 +442,7 @@ module.exports = function (Topics) { let { content } = postData; // ignore lines that start with `>` - content = content.split('\n').filter(line => !line.trim().startsWith('>')).join('\n'); + content = (content || '').split('\n').filter(line => !line.trim().startsWith('>')).join('\n'); // Scan post content for topic links const matches = [...content.matchAll(backlinkRegex)]; if (!matches) { diff --git a/test/activitypub/actors.js b/test/activitypub/actors.js index 37fc74280d..ac2d11e5da 100644 --- a/test/activitypub/actors.js +++ b/test/activitypub/actors.js @@ -449,7 +449,8 @@ describe('Inbox resolution', () => { await activitypub.actors.assert(id); const inboxes = await activitypub.resolveInboxes([id]); - + console.log('inboxes', inboxes); + console.log('actor', actor); assert(inboxes && Array.isArray(inboxes)); assert.strictEqual(inboxes.length, 1); assert.strictEqual(inboxes[0], actor.inbox); diff --git a/test/activitypub/notes.js b/test/activitypub/notes.js index fbbf0c59ec..57ffce4259 100644 --- a/test/activitypub/notes.js +++ b/test/activitypub/notes.js @@ -650,7 +650,7 @@ describe('Notes', () => { it('should upvote an asserted remote post', async () => { const { id } = helpers.mocks.note(); - await activitypub.notes.assert(0, [id], { skipChecks: true }); + await activitypub.notes.assert(0, id, { skipChecks: true }); const { activity: like } = helpers.mocks.like({ object: id, }); @@ -672,7 +672,7 @@ describe('Notes', () => { it('should update a note\'s content', async () => { const { id: actor } = helpers.mocks.person(); const { id, note } = helpers.mocks.note({ attributedTo: actor }); - await activitypub.notes.assert(0, [id], { skipChecks: true }); + await activitypub.notes.assert(0, id, { skipChecks: true }); note.content = utils.generateUUID(); const { activity: update } = helpers.mocks.update({ object: note }); const { activity } = helpers.mocks.announce({ object: update }); diff --git a/test/database/sorted.js b/test/database/sorted.js index b98d969730..a375b2ec48 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -501,7 +501,9 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea ['byScoreWithScoresKeys1', 1, 'value1'], ['byScoreWithScoresKeys2', 2, 'value2'], ]); - const data = await db.getSortedSetRevRangeByScoreWithScores(['byScoreWithScoresKeys1', 'byScoreWithScoresKeys2'], 0, -1, 5, -5); + const data = await db.getSortedSetRevRangeByScoreWithScores([ + 'byScoreWithScoresKeys1', 'byScoreWithScoresKeys2', + ], 0, -1, 5, -5); assert.deepStrictEqual(data, [{ value: 'value2', score: 2 }, { value: 'value1', score: 1 }]); }); }); @@ -1144,23 +1146,17 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea assert.strictEqual(await db.exists('sorted3'), false); }); - it('should remove multiple values from multiple keys', (done) => { - db.sortedSetAdd('multiTest1', [1, 2, 3, 4], ['one', 'two', 'three', 'four'], (err) => { - assert.ifError(err); - db.sortedSetAdd('multiTest2', [3, 4, 5, 6], ['three', 'four', 'five', 'six'], (err) => { - assert.ifError(err); - db.sortedSetRemove(['multiTest1', 'multiTest2'], ['two', 'three', 'four', 'five', 'doesnt exist'], (err) => { - assert.ifError(err); - db.getSortedSetsMembers(['multiTest1', 'multiTest2'], (err, members) => { - assert.ifError(err); - assert.equal(members[0].length, 1); - assert.equal(members[1].length, 1); - assert.deepEqual(members, [['one'], ['six']]); - done(); - }); - }); - }); - }); + it('should remove multiple values from multiple keys', async () => { + await db.sortedSetAdd('multiTest1', [1, 2, 3, 4], ['one', 'two', 'three', 'four']); + await db.sortedSetAdd('multiTest2', [3, 4, 5, 6], ['three', 'four', 'five', 'six']); + + await db.sortedSetRemove(['multiTest1', 'multiTest2'], ['two', 'three', 'four', 'five', 'doesnt exist']); + + const members = await db.getSortedSetsMembers(['multiTest1', 'multiTest2']); + + assert.equal(members[0].length, 1); + assert.equal(members[1].length, 1); + assert.deepEqual(members, [['one'], ['six']]); }); it('should remove value from multiple keys', async () => { @@ -1171,24 +1167,15 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea assert.deepStrictEqual(await db.getSortedSetRange('multiTest4', 0, -1), ['four', 'five', 'six']); }); - it('should remove multiple values from multiple keys', (done) => { - db.sortedSetAdd('multiTest5', [1], ['one'], (err) => { - assert.ifError(err); - db.sortedSetAdd('multiTest6', [2], ['two'], (err) => { - assert.ifError(err); - db.sortedSetAdd('multiTest7', [3], [333], (err) => { - assert.ifError(err); - db.sortedSetRemove(['multiTest5', 'multiTest6', 'multiTest7'], ['one', 'two', 333], (err) => { - assert.ifError(err); - db.getSortedSetsMembers(['multiTest5', 'multiTest6', 'multiTest7'], (err, members) => { - assert.ifError(err); - assert.deepEqual(members, [[], [], []]); - done(); - }); - }); - }); - }); - }); + it('should remove multiple values from multiple keys', async () => { + await db.sortedSetAdd('multiTest5', [1], ['one']); + await db.sortedSetAdd('multiTest6', [2], ['two']); + await db.sortedSetAdd('multiTest7', [3], [333]); + + await db.sortedSetRemove(['multiTest5', 'multiTest6', 'multiTest7'], ['one', 'two', 333]); + + const members = await db.getSortedSetsMembers(['multiTest5', 'multiTest6', 'multiTest7']); + assert.deepEqual(members, [[], [], []]); }); it('should not remove anything if values is empty array', (done) => { @@ -1379,7 +1366,10 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea weights: [1, 0.5], }, (err, data) => { assert.ifError(err); - assert.deepEqual([{ value: 'value2', score: 4 }, { value: 'value3', score: 5.5 }], data); + assert.deepEqual([ + { value: 'value2', score: 4 }, + { value: 'value3', score: 5.5 }, + ], data); done(); }); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 6a568109b7..9416962b07 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -171,7 +171,6 @@ before(async function () { require('../../src/user').startJobs(); await webserver.listen(); - // Iterate over all of the test suites/contexts this.test.parent.suites.forEach((suite) => { // Attach an afterAll listener that resets the defaults From f7f70468fd5dda007adff3b7d2f604b4ee342d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 13:17:29 -0400 Subject: [PATCH 3138/4744] fix: pubsub on node-redis --- src/database/redis/pubsub.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/database/redis/pubsub.js b/src/database/redis/pubsub.js index a7d220682d..1868ac86ad 100644 --- a/src/database/redis/pubsub.js +++ b/src/database/redis/pubsub.js @@ -13,12 +13,7 @@ const PubSub = function () { self.queue = []; connection.connect().then((client) => { self.subClient = client; - self.subClient.subscribe(channelName); - self.subClient.on('message', (channel, message) => { - if (channel !== channelName) { - return; - } - + self.subClient.subscribe(channelName, (message) => { try { const msg = JSON.parse(message); self.emit(msg.event, msg.data); From d3faff3680d19c8a634569d3d072702f49e88e65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:18:36 -0400 Subject: [PATCH 3139/4744] fix(deps): update dependency connect-redis to v9 (#13497) 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 a42eb52182..2444e00d91 100644 --- a/install/package.json +++ b/install/package.json @@ -59,7 +59,7 @@ "connect-flash": "0.1.1", "connect-mongo": "5.1.0", "connect-pg-simple": "10.0.0", - "connect-redis": "8.1.0", + "connect-redis": "9.0.0", "cookie-parser": "1.4.7", "cron": "4.3.1", "cropperjs": "1.6.2", From e84fc7393915438e88b9436f8b26bdc6f2f9a331 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:18:50 -0400 Subject: [PATCH 3140/4744] fix(deps): update dependency bootstrap to v5.3.7 (#13499) 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 2444e00d91..4f9f9845f8 100644 --- a/install/package.json +++ b/install/package.json @@ -47,7 +47,7 @@ "benchpressjs": "2.5.5", "body-parser": "2.2.0", "bootbox": "6.0.4", - "bootstrap": "5.3.6", + "bootstrap": "5.3.7", "bootswatch": "5.3.6", "chalk": "4.1.2", "chart.js": "4.5.0", From 0a0dd1c14dbce63c8360bee5cbe1472b1a6c87c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:19:01 -0400 Subject: [PATCH 3141/4744] chore(deps): update dependency mocha to v11.7.0 (#13502) 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 4f9f9845f8..244486ef13 100644 --- a/install/package.json +++ b/install/package.json @@ -170,7 +170,7 @@ "husky": "8.0.3", "jsdom": "26.1.0", "lint-staged": "16.1.2", - "mocha": "11.6.0", + "mocha": "11.7.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", From 1fc91d5e751d36bd10eb9fea49ab0c6119315efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 13:21:18 -0400 Subject: [PATCH 3142/4744] test: add a null field test --- test/database/hash.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/database/hash.js b/test/database/hash.js index 571cf8bb95..55b75c0df6 100644 --- a/test/database/hash.js +++ b/test/database/hash.js @@ -117,6 +117,12 @@ describe('Hash methods', () => { const result = await db.getObject('emptykey'); assert.deepStrictEqual(result, null); }); + + it('should return null if a field is set to null', async () => { + await db.setObject('nullFieldTest', { baz: 'baz', foo: null }); + const data = await db.getObjectFields('nullFieldTest', ['baz', 'foo']); + assert.deepStrictEqual(data, { baz: 'baz', foo: null }); + }); }); describe('setObjectField()', () => { From 3e961257ec0904dbc3b3c64dab3d4cbdffcfbbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 13:25:36 -0400 Subject: [PATCH 3143/4744] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c5d80e0fd..fb8b8702fe 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Our minimalist "Harmony" theme gets you going right away, no coding experience r NodeBB requires the following software to be installed: * A version of Node.js at least 20 or greater ([installation/upgrade instructions](https://github.com/nodesource/distributions)) -* MongoDB, version 3.6 or greater **or** Redis, version 2.8.9 or greater +* MongoDB, version 5 or greater **or** Redis, version 7.2 or greater * If you are using [clustering](https://docs.nodebb.org/configuring/scaling/) you need Redis installed and configured. * nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB) From 0315e369411c3f18c3bdaac10f6817a8a8f23f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 13:34:55 -0400 Subject: [PATCH 3144/4744] chore: remove logs --- test/activitypub/actors.js | 2 -- test/mocks/databasemock.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/activitypub/actors.js b/test/activitypub/actors.js index ac2d11e5da..8271fdb335 100644 --- a/test/activitypub/actors.js +++ b/test/activitypub/actors.js @@ -449,8 +449,6 @@ describe('Inbox resolution', () => { await activitypub.actors.assert(id); const inboxes = await activitypub.resolveInboxes([id]); - console.log('inboxes', inboxes); - console.log('actor', actor); assert(inboxes && Array.isArray(inboxes)); assert.strictEqual(inboxes.length, 1); assert.strictEqual(inboxes[0], actor.inbox); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 9416962b07..a252bc69d8 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -14,7 +14,7 @@ const util = require('util'); process.env.NODE_ENV = process.env.TEST_ENV || 'production'; global.env = process.env.NODE_ENV || 'production'; - +process.env.CI = 'true'; const winston = require('winston'); const packageInfo = require('../../package.json'); From 819e28052aa672f565e0196c7036f5b778007ae5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:35:38 -0400 Subject: [PATCH 3145/4744] fix(deps): update dependency pg to v8.16.1 (#13503) 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 244486ef13..04d03164d8 100644 --- a/install/package.json +++ b/install/package.json @@ -116,7 +116,7 @@ "passport": "0.7.0", "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", - "pg": "8.16.0", + "pg": "8.16.1", "pg-cursor": "2.15.0", "postcss": "8.5.6", "postcss-clean": "1.2.0", From 39d243b04f63a44670ac69211604bbbfba37caf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 13:42:19 -0400 Subject: [PATCH 3146/4744] test: remove ci env --- test/mocks/databasemock.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index a252bc69d8..41b01b2c72 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -14,7 +14,6 @@ const util = require('util'); process.env.NODE_ENV = process.env.TEST_ENV || 'production'; global.env = process.env.NODE_ENV || 'production'; -process.env.CI = 'true'; const winston = require('winston'); const packageInfo = require('../../package.json'); From 0b9bfc1ce1bf381aba0170c1c860b8dd007357b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Jun 2025 16:59:57 -0400 Subject: [PATCH 3147/4744] refactor: parallel socket.io adapter --- src/database/redis.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/database/redis.js b/src/database/redis.js index 472dae8de6..d2118aa925 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -104,8 +104,11 @@ redisModule.info = async function (cxn) { redisModule.socketAdapter = async function () { const redisAdapter = require('@socket.io/redis-adapter'); - const pub = await connection.connect(nconf.get('redis')); - const sub = await connection.connect(nconf.get('redis')); + const redisConfig = nconf.get('redis'); + const [pub, sub] = await Promise.all([ + connection.connect(redisConfig), + connection.connect(redisConfig), + ]); return redisAdapter(pub, sub, { key: `db:${nconf.get('redis:database')}:adapter_key`, }); From 3b364ba12046a2bffbc366367f19bd2af8c0c931 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:13:33 -0400 Subject: [PATCH 3148/4744] fix(deps): update dependency pg-cursor to v2.15.1 (#13504) 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 85aea01d55..356efb7ae7 100644 --- a/install/package.json +++ b/install/package.json @@ -117,7 +117,7 @@ "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", "pg": "8.16.1", - "pg-cursor": "2.15.0", + "pg-cursor": "2.15.1", "postcss": "8.5.6", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", From f9c6d24c73dd012835adbf2c5f80e5014a794fba Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 21 Jun 2025 09:19:27 +0000 Subject: [PATCH 3149/4744] Latest translations and fallbacks --- .../es/admin/manage/user-custom-fields.json | 44 +++++++++---------- public/language/es/user.json | 4 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/public/language/es/admin/manage/user-custom-fields.json b/public/language/es/admin/manage/user-custom-fields.json index dab10670d2..49096d7021 100644 --- a/public/language/es/admin/manage/user-custom-fields.json +++ b/public/language/es/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", + "title": "Gestionar campos personalizados del usuario", + "create-field": "Crear campo", + "edit-field": "Editar campo", + "manage-custom-fields": "Gestionar campos personalizados", + "type-of-input": "Tipo de campo", + "key": "Clave", + "name": "Nombre", + "icon": "Icono", + "type": "Tipo", + "min-rep": "Reputación mínima", + "input-type-text": "Campo (Texto)", + "input-type-link": "Campo (Link)", + "input-type-number": "Campo (Número)", + "input-type-date": "Campo (Fecha)", + "input-type-select": "Selector", + "input-type-select-multi": "Selector múltiple", + "select-options": "Opciones", "select-options-help": "Add one option per line for the select element", - "minimum-reputation": "Minimum reputation", + "minimum-reputation": "Reputación mínima", "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", + "custom-fields-saved": "Campos personalizados guardados", + "visibility": "Visibilidad", + "visibility-all": "Todo el mundo puede ver este campo", + "visibility-loggedin": "Solo usuarios logeados pueden ver este campo", "visibility-privileged": "Only privileged users like admins & moderators can see the field" } \ No newline at end of file diff --git a/public/language/es/user.json b/public/language/es/user.json index 66dfc6c8dd..76e9c3c181 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -105,10 +105,10 @@ "show-email": "Mostrar mi correo electrónico", "show-fullname": "Mostrar mi nombre completo", "restrict-chats": "Solo permitir mensajes de chat de usuarios a los que sigo", - "disable-incoming-chats": "Disable incoming chat messages ", + "disable-incoming-chats": "Desactivar mensajes de chat entrantes", "chat-allow-list": "Allow chat messages from the following users", "chat-deny-list": "Deny chat messages from the following users", - "chat-list-add-user": "Add user", + "chat-list-add-user": "Agregar usuario", "digest-label": "Suscribirse al resumen", "digest-description": "Suscribirse a actualizaciones por correo electrónico a este foro (nuevas notificaciones y temas) de acuerdo a una recurrencia definida", "digest-off": "Apagado", From e360f649b3f6dc8215ae841da7650458b0eb597b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:19:33 -0400 Subject: [PATCH 3150/4744] fix(deps): update dependency ace-builds to v1.43.0 (#13507) 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 356efb7ae7..fd1f0f7c0a 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.42.0", + "ace-builds": "1.43.0", "archiver": "7.0.1", "async": "3.2.6", "autoprefixer": "10.4.21", From 10f7b49be8502460491def251af3a13bdb63a7c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:20:37 -0400 Subject: [PATCH 3151/4744] fix(deps): update dependency pg-cursor to v2.15.2 (#13506) 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 fd1f0f7c0a..24bdcffffe 100644 --- a/install/package.json +++ b/install/package.json @@ -117,7 +117,7 @@ "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", "pg": "8.16.1", - "pg-cursor": "2.15.1", + "pg-cursor": "2.15.2", "postcss": "8.5.6", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", From 1eefaf5cd819b30d58f2612a2dba8f1014265bc1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:43:41 -0400 Subject: [PATCH 3152/4744] fix(deps): update dependency bootswatch to v5.3.7 (#13510) 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 24bdcffffe..e71f85ca34 100644 --- a/install/package.json +++ b/install/package.json @@ -48,7 +48,7 @@ "body-parser": "2.2.0", "bootbox": "6.0.4", "bootstrap": "5.3.7", - "bootswatch": "5.3.6", + "bootswatch": "5.3.7", "chalk": "4.1.2", "chart.js": "4.5.0", "cli-graph": "3.2.2", From 82c8034cfbf475e9f3ce6af7f41fba2ca2d1978d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Jun 2025 12:55:31 -0400 Subject: [PATCH 3153/4744] test: testing timeout on failing test --- test/api.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/api.js b/test/api.js index d10433a88b..53699d0a20 100644 --- a/test/api.js +++ b/test/api.js @@ -487,6 +487,7 @@ describe('API', async () => { }); it('should not error out when called', async () => { + this.timeout(0); await setupData(); if (csrfToken) { @@ -513,6 +514,7 @@ describe('API', async () => { redirect: 'manual', headers: headers, body: body, + timeout: 10000, }); } else if (type === 'form') { result = await helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken); From 1a85fafbaf7d5db4f900f1955add4abc9a244fa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Jun 2025 13:01:28 -0400 Subject: [PATCH 3154/4744] test: on more --- test/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api.js b/test/api.js index 53699d0a20..70868ba489 100644 --- a/test/api.js +++ b/test/api.js @@ -486,7 +486,7 @@ describe('API', async () => { } }); - it('should not error out when called', async () => { + it('should not error out when called', async function () { this.timeout(0); await setupData(); From fa31ba0560b9f16452b1cbbc10b64bf67ff20456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Jun 2025 13:10:11 -0400 Subject: [PATCH 3155/4744] test: increase timeout --- test/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api.js b/test/api.js index 70868ba489..f7616904c8 100644 --- a/test/api.js +++ b/test/api.js @@ -514,7 +514,7 @@ describe('API', async () => { redirect: 'manual', headers: headers, body: body, - timeout: 10000, + timeout: 30000, }); } else if (type === 'form') { result = await helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken); From 4be2e82b5afe0e02769104dfa246e9402ea6806d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:20:02 -0400 Subject: [PATCH 3156/4744] fix(deps): update dependency nodebb-theme-harmony to v2.1.16 (#13513) 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 e71f85ca34..1b3fcf6e81 100644 --- a/install/package.json +++ b/install/package.json @@ -106,7 +106,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.4", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.1.15", + "nodebb-theme-harmony": "2.1.16", "nodebb-theme-lavender": "7.1.19", "nodebb-theme-peace": "2.2.43", "nodebb-theme-persona": "14.1.12", From 59090931039e25f6dcb9ee15a0e1b9bc6ab9f24a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:20:12 -0400 Subject: [PATCH 3157/4744] fix(deps): update dependency nodebb-theme-peace to v2.2.44 (#13514) 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 1b3fcf6e81..31be2dd161 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-rewards-essentials": "1.0.2", "nodebb-theme-harmony": "2.1.16", "nodebb-theme-lavender": "7.1.19", - "nodebb-theme-peace": "2.2.43", + "nodebb-theme-peace": "2.2.44", "nodebb-theme-persona": "14.1.12", "nodebb-widget-essentials": "7.0.38", "nodemailer": "7.0.3", From bbacd8f6e420c1b8ddf0d216f854b3eda3e87156 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 20:29:07 -0400 Subject: [PATCH 3158/4744] chore(deps): update dependency mocha to v11.7.1 (#13509) 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 31be2dd161..4dbc75762f 100644 --- a/install/package.json +++ b/install/package.json @@ -170,7 +170,7 @@ "husky": "8.0.3", "jsdom": "26.1.0", "lint-staged": "16.1.2", - "mocha": "11.7.0", + "mocha": "11.7.1", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", From d2f0944eabd274967d6333fa855376eedbf6e2bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 20:29:27 -0400 Subject: [PATCH 3159/4744] fix(deps): update dependency pg to v8.16.2 (#13505) 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 4dbc75762f..48dcbdc61d 100644 --- a/install/package.json +++ b/install/package.json @@ -116,7 +116,7 @@ "passport": "0.7.0", "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", - "pg": "8.16.1", + "pg": "8.16.2", "pg-cursor": "2.15.2", "postcss": "8.5.6", "postcss-clean": "1.2.0", From a41d2c0b1a9e60c2765c883b7175a58a04138568 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 09:43:26 -0400 Subject: [PATCH 3160/4744] chore(deps): update dependency smtp-server to v3.14.0 (#13515) 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 48dcbdc61d..07981f8855 100644 --- a/install/package.json +++ b/install/package.json @@ -174,7 +174,7 @@ "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "17.1.0", - "smtp-server": "3.13.8" + "smtp-server": "3.14.0" }, "optionalDependencies": { "sass-embedded": "1.89.2" From 92a3859f7bdd68b52e44551b3ce62fd55ce6e834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Jun 2025 14:18:53 -0400 Subject: [PATCH 3161/4744] feat: add option to toggle chat join/leave message closes #13508 --- public/language/en-GB/modules.json | 1 + public/openapi/components/schemas/Chats.yaml | 4 ++ .../read/user/userslug/chats/roomid.yaml | 4 ++ .../write/chats/roomId/messages/mid.yaml | 6 +-- .../write/chats/roomId/messages/mid/ip.yaml | 2 +- public/src/client/chats/manage.js | 4 ++ src/api/chats.js | 12 +++++- src/messaging/rooms.js | 37 ++++++++++++++----- src/views/modals/manage-room.tpl | 8 +++- 9 files changed, 62 insertions(+), 16 deletions(-) diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 29ba02726f..f4b473992a 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/openapi/components/schemas/Chats.yaml b/public/openapi/components/schemas/Chats.yaml index dc84aca4ef..036b937158 100644 --- a/public/openapi/components/schemas/Chats.yaml +++ b/public/openapi/components/schemas/Chats.yaml @@ -27,6 +27,10 @@ RoomObject: description: Timestamp of when room was created notificationSetting: type: number + description: The notification setting for the room, 0 = no notifications, 1 = only mentions, 2 = all messages + joinLeaveMessages: + type: number + description: Whether join/leave messages are enabled in the room MessageObject: type: object properties: diff --git a/public/openapi/read/user/userslug/chats/roomid.yaml b/public/openapi/read/user/userslug/chats/roomid.yaml index 5c5fd1c296..73c4a62da9 100644 --- a/public/openapi/read/user/userslug/chats/roomid.yaml +++ b/public/openapi/read/user/userslug/chats/roomid.yaml @@ -56,6 +56,8 @@ get: type: array notificationOptionsIcon: type: string + joinLeaveMessages: + type: number messages: type: array items: @@ -360,6 +362,8 @@ get: type: string notificationSetting: type: number + joinLeaveMessages: + type: number publicRooms: type: array items: diff --git a/public/openapi/write/chats/roomId/messages/mid.yaml b/public/openapi/write/chats/roomId/messages/mid.yaml index 5053f1546d..dfa06e7811 100644 --- a/public/openapi/write/chats/roomId/messages/mid.yaml +++ b/public/openapi/write/chats/roomId/messages/mid.yaml @@ -49,7 +49,7 @@ put: type: number required: true description: a valid message id - example: 5 + example: 3 requestBody: required: true content: @@ -92,7 +92,7 @@ delete: type: number required: true description: a valid message id - example: 5 + example: 3 responses: '200': description: Message successfully deleted @@ -125,7 +125,7 @@ post: type: number required: true description: a valid message id - example: 5 + example: 3 responses: '200': description: message successfully restored diff --git a/public/openapi/write/chats/roomId/messages/mid/ip.yaml b/public/openapi/write/chats/roomId/messages/mid/ip.yaml index 0d2a82cba9..1730542213 100644 --- a/public/openapi/write/chats/roomId/messages/mid/ip.yaml +++ b/public/openapi/write/chats/roomId/messages/mid/ip.yaml @@ -17,7 +17,7 @@ get: type: string required: true description: a valid chat message id - example: 5 + example: 3 responses: '200': description: Chat message ip address retrieved diff --git a/public/src/client/chats/manage.js b/public/src/client/chats/manage.js index 2ab92c4295..f2e5cbc04c 100644 --- a/public/src/client/chats/manage.js +++ b/public/src/client/chats/manage.js @@ -75,12 +75,16 @@ define('forum/chats/manage', [ modal.find('[component="chat/manage/save"]').on('click', () => { const notifSettingEl = modal.find('[component="chat/room/notification/setting"]'); + const joinLeaveMessagesEl = modal.find('[component="chat/room/join-leave-messages"]'); + api.put(`/chats/${roomId}`, { groups: modal.find('[component="chat/room/groups"]').val(), notificationSetting: notifSettingEl.val(), + joinLeaveMessages: joinLeaveMessagesEl.is(':checked') ? 1 : 0, }).then((payload) => { ajaxify.data.groups = payload.groups; ajaxify.data.notificationSetting = payload.notificationSetting; + ajaxify.data.joinLeaveMessages = payload.joinLeaveMessages; const roomDefaultOption = payload.notificationOptions[0]; $('[component="chat/notification/setting"] [data-icon]').first().attr( 'data-icon', roomDefaultOption.icon diff --git a/src/api/chats.js b/src/api/chats.js index e1538c4426..5099479b51 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -162,9 +162,17 @@ chatsAPI.update = async (caller, data) => { await db.setObjectField(`chat:room:${data.roomId}`, 'groups', JSON.stringify(data.groups)); } } - if (data.hasOwnProperty('notificationSetting') && isAdmin) { - await db.setObjectField(`chat:room:${data.roomId}`, 'notificationSetting', data.notificationSetting); + if (isAdmin) { + const updateData = {}; + if (data.hasOwnProperty('notificationSetting')) { + updateData.notificationSetting = data.notificationSetting; + } + if (data.hasOwnProperty('joinLeaveMessages')) { + updateData.joinLeaveMessages = data.joinLeaveMessages; + } + await db.setObject(`chat:room:${data.roomId}`, updateData); } + const loadedRoom = await messaging.loadRoom(caller.uid, { roomId: data.roomId, }); diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 8b57b81da7..4bf4deed2c 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -22,7 +22,7 @@ const roomUidCache = cacheCreate({ }); const intFields = [ - 'roomId', 'timestamp', 'userCount', 'messageCount', + 'roomId', 'timestamp', 'userCount', 'messageCount', 'joinLeaveMessages', ]; module.exports = function (Messaging) { @@ -88,6 +88,7 @@ module.exports = function (Messaging) { timestamp: now, notificationSetting: data.notificationSetting, messageCount: 0, + joinLeaveMessages: 0, }; if (data.hasOwnProperty('roomName') && data.roomName) { @@ -280,12 +281,22 @@ module.exports = function (Messaging) { async function addUidsToRoom(uids, roomId) { const now = Date.now(); const timestamps = uids.map(() => now); + await Promise.all([ db.sortedSetAdd(`chat:room:${roomId}:uids`, timestamps, uids), db.sortedSetAdd(`chat:room:${roomId}:uids:online`, timestamps, uids), ]); await updateUserCount([roomId]); - await Promise.all(uids.map(uid => Messaging.addSystemMessage('user-join', uid, roomId))); + if (await joinLeaveMessagesEnabled(roomId)) { + await Promise.all( + uids.map(uid => Messaging.addSystemMessage('user-join', uid, roomId)) + ); + } + } + + async function joinLeaveMessagesEnabled(roomId) { + const roomData = await Messaging.getRoomData(roomId, ['joinLeaveMessages']); + return roomData && roomData.joinLeaveMessages === 1; } Messaging.removeUsersFromRoom = async (uid, uids, roomId) => { @@ -319,7 +330,9 @@ module.exports = function (Messaging) { } Messaging.leaveRoom = async (uids, roomId) => { - const isInRoom = await Promise.all(uids.map(uid => Messaging.isUserInRoom(uid, roomId))); + const isInRoom = await Promise.all( + uids.map(uid => Messaging.isUserInRoom(uid, roomId)) + ); uids = uids.filter((uid, index) => isInRoom[index]); const keys = uids @@ -334,8 +347,11 @@ module.exports = function (Messaging) { ], uids), db.sortedSetsRemove(keys, roomId), ]); - - await Promise.all(uids.map(uid => Messaging.addSystemMessage('user-leave', uid, roomId))); + if (await joinLeaveMessagesEnabled(roomId)) { + await Promise.all( + uids.map(uid => Messaging.addSystemMessage('user-leave', uid, roomId)) + ); + } await updateOwner(roomId); await updateUserCount([roomId]); }; @@ -357,10 +373,13 @@ module.exports = function (Messaging) { ], roomIds), ]); - await Promise.all( - roomIds.map(roomId => updateOwner(roomId)) - .concat(roomIds.map(roomId => Messaging.addSystemMessage('user-leave', uid, roomId))) - ); + await Promise.all(roomIds.map(async (roomId) => { + await updateOwner(roomId); + if (await joinLeaveMessagesEnabled(roomId)) { + await Messaging.addSystemMessage('user-leave', uid, roomId); + } + })); + await updateUserCount(roomIds); }; diff --git a/src/views/modals/manage-room.tpl b/src/views/modals/manage-room.tpl index 08c96ccb0b..55f9390de7 100644 --- a/src/views/modals/manage-room.tpl +++ b/src/views/modals/manage-room.tpl @@ -1,6 +1,6 @@
    {{{ if user.isAdmin }}} -
    +
    +
    + +
    + +
    +

    {{{ end }}} From f5aca1144d776478f6b7a0821fe0684e9816a5c3 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 27 Jun 2025 18:19:19 +0000 Subject: [PATCH 3162/4744] chore(i18n): fallback strings for new resources: nodebb.modules --- public/language/ar/modules.json | 1 + public/language/az/modules.json | 1 + public/language/bg/modules.json | 1 + public/language/bn/modules.json | 1 + public/language/cs/modules.json | 1 + public/language/da/modules.json | 1 + public/language/de/modules.json | 1 + public/language/el/modules.json | 1 + public/language/en-US/modules.json | 1 + public/language/en-x-pirate/modules.json | 1 + public/language/es/modules.json | 1 + public/language/et/modules.json | 1 + public/language/fa-IR/modules.json | 1 + public/language/fi/modules.json | 1 + public/language/fr/modules.json | 1 + public/language/gl/modules.json | 1 + public/language/he/modules.json | 1 + public/language/hr/modules.json | 1 + public/language/hu/modules.json | 1 + public/language/hy/modules.json | 1 + public/language/id/modules.json | 1 + public/language/it/modules.json | 1 + public/language/ja/modules.json | 1 + public/language/ko/modules.json | 1 + public/language/lt/modules.json | 1 + public/language/lv/modules.json | 1 + public/language/ms/modules.json | 1 + public/language/nb/modules.json | 1 + public/language/nl/modules.json | 1 + public/language/nn-NO/modules.json | 1 + public/language/pl/modules.json | 1 + public/language/pt-BR/modules.json | 1 + public/language/pt-PT/modules.json | 1 + public/language/ro/modules.json | 1 + public/language/ru/modules.json | 1 + public/language/rw/modules.json | 1 + public/language/sc/modules.json | 1 + public/language/sk/modules.json | 1 + public/language/sl/modules.json | 1 + public/language/sq-AL/modules.json | 1 + public/language/sr/modules.json | 1 + public/language/sv/modules.json | 1 + public/language/th/modules.json | 1 + public/language/tr/modules.json | 1 + public/language/uk/modules.json | 1 + public/language/vi/modules.json | 1 + public/language/zh-CN/modules.json | 1 + public/language/zh-TW/modules.json | 1 + 48 files changed, 48 insertions(+) diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json index 1bf14cdc27..cb15105037 100644 --- a/public/language/ar/modules.json +++ b/public/language/ar/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/az/modules.json b/public/language/az/modules.json index dad624a47f..b82661a7e1 100644 --- a/public/language/az/modules.json +++ b/public/language/az/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "İstifadəçi əlavə et", "chat.notification-settings": "Bildiriş parametrləri", "chat.default-notification-setting": "Defolt bildiriş parametri", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Defolt otaq", "chat.notification-setting-none": "Bildiriş yoxdur", "chat.notification-setting-at-mention-only": "yalnız @qeyd", diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index f6485e5213..864c179ad7 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Добавяне на потребител", "chat.notification-settings": "Настройки за известията", "chat.default-notification-setting": "Стандартни настройки за известията", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "По подразбиране за стаята", "chat.notification-setting-none": "Без известия", "chat.notification-setting-at-mention-only": "Само @споменавания", diff --git a/public/language/bn/modules.json b/public/language/bn/modules.json index d010f1ad37..45ce1f0e8c 100644 --- a/public/language/bn/modules.json +++ b/public/language/bn/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json index f8d89bde37..7ba9a7a2a3 100644 --- a/public/language/cs/modules.json +++ b/public/language/cs/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/da/modules.json b/public/language/da/modules.json index 85a9e8fdfa..e8ceb340f2 100644 --- a/public/language/da/modules.json +++ b/public/language/da/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/de/modules.json b/public/language/de/modules.json index 5edc6169e8..cf5c776242 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Benutzer hinzufügen", "chat.notification-settings": "Benachrichtigungseinstellungen", "chat.default-notification-setting": "Standardeinstellung für die Benachrichtigung", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Raum Standard", "chat.notification-setting-none": "Keine Benachrichtigungen", "chat.notification-setting-at-mention-only": "@nur Erwähnung", diff --git a/public/language/el/modules.json b/public/language/el/modules.json index a1d1259471..2768fec8a4 100644 --- a/public/language/el/modules.json +++ b/public/language/el/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/en-US/modules.json b/public/language/en-US/modules.json index a1d1259471..2768fec8a4 100644 --- a/public/language/en-US/modules.json +++ b/public/language/en-US/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/en-x-pirate/modules.json b/public/language/en-x-pirate/modules.json index c78a052be8..fb7c802ccb 100644 --- a/public/language/en-x-pirate/modules.json +++ b/public/language/en-x-pirate/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/es/modules.json b/public/language/es/modules.json index 1fd0eda552..be35e9c442 100644 --- a/public/language/es/modules.json +++ b/public/language/es/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/et/modules.json b/public/language/et/modules.json index 1aa6c0d427..8a8c22cc93 100644 --- a/public/language/et/modules.json +++ b/public/language/et/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/fa-IR/modules.json b/public/language/fa-IR/modules.json index ddd073ddbf..0115f29ae5 100644 --- a/public/language/fa-IR/modules.json +++ b/public/language/fa-IR/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/fi/modules.json b/public/language/fi/modules.json index d68c1bbaef..dcf1fcea47 100644 --- a/public/language/fi/modules.json +++ b/public/language/fi/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Ilmoitusasetukset", "chat.default-notification-setting": "Ilmoitusten oletusasetukset", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Huoneen oletus", "chat.notification-setting-none": "Ilmoituksia ei ole", "chat.notification-setting-at-mention-only": "vain @maininta", diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json index 475fd1d297..2b54930f35 100644 --- a/public/language/fr/modules.json +++ b/public/language/fr/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Ajouter un utilisateur", "chat.notification-settings": "Paramètres de notification", "chat.default-notification-setting": "Paramètres de notification par défaut", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Salon par défaut", "chat.notification-setting-none": "Aucune notification", "chat.notification-setting-at-mention-only": "@mention seulement", diff --git a/public/language/gl/modules.json b/public/language/gl/modules.json index 437ec89919..02ae4bd868 100644 --- a/public/language/gl/modules.json +++ b/public/language/gl/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/he/modules.json b/public/language/he/modules.json index fb546965c1..12e592b6e2 100644 --- a/public/language/he/modules.json +++ b/public/language/he/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "הוספת משתמש", "chat.notification-settings": "הגדרות התראות", "chat.default-notification-setting": "הגדרת ברירת מחדל להתראות", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "ברירת המחדל של החדר", "chat.notification-setting-none": "ללא התראות", "chat.notification-setting-at-mention-only": "@אזכור בלבד", diff --git a/public/language/hr/modules.json b/public/language/hr/modules.json index ae32d6f0d7..c3d8f47ad4 100644 --- a/public/language/hr/modules.json +++ b/public/language/hr/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/hu/modules.json b/public/language/hu/modules.json index 8b50449c1d..a3544a3bf1 100644 --- a/public/language/hu/modules.json +++ b/public/language/hu/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/hy/modules.json b/public/language/hy/modules.json index 2de14aeb1e..381dcdfa51 100644 --- a/public/language/hy/modules.json +++ b/public/language/hy/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Ավելացնել օգտատեր", "chat.notification-settings": "Ծանուցման կարգավորումներ", "chat.default-notification-setting": "Ծանուցման հիմնական կարգավորումներ", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Սենյակի հիմնական վիճակ", "chat.notification-setting-none": "Ծանուցումներ չկան", "chat.notification-setting-at-mention-only": "@նշում միայն", diff --git a/public/language/id/modules.json b/public/language/id/modules.json index 5b15156ff3..86afe16b09 100644 --- a/public/language/id/modules.json +++ b/public/language/id/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/it/modules.json b/public/language/it/modules.json index 2646ad5d5f..8cba8bc989 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Aggiungi utente", "chat.notification-settings": "Impostazioni di notifica", "chat.default-notification-setting": "Impostazioni di notifica predefinite", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Stanza predefinita", "chat.notification-setting-none": "Nessuna notifica", "chat.notification-setting-at-mention-only": "@solo menzione", diff --git a/public/language/ja/modules.json b/public/language/ja/modules.json index e177131774..19b23706f7 100644 --- a/public/language/ja/modules.json +++ b/public/language/ja/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json index 090984bfd0..b79e2f7028 100644 --- a/public/language/ko/modules.json +++ b/public/language/ko/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "사용자 추가", "chat.notification-settings": "알림 설정", "chat.default-notification-setting": "기본 알림 설정", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "방 기본값", "chat.notification-setting-none": "알림 없음", "chat.notification-setting-at-mention-only": "@언급만", diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json index 6a1a9c3c7a..6f9cb47059 100644 --- a/public/language/lt/modules.json +++ b/public/language/lt/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/lv/modules.json b/public/language/lv/modules.json index 0c1a36c576..940d9e52a1 100644 --- a/public/language/lv/modules.json +++ b/public/language/lv/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/ms/modules.json b/public/language/ms/modules.json index a2de6c8f89..d3f710ccaa 100644 --- a/public/language/ms/modules.json +++ b/public/language/ms/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json index bd0436e435..4a3d1439fa 100644 --- a/public/language/nb/modules.json +++ b/public/language/nb/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Legg til bruker", "chat.notification-settings": "Varslingsinnstillinger", "chat.default-notification-setting": "Standard varslingsinnstilling", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Romstandard", "chat.notification-setting-none": "Ingen varsler", "chat.notification-setting-at-mention-only": "Kun ved @nevning", diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json index cbe6bc5603..1e9e509560 100644 --- a/public/language/nl/modules.json +++ b/public/language/nl/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/nn-NO/modules.json b/public/language/nn-NO/modules.json index 061d785238..e2e97396c5 100644 --- a/public/language/nn-NO/modules.json +++ b/public/language/nn-NO/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Legg til brukar", "chat.notification-settings": "Varslingsinnstillingar", "chat.default-notification-setting": "Standard varslingsinnstillinger", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Standard for rommet", "chat.notification-setting-none": "Ingen varsel", "chat.notification-setting-at-mention-only": "Berre @nemning", diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json index 65f5d143de..09578d0f71 100644 --- a/public/language/pl/modules.json +++ b/public/language/pl/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Dodaj użytkownika", "chat.notification-settings": "Ustawienia powiadomień", "chat.default-notification-setting": "Domyślne ustawienia powiadomień", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Domyślne dla pokoju", "chat.notification-setting-none": "Brak powiadomień", "chat.notification-setting-at-mention-only": "Tylko zawołania z użyciem @", diff --git a/public/language/pt-BR/modules.json b/public/language/pt-BR/modules.json index 64a27be0d1..823391370f 100644 --- a/public/language/pt-BR/modules.json +++ b/public/language/pt-BR/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/pt-PT/modules.json b/public/language/pt-PT/modules.json index 5b3018d4fa..6e918bcc11 100644 --- a/public/language/pt-PT/modules.json +++ b/public/language/pt-PT/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/ro/modules.json b/public/language/ro/modules.json index e45d834abc..980870abbc 100644 --- a/public/language/ro/modules.json +++ b/public/language/ro/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json index d59776b42d..20e28da7b5 100644 --- a/public/language/ru/modules.json +++ b/public/language/ru/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Добавить пользователя", "chat.notification-settings": "Настройки уведомлений", "chat.default-notification-setting": "Настройка уведомлений по умолчанию", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Комната по умолчанию", "chat.notification-setting-none": "Нет уведомлений", "chat.notification-setting-at-mention-only": "только @упоминание", diff --git a/public/language/rw/modules.json b/public/language/rw/modules.json index 340d16cac4..8440752ccf 100644 --- a/public/language/rw/modules.json +++ b/public/language/rw/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/sc/modules.json b/public/language/sc/modules.json index 7145e11029..981e4a6bc4 100644 --- a/public/language/sc/modules.json +++ b/public/language/sc/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json index 51c3df2339..1335f8c9c5 100644 --- a/public/language/sk/modules.json +++ b/public/language/sk/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/sl/modules.json b/public/language/sl/modules.json index 5571ad4c35..9768079262 100644 --- a/public/language/sl/modules.json +++ b/public/language/sl/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/sq-AL/modules.json b/public/language/sq-AL/modules.json index e715ed133c..dfeb88b7f5 100644 --- a/public/language/sq-AL/modules.json +++ b/public/language/sq-AL/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/sr/modules.json b/public/language/sr/modules.json index f10f3e5d83..62ac69ee64 100644 --- a/public/language/sr/modules.json +++ b/public/language/sr/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Додај корисника", "chat.notification-settings": "Подешавања обавештења", "chat.default-notification-setting": "Подразумевано подешавање обавештења", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Подразумевана соба", "chat.notification-setting-none": "Без обавештења", "chat.notification-setting-at-mention-only": "@помињање само", diff --git a/public/language/sv/modules.json b/public/language/sv/modules.json index 14b07ee511..14d3713957 100644 --- a/public/language/sv/modules.json +++ b/public/language/sv/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/th/modules.json b/public/language/th/modules.json index c8198f1d99..f2a1a40e5e 100644 --- a/public/language/th/modules.json +++ b/public/language/th/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "เพิ่มผู้ใช้งาน", "chat.notification-settings": "การตั้งค่าการแจ้งเตือน", "chat.default-notification-setting": "ค่าเริ่มต้นการแจ้งเตือน", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "ค่าเริ่มต้นของห้อง", "chat.notification-setting-none": "ไม่มีการแจ้งเตือน", "chat.notification-setting-at-mention-only": "เฉพาะเมื่อ @ถูกพูดถึง", diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json index d85dbd2821..929c0cf962 100644 --- a/public/language/tr/modules.json +++ b/public/language/tr/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Kullanıcı Ekle", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/uk/modules.json b/public/language/uk/modules.json index 476a18f3f0..dd7a740981 100644 --- a/public/language/uk/modules.json +++ b/public/language/uk/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 2d5b2e4b92..37a40832a5 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Thêm Người", "chat.notification-settings": "Cài Đặt Thông Báo", "chat.default-notification-setting": "Cài Đặt Thông Báo Mặc Định", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Phòng Mặc Định", "chat.notification-setting-none": "Không thông báo", "chat.notification-setting-at-mention-only": "Chỉ khi @đề cập", diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json index 184fc4dec0..dddb4a8de1 100644 --- a/public/language/zh-CN/modules.json +++ b/public/language/zh-CN/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "添加用户", "chat.notification-settings": "通知设置", "chat.default-notification-setting": "默认通知设置", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "默认房间", "chat.notification-setting-none": "无通知", "chat.notification-setting-at-mention-only": "仅@提及", diff --git a/public/language/zh-TW/modules.json b/public/language/zh-TW/modules.json index a0d0f953d3..0262c03fb0 100644 --- a/public/language/zh-TW/modules.json +++ b/public/language/zh-TW/modules.json @@ -48,6 +48,7 @@ "chat.add-user": "Add User", "chat.notification-settings": "Notification Settings", "chat.default-notification-setting": "Default Notification Setting", + "chat.join-leave-messages": "Join/Leave Messages", "chat.notification-setting-room-default": "Room Default", "chat.notification-setting-none": "No notifications", "chat.notification-setting-at-mention-only": "@mention only", From 7acd63c2a0ab8f4535fbbd868d90869c49281273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Jun 2025 15:03:23 -0400 Subject: [PATCH 3163/4744] test: fix test, add joinLeaveMessages to newRoom --- src/messaging/rooms.js | 4 ++-- test/messaging.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 4bf4deed2c..934bf4e40d 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -88,7 +88,7 @@ module.exports = function (Messaging) { timestamp: now, notificationSetting: data.notificationSetting, messageCount: 0, - joinLeaveMessages: 0, + joinLeaveMessages: data.joinLeaveMessages || 0, }; if (data.hasOwnProperty('roomName') && data.roomName) { @@ -127,7 +127,7 @@ module.exports = function (Messaging) { 'chat:rooms:public:order:all', ]); - if (!isPublic) { + if (!isPublic && parseInt(room.joinLeaveMessages, 10) === 1) { // chat owner should also get the user-join system message await Messaging.addSystemMessage('user-join', uid, roomId); } diff --git a/test/messaging.js b/test/messaging.js index 4429fd6cd7..fbde5d72f6 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -184,6 +184,7 @@ describe('Messaging Library', () => { await User.setSetting(mocks.users.baz.uid, 'disableIncomingMessages', '0'); const { body } = await callv3API('post', `/chats`, { uids: [mocks.users.baz.uid], + joinLeaveMessages: 1, }, 'foo'); await User.setSetting(mocks.users.baz.uid, 'disableIncomingMessages', '1'); @@ -803,7 +804,7 @@ describe('Messaging Library', () => { assert.equal(response.statusCode, 200); assert(Array.isArray(body.rooms)); - assert.equal(body.rooms.length, 3); + assert.equal(body.rooms.length, 2); assert.equal(body.title, '[[pages:chats]]'); }); From 22d1972f83f87adc72baf3cb4b134fdb68ddf66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Jun 2025 15:13:16 -0400 Subject: [PATCH 3164/4744] test: one more test fix --- public/openapi/write/chats/roomId/messages/mid.yaml | 6 +++--- public/openapi/write/chats/roomId/messages/mid/ip.yaml | 2 +- test/api.js | 9 +++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/public/openapi/write/chats/roomId/messages/mid.yaml b/public/openapi/write/chats/roomId/messages/mid.yaml index dfa06e7811..c899627802 100644 --- a/public/openapi/write/chats/roomId/messages/mid.yaml +++ b/public/openapi/write/chats/roomId/messages/mid.yaml @@ -49,7 +49,7 @@ put: type: number required: true description: a valid message id - example: 3 + example: 2 requestBody: required: true content: @@ -92,7 +92,7 @@ delete: type: number required: true description: a valid message id - example: 3 + example: 2 responses: '200': description: Message successfully deleted @@ -125,7 +125,7 @@ post: type: number required: true description: a valid message id - example: 3 + example: 2 responses: '200': description: message successfully restored diff --git a/public/openapi/write/chats/roomId/messages/mid/ip.yaml b/public/openapi/write/chats/roomId/messages/mid/ip.yaml index 1730542213..2c2af8fb1b 100644 --- a/public/openapi/write/chats/roomId/messages/mid/ip.yaml +++ b/public/openapi/write/chats/roomId/messages/mid/ip.yaml @@ -17,7 +17,7 @@ get: type: string required: true description: a valid chat message id - example: 3 + example: 2 responses: '200': description: Chat message ip address retrieved diff --git a/test/api.js b/test/api.js index f7616904c8..fbb36b24b8 100644 --- a/test/api.js +++ b/test/api.js @@ -282,8 +282,13 @@ describe('API', async () => { await flags.appendNote(flagId, 1, 'test note', 1626446956652); await flags.create('post', 2, unprivUid, 'sample reasons', Date.now()); // for testing flag notes (since flag 1 deleted) - // Create a new chat room - await messaging.newRoom(adminUid, { uids: [unprivUid] }); + // Create a new chat room & send a message + const roomId = await messaging.newRoom(adminUid, { uids: [unprivUid] }); + await messaging.sendMessage({ + roomId, + uid: adminUid, + content: 'this is a chat message', + }); // Create an empty file to test DELETE /files and thumb deletion fs.closeSync(fs.openSync(path.resolve(nconf.get('upload_path'), 'files/test.txt'), 'w')); From 6e5083c263318d4437d94c0fd2f001c26c3a3689 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:20:28 -0400 Subject: [PATCH 3165/4744] fix(deps): update dependency pg-cursor to v2.15.3 (#13516) 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 07981f8855..a234787dff 100644 --- a/install/package.json +++ b/install/package.json @@ -117,7 +117,7 @@ "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", "pg": "8.16.2", - "pg-cursor": "2.15.2", + "pg-cursor": "2.15.3", "postcss": "8.5.6", "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", From c056bf5618004db28c661860074b9785c0ef0ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Jun 2025 15:22:39 -0400 Subject: [PATCH 3166/4744] chore: up eslint --- install/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/package.json b/install/package.json index 07981f8855..87b9719041 100644 --- a/install/package.json +++ b/install/package.json @@ -162,9 +162,9 @@ "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", "@eslint/js": "9.29.0", - "@stylistic/eslint-plugin": "4.4.1", - "eslint-config-nodebb": "1.1.7", - "eslint-plugin-import": "2.31.0", + "@stylistic/eslint-plugin": "5.0.0", + "eslint-config-nodebb": "1.1.8", + "eslint-plugin-import": "2.32.0", "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", From 655a3bd3a305bf87175852341152c791dd472d8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:41:45 -0400 Subject: [PATCH 3167/4744] fix(deps): update dependency workerpool to v9.3.3 (#13518) 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 610e5a2a2f..7460d55b53 100644 --- a/install/package.json +++ b/install/package.json @@ -150,7 +150,7 @@ "webpack": "5.99.9", "webpack-merge": "6.0.1", "winston": "3.17.0", - "workerpool": "9.3.2", + "workerpool": "9.3.3", "xml": "1.0.1", "xregexp": "5.1.2", "yargs": "17.7.2", From fd82919e5ab1c7acedbb3f9a71776e0fd4919d41 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:41:54 -0400 Subject: [PATCH 3168/4744] fix(deps): update dependency pg to v8.16.3 (#13517) 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 7460d55b53..89da143141 100644 --- a/install/package.json +++ b/install/package.json @@ -116,7 +116,7 @@ "passport": "0.7.0", "passport-http-bearer": "1.0.1", "passport-local": "1.0.0", - "pg": "8.16.2", + "pg": "8.16.3", "pg-cursor": "2.15.3", "postcss": "8.5.6", "postcss-clean": "1.2.0", From 85e2d7d338009fa3fea8ba16e795bedfb8dba899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Jun 2025 16:08:51 -0400 Subject: [PATCH 3169/4744] test: psql fix --- test/activitypub/feps.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/test/activitypub/feps.js b/test/activitypub/feps.js index 65520f0f4e..adcf2bb2c6 100644 --- a/test/activitypub/feps.js +++ b/test/activitypub/feps.js @@ -269,20 +269,18 @@ describe('FEPs', () => { }); it('should be called when a post is moved to another topic', async () => { - const [{ topicData: topic1 }, { topicData: topic2 }] = await Promise.all([ - topics.post({ - uid, - cid, - title: utils.generateUUID(), - content: utils.generateUUID(), - }), - topics.post({ - uid, - cid, - title: utils.generateUUID(), - content: utils.generateUUID(), - }), - ]); + const topic1 = await topics.post({ + uid, + cid, + title: utils.generateUUID(), + content: utils.generateUUID(), + }); + const topic2 = await topics.post({ + uid, + cid, + title: utils.generateUUID(), + content: utils.generateUUID(), + }); assert(topic1 && topic2); From 22005b9ccf83f37498d6bf02afdfb1b7f60f6ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Jun 2025 16:17:06 -0400 Subject: [PATCH 3170/4744] assign correct data --- test/activitypub/feps.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/activitypub/feps.js b/test/activitypub/feps.js index adcf2bb2c6..705df788e1 100644 --- a/test/activitypub/feps.js +++ b/test/activitypub/feps.js @@ -269,13 +269,13 @@ describe('FEPs', () => { }); it('should be called when a post is moved to another topic', async () => { - const topic1 = await topics.post({ + const { topicData: topic1 } = await topics.post({ uid, cid, title: utils.generateUUID(), content: utils.generateUUID(), }); - const topic2 = await topics.post({ + const { topicData: topic2 } = await topics.post({ uid, cid, title: utils.generateUUID(), From 15ea123382fd529752a7b2c8e2180edbd6e21b78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 22:04:00 -0400 Subject: [PATCH 3171/4744] chore(deps): update dependency @eslint/js to v9.30.0 (#13519) 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 89da143141..d3cf811015 100644 --- a/install/package.json +++ b/install/package.json @@ -161,7 +161,7 @@ "@commitlint/cli": "19.8.1", "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", - "@eslint/js": "9.29.0", + "@eslint/js": "9.30.0", "@stylistic/eslint-plugin": "5.0.0", "eslint-config-nodebb": "1.1.8", "eslint-plugin-import": "2.32.0", From 48071ebbb576cf4368af2f716de240e21fc53fd8 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 29 Jun 2025 09:19:19 +0000 Subject: [PATCH 3172/4744] Latest translations and fallbacks --- public/language/bg/modules.json | 2 +- public/language/it/modules.json | 2 +- public/language/pl/modules.json | 2 +- public/language/zh-CN/modules.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index 864c179ad7..170141d17d 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -48,7 +48,7 @@ "chat.add-user": "Добавяне на потребител", "chat.notification-settings": "Настройки за известията", "chat.default-notification-setting": "Стандартни настройки за известията", - "chat.join-leave-messages": "Join/Leave Messages", + "chat.join-leave-messages": "Съобщения за присъединяване/напускане", "chat.notification-setting-room-default": "По подразбиране за стаята", "chat.notification-setting-none": "Без известия", "chat.notification-setting-at-mention-only": "Само @споменавания", diff --git a/public/language/it/modules.json b/public/language/it/modules.json index 8cba8bc989..c5b60f5c4b 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -48,7 +48,7 @@ "chat.add-user": "Aggiungi utente", "chat.notification-settings": "Impostazioni di notifica", "chat.default-notification-setting": "Impostazioni di notifica predefinite", - "chat.join-leave-messages": "Join/Leave Messages", + "chat.join-leave-messages": "Messaggi di iscrizione/uscita", "chat.notification-setting-room-default": "Stanza predefinita", "chat.notification-setting-none": "Nessuna notifica", "chat.notification-setting-at-mention-only": "@solo menzione", diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json index 09578d0f71..3e52bcddc8 100644 --- a/public/language/pl/modules.json +++ b/public/language/pl/modules.json @@ -48,7 +48,7 @@ "chat.add-user": "Dodaj użytkownika", "chat.notification-settings": "Ustawienia powiadomień", "chat.default-notification-setting": "Domyślne ustawienia powiadomień", - "chat.join-leave-messages": "Join/Leave Messages", + "chat.join-leave-messages": "Dołącz/Odłącz wiadomości", "chat.notification-setting-room-default": "Domyślne dla pokoju", "chat.notification-setting-none": "Brak powiadomień", "chat.notification-setting-at-mention-only": "Tylko zawołania z użyciem @", diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json index dddb4a8de1..d9ae48b598 100644 --- a/public/language/zh-CN/modules.json +++ b/public/language/zh-CN/modules.json @@ -48,7 +48,7 @@ "chat.add-user": "添加用户", "chat.notification-settings": "通知设置", "chat.default-notification-setting": "默认通知设置", - "chat.join-leave-messages": "Join/Leave Messages", + "chat.join-leave-messages": "加入/退出 消息", "chat.notification-setting-room-default": "默认房间", "chat.notification-setting-none": "无通知", "chat.notification-setting-at-mention-only": "仅@提及", From f1fbea7b28fbd029602ced0add72d55a38a15049 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:25:03 -0400 Subject: [PATCH 3173/4744] fix(deps): update dependency nodemailer to v7.0.4 (#13522) 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 d3cf811015..847cb84807 100644 --- a/install/package.json +++ b/install/package.json @@ -111,7 +111,7 @@ "nodebb-theme-peace": "2.2.44", "nodebb-theme-persona": "14.1.12", "nodebb-widget-essentials": "7.0.38", - "nodemailer": "7.0.3", + "nodemailer": "7.0.4", "nprogress": "0.2.0", "passport": "0.7.0", "passport-http-bearer": "1.0.1", From 18d6e5e1d64bb921415a331530a2540221f6a50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 30 Jun 2025 20:33:16 -0400 Subject: [PATCH 3174/4744] chore: up eslint-plugin --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 847cb84807..afc0a88162 100644 --- a/install/package.json +++ b/install/package.json @@ -162,8 +162,8 @@ "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", "@eslint/js": "9.30.0", - "@stylistic/eslint-plugin": "5.0.0", - "eslint-config-nodebb": "1.1.8", + "@stylistic/eslint-plugin": "5.1.0", + "eslint-config-nodebb": "1.1.9", "eslint-plugin-import": "2.32.0", "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", From 37f0fa961ea4b39ebbe4b388f1a02d672559da39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 1 Jul 2025 10:01:10 -0400 Subject: [PATCH 3175/4744] Refactor hook call for filterSortedTids --- src/topics/sorted.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 07a6215218..2112fe3ad1 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -282,7 +282,10 @@ module.exports = function (Topics) { (!tags.length || tags.every(tag => t.tags.find(topicTag => topicTag.value === tag))) )).map(t => t.tid); - const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { tids: tids, params: params }); + const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { + tids, + params, + }); return result.tids; } From aba2ddad949ac52879fbafa8592b18ab1d5e034d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:18:56 -0400 Subject: [PATCH 3176/4744] fix(deps): update dependency ace-builds to v1.43.1 (#13525) 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 afc0a88162..4e4098989f 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.0", + "ace-builds": "1.43.1", "archiver": "7.0.1", "async": "3.2.6", "autoprefixer": "10.4.21", From 6d7df13fdb899e5d129a73a1c2bbfd73298a5b70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:19:07 -0400 Subject: [PATCH 3177/4744] chore(deps): update dependency @eslint/js to v9.30.1 (#13524) 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 4e4098989f..5ae8666505 100644 --- a/install/package.json +++ b/install/package.json @@ -161,7 +161,7 @@ "@commitlint/cli": "19.8.1", "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", - "@eslint/js": "9.30.0", + "@eslint/js": "9.30.1", "@stylistic/eslint-plugin": "5.1.0", "eslint-config-nodebb": "1.1.9", "eslint-plugin-import": "2.32.0", From ceae2aa1a81342ecb470a8a0bbbaa5f0ae541965 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:19:16 -0400 Subject: [PATCH 3178/4744] fix(deps): update dependency nodebb-plugin-web-push to v0.7.5 (#13523) 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 5ae8666505..acd710b639 100644 --- a/install/package.json +++ b/install/package.json @@ -104,7 +104,7 @@ "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.4", + "nodebb-plugin-web-push": "0.7.5", "nodebb-rewards-essentials": "1.0.2", "nodebb-theme-harmony": "2.1.16", "nodebb-theme-lavender": "7.1.19", From 5a5ca8a5fb3d80e536f19cc52efa8145c5ae1247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 2 Jul 2025 17:38:35 -0400 Subject: [PATCH 3179/4744] fix: closes #13526, dont send multiple emails when user is invited --- src/user/create.js | 12 +++++++----- src/user/invite.js | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/user/create.js b/src/user/create.js index 1b18281722..ee4c3f27e7 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -109,11 +109,13 @@ module.exports = function (User) { } if (data.email && userData.uid > 1) { - await User.email.sendValidationEmail(userData.uid, { - email: data.email, - template: 'welcome', - subject: `[[email:welcome-to, ${meta.config.title || meta.config.browserTitle || 'NodeBB'}]]`, - }).catch(err => winston.error(`[user.create] Validation email failed to send\n[emailer.send] ${err.stack}`)); + if (!data.token || !await User.isInviteTokenValid(data.token, data.email)) { + await User.email.sendValidationEmail(userData.uid, { + email: data.email, + template: 'welcome', + subject: `[[email:welcome-to, ${meta.config.title || meta.config.browserTitle || 'NodeBB'}]]`, + }).catch(err => winston.error(`[user.create] Validation email failed to send\n[emailer.send] ${err.stack}`)); + } } if (userNameChanged) { await User.notifications.sendNameChangeNotification(userData.uid, userData.username); diff --git a/src/user/invite.js b/src/user/invite.js index a9a5368bb1..344e09fd6b 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -72,6 +72,12 @@ module.exports = function (User) { } }; + User.isInviteTokenValid = async function (token, enteredEmail) { + if (!token) return false; + const email = await db.getObjectField(`invitation:token:${token}`, 'email'); + return email && email === enteredEmail; + }; + User.confirmIfInviteEmailIsUsed = async function (token, enteredEmail, uid) { if (!enteredEmail) { return; From 80fabdcb33bc6ae1be7624f5f01ea45f90e5b5d3 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 3 Jul 2025 09:20:10 +0000 Subject: [PATCH 3180/4744] Latest translations and fallbacks --- public/language/vi/modules.json | 2 +- public/language/vi/topic.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 37a40832a5..c4d8c24c78 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -48,7 +48,7 @@ "chat.add-user": "Thêm Người", "chat.notification-settings": "Cài Đặt Thông Báo", "chat.default-notification-setting": "Cài Đặt Thông Báo Mặc Định", - "chat.join-leave-messages": "Join/Leave Messages", + "chat.join-leave-messages": "Tham gia/Rời đi Tin Nhắn", "chat.notification-setting-room-default": "Phòng Mặc Định", "chat.notification-setting-none": "Không thông báo", "chat.notification-setting-at-mention-only": "Chỉ khi @đề cập", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 59db71b7a6..12f0394c98 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -214,7 +214,7 @@ "last-post": "Bài viết cuối cùng", "go-to-my-next-post": "Đi tới bài kế tiếp của tôi", "no-more-next-post": "Bạn không có bài viết nào khác trong chủ đề này", - "open-composer": "Mỏ composer", + "open-composer": "Mở composer", "post-quick-reply": "Trả lời nhanh", "navigator.index": "Bài đăng %1 trên %2", "navigator.unread": "%1 chưa đọc", From 991f518e2f615d654484cbe2444ef08ad378dbd1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:47:50 -0400 Subject: [PATCH 3181/4744] fix(deps): update dependency nodebb-theme-peace to v2.2.45 (#13529) 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 acd710b639..8bfd634780 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-rewards-essentials": "1.0.2", "nodebb-theme-harmony": "2.1.16", "nodebb-theme-lavender": "7.1.19", - "nodebb-theme-peace": "2.2.44", + "nodebb-theme-peace": "2.2.45", "nodebb-theme-persona": "14.1.12", "nodebb-widget-essentials": "7.0.38", "nodemailer": "7.0.4", From bfcc36f7cbcdb297e6151f67a0b0fefdae6d3e83 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 6 Jul 2025 09:19:12 +0000 Subject: [PATCH 3182/4744] Latest translations and fallbacks --- public/language/vi/admin/manage/users.json | 2 +- public/language/vi/error.json | 10 +++++----- public/language/vi/global.json | 10 +++++----- public/language/vi/groups.json | 2 +- public/language/vi/modules.json | 4 ++-- public/language/vi/pages.json | 10 +++++----- public/language/vi/search.json | 2 +- public/language/vi/topic.json | 8 ++++---- public/language/vi/user.json | 4 ++-- public/language/vi/users.json | 2 +- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/public/language/vi/admin/manage/users.json b/public/language/vi/admin/manage/users.json index 8a0a2b0806..d3136effd4 100644 --- a/public/language/vi/admin/manage/users.json +++ b/public/language/vi/admin/manage/users.json @@ -119,7 +119,7 @@ "alerts.create-success": "Đã tạo người dùng!", "alerts.prompt-email": "Thư điện tử:", - "alerts.email-sent-to": "Email mời đã được gửi đến %1", + "alerts.email-sent-to": "Một email mời đã được gửi đến %1", "alerts.x-users-found": "Tìm được %1 người, (%2 giây)", "alerts.select-a-single-user-to-change-email": "Chọn một người dùng để thay đổi email", "export": "Xuất", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index 467b78742c..09cb43c110 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -84,8 +84,8 @@ "post-delete-duration-expired-hours-minutes": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giờ(s) 2 phút(s)", "post-delete-duration-expired-days": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s)", "post-delete-duration-expired-days-hours": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s) %2 giờ(s)", - "cant-delete-topic-has-reply": "Bạn không thể xóa chủ đề vì đã có 1 bình luận", - "cant-delete-topic-has-replies": "Bạn không thể xóa chủ đề này vì đã có %1 bình luận", + "cant-delete-topic-has-reply": "Bạn không thể xóa chủ đề của bạn sau khi nó có câu trả lời", + "cant-delete-topic-has-replies": "Bạn không thể xóa chủ đề của bạn sau khi nó có %1 trả lời", "content-too-short": "Vui lòng nhập một bài viết dài hơn. Bài viết phải chứa ít nhất %1 ký tự.", "content-too-long": "Hãy nhập một bài đăng ngắn hơn. Bài đăng không thể dài hơn %1 ký tự.", "title-too-short": "Hãy nhập tiêu đề dài hơn. Tiêu đề nên có ít nhất %1 ký tự.", @@ -127,7 +127,7 @@ "already-deleting": "Đã sẵn sàng xóa", "invalid-image": "Hình ảnh không hợp lệ", "invalid-image-type": "Định dạng ảnh không hợp lệ. Các loại được phép là: %1", - "invalid-image-extension": "Định dạng ảnh không hợp lệ", + "invalid-image-extension": "Phần mở rộng ảnh không hợp lệ", "invalid-file-type": "Loại tệp không hợp lệ. Loại cho phép là: %1", "invalid-image-dimensions": "Độ phân giải của ảnh quá lớn", "group-name-too-short": "Tên nhóm quá ngắn", @@ -137,7 +137,7 @@ "group-already-member": "Đã là thành viên của nhóm.", "group-not-member": "Không phải thành viên nhóm này.", "group-needs-owner": "Yêu cầu phải có ít nhất một chủ nhóm", - "group-already-invited": "Thành viên này đã được mời", + "group-already-invited": "Người dùng này đã được mời", "group-already-requested": "Yêu cầu tham gia thành viên của bạn đã được gửi.", "group-join-disabled": "Bạn không thể tham gia nhóm này vào lúc này", "group-leave-disabled": "Bạn không thể rời khỏi nhóm này vào lúc này", @@ -159,7 +159,7 @@ "chat-deny-list-user-already-added": "Người dùng này đã có trong danh sách từ chối của bạn", "chat-user-blocked": "Bạn đã bị chặn bởi người dùng này.", "chat-disabled": "Hệ thống trò chuyện bị tắt", - "too-many-messages": "Bạn đã gửi quá nhiều tin nhắn, vui lòng đợi trong giây lát.", + "too-many-messages": "Bạn đã gửi quá nhiều tin nhắn, vui lòng đợi một lúc.", "invalid-chat-message": "Tin nhắn trò chuyện không hợp lệ", "chat-message-too-long": "Tin nhắn trò chuyện không được dài hơn %1 ký tự.", "cant-edit-chat-message": "Bạn không được phép sửa tin nhắn này", diff --git a/public/language/vi/global.json b/public/language/vi/global.json index 3ac87d5132..5a066d9b0e 100644 --- a/public/language/vi/global.json +++ b/public/language/vi/global.json @@ -134,11 +134,11 @@ "upload": "Tải lên", "uploads": "Tải lên", "allowed-file-types": "Loại cho phép là %1", - "unsaved-changes": "Có một vài thay đổi chưa được lưu. Bạn muốn rời đi ngay?", - "reconnecting-message": "Có vẻ như bạn đã mất kết nối tới %1, vui lòng đợi một lúc để chúng tôi thử kết nối lại.", - "play": "Chơi", + "unsaved-changes": "Bạn có những thay đổi chưa lưu. Bạn có chắc muốn điều hướng đi?", + "reconnecting-message": "Có vẻ như bạn đã mất kết nối tới %1, hãy đợi trong khi chúng tôi cố gắng kết nối lại.", + "play": "Phát", "cookies.message": "Trang web này sử dụng cookie để đảm bảo bạn có được trải nghiệm tốt.", - "cookies.accept": "Đã rõ!", + "cookies.accept": "Hiểu rồi!", "cookies.learn-more": "Tìm Hiểu Thêm", "edited": "Đã Sửa", "disabled": "Đã tắt", @@ -146,7 +146,7 @@ "selected": "Đã chọn", "copied": "Đã sao chép", "user-search-prompt": "Nhập để tìm kiếm thành viên", - "hidden": "Ẩn", + "hidden": "Đã ẩn", "sort": "Xếp", "actions": "Hành Động", "rss-feed": "Nguồn RSS", diff --git a/public/language/vi/groups.json b/public/language/vi/groups.json index df781930e5..d4c6e34b97 100644 --- a/public/language/vi/groups.json +++ b/public/language/vi/groups.json @@ -29,7 +29,7 @@ "details.disableJoinRequests": "Tắt yêu cầu tham gia", "details.disableLeave": "Không cho phép người dùng rời khỏi nhóm", "details.grant": "Cấp/Huỷ bỏ quyền sở hữu", - "details.kick": "Đá ra", + "details.kick": "Loại ra", "details.kick-confirm": "Bạn có chắc chắn muốn xoá thành viên này khỏi nhóm?", "details.add-member": "Thêm Thành Viên", "details.owner-options": "Quản Trị Nhóm", diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index c4d8c24c78..c238cc8d51 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -19,7 +19,7 @@ "chat.see-all": "Tất cả trò chuyện", "chat.mark-all-read": "Đánh dấu tất cả đã đọc", "chat.no-messages": "Hãy chọn người nhận để xem lịch sử tin nhắn trò chuyện", - "chat.no-users-in-room": "Không có người nào trong phòng này.", + "chat.no-users-in-room": "Không có ai trong phòng này", "chat.recent-chats": "Trò Chuyện Gần Đây", "chat.contacts": "Liên hệ", "chat.message-history": "Lịch Sử Tin Nhắn", @@ -101,7 +101,7 @@ "composer.formatting.code": "Mã", "composer.formatting.link": "Liên kết", "composer.formatting.picture": "Liên Kết Ảnh", - "composer.upload-picture": "Tải ảnh lên", + "composer.upload-picture": "Tải Lên Ảnh", "composer.upload-file": "Tải Lên Tệp", "composer.zen-mode": "Chế Độ Zen", "composer.select-category": "Chọn chuyên mục", diff --git a/public/language/vi/pages.json b/public/language/vi/pages.json index 851486de52..44579ce5f7 100644 --- a/public/language/vi/pages.json +++ b/public/language/vi/pages.json @@ -42,11 +42,11 @@ "account/edit/username": "Chỉnh sửa tên đăng nhập của \"%1\"", "account/edit/email": "Chỉnh sửa email của \"%1\"", "account/info": "Thông Tin Tài Khoản", - "account/following": "Thành viên %1 đang theo dõi", - "account/followers": "Thành viên đang theo dõi %1", - "account/posts": "Bài viết được đăng bởi %1", + "account/following": "Người %1 theo dõi", + "account/followers": "Những người theo dõi %1", + "account/posts": "Bài viết làm bởi %1", "account/latest-posts": "Bài viết mới nhất do %1", - "account/topics": "Chủ đề được tạo bởi %1", + "account/topics": "Chủ đề tạo bởi %1", "account/groups": "Nhóm của %1", "account/watched-categories": "Danh Mục Đã Xem Của %1", "account/watched-tags": "%1's Thẻ Đã Xem", @@ -64,7 +64,7 @@ "account/uploads": "Tải lên bởi %1", "account/sessions": "Phiên Đăng Nhập", "account/shares": "Chủ đề được chia sẻ bởi %1", - "confirm": "Đã xác nhận email", + "confirm": "Đã Xác Nhận Email", "maintenance.text": "%1 hiện đang bảo trì.
    Vui lòng quay lại vào lúc khác.", "maintenance.messageIntro": "Ngoài ra, quản trị viên đã để lại thông báo này:", "throttled.text": "%1 hiện không khả dụng do quá tải. Vui lòng quay lại vào lúc khác." diff --git a/public/language/vi/search.json b/public/language/vi/search.json index 56361bb4d5..63ba203e7d 100644 --- a/public/language/vi/search.json +++ b/public/language/vi/search.json @@ -33,7 +33,7 @@ "replies": "Trả lời", "replies-atleast-count": "Trả lời: Ít nhất %1", "replies-atmost-count": "Trả lời: Nhiều nhất là %1", - "at-least": "Tối thiểu", + "at-least": "Ít nhất", "at-most": "Nhiều nhất", "relevance": "Mức độ liên quan", "time": "Thời gian", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 12f0394c98..a8960d63db 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -28,8 +28,8 @@ "move": "Di chuyển", "change-owner": "Đổi Chủ Sở Hữu", "manage-editors": "Quản Lý Biên Tập Viên", - "fork": "Tạo bản sao", - "link": "Đường dẫn", + "fork": "Chia nhánh", + "link": "Liên kết", "share": "Chia sẻ", "tools": "Công cụ", "locked": "Đã Khóa", @@ -132,7 +132,7 @@ "pin-modal-help": "Bạn có thể đặt ngày hết hạn cho các chủ đề được ghim tại đây. Ngoài ra, bạn có thể để trống để giữ chủ đề được ghim cho đến khi chủ đề được bỏ ghim theo cách thủ công.", "load-categories": "Đang Tải Chuyên Mục", "confirm-move": "Di chuyển", - "confirm-fork": "Tạo bảo sao", + "confirm-fork": "Chia nhánh", "bookmark": "Dấu trang", "bookmarks": "Dấu trang", "bookmarks.has-no-bookmarks": "Bạn chưa đánh dấu bất kỳ bài viết nào.", @@ -194,7 +194,7 @@ "most-posts": "Nhiều Bài Đăng Nhất", "most-views": "Xem Nhiều Nhất", "stale.title": "Tạo chủ đề mới thay thế?", - "stale.warning": "Chủ đề bạn đang trả lời đã khá cũ. Bạn có muốn tạo chủ đề mới, và liên kết với chủ đề hiện tại trong bài viết trả lời của bạn?", + "stale.warning": "Chủ đề bạn đang trả lời đã khá cũ. Thay vào đó, bạn có muốn tạo một chủ đề mới và tham khảo phần này trong câu trả lời của bạn không?", "stale.create": "Tạo chủ đề mới", "stale.reply-anyway": "Trả lời chủ đề này bằng mọi cách", "link-back": "Trả lời: [%1](%2)", diff --git a/public/language/vi/user.json b/public/language/vi/user.json index 9584fe0e5d..198aeb678e 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -8,7 +8,7 @@ "deleted": "Đã xoá", "username": "Tên Đăng Nhập", "joindate": "Ngày Tham Gia", - "postcount": "Số bài viết", + "postcount": "Số Bài Đăng", "email": "Thư điện tử", "confirm-email": "Xác Nhận Email", "account-info": "Thông Tin Tài Khoản", @@ -188,7 +188,7 @@ "info.ban-expired": "Hết hạn cấm", "info.banned-permanently": "Bị cấm vĩnh viễn", "info.banned-reason-label": "Lý do", - "info.banned-no-reason": "Không có lí do.", + "info.banned-no-reason": "Không có lý do nào được đưa ra.", "info.mute-history": "Lịch Sử Im Lặng Gần Đây", "info.no-mute-history": "Người dùng này chưa bao giờ bị im lặng", "info.muted-until": "Bị im lặng đến %1", diff --git a/public/language/vi/users.json b/public/language/vi/users.json index bf1c11b467..b3f9fda3a5 100644 --- a/public/language/vi/users.json +++ b/public/language/vi/users.json @@ -15,7 +15,7 @@ "invite": "Mời", "prompt-email": "Thư điện tử:", "groups-to-join": "Nhóm được tham gia khi lời mời được chấp nhận:", - "invitation-email-sent": "Email mời đã được gửi tới %1", + "invitation-email-sent": "Một email mời đã được gửi đến %1", "user-list": "Danh Sách Người Dùng", "recent-topics": "Chủ Đề Gần Đây", "popular-topics": "Chủ Đề Phổ Biến", From 24e7cf4a0078dd91d637f449501df5e1fd1f3372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Mon, 7 Jul 2025 10:22:24 -0400 Subject: [PATCH 3183/4744] refactor: move post uploads to post hash (#13533) * refactor: move post uploads to post hash * test: add uploads to api definition * refactor: move thumbs to topic hash * chore: up composer * refactor: dont use old zset --- install/package.json | 2 +- .../components/schemas/PostObject.yaml | 2 + public/openapi/write/topics/tid/thumbs.yaml | 49 ---- .../write/topics/tid/thumbs/order.yaml | 4 +- public/src/client/topic/events.js | 6 + public/src/modules/helpers.common.js | 7 + public/src/modules/topicThumbs.js | 78 ++--- src/activitypub/mocks.js | 8 +- src/api/posts.js | 4 +- src/api/topics.js | 22 +- src/controllers/write/topics.js | 15 +- src/posts/data.js | 8 + src/posts/edit.js | 12 +- src/posts/uploads.js | 42 ++- src/routes/api.js | 1 + src/routes/write/topics.js | 2 +- src/topics/create.js | 6 + src/topics/data.js | 8 + src/topics/thumbs.js | 275 +++++++++--------- src/upgrades/4.5.0/post-uploads-to-hash.js | 39 +++ src/upgrades/4.5.0/topic-thumbs-to-hash.js | 39 +++ src/views/modals/topic-thumbs.tpl | 6 +- test/posts/uploads.js | 22 +- test/topics/thumbs.js | 165 +++-------- 24 files changed, 393 insertions(+), 429 deletions(-) create mode 100644 src/upgrades/4.5.0/post-uploads-to-hash.js create mode 100644 src/upgrades/4.5.0/topic-thumbs-to-hash.js diff --git a/install/package.json b/install/package.json index 8bfd634780..5ffca3c819 100644 --- a/install/package.json +++ b/install/package.json @@ -97,7 +97,7 @@ "multer": "2.0.1", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", - "nodebb-plugin-composer-default": "10.2.51", + "nodebb-plugin-composer-default": "10.3.0", "nodebb-plugin-dbsearch": "6.3.0", "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index bcb2f79e53..1904cded51 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -300,6 +300,8 @@ PostDataObject: type: boolean attachments: type: array + uploads: + type: array replies: type: object properties: diff --git a/public/openapi/write/topics/tid/thumbs.yaml b/public/openapi/write/topics/tid/thumbs.yaml index 3817d2a5a3..5d28264266 100644 --- a/public/openapi/write/topics/tid/thumbs.yaml +++ b/public/openapi/write/topics/tid/thumbs.yaml @@ -83,55 +83,6 @@ post: type: string name: type: string -put: - tags: - - topics - summary: migrate topic thumbnail - description: This operation migrates a thumbnails from a topic or draft, to another tid or draft. - parameters: - - in: path - name: tid - schema: - type: string - required: true - description: a valid topic id or draft uuid - example: 1 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - tid: - type: string - description: a valid topic id or draft uuid - example: '1' - responses: - '200': - description: Topic thumbnails migrated - content: - application/json: - schema: - type: object - properties: - status: - $ref: ../../../components/schemas/Status.yaml#/Status - response: - type: array - description: A list of the topic thumbnails in the destination topic - items: - type: object - properties: - id: - type: string - name: - type: string - path: - type: string - url: - type: string - description: Path to a topic thumbnail delete: tags: - topics diff --git a/public/openapi/write/topics/tid/thumbs/order.yaml b/public/openapi/write/topics/tid/thumbs/order.yaml index a0f1602bc8..a8acefbf0a 100644 --- a/public/openapi/write/topics/tid/thumbs/order.yaml +++ b/public/openapi/write/topics/tid/thumbs/order.yaml @@ -2,14 +2,14 @@ put: tags: - topics summary: reorder topic thumbnail - description: This operation sets the order for a topic thumbnail. It can handle either topics (if a valid `tid` is passed in), or drafts. A 404 is returned if the topic or draft does not actually contain that thumbnail path. Paths passed in should **not** contain the path to the uploads folder (`config.upload_url` on client side) + description: This operation sets the order for a topic thumbnail. A 404 is returned if the topic does not contain path. Paths passed in should **not** contain the path to the uploads folder (`config.upload_url` on client side) parameters: - in: path name: tid schema: type: string required: true - description: a valid topic id or draft uuid + description: a valid topic id example: 2 requestBody: required: true diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 4a8a36a768..c52f8beefe 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -176,6 +176,12 @@ define('forum/topic/events', [ }); } + if (data.topic.thumbsupdated) { + require(['topicThumbs'], function (topicThumbs) { + topicThumbs.updateTopicThumbs(data.topic.tid); + }); + } + postTools.removeMenu(components.get('post', 'pid', data.post.pid)); } diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 183c1bbb70..896d0b485b 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -34,6 +34,7 @@ module.exports = function (utils, Benchpress, relative_path) { humanReadableNumber, formattedNumber, txEscape, + uploadBasename, generatePlaceholderWave, register, __escape: identity, @@ -379,6 +380,12 @@ module.exports = function (utils, Benchpress, relative_path) { return String(text).replace(/%/g, '%').replace(/,/g, ','); } + function uploadBasename(str, sep = '/') { + const hasTimestampPrefix = /^\d+-/; + const name = str.substr(str.lastIndexOf(sep) + 1); + return hasTimestampPrefix.test(name) ? name.slice(14) : name; + } + function generatePlaceholderWave(items) { const html = items.map((i) => { if (i === 'divider') { diff --git a/public/src/modules/topicThumbs.js b/public/src/modules/topicThumbs.js index 70c13218d3..340d856ded 100644 --- a/public/src/modules/topicThumbs.js +++ b/public/src/modules/topicThumbs.js @@ -7,23 +7,27 @@ define('topicThumbs', [ Thumbs.get = id => api.get(`/topics/${id}/thumbs`, { thumbsOnly: 1 }); - Thumbs.getByPid = pid => api.get(`/posts/${encodeURIComponent(pid)}`, {}).then(post => Thumbs.get(post.tid)); - Thumbs.delete = (id, path) => api.del(`/topics/${id}/thumbs`, { path: path, }); + Thumbs.updateTopicThumbs = async (tid) => { + const thumbs = await Thumbs.get(tid); + const html = await app.parseAndTranslate('partials/topic/thumbs', { thumbs }); + $('[component="topic/thumb/list"]').html(html); + }; + Thumbs.deleteAll = (id) => { Thumbs.get(id).then((thumbs) => { Promise.all(thumbs.map(thumb => Thumbs.delete(id, thumb.url))); }); }; - Thumbs.upload = id => new Promise((resolve) => { + Thumbs.upload = () => new Promise((resolve) => { uploader.show({ title: '[[topic:composer.thumb-title]]', method: 'put', - route: config.relative_path + `/api/v3/topics/${id}/thumbs`, + route: config.relative_path + `/api/topic/thumb/upload`, }, function (url) { resolve(url); }); @@ -32,24 +36,16 @@ define('topicThumbs', [ Thumbs.modal = {}; Thumbs.modal.open = function (payload) { - const { id, pid } = payload; + const { id, postData } = payload; let { modal } = payload; - let numThumbs; + const thumbs = postData.thumbs || []; return new Promise((resolve) => { - Promise.all([ - Thumbs.get(id), - pid ? Thumbs.getByPid(pid) : [], - ]).then(results => new Promise((resolve) => { - const thumbs = results.reduce((memo, cur) => memo.concat(cur)); - numThumbs = thumbs.length; - - resolve(thumbs); - })).then(thumbs => Benchpress.render('modals/topic-thumbs', { thumbs })).then((html) => { + Benchpress.render('modals/topic-thumbs', { thumbs }).then((html) => { if (modal) { translator.translate(html, function (translated) { modal.find('.bootbox-body').html(translated); - Thumbs.modal.handleSort({ modal, numThumbs }); + Thumbs.modal.handleSort({ modal, thumbs }); }); } else { modal = bootbox.dialog({ @@ -62,7 +58,11 @@ define('topicThumbs', [ label: ' [[modules:thumbs.modal.add]]', className: 'btn-success', callback: () => { - Thumbs.upload(id).then(() => { + Thumbs.upload().then((thumbUrl) => { + postData.thumbs.push( + thumbUrl.replace(new RegExp(`^${config.upload_url}`), '') + ); + Thumbs.modal.open({ ...payload, modal }); require(['composer'], (composer) => { composer.updateThumbCount(id, $(`[component="composer"][data-uuid="${id}"]`)); @@ -79,7 +79,7 @@ define('topicThumbs', [ }, }); Thumbs.modal.handleDelete({ ...payload, modal }); - Thumbs.modal.handleSort({ modal, numThumbs }); + Thumbs.modal.handleSort({ modal, thumbs }); } }); }); @@ -94,42 +94,42 @@ define('topicThumbs', [ if (!ok) { return; } - - const id = ev.target.closest('[data-id]').getAttribute('data-id'); const path = ev.target.closest('[data-path]').getAttribute('data-path'); - api.del(`/topics/${id}/thumbs`, { - path: path, - }).then(() => { + const postData = payload.postData; + if (postData && postData.thumbs && postData.thumbs.includes(path)) { + postData.thumbs = postData.thumbs.filter(thumb => thumb !== path); Thumbs.modal.open(payload); require(['composer'], (composer) => { composer.updateThumbCount(uuid, $(`[component="composer"][data-uuid="${uuid}"]`)); }); - }).catch(alerts.error); + } }); } }); }; - Thumbs.modal.handleSort = ({ modal, numThumbs }) => { - if (numThumbs > 1) { + Thumbs.modal.handleSort = ({ modal, thumbs }) => { + if (thumbs.length > 1) { const selectorEl = modal.find('.topic-thumbs-modal'); selectorEl.sortable({ - items: '[data-id]', + items: '[data-path]', + }); + selectorEl.on('sortupdate', function () { + if (!thumbs) return; + const newOrder = []; + selectorEl.find('[data-path]').each(function () { + const path = $(this).attr('data-path'); + const thumb = thumbs.find(t => t === path); + if (thumb) { + newOrder.push(thumb); + } + }); + // Mutate thumbs array in place + thumbs.length = 0; + Array.prototype.push.apply(thumbs, newOrder); }); - selectorEl.on('sortupdate', Thumbs.modal.handleSortChange); } }; - Thumbs.modal.handleSortChange = (ev, ui) => { - const items = ui.item.get(0).parentNode.querySelectorAll('[data-id]'); - Array.from(items).forEach((el, order) => { - const id = el.getAttribute('data-id'); - let path = el.getAttribute('data-path'); - path = path.replace(new RegExp(`^${config.upload_url}`), ''); - - api.put(`/topics/${id}/thumbs/order`, { path, order }).catch(alerts.error); - }); - }; - return Thumbs; }); diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index e5a8e8e363..f9995b3b72 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -715,8 +715,12 @@ Mocks.notes.public = async (post) => { // Special handling for main posts (as:Article w/ as:Note preview) const noteAttachment = isMainPost ? [...attachment] : null; - const uploads = await posts.uploads.listWithSizes(post.pid); - const isThumb = await db.isSortedSetMembers(`topic:${post.tid}:thumbs`, uploads.map(u => u.name)); + const [uploads, thumbs] = await Promise.all([ + posts.uploads.listWithSizes(post.pid), + topics.getTopicField(post.tid, 'thumbs'), + ]); + const isThumb = uploads.map(u => Array.isArray(thumbs) ? thumbs.includes(u.name) : false); + uploads.forEach(({ name, width, height }, idx) => { const mediaType = mime.getType(name); const url = `${nconf.get('url') + nconf.get('upload_url')}/${name}`; diff --git a/src/api/posts.js b/src/api/posts.js index b39c173eb6..d8971796b3 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -120,9 +120,7 @@ postsAPI.edit = async function (caller, data) { data.timestamp = parseInt(data.timestamp, 10) || Date.now(); const editResult = await posts.edit(data); - if (editResult.topic.isMainPost) { - await topics.thumbs.migrate(data.uuid, editResult.topic.tid); - } + const selfPost = parseInt(caller.uid, 10) === parseInt(editResult.post.uid, 10); if (!selfPost && editResult.post.changed) { await events.log({ diff --git a/src/api/topics.js b/src/api/topics.js index 0155429ecc..38e3cefbf8 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -1,7 +1,5 @@ 'use strict'; -const validator = require('validator'); - const user = require('../user'); const topics = require('../topics'); const categories = require('../categories'); @@ -23,17 +21,13 @@ const socketHelpers = require('../socket.io/helpers'); const topicsAPI = module.exports; topicsAPI._checkThumbPrivileges = async function ({ tid, uid }) { - // req.params.tid could be either a tid (pushing a new thumb to an existing topic) - // or a post UUID (a new topic being composed) - const isUUID = validator.isUUID(tid); - // Sanity-check the tid if it's strictly not a uuid - if (!isUUID && (isNaN(parseInt(tid, 10)) || !await topics.exists(tid))) { + if ((isNaN(parseInt(tid, 10)) || !await topics.exists(tid))) { throw new Error('[[error:no-topic]]'); } // While drafts are not protected, tids are - if (!isUUID && !await privileges.topics.canEdit(tid, uid)) { + if (!await privileges.topics.canEdit(tid, uid)) { throw new Error('[[error:no-privileges]]'); } }; @@ -80,7 +74,6 @@ topicsAPI.create = async function (caller, data) { } const result = await topics.post(payload); - await topics.thumbs.migrate(data.uuid, result.topicData.tid); socketHelpers.emitToUids('event:new_post', { posts: [result.postData] }, [caller.uid]); socketHelpers.emitToUids('event:new_topic', result.topicData, [caller.uid]); @@ -233,17 +226,6 @@ topicsAPI.getThumbs = async (caller, { tid, thumbsOnly }) => { return await topics.thumbs.get(tid, { thumbsOnly }); }; -// topicsAPI.addThumb - -topicsAPI.migrateThumbs = async (caller, { from, to }) => { - await Promise.all([ - topicsAPI._checkThumbPrivileges({ tid: from, uid: caller.uid }), - topicsAPI._checkThumbPrivileges({ tid: to, uid: caller.uid }), - ]); - - await topics.thumbs.migrate(from, to); -}; - topicsAPI.deleteThumb = async (caller, { tid, path }) => { await topicsAPI._checkThumbPrivileges({ tid: tid, uid: caller.uid }); await topics.thumbs.delete(tid, path); diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index b46002bb65..06aded5913 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -138,25 +138,18 @@ Topics.addThumb = async (req, res) => { const files = await uploadsController.uploadThumb(req, res); // response is handled here - // Add uploaded files to topic zset + // Add uploaded files to topic hash if (files && files.length) { - await Promise.all(files.map(async (fileObj) => { + for (const fileObj of files) { + // eslint-disable-next-line no-await-in-loop await topics.thumbs.associate({ id: req.params.tid, path: fileObj.url, }); - })); + } } }; -Topics.migrateThumbs = async (req, res) => { - await api.topics.migrateThumbs(req, { - from: req.params.tid, - to: req.body.tid, - }); - - helpers.formatApiResponse(200, res, await api.topics.getThumbs(req, { tid: req.body.tid })); -}; Topics.deleteThumb = async (req, res) => { if (!req.body.path.startsWith('http')) { diff --git a/src/posts/data.js b/src/posts/data.js index d74a22e69d..28e6c24aaa 100644 --- a/src/posts/data.js +++ b/src/posts/data.js @@ -70,5 +70,13 @@ function modifyPost(post, fields) { if (!fields.length || fields.includes('attachments')) { post.attachments = (post.attachments || '').split(',').filter(Boolean); } + + if (!fields.length || fields.includes('uploads')) { + try { + post.uploads = post.uploads ? JSON.parse(post.uploads) : []; + } catch (err) { + post.uploads = []; + } + } } } diff --git a/src/posts/edit.js b/src/posts/edit.js index 077616b29e..b18bf99078 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -29,7 +29,7 @@ module.exports = function (Posts) { } const topicData = await topics.getTopicFields(postData.tid, [ - 'cid', 'mainPid', 'title', 'timestamp', 'scheduled', 'slug', 'tags', + 'cid', 'mainPid', 'title', 'timestamp', 'scheduled', 'slug', 'tags', 'thumbs', ]); await scheduledTopicCheck(data, topicData); @@ -142,6 +142,15 @@ module.exports = function (Posts) { await topics.validateTags(data.tags, topicData.cid, data.uid, tid); } + const thumbs = topics.thumbs.filterThumbs(data.thumbs); + const thumbsupdated = Array.isArray(data.thumbs) && + !_.isEqual(data.thumbs, topicData.thumbs); + + if (thumbsupdated) { + newTopicData.thumbs = JSON.stringify(thumbs); + newTopicData.numThumbs = thumbs.length; + } + const results = await plugins.hooks.fire('filter:topic.edit', { req: data.req, topic: newTopicData, @@ -172,6 +181,7 @@ module.exports = function (Posts) { renamed: renamed, tagsupdated: tagsupdated, tags: tags, + thumbsupdated: thumbsupdated, oldTags: topicData.tags, rescheduled: rescheduling(data, topicData), }; diff --git a/src/posts/uploads.js b/src/posts/uploads.js index 17e82250ba..372c30ca1e 100644 --- a/src/posts/uploads.js +++ b/src/posts/uploads.js @@ -46,12 +46,14 @@ module.exports = function (Posts) { Posts.uploads.sync = async function (pid) { // Scans a post's content and updates sorted set of uploads - const [content, currentUploads, isMainPost] = await Promise.all([ - Posts.getPostField(pid, 'content'), - Posts.uploads.list(pid), + const [postData, isMainPost] = await Promise.all([ + Posts.getPostFields(pid, ['content', 'uploads']), Posts.isMain(pid), ]); + const content = postData.content || ''; + const currentUploads = postData.uploads || []; + // Extract upload file paths from post content let match = searchRegex.exec(content); let uploads = new Set(); @@ -75,14 +77,19 @@ module.exports = function (Posts) { // Create add/remove sets const add = uploads.filter(path => !currentUploads.includes(path)); const remove = currentUploads.filter(path => !uploads.includes(path)); - await Promise.all([ - Posts.uploads.associate(pid, add), - Posts.uploads.dissociate(pid, remove), - ]); + await Posts.uploads.associate(pid, add); + await Posts.uploads.dissociate(pid, remove); }; - Posts.uploads.list = async function (pid) { - return await db.getSortedSetMembers(`post:${pid}:uploads`); + Posts.uploads.list = async function (pids) { + const isArray = Array.isArray(pids); + if (isArray) { + const uploads = await Posts.getPostsFields(pids, ['uploads']); + return uploads.map(p => p.uploads || []); + } + + const uploads = await Posts.getPostField(pids, 'uploads'); + return uploads; }; Posts.uploads.listWithSizes = async function (pid) { @@ -157,33 +164,38 @@ module.exports = function (Posts) { }; Posts.uploads.associate = async function (pid, filePaths) { - // Adds an upload to a post's sorted set of uploads filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; if (!filePaths.length) { return; } filePaths = await _filterValidPaths(filePaths); // Only process files that exist and are within uploads directory + const currentUploads = await Posts.uploads.list(pid); + filePaths.forEach((path) => { + if (!currentUploads.includes(path)) { + currentUploads.push(path); + } + }); const now = Date.now(); - const scores = filePaths.map((p, i) => now + i); const bulkAdd = filePaths.map(path => [`upload:${md5(path)}:pids`, now, pid]); + await Promise.all([ - db.sortedSetAdd(`post:${pid}:uploads`, scores, filePaths), + db.setObjectField(`post:${pid}`, 'uploads', JSON.stringify(currentUploads)), db.sortedSetAddBulk(bulkAdd), Posts.uploads.saveSize(filePaths), ]); }; Posts.uploads.dissociate = async function (pid, filePaths) { - // Removes an upload from a post's sorted set of uploads filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; if (!filePaths.length) { return; } - + let currentUploads = await Posts.uploads.list(pid); + currentUploads = currentUploads.filter(upload => !filePaths.includes(upload)); const bulkRemove = filePaths.map(path => [`upload:${md5(path)}:pids`, pid]); const promises = [ - db.sortedSetRemove(`post:${pid}:uploads`, filePaths), + db.setObjectField(`post:${pid}`, 'uploads', JSON.stringify(currentUploads)), db.sortedSetRemoveBulk(bulkRemove), ]; diff --git a/src/routes/api.js b/src/routes/api.js index e374e242a4..4424d9a979 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -36,6 +36,7 @@ module.exports = function (app, middleware, controllers) { ]; router.post('/post/upload', postMiddlewares, helpers.tryRoute(uploadsController.uploadPost)); + router.post('/topic/thumb/upload', postMiddlewares, helpers.tryRoute(uploadsController.uploadThumb)); router.post('/user/:userslug/uploadpicture', [ ...middlewares, ...postMiddlewares, diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index eb56cdaf42..2b159ee3c0 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -41,7 +41,7 @@ module.exports = function () { ...middlewares, ], controllers.write.topics.addThumb); - setupApiRoute(router, 'put', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['tid'])], controllers.write.topics.migrateThumbs); + setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['path'])], controllers.write.topics.deleteThumb); setupApiRoute(router, 'put', '/:tid/thumbs/order', [...middlewares, middleware.checkRequired.bind(null, ['path', 'order'])], controllers.write.topics.reorderThumbs); diff --git a/src/topics/create.js b/src/topics/create.js index 352823a202..2f41c822b1 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -41,6 +41,12 @@ module.exports = function (Topics) { topicData.tags = data.tags.join(','); } + if (Array.isArray(data.thumbs) && data.thumbs.length) { + const thumbs = Topics.thumbs.filterThumbs(data.thumbs); + topicData.thumbs = JSON.stringify(thumbs); + topicData.numThumbs = thumbs.length; + } + const result = await plugins.hooks.fire('filter:topic.create', { topic: topicData, data: data }); topicData = result.topic; await db.setObject(`topic:${topicData.tid}`, topicData); diff --git a/src/topics/data.js b/src/topics/data.js index 76c027121d..a5801e0475 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -140,4 +140,12 @@ function modifyTopic(topic, fields) { }; }); } + + if (fields.includes('thumbs') || !fields.length) { + try { + topic.thumbs = topic.thumbs ? JSON.parse(String(topic.thumbs || '[]')) : []; + } catch (e) { + topic.thumbs = []; + } + } } diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index be2916a05d..2c3482664d 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -5,29 +5,26 @@ const _ = require('lodash'); const nconf = require('nconf'); const path = require('path'); const mime = require('mime'); - -const db = require('../database'); -const file = require('../file'); const plugins = require('../plugins'); const posts = require('../posts'); const meta = require('../meta'); -const cache = require('../cache'); const topics = module.parent.exports; const Thumbs = module.exports; -Thumbs.exists = async function (id, path) { - const isDraft = !await topics.exists(id); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; +const upload_url = nconf.get('relative_path') + nconf.get('upload_url'); +const upload_path = nconf.get('upload_path'); - return db.isSortedSetMember(set, path); +Thumbs.exists = async function (tid, path) { + const thumbs = await topics.getTopicField(tid, 'thumbs'); + return thumbs.includes(path); }; Thumbs.load = async function (topicData) { const mainPids = topicData.filter(Boolean).map(t => t.mainPid); - let hashes = await posts.getPostsFields(mainPids, ['attachments']); - const hasUploads = await db.exists(mainPids.map(pid => `post:${pid}:uploads`)); - hashes = hashes.map(o => o.attachments); + const mainPostData = await posts.getPostsFields(mainPids, ['attachments', 'uploads']); + const hasUploads = mainPostData.map(p => Array.isArray(p.uploads) && p.uploads.length > 0); + const hashes = mainPostData.map(o => o.attachments); let hasThumbs = topicData.map((t, idx) => t && (parseInt(t.numThumbs, 10) > 0 || !!(hashes[idx] && hashes[idx].length) || @@ -36,11 +33,70 @@ Thumbs.load = async function (topicData) { const topicsWithThumbs = topicData.filter((tid, idx) => hasThumbs[idx]); const tidsWithThumbs = topicsWithThumbs.map(t => t.tid); - const thumbs = await Thumbs.get(tidsWithThumbs); + + const thumbs = await loadFromTopicData(topicsWithThumbs); + const tidToThumbs = _.zipObject(tidsWithThumbs, thumbs); return topicData.map(t => (t && t.tid ? (tidToThumbs[t.tid] || []) : [])); }; +async function loadFromTopicData(topicData, options = {}) { + const tids = topicData.map(t => t.tid); + const thumbs = topicData.map(t => t ? t.thumbs : []); + + if (!options.thumbsOnly) { + const mainPids = topicData.map(t => t.mainPid); + const [mainPidUploads, mainPidAttachments] = await Promise.all([ + posts.uploads.list(mainPids), + posts.attachments.get(mainPids), + ]); + + // Add uploaded media to thumb sets + mainPidUploads.forEach((uploads, idx) => { + uploads = uploads.filter((upload) => { + const type = mime.getType(upload); + return !thumbs[idx].includes(upload) && type && type.startsWith('image/'); + }); + + if (uploads.length) { + thumbs[idx].push(...uploads); + } + }); + + // Add attachments to thumb sets + mainPidAttachments.forEach((attachments, idx) => { + attachments = attachments.filter( + attachment => !thumbs[idx].includes(attachment.url) && (attachment.mediaType && attachment.mediaType.startsWith('image/')) + ); + + if (attachments.length) { + thumbs[idx].push(...attachments.map(attachment => attachment.url)); + } + }); + } + + const hasTimestampPrefix = /^\d+-/; + + let response = thumbs.map((thumbSet, idx) => thumbSet.map(thumb => ({ + id: String(tids[idx]), + name: (() => { + const name = path.basename(thumb); + return hasTimestampPrefix.test(name) ? name.slice(14) : name; + })(), + path: thumb, + url: thumb.startsWith('http') ? + thumb : + path.posix.join(upload_url, thumb.replace(/\\/g, '/')), + }))); + + ({ thumbs: response } = await plugins.hooks.fire('filter:topics.getThumbs', { + tids, + thumbsOnly: options.thumbsOnly, + thumbs: response, + })); + return response; +}; + Thumbs.get = async function (tids, options) { // Allow singular or plural usage let singular = false; @@ -54,118 +110,77 @@ Thumbs.get = async function (tids, options) { thumbsOnly: false, }; } - - const isDraft = (await topics.exists(tids)).map(exists => !exists); - if (!meta.config.allowTopicsThumbnail || !tids.length) { return singular ? [] : tids.map(() => []); } - const hasTimestampPrefix = /^\d+-/; - const upload_url = nconf.get('relative_path') + nconf.get('upload_url'); - const sets = tids.map((tid, idx) => `${isDraft[idx] ? 'draft' : 'topic'}:${tid}:thumbs`); - const thumbs = await Promise.all(sets.map(getThumbs)); - - let mainPids = await topics.getTopicsFields(tids, ['mainPid']); - mainPids = mainPids.map(o => o.mainPid); - - if (!options.thumbsOnly) { - // Add uploaded media to thumb sets - const mainPidUploads = await Promise.all(mainPids.map(posts.uploads.list)); - mainPidUploads.forEach((uploads, idx) => { - uploads = uploads.filter((upload) => { - const type = mime.getType(upload); - return !thumbs[idx].includes(upload) && type && type.startsWith('image/'); - }); - - if (uploads.length) { - thumbs[idx].push(...uploads); - } - }); - - // Add attachments to thumb sets - const mainPidAttachments = await posts.attachments.get(mainPids); - mainPidAttachments.forEach((attachments, idx) => { - attachments = attachments.filter( - attachment => !thumbs[idx].includes(attachment.url) && (attachment.mediaType && attachment.mediaType.startsWith('image/')) - ); - - if (attachments.length) { - thumbs[idx].push(...attachments.map(attachment => attachment.url)); - } - }); - } - - let response = thumbs.map((thumbSet, idx) => thumbSet.map(thumb => ({ - id: tids[idx], - name: (() => { - const name = path.basename(thumb); - return hasTimestampPrefix.test(name) ? name.slice(14) : name; - })(), - path: thumb, - url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, thumb.replace(/\\/g, '/')), - }))); - - ({ thumbs: response } = await plugins.hooks.fire('filter:topics.getThumbs', { - tids, - thumbsOnly: options.thumbsOnly, - thumbs: response, - })); - return singular ? response.pop() : response; + const topicData = await topics.getTopicsFields(tids, ['tid', 'mainPid', 'thumbs']); + const response = await loadFromTopicData(topicData, options); + return singular ? response[0] : response; }; -async function getThumbs(set) { - const cached = cache.get(set); - if (cached !== undefined) { - return cached.slice(); - } - const thumbs = await db.getSortedSetRange(set, 0, -1); - cache.set(set, thumbs); - return thumbs.slice(); -} Thumbs.associate = async function ({ id, path, score }) { - // Associates a newly uploaded file as a thumb to the passed-in draft or topic - const isDraft = !await topics.exists(id); + // Associates a newly uploaded file as a thumb to the passed-in topic + const topicData = await topics.getTopicData(id); + if (!topicData) { + return; + } const isLocal = !path.startsWith('http'); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; - const numThumbs = await db.sortedSetCard(set); // Normalize the path to allow for changes in upload_path (and so upload_url can be appended if needed) if (isLocal) { path = path.replace(nconf.get('relative_path'), ''); path = path.replace(nconf.get('upload_url'), ''); } - await db.sortedSetAdd(set, isFinite(score) ? score : numThumbs, path); - if (!isDraft) { - const numThumbs = await db.sortedSetCard(set); - await topics.setTopicField(id, 'numThumbs', numThumbs); - } - cache.del(set); - // Associate thumbnails with the main pid (only on local upload) - if (!isDraft && isLocal) { - const mainPid = (await topics.getMainPids([id]))[0]; - await posts.uploads.associate(mainPid, path); + if (Array.isArray(topicData.thumbs)) { + const currentIdx = topicData.thumbs.indexOf(path); + const insertIndex = (typeof score === 'number' && score >= 0 && score < topicData.thumbs.length) ? + score : + topicData.thumbs.length; + + if (currentIdx !== -1) { + // Remove from current position + topicData.thumbs.splice(currentIdx, 1); + // Adjust insertIndex if needed + const adjustedIndex = currentIdx < insertIndex ? insertIndex - 1 : insertIndex; + topicData.thumbs.splice(adjustedIndex, 0, path); + } else { + topicData.thumbs.splice(insertIndex, 0, path); + } + + await topics.setTopicFields(id, { + thumbs: JSON.stringify(topicData.thumbs), + numThumbs: topicData.thumbs.length, + }); + // Associate thumbnails with the main pid (only on local upload) + if (isLocal && currentIdx === -1) { + await posts.uploads.associate(topicData.mainPid, path); + } } }; -Thumbs.migrate = async function (uuid, id) { - // Converts the draft thumb zset to the topic zset (combines thumbs if applicable) - const set = `draft:${uuid}:thumbs`; - const thumbs = await db.getSortedSetRangeWithScores(set, 0, -1); - await Promise.all(thumbs.map(async thumb => await Thumbs.associate({ - id, - path: thumb.value, - score: thumb.score, - }))); - await db.delete(set); - cache.del(set); +Thumbs.filterThumbs = function (thumbs) { + if (!Array.isArray(thumbs)) { + return []; + } + thumbs = thumbs.filter((thumb) => { + if (thumb.startsWith('http')) { + return true; + } + // ensure it is in upload path + const fullPath = path.join(upload_path, thumb); + return fullPath.startsWith(upload_path); + }); + return thumbs; }; -Thumbs.delete = async function (id, relativePaths) { - const isDraft = !await topics.exists(id); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; +Thumbs.delete = async function (tid, relativePaths) { + const topicData = await topics.getTopicData(tid); + if (!topicData) { + return; + } if (typeof relativePaths === 'string') { relativePaths = [relativePaths]; @@ -173,48 +188,28 @@ Thumbs.delete = async function (id, relativePaths) { throw new Error('[[error:invalid-data]]'); } - const absolutePaths = relativePaths.map(relativePath => path.join(nconf.get('upload_path'), relativePath)); - const [associated, existsOnDisk] = await Promise.all([ - db.isSortedSetMembers(set, relativePaths), - Promise.all(absolutePaths.map(async absolutePath => file.exists(absolutePath))), - ]); + const toRemove = relativePaths.map( + relativePath => topicData.thumbs.includes(relativePath) ? relativePath : null + ).filter(Boolean); - const toRemove = []; - const toDelete = []; - relativePaths.forEach((relativePath, idx) => { - if (associated[idx]) { - toRemove.push(relativePath); - } - - if (existsOnDisk[idx]) { - toDelete.push(absolutePaths[idx]); - } - }); - - await db.sortedSetRemove(set, toRemove); - - if (isDraft && toDelete.length) { // drafts only; post upload dissociation handles disk deletion for topics - await Promise.all(toDelete.map(path => file.delete(path))); - } - - if (toRemove.length && !isDraft) { - const topics = require('.'); - const mainPid = (await topics.getMainPids([id]))[0]; + if (toRemove.length) { + const { mainPid } = topicData.mainPid; + topicData.thumbs = topicData.thumbs.filter(thumb => !toRemove.includes(thumb)); await Promise.all([ - db.incrObjectFieldBy(`topic:${id}`, 'numThumbs', -toRemove.length), + topics.setTopicFields(tid, { + thumbs: JSON.stringify(topicData.thumbs), + numThumbs: topicData.thumbs.length, + }), Promise.all(toRemove.map(async relativePath => posts.uploads.dissociate(mainPid, relativePath))), ]); } - if (toRemove.length) { - cache.del(set); +}; + +Thumbs.deleteAll = async (tid) => { + const topicData = await topics.getTopicData(tid); + if (!topicData) { + return; } -}; - -Thumbs.deleteAll = async (id) => { - const isDraft = !await topics.exists(id); - const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; - - const thumbs = await db.getSortedSetRange(set, 0, -1); - await Thumbs.delete(id, thumbs); + await Thumbs.delete(tid, topicData.thumbs); }; diff --git a/src/upgrades/4.5.0/post-uploads-to-hash.js b/src/upgrades/4.5.0/post-uploads-to-hash.js new file mode 100644 index 0000000000..bef3b72df7 --- /dev/null +++ b/src/upgrades/4.5.0/post-uploads-to-hash.js @@ -0,0 +1,39 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Move post::uploads to post hash', + timestamp: Date.UTC(2025, 6, 5), + method: async function () { + const { progress } = this; + + const postCount = await db.sortedSetCard('posts:pid'); + progress.total = postCount; + + await batch.processSortedSet('posts:pid', async (pids) => { + const keys = pids.map(pid => `post:${pid}:uploads`); + + const postUploadData = await db.getSortedSetsMembersWithScores(keys); + + const bulkSet = []; + postUploadData.forEach((postUploads, idx) => { + const pid = pids[idx]; + if (Array.isArray(postUploads) && postUploads.length > 0) { + bulkSet.push([ + `post:${pid}`, + { uploads: JSON.stringify(postUploads.map(upload => upload.value)) }, + ]); + } + }); + + await db.setObjectBulk(bulkSet); + await db.deleteAll(keys); + + progress.incr(pids.length); + }, { + batch: 500, + }); + }, +}; diff --git a/src/upgrades/4.5.0/topic-thumbs-to-hash.js b/src/upgrades/4.5.0/topic-thumbs-to-hash.js new file mode 100644 index 0000000000..3385052c90 --- /dev/null +++ b/src/upgrades/4.5.0/topic-thumbs-to-hash.js @@ -0,0 +1,39 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Move topic::thumbs to topic hash', + timestamp: Date.UTC(2025, 6, 5), + method: async function () { + const { progress } = this; + + const topicCount = await db.sortedSetCard('topics:tid'); + progress.total = topicCount; + + await batch.processSortedSet('topics:tid', async (tids) => { + const keys = tids.map(tid => `topic:${tid}:thumbs`); + + const topicThumbData = await db.getSortedSetsMembersWithScores(keys); + + const bulkSet = []; + topicThumbData.forEach((topicThumbs, idx) => { + const tid = tids[idx]; + if (Array.isArray(topicThumbs) && topicThumbs.length > 0) { + bulkSet.push([ + `topic:${tid}`, + { thumbs: JSON.stringify(topicThumbs.map(thumb => thumb.value)) }, + ]); + } + }); + + await db.setObjectBulk(bulkSet); + await db.deleteAll(keys); + + progress.incr(tids.length); + }, { + batch: 500, + }); + }, +}; diff --git a/src/views/modals/topic-thumbs.tpl b/src/views/modals/topic-thumbs.tpl index ead862457c..d59f189be1 100644 --- a/src/views/modals/topic-thumbs.tpl +++ b/src/views/modals/topic-thumbs.tpl @@ -3,13 +3,13 @@
    [[modules:thumbs.modal.no-thumbs]]
    {{{ end }}} {{{ each thumbs }}} -
    +
    - +

    - {./name} + {uploadBasename(@value)}

    diff --git a/test/posts/uploads.js b/test/posts/uploads.js index 8f22f3d1f9..45ea10411f 100644 --- a/test/posts/uploads.js +++ b/test/posts/uploads.js @@ -62,13 +62,13 @@ describe('upload methods', () => { }); describe('.sync()', () => { - it('should properly add new images to the post\'s zset', (done) => { + it('should properly add new images to the post\'s hash', (done) => { posts.uploads.sync(pid, (err) => { assert.ifError(err); - db.sortedSetCard(`post:${pid}:uploads`, (err, length) => { + posts.uploads.list(pid, (err, uploads) => { assert.ifError(err); - assert.strictEqual(length, 2); + assert.strictEqual(uploads.length, 2); done(); }); }); @@ -81,8 +81,8 @@ describe('upload methods', () => { content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png)... AND NO MORE!', }); await posts.uploads.sync(pid); - const length = await db.sortedSetCard(`post:${pid}:uploads`); - assert.strictEqual(1, length); + const uploads = await posts.uploads.list(pid); + assert.strictEqual(1, uploads.length); }); }); @@ -345,13 +345,11 @@ describe('post uploads management', () => { reply = replyData; }); - it('should automatically sync uploads on topic create and reply', (done) => { - db.sortedSetsCard([`post:${topic.topicData.mainPid}:uploads`, `post:${reply.pid}:uploads`], (err, lengths) => { - assert.ifError(err); - assert.strictEqual(lengths[0], 1); - assert.strictEqual(lengths[1], 1); - done(); - }); + it('should automatically sync uploads on topic create and reply', async () => { + const uploads1 = await posts.uploads.list(topic.topicData.mainPid); + const uploads2 = await posts.uploads.list(reply.pid); + assert.strictEqual(uploads1.length, 1); + assert.strictEqual(uploads2.length, 1); }); it('should automatically sync uploads on post edit', async () => { diff --git a/test/topics/thumbs.js b/test/topics/thumbs.js index afc13e5b22..b740596c74 100644 --- a/test/topics/thumbs.js +++ b/test/topics/thumbs.js @@ -37,8 +37,6 @@ describe('Topic thumbs', () => { const relativeThumbPaths = thumbPaths.map(path => path.replace(nconf.get('upload_path'), '')); - const uuid = utils.generateUUID(); - function createFiles() { fs.closeSync(fs.openSync(path.resolve(__dirname, '../uploads', thumbPaths[0]), 'w')); fs.closeSync(fs.openSync(path.resolve(__dirname, '../uploads', thumbPaths[1]), 'w')); @@ -70,7 +68,11 @@ describe('Topic thumbs', () => { // Touch a couple files and associate it to a topic createFiles(); - await db.sortedSetAdd(`topic:${topicObj.topicData.tid}:thumbs`, 0, `${relativeThumbPaths[0]}`); + + await topics.setTopicFields(topicObj.topicData.tid, { + numThumbs: 1, + thumbs: JSON.stringify([relativeThumbPaths[0]]), + }); }); it('should return bool for whether a thumb exists', async () => { @@ -80,10 +82,9 @@ describe('Topic thumbs', () => { describe('.get()', () => { it('should return an array of thumbs', async () => { - require('../../src/cache').del(`topic:${topicObj.topicData.tid}:thumbs`); const thumbs = await topics.thumbs.get(topicObj.topicData.tid); assert.deepStrictEqual(thumbs, [{ - id: topicObj.topicData.tid, + id: String(topicObj.topicData.tid), name: 'test.png', path: `${relativeThumbPaths[0]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, @@ -94,7 +95,7 @@ describe('Topic thumbs', () => { const thumbs = await topics.thumbs.get([topicObj.topicData.tid, topicObj.topicData.tid + 1]); assert.deepStrictEqual(thumbs, [ [{ - id: topicObj.topicData.tid, + id: String(topicObj.topicData.tid), name: 'test.png', path: `${relativeThumbPaths[0]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, @@ -119,25 +120,13 @@ describe('Topic thumbs', () => { mainPid = topicObj.postData.pid; }); - it('should add an uploaded file to a zset', async () => { + it('should add an uploaded file to the topic hash', async () => { await topics.thumbs.associate({ id: tid, path: relativeThumbPaths[0], }); - - const exists = await db.isSortedSetMember(`topic:${tid}:thumbs`, relativeThumbPaths[0]); - assert(exists); - }); - - it('should also work with UUIDs', async () => { - await topics.thumbs.associate({ - id: uuid, - path: relativeThumbPaths[1], - score: 5, - }); - - const exists = await db.isSortedSetMember(`draft:${uuid}:thumbs`, relativeThumbPaths[1]); - assert(exists); + const topicData = await topics.getTopicData(tid); + assert(topicData.thumbs.includes(relativeThumbPaths[0])); }); it('should also work with a URL', async () => { @@ -145,14 +134,8 @@ describe('Topic thumbs', () => { id: tid, path: relativeThumbPaths[2], }); - - const exists = await db.isSortedSetMember(`topic:${tid}:thumbs`, relativeThumbPaths[2]); - assert(exists); - }); - - it('should have a score equal to the number of thumbs prior to addition', async () => { - const scores = await db.sortedSetScores(`topic:${tid}:thumbs`, [relativeThumbPaths[0], relativeThumbPaths[2]]); - assert.deepStrictEqual(scores, [0, 1]); + const topicData = await topics.getTopicData(tid); + assert(topicData.thumbs.includes(relativeThumbPaths[2])); }); it('should update the relevant topic hash with the number of thumbnails', async () => { @@ -166,23 +149,19 @@ describe('Topic thumbs', () => { path: relativeThumbPaths[0], }); - const score = await db.sortedSetScore(`topic:${tid}:thumbs`, relativeThumbPaths[0]); - - assert(isFinite(score)); // exists in set - assert.strictEqual(score, 2); + const topicData = await topics.getTopicData(tid); + assert.strictEqual(topicData.thumbs.indexOf(relativeThumbPaths[0]), 1); }); - it('should update the score to be passed in as the third argument', async () => { + it('should update the index to be passed in as the third argument', async () => { await topics.thumbs.associate({ id: tid, path: relativeThumbPaths[0], score: 0, }); - const score = await db.sortedSetScore(`topic:${tid}:thumbs`, relativeThumbPaths[0]); - - assert(isFinite(score)); // exists in set - assert.strictEqual(score, 0); + const topicData = await topics.getTopicData(tid); + assert.strictEqual(topicData.thumbs.indexOf(relativeThumbPaths[0]), 0); }); it('should associate the thumbnail with that topic\'s main pid\'s uploads', async () => { @@ -195,33 +174,6 @@ describe('Topic thumbs', () => { const uploads = await posts.uploads.list(mainPid); assert(uploads.includes(relativeThumbPaths[0])); }); - - it('should combine the thumbs uploaded to a UUID zset and combine it with a topic\'s thumb zset', async () => { - await topics.thumbs.migrate(uuid, tid); - - const thumbs = await topics.thumbs.get(tid); - assert.strictEqual(thumbs.length, 3); - assert.deepStrictEqual(thumbs, [ - { - id: tid, - name: 'test.png', - path: relativeThumbPaths[0], - url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, - }, - { - id: tid, - name: 'example.org', - path: 'https://example.org', - url: 'https://example.org', - }, - { - id: tid, - name: 'test2.png', - path: relativeThumbPaths[1], - url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[1]}`, - }, - ]); - }); }); describe(`.delete()`, () => { @@ -231,8 +183,8 @@ describe('Topic thumbs', () => { path: `/files/test.png`, }); await topics.thumbs.delete(1, `/files/test.png`); - - assert.strictEqual(await db.isSortedSetMember('topic:1:thumbs', '/files/test.png'), false); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.strictEqual(thumbs.includes(`/files/test.png`), false); }); it('should no longer be associated with that topic\'s main pid\'s uploads', async () => { @@ -241,40 +193,12 @@ describe('Topic thumbs', () => { assert(!uploads.includes(path.basename(relativeThumbPaths[0]))); }); - it('should also work with UUIDs', async () => { - await topics.thumbs.associate({ - id: uuid, - path: `/files/test.png`, - }); - await topics.thumbs.delete(uuid, '/files/test.png'); - - assert.strictEqual(await db.isSortedSetMember(`draft:${uuid}:thumbs`, '/files/test.png'), false); - assert.strictEqual(await file.exists(path.join(`${nconf.get('upload_path')}`, '/files/test.png')), false); - }); - - it('should also work with URLs', async () => { - await topics.thumbs.associate({ - id: uuid, - path: thumbPaths[2], - }); - await topics.thumbs.delete(uuid, relativeThumbPaths[2]); - - assert.strictEqual(await db.isSortedSetMember(`draft:${uuid}:thumbs`, relativeThumbPaths[2]), false); - }); - - it('should not delete the file from disk if not associated with the tid', async () => { - createFiles(); - await topics.thumbs.delete(uuid, thumbPaths[0]); - assert.strictEqual(await file.exists(thumbPaths[0]), true); - }); - it('should have no more thumbs left', async () => { - const associated = await db.isSortedSetMembers(`topic:1:thumbs`, [relativeThumbPaths[0], relativeThumbPaths[1]]); - assert.strictEqual(associated.some(Boolean), false); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.strictEqual(thumbs.length, 0); }); it('should decrement numThumbs if dissociated one by one', async () => { - console.log('before', await db.getSortedSetRange(`topic:1:thumbs`, 0, -1)); await topics.thumbs.associate({ id: 1, path: `${nconf.get('relative_path')}${nconf.get('upload_url')}/files/test.png` }); await topics.thumbs.associate({ id: 1, path: `${nconf.get('relative_path')}${nconf.get('upload_url')}/files/test2.png` }); @@ -290,18 +214,14 @@ describe('Topic thumbs', () => { describe('.deleteAll()', () => { before(async () => { - await Promise.all([ - topics.thumbs.associate({ id: 1, path: '/files/test.png' }), - topics.thumbs.associate({ id: 1, path: '/files/test2.png' }), - ]); + await topics.thumbs.associate({ id: 1, path: '/files/test.png' }); + await topics.thumbs.associate({ id: 1, path: '/files/test2.png' }); createFiles(); }); it('should have thumbs prior to tests', async () => { - const associated = await db.isSortedSetMembers( - `topic:1:thumbs`, ['/files/test.png', '/files/test2.png'] - ); - assert.strictEqual(associated.every(Boolean), true); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.deepStrictEqual(thumbs, ['/files/test.png', '/files/test2.png']); }); it('should not error out', async () => { @@ -309,14 +229,8 @@ describe('Topic thumbs', () => { }); it('should remove all associated thumbs with that topic', async () => { - const associated = await db.isSortedSetMembers( - `topic:1:thumbs`, ['/files/test.png', '/files/test2.png'] - ); - assert.strictEqual(associated.some(Boolean), false); - }); - - it('should no longer have a :thumbs zset', async () => { - assert.strictEqual(await db.exists('topic:1:thumbs'), false); + const thumbs = await topics.getTopicField(1, 'thumbs'); + assert.deepStrictEqual(thumbs, []); }); }); @@ -330,11 +244,6 @@ describe('Topic thumbs', () => { assert.strictEqual(response.statusCode, 200); }); - it('should succeed with a uuid', async () => { - const { response } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); - assert.strictEqual(response.statusCode, 200); - }); - it('should succeed with uploader plugins', async () => { const hookMethod = async () => ({ name: 'test.png', @@ -346,7 +255,7 @@ describe('Topic thumbs', () => { }); const { response } = await helpers.uploadFile( - `${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, + `${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, @@ -375,7 +284,7 @@ describe('Topic thumbs', () => { it('should fail if thumbnails are not enabled', async () => { meta.config.allowTopicsThumbnail = 0; - const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/test.png'), {}, adminJar, adminCSRF); assert.strictEqual(response.statusCode, 503); assert(body && body.status); assert.strictEqual(body.status.message, 'Topic thumbnails are disabled.'); @@ -384,7 +293,7 @@ describe('Topic thumbs', () => { it('should fail if file is not image', async () => { meta.config.allowTopicsThumbnail = 1; - const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/${uuid}/thumbs`, path.join(__dirname, '../files/503.html'), {}, adminJar, adminCSRF); + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/v3/topics/1/thumbs`, path.join(__dirname, '../files/503.html'), {}, adminJar, adminCSRF); assert.strictEqual(response.statusCode, 500); assert(body && body.status); assert.strictEqual(body.status.message, 'Invalid File'); @@ -402,21 +311,17 @@ describe('Topic thumbs', () => { content: 'The content of test topic', }); - await Promise.all([ - topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[0] }), - topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[1] }), - ]); + + await topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[0] }); + await topics.thumbs.associate({ id: topicObj.tid, path: thumbPaths[1] }); + createFiles(); await topics.purge(topicObj.tid, adminUid); }); - it('should no longer have a :thumbs zset', async () => { - assert.strictEqual(await db.exists(`topic:${topicObj.tid}:thumbs`), false); - }); - it('should not leave post upload associations behind', async () => { - const uploads = await db.getSortedSetMembers(`post:${topicObj.postData.pid}:uploads`); + const uploads = await posts.uploads.list(topicObj.postData.pid); assert.strictEqual(uploads.length, 0); }); }); From 72fec565c21d8bf03752e6fa2763d2609a8fd849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 7 Jul 2025 11:28:22 -0400 Subject: [PATCH 3184/4744] fix: check topic and thumbs --- src/topics/thumbs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index 2c3482664d..22fa2c48bd 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -41,8 +41,8 @@ Thumbs.load = async function (topicData) { }; async function loadFromTopicData(topicData, options = {}) { - const tids = topicData.map(t => t.tid); - const thumbs = topicData.map(t => t ? t.thumbs : []); + const tids = topicData.map(t => t && t.tid); + const thumbs = topicData.map(t => t && Array.isArray(t.thumbs) ? t.thumbs : []); if (!options.thumbsOnly) { const mainPids = topicData.map(t => t.mainPid); From 329f98d5db866be9a624774e7c1430ed45d71cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 7 Jul 2025 12:16:08 -0400 Subject: [PATCH 3185/4744] fix: for attribute, remove upload trigger when click inputs user can input an absolute url in the inputs --- src/views/admin/settings/general.tpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index 0d75c9d601..5dccda3f0a 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -120,9 +120,9 @@
    - +
    - + @@ -132,7 +132,7 @@
    - +
    @@ -144,7 +144,7 @@
    - + From 113607829f77b20b7841cbbdcc1c1b8146092bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 7 Jul 2025 17:09:42 -0400 Subject: [PATCH 3186/4744] remove log --- src/controllers/activitypub/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 6751cb30cd..b3bc85aab6 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -145,7 +145,6 @@ Controller.postInbox = async (req, res) => { const method = String(req.body.type).toLowerCase(); if (!activitypub.inbox.hasOwnProperty(method)) { winston.warn(`[activitypub/inbox] Received Activity of type ${method} but unable to handle. Ignoring.`); - console.log('[activitypub/inbox] method not found', method, req.body); return res.sendStatus(200); } From c6f4148b214409fab97ec0e916062c6d3c253dc3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:38:43 -0400 Subject: [PATCH 3187/4744] fix(deps): update dependency nodemailer to v7.0.5 (#13537) 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 5ffca3c819..285fc971d5 100644 --- a/install/package.json +++ b/install/package.json @@ -111,7 +111,7 @@ "nodebb-theme-peace": "2.2.45", "nodebb-theme-persona": "14.1.12", "nodebb-widget-essentials": "7.0.38", - "nodemailer": "7.0.4", + "nodemailer": "7.0.5", "nprogress": "0.2.0", "passport": "0.7.0", "passport-http-bearer": "1.0.1", From 8960fdb3a5f85c5d28100bae1e74a202a753f1b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:39:03 -0400 Subject: [PATCH 3188/4744] fix(deps): update dependency esbuild to v0.25.6 (#13538) 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 285fc971d5..98d0fd038e 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.5", + "esbuild": "0.25.6", "express": "4.21.2", "express-session": "1.18.1", "express-useragent": "1.0.15", From dbed2db992e80d12e031790923ee2892819596f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 8 Jul 2025 11:03:02 -0400 Subject: [PATCH 3189/4744] fix: make clickable element anchor add rounded corners --- public/src/client/topic.js | 2 +- src/views/modals/topic-thumbs-view.tpl | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 43a64a9fa3..cce6f1f99a 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -202,7 +202,7 @@ define('forum/topic', [ $('[component="topic/thumb/select"]').removeClass('border-primary'); $(this).addClass('border-primary'); $('[component="topic/thumb/current"]') - .attr('src', $(this).attr('src')); + .attr('src', $(this).find('img').attr('src')); }); } }); diff --git a/src/views/modals/topic-thumbs-view.tpl b/src/views/modals/topic-thumbs-view.tpl index 9603e9bc36..a0ee682f80 100644 --- a/src/views/modals/topic-thumbs-view.tpl +++ b/src/views/modals/topic-thumbs-view.tpl @@ -1,14 +1,14 @@
    - +
    {{{ if (thumbs.length != "1") }}}
    {{{ each thumbs }}} -
    - -
    + + + {{{ end }}}
    {{{ end }}} From 0ef98ec495c4d91f00fc5963cd7f4878039faa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 8 Jul 2025 13:34:41 -0400 Subject: [PATCH 3190/4744] fix: set to empty string if undefined --- src/controllers/admin/events.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js index 72d9b4c3e1..9b4d493071 100644 --- a/src/controllers/admin/events.js +++ b/src/controllers/admin/events.js @@ -60,11 +60,11 @@ eventsController.get = async function (req, res) { pagination: pagination.create(page, pageCount, req.query), types: types, query: { - start: validator.escape(String(req.query.start)), - end: validator.escape(String(req.query.end)), - username: validator.escape(String(req.query.username)), - group: validator.escape(String(req.query.group)), - perPage: validator.escape(String(req.query.perPage)), + start: validator.escape(String(req.query.start || '')), + end: validator.escape(String(req.query.end || '')), + username: validator.escape(String(req.query.username || '')), + group: validator.escape(String(req.query.group || '')), + perPage: validator.escape(String(req.query.perPage || '')), }, }); }; From a6cb933bac58220c60ccafd2ae2c13d3ffc8e28a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:55:29 -0400 Subject: [PATCH 3191/4744] fix(deps): update dependency redis to v5.6.0 (#13540) 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 98d0fd038e..626fb01958 100644 --- a/install/package.json +++ b/install/package.json @@ -122,7 +122,7 @@ "postcss-clean": "1.2.0", "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", - "redis": "5.5.6", + "redis": "5.6.0", "rimraf": "6.0.1", "rss": "1.2.2", "rtlcss": "4.3.0", From dae81b76fbe76acd4b669789903eb9d57068527b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 8 Jul 2025 14:03:56 -0400 Subject: [PATCH 3192/4744] 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 98d0fd038e..580cf6095a 100644 --- a/install/package.json +++ b/install/package.json @@ -98,7 +98,7 @@ "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.3.0", - "nodebb-plugin-dbsearch": "6.3.0", + "nodebb-plugin-dbsearch": "6.3.1", "nodebb-plugin-emoji": "6.0.3", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.2.1", From 1b80910e806ea2a7348abcef71a57dbeaff89f4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:09:15 -0400 Subject: [PATCH 3193/4744] chore(deps): update redis docker tag to v8.0.3 (#13539) 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 cf72066b92..e3bdaf2ebd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: - 5432:5432 redis: - image: 'redis:8.0.2' + image: 'redis:8.0.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 8a67d5964d..0cfee0de3a 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.0.2-alpine + image: redis:8.0.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 d8855ccac4..dd415cc5fb 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.0.2-alpine + image: redis:8.0.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 a53acdfef2..06273610b7 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.0.2-alpine + image: redis:8.0.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 4a5a4fe6bd387dbea4f1d0b9f42d7ce74740c5a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:03:23 -0400 Subject: [PATCH 3194/4744] fix(deps): update dependency webpack to v5.100.0 (#13541) 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 b59d90be8c..4fab30daf1 100644 --- a/install/package.json +++ b/install/package.json @@ -147,7 +147,7 @@ "tough-cookie": "5.1.2", "undici": "^7.10.0", "validator": "13.15.15", - "webpack": "5.99.9", + "webpack": "5.100.0", "webpack-merge": "6.0.1", "winston": "3.17.0", "workerpool": "9.3.3", From e4f56e8392e68e40076a3b2f577cd1529b479186 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 09:10:00 -0400 Subject: [PATCH 3195/4744] fix(deps): update dependency nodebb-theme-peace to v2.2.46 (#13542) 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 4fab30daf1..b3c3f78258 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-rewards-essentials": "1.0.2", "nodebb-theme-harmony": "2.1.16", "nodebb-theme-lavender": "7.1.19", - "nodebb-theme-peace": "2.2.45", + "nodebb-theme-peace": "2.2.46", "nodebb-theme-persona": "14.1.12", "nodebb-widget-essentials": "7.0.38", "nodemailer": "7.0.5", From f88329dbbe9a6ab99eec0eaee3a4b80efbcfe820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Jul 2025 08:50:53 -0400 Subject: [PATCH 3196/4744] feat: add heap snapshot --- .../language/en-GB/admin/development/info.json | 3 ++- public/openapi/read.yaml | 2 ++ .../openapi/read/admin/advanced/heap/dump.yaml | 18 ++++++++++++++++++ src/controllers/admin/info.js | 18 ++++++++++++++++++ src/routes/admin.js | 1 + src/views/admin/development/info.tpl | 7 ++++++- 6 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 public/openapi/read/admin/advanced/heap/dump.yaml diff --git a/public/language/en-GB/admin/development/info.json b/public/language/en-GB/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/en-GB/admin/development/info.json +++ b/public/language/en-GB/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 3ec355bd95..9228ba9c7d 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -174,6 +174,8 @@ paths: $ref: 'read/admin/advanced/cache.yaml' /api/admin/advanced/cache/dump: $ref: 'read/admin/advanced/cache/dump.yaml' + /api/admin/advanced/heap/dump: + $ref: 'read/admin/advanced/heap/dump.yaml' /api/admin/development/logger: $ref: 'read/admin/development/logger.yaml' /api/admin/development/info: diff --git a/public/openapi/read/admin/advanced/heap/dump.yaml b/public/openapi/read/admin/advanced/heap/dump.yaml new file mode 100644 index 0000000000..2c0465eed4 --- /dev/null +++ b/public/openapi/read/admin/advanced/heap/dump.yaml @@ -0,0 +1,18 @@ +get: + tags: + - admin + summary: Get nodejs heap snapshot + description: Downloads a Node.js heap snapshot for memory analysis. + parameters: [] + responses: + "200": + description: Heap snapshot file (in .heapsnapshot format) + content: + application/octet-stream: + schema: + type: string + format: binary + examples: + heapSnapshot: + summary: Example Heap Snapshot Download + description: A binary heap snapshot file. diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js index 6f63faf8a9..d50af63ebf 100644 --- a/src/controllers/admin/info.js +++ b/src/controllers/admin/info.js @@ -143,3 +143,21 @@ async function getGitInfo() { ]); return { hash: hash, hashShort: hash.slice(0, 6), branch: branch }; } + +infoController.getHeapdump = async function (req, res) { + const v8 = require('v8'); + const path = require('path'); + const fs = require('fs'); + const filename = path.join(nconf.get('upload_path'), `heapdump-${Date.now()}.heapsnapshot`); + const stored = v8.writeHeapSnapshot(filename, {}); + res.download(stored, 'heapdump.heapsnapshot', (err) => { + if (err) { + winston.error(err.stack); + } + fs.unlink(stored, (unlinkErr) => { + if (unlinkErr) { + winston.error(unlinkErr.stack); + } + }); + }); +}; \ No newline at end of file diff --git a/src/routes/admin.js b/src/routes/admin.js index b7e751695c..94ba8e9e02 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -81,6 +81,7 @@ function apiRoutes(router, name, middleware, controllers) { router.get(`/api/${name}/groups/:groupname/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.groups.getCSV)); router.get(`/api/${name}/analytics`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.dashboard.getAnalytics)); router.get(`/api/${name}/advanced/cache/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.cache.dump)); + router.get(`/api/${name}/advanced/heap/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.info.getHeapdump)); const multer = require('multer'); const storage = multer.diskStorage({}); diff --git a/src/views/admin/development/info.tpl b/src/views/admin/development/info.tpl index 493e16ac93..49301f3543 100644 --- a/src/views/admin/development/info.tpl +++ b/src/views/admin/development/info.tpl @@ -5,7 +5,12 @@
    - [[admin/development/info:nodes-responded, {nodeCount}, {timeout}]] +
    + [[admin/development/info:nodes-responded, {nodeCount}, {timeout}]] + + [[admin/development/info:heap-dump]] + +
    From 5b54e926f7402a7a3548e863732e23d52eaee79d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 11 Jul 2025 12:51:18 +0000 Subject: [PATCH 3197/4744] chore(i18n): fallback strings for new resources: nodebb.admin-development-info --- public/language/ar/admin/development/info.json | 3 ++- public/language/az/admin/development/info.json | 3 ++- public/language/bg/admin/development/info.json | 3 ++- public/language/bn/admin/development/info.json | 3 ++- public/language/cs/admin/development/info.json | 3 ++- public/language/da/admin/development/info.json | 3 ++- public/language/de/admin/development/info.json | 3 ++- public/language/el/admin/development/info.json | 3 ++- public/language/en-US/admin/development/info.json | 3 ++- public/language/en-x-pirate/admin/development/info.json | 3 ++- public/language/es/admin/development/info.json | 3 ++- public/language/et/admin/development/info.json | 3 ++- public/language/fa-IR/admin/development/info.json | 3 ++- public/language/fi/admin/development/info.json | 3 ++- public/language/fr/admin/development/info.json | 3 ++- public/language/gl/admin/development/info.json | 3 ++- public/language/he/admin/development/info.json | 3 ++- public/language/hr/admin/development/info.json | 3 ++- public/language/hu/admin/development/info.json | 3 ++- public/language/hy/admin/development/info.json | 3 ++- public/language/id/admin/development/info.json | 3 ++- public/language/it/admin/development/info.json | 3 ++- public/language/ja/admin/development/info.json | 3 ++- public/language/ko/admin/development/info.json | 3 ++- public/language/lt/admin/development/info.json | 3 ++- public/language/lv/admin/development/info.json | 3 ++- public/language/ms/admin/development/info.json | 3 ++- public/language/nb/admin/development/info.json | 3 ++- public/language/nl/admin/development/info.json | 3 ++- public/language/nn-NO/admin/development/info.json | 3 ++- public/language/pl/admin/development/info.json | 3 ++- public/language/pt-BR/admin/development/info.json | 3 ++- public/language/pt-PT/admin/development/info.json | 3 ++- public/language/ro/admin/development/info.json | 3 ++- public/language/ru/admin/development/info.json | 3 ++- public/language/rw/admin/development/info.json | 3 ++- public/language/sc/admin/development/info.json | 3 ++- public/language/sk/admin/development/info.json | 3 ++- public/language/sl/admin/development/info.json | 3 ++- public/language/sq-AL/admin/development/info.json | 3 ++- public/language/sr/admin/development/info.json | 3 ++- public/language/sv/admin/development/info.json | 3 ++- public/language/th/admin/development/info.json | 3 ++- public/language/tr/admin/development/info.json | 3 ++- public/language/uk/admin/development/info.json | 3 ++- public/language/vi/admin/development/info.json | 3 ++- public/language/zh-CN/admin/development/info.json | 3 ++- public/language/zh-TW/admin/development/info.json | 3 ++- 48 files changed, 96 insertions(+), 48 deletions(-) diff --git a/public/language/ar/admin/development/info.json b/public/language/ar/admin/development/info.json index 4c97beee13..386c47e050 100644 --- a/public/language/ar/admin/development/info.json +++ b/public/language/ar/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/az/admin/development/info.json b/public/language/az/admin/development/info.json index a61eab10d3..22b43c2bc7 100644 --- a/public/language/az/admin/development/info.json +++ b/public/language/az/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Bağlantı sayı", "guests": "Qonaqlar", - "info": "Məlumat" + "info": "Məlumat", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/bg/admin/development/info.json b/public/language/bg/admin/development/info.json index 71f1e63c4b..8caea710f7 100644 --- a/public/language/bg/admin/development/info.json +++ b/public/language/bg/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Брой връзки", "guests": "Гости", - "info": "Информация" + "info": "Информация", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/bn/admin/development/info.json b/public/language/bn/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/bn/admin/development/info.json +++ b/public/language/bn/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/cs/admin/development/info.json b/public/language/cs/admin/development/info.json index 19fb830e9d..2a5e981a74 100644 --- a/public/language/cs/admin/development/info.json +++ b/public/language/cs/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Hosté", - "info": "Informace" + "info": "Informace", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/da/admin/development/info.json b/public/language/da/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/da/admin/development/info.json +++ b/public/language/da/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/de/admin/development/info.json b/public/language/de/admin/development/info.json index b5dd27dfe5..296aa0af43 100644 --- a/public/language/de/admin/development/info.json +++ b/public/language/de/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Verbindungsanzahl", "guests": "Gäste", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/el/admin/development/info.json b/public/language/el/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/el/admin/development/info.json +++ b/public/language/el/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/en-US/admin/development/info.json b/public/language/en-US/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/en-US/admin/development/info.json +++ b/public/language/en-US/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/development/info.json b/public/language/en-x-pirate/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/en-x-pirate/admin/development/info.json +++ b/public/language/en-x-pirate/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/es/admin/development/info.json b/public/language/es/admin/development/info.json index 809d0c85bc..92821fcf36 100644 --- a/public/language/es/admin/development/info.json +++ b/public/language/es/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Número de conexiones", "guests": "Invitados", - "info": "Información" + "info": "Información", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/et/admin/development/info.json b/public/language/et/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/et/admin/development/info.json +++ b/public/language/et/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/fa-IR/admin/development/info.json b/public/language/fa-IR/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/fa-IR/admin/development/info.json +++ b/public/language/fa-IR/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/fi/admin/development/info.json b/public/language/fi/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/fi/admin/development/info.json +++ b/public/language/fi/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/fr/admin/development/info.json b/public/language/fr/admin/development/info.json index afcbe555aa..3a17f92ad9 100644 --- a/public/language/fr/admin/development/info.json +++ b/public/language/fr/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "nombre de connexions", "guests": "Invités", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/gl/admin/development/info.json b/public/language/gl/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/gl/admin/development/info.json +++ b/public/language/gl/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/he/admin/development/info.json b/public/language/he/admin/development/info.json index d20aa255f5..0532e8febf 100644 --- a/public/language/he/admin/development/info.json +++ b/public/language/he/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "כמות חיבורים", "guests": "אורחים", - "info": "מידע" + "info": "מידע", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/hr/admin/development/info.json b/public/language/hr/admin/development/info.json index 3b21db6c9a..ebc7612366 100644 --- a/public/language/hr/admin/development/info.json +++ b/public/language/hr/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Gosti", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/hu/admin/development/info.json b/public/language/hu/admin/development/info.json index 48bf12d34b..130d0efdda 100644 --- a/public/language/hu/admin/development/info.json +++ b/public/language/hu/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Vendégek", - "info": "Információ" + "info": "Információ", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/hy/admin/development/info.json b/public/language/hy/admin/development/info.json index ca2bdf9f66..cf5128c6fc 100644 --- a/public/language/hy/admin/development/info.json +++ b/public/language/hy/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Հյուրեր", - "info": "տեղեկատվություն" + "info": "տեղեկատվություն", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/id/admin/development/info.json b/public/language/id/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/id/admin/development/info.json +++ b/public/language/id/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/it/admin/development/info.json b/public/language/it/admin/development/info.json index d5bb340bc5..7e2ef613ef 100644 --- a/public/language/it/admin/development/info.json +++ b/public/language/it/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Conteggio connessioni", "guests": "Ospiti", - "info": "Informazioni" + "info": "Informazioni", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/ja/admin/development/info.json b/public/language/ja/admin/development/info.json index 857c419a66..1214f8a023 100644 --- a/public/language/ja/admin/development/info.json +++ b/public/language/ja/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "ゲスト数", - "info": "情報" + "info": "情報", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/ko/admin/development/info.json b/public/language/ko/admin/development/info.json index 08cb8a739b..7184d8060f 100644 --- a/public/language/ko/admin/development/info.json +++ b/public/language/ko/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "비회원", - "info": "정보" + "info": "정보", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/lt/admin/development/info.json b/public/language/lt/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/lt/admin/development/info.json +++ b/public/language/lt/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/lv/admin/development/info.json b/public/language/lv/admin/development/info.json index 87cb89d8cf..d1798f4254 100644 --- a/public/language/lv/admin/development/info.json +++ b/public/language/lv/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Viesi", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/ms/admin/development/info.json b/public/language/ms/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/ms/admin/development/info.json +++ b/public/language/ms/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/nb/admin/development/info.json b/public/language/nb/admin/development/info.json index deb1975761..fc3af734be 100644 --- a/public/language/nb/admin/development/info.json +++ b/public/language/nb/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Antall tilkoblinger", "guests": "Gjester", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/nl/admin/development/info.json b/public/language/nl/admin/development/info.json index b5ee1a8838..19fa4d7cfd 100644 --- a/public/language/nl/admin/development/info.json +++ b/public/language/nl/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Gasten", - "info": "Informatie" + "info": "Informatie", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/nn-NO/admin/development/info.json b/public/language/nn-NO/admin/development/info.json index 2d28c9fcfb..30c27cddb3 100644 --- a/public/language/nn-NO/admin/development/info.json +++ b/public/language/nn-NO/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Tilkoplingar", "guests": "Gjestar", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/pl/admin/development/info.json b/public/language/pl/admin/development/info.json index a56206ad41..3a90dc57cb 100644 --- a/public/language/pl/admin/development/info.json +++ b/public/language/pl/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Liczba połączeń", "guests": "Goście", - "info": "Informacja" + "info": "Informacja", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/pt-BR/admin/development/info.json b/public/language/pt-BR/admin/development/info.json index e0fabb3ddb..bc6c6aeecd 100644 --- a/public/language/pt-BR/admin/development/info.json +++ b/public/language/pt-BR/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Visitantes", - "info": "Informação" + "info": "Informação", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/pt-PT/admin/development/info.json b/public/language/pt-PT/admin/development/info.json index 9dc6b6665e..5f7c5e7d6c 100644 --- a/public/language/pt-PT/admin/development/info.json +++ b/public/language/pt-PT/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Convidados", - "info": "Informação" + "info": "Informação", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/ro/admin/development/info.json b/public/language/ro/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/ro/admin/development/info.json +++ b/public/language/ro/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/ru/admin/development/info.json b/public/language/ru/admin/development/info.json index a7f1481f88..748d45e9ed 100644 --- a/public/language/ru/admin/development/info.json +++ b/public/language/ru/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Гостей", - "info": "Сырые данные" + "info": "Сырые данные", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/rw/admin/development/info.json b/public/language/rw/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/rw/admin/development/info.json +++ b/public/language/rw/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/sc/admin/development/info.json b/public/language/sc/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/sc/admin/development/info.json +++ b/public/language/sc/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/sk/admin/development/info.json b/public/language/sk/admin/development/info.json index c6dbfd349e..eb4ce931a7 100644 --- a/public/language/sk/admin/development/info.json +++ b/public/language/sk/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Hostia", - "info": "Informácie" + "info": "Informácie", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/sl/admin/development/info.json b/public/language/sl/admin/development/info.json index f53089170f..36dbf4ba32 100644 --- a/public/language/sl/admin/development/info.json +++ b/public/language/sl/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Gostje", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/sq-AL/admin/development/info.json b/public/language/sq-AL/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/sq-AL/admin/development/info.json +++ b/public/language/sq-AL/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/sr/admin/development/info.json b/public/language/sr/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/sr/admin/development/info.json +++ b/public/language/sr/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/sv/admin/development/info.json b/public/language/sv/admin/development/info.json index 9834719daf..40e8fded15 100644 --- a/public/language/sv/admin/development/info.json +++ b/public/language/sv/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info" + "info": "Info", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/th/admin/development/info.json b/public/language/th/admin/development/info.json index b78747dc04..f161f88b83 100644 --- a/public/language/th/admin/development/info.json +++ b/public/language/th/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "จำนวนการเชื่อมต่อ", "guests": "ผู้เยี่ยมเยียน", - "info": "ข้อมูล" + "info": "ข้อมูล", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/tr/admin/development/info.json b/public/language/tr/admin/development/info.json index d1bccbc674..cc0eaf9242 100644 --- a/public/language/tr/admin/development/info.json +++ b/public/language/tr/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Ziyaretçiler", - "info": "Bilgi" + "info": "Bilgi", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/uk/admin/development/info.json b/public/language/uk/admin/development/info.json index 42f90c8282..0db8f04563 100644 --- a/public/language/uk/admin/development/info.json +++ b/public/language/uk/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Connection Count", "guests": "Гостей", - "info": "Інфо" + "info": "Інфо", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/vi/admin/development/info.json b/public/language/vi/admin/development/info.json index f8e2480b94..088d0fb147 100644 --- a/public/language/vi/admin/development/info.json +++ b/public/language/vi/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "Số Lượng Kết Nối", "guests": "Khách", - "info": "Thông tin" + "info": "Thông tin", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/development/info.json b/public/language/zh-CN/admin/development/info.json index ac389d848e..2a7845e5ca 100644 --- a/public/language/zh-CN/admin/development/info.json +++ b/public/language/zh-CN/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "连接数", "guests": "游客", - "info": "信息" + "info": "信息", + "heap-dump": "Heap Dump" } \ No newline at end of file diff --git a/public/language/zh-TW/admin/development/info.json b/public/language/zh-TW/admin/development/info.json index df3bc6cd12..d57fd95abd 100644 --- a/public/language/zh-TW/admin/development/info.json +++ b/public/language/zh-TW/admin/development/info.json @@ -22,5 +22,6 @@ "connection-count": "連線次數", "guests": "訪客", - "info": "資訊" + "info": "資訊", + "heap-dump": "Heap Dump" } \ No newline at end of file From 930ff21f335de42d015985ae438bb96b009baef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Jul 2025 09:01:33 -0400 Subject: [PATCH 3198/4744] test: disable timeout --- src/controllers/admin/info.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js index d50af63ebf..4e9e95df9e 100644 --- a/src/controllers/admin/info.js +++ b/src/controllers/admin/info.js @@ -145,6 +145,7 @@ async function getGitInfo() { } infoController.getHeapdump = async function (req, res) { + req.timeout(0); // Disable timeout for this request const v8 = require('v8'); const path = require('path'); const fs = require('fs'); From 27aab921910f575e06a94a1805907f1c902d48db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Jul 2025 09:05:43 -0400 Subject: [PATCH 3199/4744] test: try timeout again --- src/controllers/admin/info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js index 4e9e95df9e..e244003a1a 100644 --- a/src/controllers/admin/info.js +++ b/src/controllers/admin/info.js @@ -145,7 +145,7 @@ async function getGitInfo() { } infoController.getHeapdump = async function (req, res) { - req.timeout(0); // Disable timeout for this request + req.setTimeout(0); const v8 = require('v8'); const path = require('path'); const fs = require('fs'); From e74996fbb950765795bb58687a6b9f0398262ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Jul 2025 10:22:37 -0400 Subject: [PATCH 3200/4744] revert: remove heapdump --- .../en-GB/admin/development/info.json | 3 +-- public/openapi/read.yaml | 2 -- .../read/admin/advanced/heap/dump.yaml | 18 ------------------ src/controllers/admin/info.js | 19 ------------------- src/routes/admin.js | 1 - src/views/admin/development/info.tpl | 7 +------ 6 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 public/openapi/read/admin/advanced/heap/dump.yaml diff --git a/public/language/en-GB/admin/development/info.json b/public/language/en-GB/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/en-GB/admin/development/info.json +++ b/public/language/en-GB/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 9228ba9c7d..3ec355bd95 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -174,8 +174,6 @@ paths: $ref: 'read/admin/advanced/cache.yaml' /api/admin/advanced/cache/dump: $ref: 'read/admin/advanced/cache/dump.yaml' - /api/admin/advanced/heap/dump: - $ref: 'read/admin/advanced/heap/dump.yaml' /api/admin/development/logger: $ref: 'read/admin/development/logger.yaml' /api/admin/development/info: diff --git a/public/openapi/read/admin/advanced/heap/dump.yaml b/public/openapi/read/admin/advanced/heap/dump.yaml deleted file mode 100644 index 2c0465eed4..0000000000 --- a/public/openapi/read/admin/advanced/heap/dump.yaml +++ /dev/null @@ -1,18 +0,0 @@ -get: - tags: - - admin - summary: Get nodejs heap snapshot - description: Downloads a Node.js heap snapshot for memory analysis. - parameters: [] - responses: - "200": - description: Heap snapshot file (in .heapsnapshot format) - content: - application/octet-stream: - schema: - type: string - format: binary - examples: - heapSnapshot: - summary: Example Heap Snapshot Download - description: A binary heap snapshot file. diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js index e244003a1a..6f63faf8a9 100644 --- a/src/controllers/admin/info.js +++ b/src/controllers/admin/info.js @@ -143,22 +143,3 @@ async function getGitInfo() { ]); return { hash: hash, hashShort: hash.slice(0, 6), branch: branch }; } - -infoController.getHeapdump = async function (req, res) { - req.setTimeout(0); - const v8 = require('v8'); - const path = require('path'); - const fs = require('fs'); - const filename = path.join(nconf.get('upload_path'), `heapdump-${Date.now()}.heapsnapshot`); - const stored = v8.writeHeapSnapshot(filename, {}); - res.download(stored, 'heapdump.heapsnapshot', (err) => { - if (err) { - winston.error(err.stack); - } - fs.unlink(stored, (unlinkErr) => { - if (unlinkErr) { - winston.error(unlinkErr.stack); - } - }); - }); -}; \ No newline at end of file diff --git a/src/routes/admin.js b/src/routes/admin.js index 94ba8e9e02..b7e751695c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -81,7 +81,6 @@ function apiRoutes(router, name, middleware, controllers) { router.get(`/api/${name}/groups/:groupname/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.groups.getCSV)); router.get(`/api/${name}/analytics`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.dashboard.getAnalytics)); router.get(`/api/${name}/advanced/cache/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.cache.dump)); - router.get(`/api/${name}/advanced/heap/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.info.getHeapdump)); const multer = require('multer'); const storage = multer.diskStorage({}); diff --git a/src/views/admin/development/info.tpl b/src/views/admin/development/info.tpl index 49301f3543..493e16ac93 100644 --- a/src/views/admin/development/info.tpl +++ b/src/views/admin/development/info.tpl @@ -5,12 +5,7 @@
    -
    - [[admin/development/info:nodes-responded, {nodeCount}, {timeout}]] - - [[admin/development/info:heap-dump]] - -
    + [[admin/development/info:nodes-responded, {nodeCount}, {timeout}]]
    From 59c1ce853f905c2f4a9af799a24b2fb76fc3bdf9 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 11 Jul 2025 14:23:04 +0000 Subject: [PATCH 3201/4744] chore(i18n): fallback strings for new resources: nodebb.admin-development-info --- public/language/ar/admin/development/info.json | 3 +-- public/language/az/admin/development/info.json | 3 +-- public/language/bg/admin/development/info.json | 3 +-- public/language/bn/admin/development/info.json | 3 +-- public/language/cs/admin/development/info.json | 3 +-- public/language/da/admin/development/info.json | 3 +-- public/language/de/admin/development/info.json | 3 +-- public/language/el/admin/development/info.json | 3 +-- public/language/en-US/admin/development/info.json | 3 +-- public/language/en-x-pirate/admin/development/info.json | 3 +-- public/language/es/admin/development/info.json | 3 +-- public/language/et/admin/development/info.json | 3 +-- public/language/fa-IR/admin/development/info.json | 3 +-- public/language/fi/admin/development/info.json | 3 +-- public/language/fr/admin/development/info.json | 3 +-- public/language/gl/admin/development/info.json | 3 +-- public/language/he/admin/development/info.json | 3 +-- public/language/hr/admin/development/info.json | 3 +-- public/language/hu/admin/development/info.json | 3 +-- public/language/hy/admin/development/info.json | 3 +-- public/language/id/admin/development/info.json | 3 +-- public/language/it/admin/development/info.json | 3 +-- public/language/ja/admin/development/info.json | 3 +-- public/language/ko/admin/development/info.json | 3 +-- public/language/lt/admin/development/info.json | 3 +-- public/language/lv/admin/development/info.json | 3 +-- public/language/ms/admin/development/info.json | 3 +-- public/language/nb/admin/development/info.json | 3 +-- public/language/nl/admin/development/info.json | 3 +-- public/language/nn-NO/admin/development/info.json | 3 +-- public/language/pl/admin/development/info.json | 3 +-- public/language/pt-BR/admin/development/info.json | 3 +-- public/language/pt-PT/admin/development/info.json | 3 +-- public/language/ro/admin/development/info.json | 3 +-- public/language/ru/admin/development/info.json | 3 +-- public/language/rw/admin/development/info.json | 3 +-- public/language/sc/admin/development/info.json | 3 +-- public/language/sk/admin/development/info.json | 3 +-- public/language/sl/admin/development/info.json | 3 +-- public/language/sq-AL/admin/development/info.json | 3 +-- public/language/sr/admin/development/info.json | 3 +-- public/language/sv/admin/development/info.json | 3 +-- public/language/th/admin/development/info.json | 3 +-- public/language/tr/admin/development/info.json | 3 +-- public/language/uk/admin/development/info.json | 3 +-- public/language/vi/admin/development/info.json | 3 +-- public/language/zh-CN/admin/development/info.json | 3 +-- public/language/zh-TW/admin/development/info.json | 3 +-- 48 files changed, 48 insertions(+), 96 deletions(-) diff --git a/public/language/ar/admin/development/info.json b/public/language/ar/admin/development/info.json index 386c47e050..4c97beee13 100644 --- a/public/language/ar/admin/development/info.json +++ b/public/language/ar/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/az/admin/development/info.json b/public/language/az/admin/development/info.json index 22b43c2bc7..a61eab10d3 100644 --- a/public/language/az/admin/development/info.json +++ b/public/language/az/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Bağlantı sayı", "guests": "Qonaqlar", - "info": "Məlumat", - "heap-dump": "Heap Dump" + "info": "Məlumat" } \ No newline at end of file diff --git a/public/language/bg/admin/development/info.json b/public/language/bg/admin/development/info.json index 8caea710f7..71f1e63c4b 100644 --- a/public/language/bg/admin/development/info.json +++ b/public/language/bg/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Брой връзки", "guests": "Гости", - "info": "Информация", - "heap-dump": "Heap Dump" + "info": "Информация" } \ No newline at end of file diff --git a/public/language/bn/admin/development/info.json b/public/language/bn/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/bn/admin/development/info.json +++ b/public/language/bn/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/cs/admin/development/info.json b/public/language/cs/admin/development/info.json index 2a5e981a74..19fb830e9d 100644 --- a/public/language/cs/admin/development/info.json +++ b/public/language/cs/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Hosté", - "info": "Informace", - "heap-dump": "Heap Dump" + "info": "Informace" } \ No newline at end of file diff --git a/public/language/da/admin/development/info.json b/public/language/da/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/da/admin/development/info.json +++ b/public/language/da/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/de/admin/development/info.json b/public/language/de/admin/development/info.json index 296aa0af43..b5dd27dfe5 100644 --- a/public/language/de/admin/development/info.json +++ b/public/language/de/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Verbindungsanzahl", "guests": "Gäste", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/el/admin/development/info.json b/public/language/el/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/el/admin/development/info.json +++ b/public/language/el/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/en-US/admin/development/info.json b/public/language/en-US/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/en-US/admin/development/info.json +++ b/public/language/en-US/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/development/info.json b/public/language/en-x-pirate/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/en-x-pirate/admin/development/info.json +++ b/public/language/en-x-pirate/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/es/admin/development/info.json b/public/language/es/admin/development/info.json index 92821fcf36..809d0c85bc 100644 --- a/public/language/es/admin/development/info.json +++ b/public/language/es/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Número de conexiones", "guests": "Invitados", - "info": "Información", - "heap-dump": "Heap Dump" + "info": "Información" } \ No newline at end of file diff --git a/public/language/et/admin/development/info.json b/public/language/et/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/et/admin/development/info.json +++ b/public/language/et/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/fa-IR/admin/development/info.json b/public/language/fa-IR/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/fa-IR/admin/development/info.json +++ b/public/language/fa-IR/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/fi/admin/development/info.json b/public/language/fi/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/fi/admin/development/info.json +++ b/public/language/fi/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/fr/admin/development/info.json b/public/language/fr/admin/development/info.json index 3a17f92ad9..afcbe555aa 100644 --- a/public/language/fr/admin/development/info.json +++ b/public/language/fr/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "nombre de connexions", "guests": "Invités", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/gl/admin/development/info.json b/public/language/gl/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/gl/admin/development/info.json +++ b/public/language/gl/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/he/admin/development/info.json b/public/language/he/admin/development/info.json index 0532e8febf..d20aa255f5 100644 --- a/public/language/he/admin/development/info.json +++ b/public/language/he/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "כמות חיבורים", "guests": "אורחים", - "info": "מידע", - "heap-dump": "Heap Dump" + "info": "מידע" } \ No newline at end of file diff --git a/public/language/hr/admin/development/info.json b/public/language/hr/admin/development/info.json index ebc7612366..3b21db6c9a 100644 --- a/public/language/hr/admin/development/info.json +++ b/public/language/hr/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Gosti", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/hu/admin/development/info.json b/public/language/hu/admin/development/info.json index 130d0efdda..48bf12d34b 100644 --- a/public/language/hu/admin/development/info.json +++ b/public/language/hu/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Vendégek", - "info": "Információ", - "heap-dump": "Heap Dump" + "info": "Információ" } \ No newline at end of file diff --git a/public/language/hy/admin/development/info.json b/public/language/hy/admin/development/info.json index cf5128c6fc..ca2bdf9f66 100644 --- a/public/language/hy/admin/development/info.json +++ b/public/language/hy/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Հյուրեր", - "info": "տեղեկատվություն", - "heap-dump": "Heap Dump" + "info": "տեղեկատվություն" } \ No newline at end of file diff --git a/public/language/id/admin/development/info.json b/public/language/id/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/id/admin/development/info.json +++ b/public/language/id/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/it/admin/development/info.json b/public/language/it/admin/development/info.json index 7e2ef613ef..d5bb340bc5 100644 --- a/public/language/it/admin/development/info.json +++ b/public/language/it/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Conteggio connessioni", "guests": "Ospiti", - "info": "Informazioni", - "heap-dump": "Heap Dump" + "info": "Informazioni" } \ No newline at end of file diff --git a/public/language/ja/admin/development/info.json b/public/language/ja/admin/development/info.json index 1214f8a023..857c419a66 100644 --- a/public/language/ja/admin/development/info.json +++ b/public/language/ja/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "ゲスト数", - "info": "情報", - "heap-dump": "Heap Dump" + "info": "情報" } \ No newline at end of file diff --git a/public/language/ko/admin/development/info.json b/public/language/ko/admin/development/info.json index 7184d8060f..08cb8a739b 100644 --- a/public/language/ko/admin/development/info.json +++ b/public/language/ko/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "비회원", - "info": "정보", - "heap-dump": "Heap Dump" + "info": "정보" } \ No newline at end of file diff --git a/public/language/lt/admin/development/info.json b/public/language/lt/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/lt/admin/development/info.json +++ b/public/language/lt/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/lv/admin/development/info.json b/public/language/lv/admin/development/info.json index d1798f4254..87cb89d8cf 100644 --- a/public/language/lv/admin/development/info.json +++ b/public/language/lv/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Viesi", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/ms/admin/development/info.json b/public/language/ms/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/ms/admin/development/info.json +++ b/public/language/ms/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/nb/admin/development/info.json b/public/language/nb/admin/development/info.json index fc3af734be..deb1975761 100644 --- a/public/language/nb/admin/development/info.json +++ b/public/language/nb/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Antall tilkoblinger", "guests": "Gjester", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/nl/admin/development/info.json b/public/language/nl/admin/development/info.json index 19fa4d7cfd..b5ee1a8838 100644 --- a/public/language/nl/admin/development/info.json +++ b/public/language/nl/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Gasten", - "info": "Informatie", - "heap-dump": "Heap Dump" + "info": "Informatie" } \ No newline at end of file diff --git a/public/language/nn-NO/admin/development/info.json b/public/language/nn-NO/admin/development/info.json index 30c27cddb3..2d28c9fcfb 100644 --- a/public/language/nn-NO/admin/development/info.json +++ b/public/language/nn-NO/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Tilkoplingar", "guests": "Gjestar", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/pl/admin/development/info.json b/public/language/pl/admin/development/info.json index 3a90dc57cb..a56206ad41 100644 --- a/public/language/pl/admin/development/info.json +++ b/public/language/pl/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Liczba połączeń", "guests": "Goście", - "info": "Informacja", - "heap-dump": "Heap Dump" + "info": "Informacja" } \ No newline at end of file diff --git a/public/language/pt-BR/admin/development/info.json b/public/language/pt-BR/admin/development/info.json index bc6c6aeecd..e0fabb3ddb 100644 --- a/public/language/pt-BR/admin/development/info.json +++ b/public/language/pt-BR/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Visitantes", - "info": "Informação", - "heap-dump": "Heap Dump" + "info": "Informação" } \ No newline at end of file diff --git a/public/language/pt-PT/admin/development/info.json b/public/language/pt-PT/admin/development/info.json index 5f7c5e7d6c..9dc6b6665e 100644 --- a/public/language/pt-PT/admin/development/info.json +++ b/public/language/pt-PT/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Convidados", - "info": "Informação", - "heap-dump": "Heap Dump" + "info": "Informação" } \ No newline at end of file diff --git a/public/language/ro/admin/development/info.json b/public/language/ro/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/ro/admin/development/info.json +++ b/public/language/ro/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/ru/admin/development/info.json b/public/language/ru/admin/development/info.json index 748d45e9ed..a7f1481f88 100644 --- a/public/language/ru/admin/development/info.json +++ b/public/language/ru/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Гостей", - "info": "Сырые данные", - "heap-dump": "Heap Dump" + "info": "Сырые данные" } \ No newline at end of file diff --git a/public/language/rw/admin/development/info.json b/public/language/rw/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/rw/admin/development/info.json +++ b/public/language/rw/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/sc/admin/development/info.json b/public/language/sc/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/sc/admin/development/info.json +++ b/public/language/sc/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/sk/admin/development/info.json b/public/language/sk/admin/development/info.json index eb4ce931a7..c6dbfd349e 100644 --- a/public/language/sk/admin/development/info.json +++ b/public/language/sk/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Hostia", - "info": "Informácie", - "heap-dump": "Heap Dump" + "info": "Informácie" } \ No newline at end of file diff --git a/public/language/sl/admin/development/info.json b/public/language/sl/admin/development/info.json index 36dbf4ba32..f53089170f 100644 --- a/public/language/sl/admin/development/info.json +++ b/public/language/sl/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Gostje", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/sq-AL/admin/development/info.json b/public/language/sq-AL/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/sq-AL/admin/development/info.json +++ b/public/language/sq-AL/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/sr/admin/development/info.json b/public/language/sr/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/sr/admin/development/info.json +++ b/public/language/sr/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/sv/admin/development/info.json b/public/language/sv/admin/development/info.json index 40e8fded15..9834719daf 100644 --- a/public/language/sv/admin/development/info.json +++ b/public/language/sv/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Guests", - "info": "Info", - "heap-dump": "Heap Dump" + "info": "Info" } \ No newline at end of file diff --git a/public/language/th/admin/development/info.json b/public/language/th/admin/development/info.json index f161f88b83..b78747dc04 100644 --- a/public/language/th/admin/development/info.json +++ b/public/language/th/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "จำนวนการเชื่อมต่อ", "guests": "ผู้เยี่ยมเยียน", - "info": "ข้อมูล", - "heap-dump": "Heap Dump" + "info": "ข้อมูล" } \ No newline at end of file diff --git a/public/language/tr/admin/development/info.json b/public/language/tr/admin/development/info.json index cc0eaf9242..d1bccbc674 100644 --- a/public/language/tr/admin/development/info.json +++ b/public/language/tr/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Ziyaretçiler", - "info": "Bilgi", - "heap-dump": "Heap Dump" + "info": "Bilgi" } \ No newline at end of file diff --git a/public/language/uk/admin/development/info.json b/public/language/uk/admin/development/info.json index 0db8f04563..42f90c8282 100644 --- a/public/language/uk/admin/development/info.json +++ b/public/language/uk/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Connection Count", "guests": "Гостей", - "info": "Інфо", - "heap-dump": "Heap Dump" + "info": "Інфо" } \ No newline at end of file diff --git a/public/language/vi/admin/development/info.json b/public/language/vi/admin/development/info.json index 088d0fb147..f8e2480b94 100644 --- a/public/language/vi/admin/development/info.json +++ b/public/language/vi/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "Số Lượng Kết Nối", "guests": "Khách", - "info": "Thông tin", - "heap-dump": "Heap Dump" + "info": "Thông tin" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/development/info.json b/public/language/zh-CN/admin/development/info.json index 2a7845e5ca..ac389d848e 100644 --- a/public/language/zh-CN/admin/development/info.json +++ b/public/language/zh-CN/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "连接数", "guests": "游客", - "info": "信息", - "heap-dump": "Heap Dump" + "info": "信息" } \ No newline at end of file diff --git a/public/language/zh-TW/admin/development/info.json b/public/language/zh-TW/admin/development/info.json index d57fd95abd..df3bc6cd12 100644 --- a/public/language/zh-TW/admin/development/info.json +++ b/public/language/zh-TW/admin/development/info.json @@ -22,6 +22,5 @@ "connection-count": "連線次數", "guests": "訪客", - "info": "資訊", - "heap-dump": "Heap Dump" + "info": "資訊" } \ No newline at end of file From 559a2d233de905ec56fe95eb9d51a2d260617dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Jul 2025 15:09:55 -0400 Subject: [PATCH 3202/4744] feat: add ap pageviews analytics --- public/language/en-GB/admin/dashboard.json | 1 + public/src/admin/dashboard.js | 21 ++++++++++++++++++--- src/analytics.js | 11 +++++++++++ src/controllers/admin/dashboard.js | 2 +- src/middleware/activitypub.js | 6 ++++++ src/routes/activitypub.js | 1 + src/socket.io/admin/analytics.js | 1 + 7 files changed, 39 insertions(+), 4 deletions(-) diff --git a/public/language/en-GB/admin/dashboard.json b/public/language/en-GB/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/en-GB/admin/dashboard.json +++ b/public/language/en-GB/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/src/admin/dashboard.js b/public/src/admin/dashboard.js index a2b624c5a8..5fa55a7e0d 100644 --- a/public/src/admin/dashboard.js +++ b/public/src/admin/dashboard.js @@ -165,6 +165,7 @@ function setupGraphs(callback) { t.translateKey('admin/dashboard:graphs.page-views-registered', []), t.translateKey('admin/dashboard:graphs.page-views-guest', []), t.translateKey('admin/dashboard:graphs.page-views-bot', []), + t.translateKey('admin/dashboard:graphs.page-views-ap', []), t.translateKey('admin/dashboard:graphs.unique-visitors', []), t.translateKey('admin/dashboard:graphs.registered-users', []), t.translateKey('admin/dashboard:graphs.guest-users', []), @@ -231,6 +232,18 @@ function setupGraphs(callback) { fill: 'origin', tension: tension, backgroundColor: 'rgba(151,187,205,0.2)', + borderColor: 'rgba(110, 187, 132, 1)', + pointBackgroundColor: 'rgba(110, 187, 132, 1)', + pointHoverBackgroundColor: 'rgba(110, 187, 132, 1)', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(110, 187, 132, 1)', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + { + label: translations[5], + fill: 'origin', + tension: tension, + backgroundColor: 'rgba(151,187,205,0.2)', borderColor: 'rgba(151,187,205,1)', pointBackgroundColor: 'rgba(151,187,205,1)', pointHoverBackgroundColor: 'rgba(151,187,205,1)', @@ -247,7 +260,8 @@ function setupGraphs(callback) { data.datasets[1].yAxisID = 'left-y-axis'; data.datasets[2].yAxisID = 'left-y-axis'; data.datasets[3].yAxisID = 'left-y-axis'; - data.datasets[4].yAxisID = 'right-y-axis'; + data.datasets[4].yAxisID = 'left-y-axis'; + data.datasets[5].yAxisID = 'right-y-axis'; graphs.traffic = new Chart(trafficCtx, { type: 'line', @@ -269,7 +283,7 @@ function setupGraphs(callback) { type: 'linear', title: { display: true, - text: translations[4], + text: translations[5], }, beginAtZero: true, }, @@ -446,7 +460,8 @@ function updateTrafficGraph(units, until, amount) { graphs.traffic.data.datasets[1].data = data.pageviewsRegistered; graphs.traffic.data.datasets[2].data = data.pageviewsGuest; graphs.traffic.data.datasets[3].data = data.pageviewsBot; - graphs.traffic.data.datasets[4].data = data.uniqueVisitors; + graphs.traffic.data.datasets[4].data = data.appageviews; + graphs.traffic.data.datasets[5].data = data.uniqueVisitors; graphs.traffic.data.labels = graphs.traffic.data.xLabels; graphs.traffic.update(); diff --git a/src/analytics.js b/src/analytics.js index b70aec7e5c..a0fadeab68 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -21,6 +21,7 @@ let local = { pageViewsRegistered: 0, pageViewsGuest: 0, pageViewsBot: 0, + apPageViews: 0, uniquevisitors: 0, }; const empty = _.cloneDeep(local); @@ -117,6 +118,10 @@ Analytics.pageView = async function (payload) { } }; +Analytics.apPageView = function () { + local.apPageViews += 1; +}; + Analytics.writeData = async function () { const today = new Date(); const month = new Date(); @@ -162,6 +167,12 @@ Analytics.writeData = async function () { total.pageViewsBot = 0; } + if (total.apPageViews > 0) { + incrByBulk.push(['analytics:pageviews:ap', total.apPageViews, today.getTime()]); + incrByBulk.push(['analytics:pageviews:ap:month', total.apPageViews, month.getTime()]); + total.apPageViews = 0; + } + if (total.uniquevisitors > 0) { incrByBulk.push(['analytics:uniquevisitors', total.uniquevisitors, today.getTime()]); total.uniquevisitors = 0; diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index aa173eca07..20f31c2b67 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -91,7 +91,7 @@ async function getLatestVersion() { dashboardController.getAnalytics = async (req, res, next) => { // Basic validation const validUnits = ['days', 'hours']; - const validSets = ['uniquevisitors', 'pageviews', 'pageviews:registered', 'pageviews:bot', 'pageviews:guest']; + const validSets = ['uniquevisitors', 'pageviews', 'pageviews:registered', 'pageviews:bot', 'pageviews:guest', 'pageviews:ap']; const until = req.query.until ? new Date(parseInt(req.query.until, 10)) : Date.now(); const count = req.query.count || (req.query.units === 'hours' ? 24 : 30); if (isNaN(until) || !validUnits.includes(req.query.units)) { diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index b38caa552a..4390335c0b 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -3,11 +3,17 @@ const db = require('../database'); const meta = require('../meta'); const activitypub = require('../activitypub'); +const analytics = require('../analytics'); const middleware = module.exports; middleware.enabled = async (req, res, next) => next(!meta.config.activitypubEnabled ? 'route' : undefined); +middleware.pageview = async (req, res, next) => { + analytics.apPageView(); + next(); +}; + middleware.assertS2S = async function (req, res, next) { // For whatever reason, express accepts does not recognize "profile" as a valid differentiator // Therefore, manual header parsing is used here. diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 760287e102..815fd738ef 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -14,6 +14,7 @@ module.exports = function (app, middleware, controllers) { const middlewares = [ middleware.activitypub.enabled, + middleware.activitypub.pageview, middleware.activitypub.assertS2S, middleware.activitypub.verify, middleware.activitypub.configureResponse, diff --git a/src/socket.io/admin/analytics.js b/src/socket.io/admin/analytics.js index 8af8881873..03cffb6d20 100644 --- a/src/socket.io/admin/analytics.js +++ b/src/socket.io/admin/analytics.js @@ -30,6 +30,7 @@ Analytics.get = async function (socket, data) { pageviewsRegistered: getStats('analytics:pageviews:registered', until, data.amount), pageviewsGuest: getStats('analytics:pageviews:guest', until, data.amount), pageviewsBot: getStats('analytics:pageviews:bot', until, data.amount), + appageviews: getStats('analytics:pageviews:ap', until, data.amount), summary: analytics.getSummary(), }); result.pastDay = result.pageviews.reduce((a, b) => parseInt(a, 10) + parseInt(b, 10)); From 5d16fdc93f7469d000720c8accd2ebdbd4e82864 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 11 Jul 2025 19:10:21 +0000 Subject: [PATCH 3203/4744] chore(i18n): fallback strings for new resources: nodebb.admin-dashboard --- public/language/ar/admin/dashboard.json | 1 + public/language/az/admin/dashboard.json | 1 + public/language/bg/admin/dashboard.json | 1 + public/language/bn/admin/dashboard.json | 1 + public/language/cs/admin/dashboard.json | 1 + public/language/da/admin/dashboard.json | 1 + public/language/de/admin/dashboard.json | 1 + public/language/el/admin/dashboard.json | 1 + public/language/en-US/admin/dashboard.json | 1 + public/language/en-x-pirate/admin/dashboard.json | 1 + public/language/es/admin/dashboard.json | 1 + public/language/et/admin/dashboard.json | 1 + public/language/fa-IR/admin/dashboard.json | 1 + public/language/fi/admin/dashboard.json | 1 + public/language/fr/admin/dashboard.json | 1 + public/language/gl/admin/dashboard.json | 1 + public/language/he/admin/dashboard.json | 1 + public/language/hr/admin/dashboard.json | 1 + public/language/hu/admin/dashboard.json | 1 + public/language/hy/admin/dashboard.json | 1 + public/language/id/admin/dashboard.json | 1 + public/language/it/admin/dashboard.json | 1 + public/language/ja/admin/dashboard.json | 1 + public/language/ko/admin/dashboard.json | 1 + public/language/lt/admin/dashboard.json | 1 + public/language/lv/admin/dashboard.json | 1 + public/language/ms/admin/dashboard.json | 1 + public/language/nb/admin/dashboard.json | 1 + public/language/nl/admin/dashboard.json | 1 + public/language/nn-NO/admin/dashboard.json | 1 + public/language/pl/admin/dashboard.json | 1 + public/language/pt-BR/admin/dashboard.json | 1 + public/language/pt-PT/admin/dashboard.json | 1 + public/language/ro/admin/dashboard.json | 1 + public/language/ru/admin/dashboard.json | 1 + public/language/rw/admin/dashboard.json | 1 + public/language/sc/admin/dashboard.json | 1 + public/language/sk/admin/dashboard.json | 1 + public/language/sl/admin/dashboard.json | 1 + public/language/sq-AL/admin/dashboard.json | 1 + public/language/sr/admin/dashboard.json | 1 + public/language/sv/admin/dashboard.json | 1 + public/language/th/admin/dashboard.json | 1 + public/language/tr/admin/dashboard.json | 1 + public/language/uk/admin/dashboard.json | 1 + public/language/vi/admin/dashboard.json | 1 + public/language/zh-CN/admin/dashboard.json | 1 + public/language/zh-TW/admin/dashboard.json | 1 + 48 files changed, 48 insertions(+) diff --git a/public/language/ar/admin/dashboard.json b/public/language/ar/admin/dashboard.json index b44c50d859..fe7d88a0c9 100644 --- a/public/language/ar/admin/dashboard.json +++ b/public/language/ar/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "زيارات الصفحات المسجلة", "graphs.page-views-guest": "زيارات الصفحات للزوار", "graphs.page-views-bot": "زيارات الصفحات الآلية", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "زوار فريدين", "graphs.registered-users": "مستخدمين مسجلين", "graphs.guest-users": "المستخدمين الزوار", diff --git a/public/language/az/admin/dashboard.json b/public/language/az/admin/dashboard.json index 8e7b0253d4..ec74e422a9 100644 --- a/public/language/az/admin/dashboard.json +++ b/public/language/az/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Səhifə Baxışları qeydə alınıb", "graphs.page-views-guest": "Səhifə baxışı qonaq", "graphs.page-views-bot": "Səhifə baxış botu", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Unikal ziyarətçilər", "graphs.registered-users": "Qeydiyyatdan keçmiş istifadəçilər", "graphs.guest-users": "Qonaqlar", diff --git a/public/language/bg/admin/dashboard.json b/public/language/bg/admin/dashboard.json index d7839ed1ff..6473ec0b24 100644 --- a/public/language/bg/admin/dashboard.json +++ b/public/language/bg/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Преглеждания на страниците от регистрирани потребители", "graphs.page-views-guest": "Преглеждания на страниците от гости", "graphs.page-views-bot": "Преглеждания на страниците от ботове", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Уникални посетители", "graphs.registered-users": "Регистрирани потребители", "graphs.guest-users": "Гости", diff --git a/public/language/bn/admin/dashboard.json b/public/language/bn/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/bn/admin/dashboard.json +++ b/public/language/bn/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/cs/admin/dashboard.json b/public/language/cs/admin/dashboard.json index ad5fd7cd94..7fccda35b3 100644 --- a/public/language/cs/admin/dashboard.json +++ b/public/language/cs/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Zobrazených stránek/registrovaní", "graphs.page-views-guest": "Zobrazených stránek/hosté", "graphs.page-views-bot": "Zobrazených stránek/bot", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Jedineční návštěvníci", "graphs.registered-users": "Registrovaní uživatelé", "graphs.guest-users": "Guest Users", diff --git a/public/language/da/admin/dashboard.json b/public/language/da/admin/dashboard.json index 98aeb80e34..4774deb49f 100644 --- a/public/language/da/admin/dashboard.json +++ b/public/language/da/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/de/admin/dashboard.json b/public/language/de/admin/dashboard.json index 9229f720aa..4067ac8311 100644 --- a/public/language/de/admin/dashboard.json +++ b/public/language/de/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Registrierte Seitenaufrufe", "graphs.page-views-guest": "Seitenaufrufe von Gästen", "graphs.page-views-bot": "Seitenaufrufe von Bots", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Verschiedene Besucher", "graphs.registered-users": "Registrierte Benutzer", "graphs.guest-users": "Gast-Benutzer", diff --git a/public/language/el/admin/dashboard.json b/public/language/el/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/el/admin/dashboard.json +++ b/public/language/el/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/en-US/admin/dashboard.json b/public/language/en-US/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/en-US/admin/dashboard.json +++ b/public/language/en-US/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/en-x-pirate/admin/dashboard.json b/public/language/en-x-pirate/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/en-x-pirate/admin/dashboard.json +++ b/public/language/en-x-pirate/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/es/admin/dashboard.json b/public/language/es/admin/dashboard.json index 791546b5ee..211df77bc0 100644 --- a/public/language/es/admin/dashboard.json +++ b/public/language/es/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Vistas de la página registradas", "graphs.page-views-guest": "Vistas de la página visitantes", "graphs.page-views-bot": "Vistas de la página Bot", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Visitantes Unicos", "graphs.registered-users": "Usuarios Registrados", "graphs.guest-users": "Guest Users", diff --git a/public/language/et/admin/dashboard.json b/public/language/et/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/et/admin/dashboard.json +++ b/public/language/et/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/fa-IR/admin/dashboard.json b/public/language/fa-IR/admin/dashboard.json index 77ff6d56e8..b5f458d0c0 100644 --- a/public/language/fa-IR/admin/dashboard.json +++ b/public/language/fa-IR/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Page Views Registered", "graphs.page-views-guest": "Page Views Guest", "graphs.page-views-bot": "ربات بازدید از صفحه", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "بازدیدکنندگان منحصر به فرد", "graphs.registered-users": "کاربران ثبت نام شده", "graphs.guest-users": "کاربران مهمان", diff --git a/public/language/fi/admin/dashboard.json b/public/language/fi/admin/dashboard.json index 83c4b42c58..57c63fdf9d 100644 --- a/public/language/fi/admin/dashboard.json +++ b/public/language/fi/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Kirjautuneiden sivulatausta", "graphs.page-views-guest": "Vieraiden sivulatausta", "graphs.page-views-bot": "Bottien sivulatausta", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Ainutalaatuista kävijää", "graphs.registered-users": "Rekisteröitynyttä käyttäjää", "graphs.guest-users": "Vieraskäyttäjää", diff --git a/public/language/fr/admin/dashboard.json b/public/language/fr/admin/dashboard.json index eb7bc530e3..6206354f7b 100644 --- a/public/language/fr/admin/dashboard.json +++ b/public/language/fr/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Membres", "graphs.page-views-guest": "Invités", "graphs.page-views-bot": "Robots", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Visiteurs uniques", "graphs.registered-users": "Utilisateurs enregistrés", "graphs.guest-users": "Utilisateurs invités", diff --git a/public/language/gl/admin/dashboard.json b/public/language/gl/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/gl/admin/dashboard.json +++ b/public/language/gl/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/he/admin/dashboard.json b/public/language/he/admin/dashboard.json index bdaa869915..17233a0e35 100644 --- a/public/language/he/admin/dashboard.json +++ b/public/language/he/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "צפיות בדפים-רשומים", "graphs.page-views-guest": "צפיות בדפים-אורחים", "graphs.page-views-bot": "צפיות בדפים-בוטים", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "מבקרים ייחודיים", "graphs.registered-users": "משתמשים רשומים", "graphs.guest-users": "משתמשים אורחים", diff --git a/public/language/hr/admin/dashboard.json b/public/language/hr/admin/dashboard.json index 1e4fedf429..e1c060d8b7 100644 --- a/public/language/hr/admin/dashboard.json +++ b/public/language/hr/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Jedninstveni posjetitelji", "graphs.registered-users": "Registrirani korisnici", "graphs.guest-users": "Guest Users", diff --git a/public/language/hu/admin/dashboard.json b/public/language/hu/admin/dashboard.json index 1016b5b833..0427cd36c6 100644 --- a/public/language/hu/admin/dashboard.json +++ b/public/language/hu/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Regisztrált látogatások", "graphs.page-views-guest": "Vendég látogatások", "graphs.page-views-bot": "Bot látogatások", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Egyedi látogatók", "graphs.registered-users": "Regisztrált felhasználók", "graphs.guest-users": "Vendég Felhasználók", diff --git a/public/language/hy/admin/dashboard.json b/public/language/hy/admin/dashboard.json index 5e4f140474..b6eef0c2ee 100644 --- a/public/language/hy/admin/dashboard.json +++ b/public/language/hy/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Գրանցված Էջի դիտումներ ", "graphs.page-views-guest": "Էջի դիտումներ Հյուր", "graphs.page-views-bot": "Էջի դիտումների բոտ", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Եզակի այցելուներ", "graphs.registered-users": "Գրանցված օգտատերեր", "graphs.guest-users": "Հյուր օգտատերեր", diff --git a/public/language/id/admin/dashboard.json b/public/language/id/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/id/admin/dashboard.json +++ b/public/language/id/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/it/admin/dashboard.json b/public/language/it/admin/dashboard.json index 691b2914e7..46188e52a9 100644 --- a/public/language/it/admin/dashboard.json +++ b/public/language/it/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Pagine viste Registrati", "graphs.page-views-guest": "Pagine viste Ospite", "graphs.page-views-bot": "Pagine viste Bot", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Visitatori Unici", "graphs.registered-users": "Utenti Registrati", "graphs.guest-users": "Utenti ospiti", diff --git a/public/language/ja/admin/dashboard.json b/public/language/ja/admin/dashboard.json index 60e2fad225..f9354ce880 100644 --- a/public/language/ja/admin/dashboard.json +++ b/public/language/ja/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "ページビュー登録済み", "graphs.page-views-guest": "ページビューゲスト", "graphs.page-views-bot": "ページビューBot", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "ユニークな訪問者", "graphs.registered-users": "登録したユーザー", "graphs.guest-users": "Guest Users", diff --git a/public/language/ko/admin/dashboard.json b/public/language/ko/admin/dashboard.json index b0dd16eb86..8828269697 100644 --- a/public/language/ko/admin/dashboard.json +++ b/public/language/ko/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "등록된 사용자 페이지 뷰", "graphs.page-views-guest": "비회원 페이지 뷰", "graphs.page-views-bot": "봇 페이지 뷰", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "고유 방문자", "graphs.registered-users": "등록된 사용자", "graphs.guest-users": "비회원 사용자", diff --git a/public/language/lt/admin/dashboard.json b/public/language/lt/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/lt/admin/dashboard.json +++ b/public/language/lt/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/lv/admin/dashboard.json b/public/language/lv/admin/dashboard.json index ee1358bb7b..4f55f41c5d 100644 --- a/public/language/lv/admin/dashboard.json +++ b/public/language/lv/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Lapu skatījumi no lietotājiem", "graphs.page-views-guest": "Lapu skatījumi no viesiem", "graphs.page-views-bot": "Lapu skatījumi no botiem", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Unikālie apmeklētāji", "graphs.registered-users": "Reģistrētie lietotāji", "graphs.guest-users": "Guest Users", diff --git a/public/language/ms/admin/dashboard.json b/public/language/ms/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/ms/admin/dashboard.json +++ b/public/language/ms/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/nb/admin/dashboard.json b/public/language/nb/admin/dashboard.json index 8662f49f25..2a2e0ee67b 100644 --- a/public/language/nb/admin/dashboard.json +++ b/public/language/nb/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Sidevisninger for registrerte", "graphs.page-views-guest": "Sidevisninger for gjester", "graphs.page-views-bot": "Sidevisninger for botter", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Unike besøkende", "graphs.registered-users": "Registrerte brukere", "graphs.guest-users": "Gjestebrukere", diff --git a/public/language/nl/admin/dashboard.json b/public/language/nl/admin/dashboard.json index 63bae0694f..d0b9c8db04 100644 --- a/public/language/nl/admin/dashboard.json +++ b/public/language/nl/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unieke bezoekers", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/nn-NO/admin/dashboard.json b/public/language/nn-NO/admin/dashboard.json index c852084c8f..80004f87e3 100644 --- a/public/language/nn-NO/admin/dashboard.json +++ b/public/language/nn-NO/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Diagram over sidevisningar (registrerte)", "graphs.page-views-guest": "Diagram over sidevisningar (gjestar)", "graphs.page-views-bot": "Diagram over sidevisningar (botar)", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Diagram over unike besøkande", "graphs.registered-users": "Diagram over registrerte brukarar", "graphs.guest-users": "Diagram over gjestar", diff --git a/public/language/pl/admin/dashboard.json b/public/language/pl/admin/dashboard.json index 5e7684f78a..9e65a94b2f 100644 --- a/public/language/pl/admin/dashboard.json +++ b/public/language/pl/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Wyświetlenia użytkowników", "graphs.page-views-guest": "Wyświetlenia gości", "graphs.page-views-bot": "Wyświetlenia botów", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Unikalni użytkownicy", "graphs.registered-users": "Zarejestrowani użytkownicy", "graphs.guest-users": "Użytkownicy-goście", diff --git a/public/language/pt-BR/admin/dashboard.json b/public/language/pt-BR/admin/dashboard.json index 49dd02ba69..0e42f09063 100644 --- a/public/language/pt-BR/admin/dashboard.json +++ b/public/language/pt-BR/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Páginas Visualizadas por Registrados", "graphs.page-views-guest": "Páginas Visualizadas por Visitantes", "graphs.page-views-bot": "Páginas Visualizadas por Bot", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Visitantes Únicos", "graphs.registered-users": "Usuários Registrados", "graphs.guest-users": "Guest Users", diff --git a/public/language/pt-PT/admin/dashboard.json b/public/language/pt-PT/admin/dashboard.json index 6f82ecb7e2..64fda774f5 100644 --- a/public/language/pt-PT/admin/dashboard.json +++ b/public/language/pt-PT/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Visualizações de páginas por utilizadores registados", "graphs.page-views-guest": "Visualizações de páginas por convidados", "graphs.page-views-bot": "Visualizações de páginas por bots", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Visitantes únicos", "graphs.registered-users": "Utilizadores Registados", "graphs.guest-users": "Guest Users", diff --git a/public/language/ro/admin/dashboard.json b/public/language/ro/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/ro/admin/dashboard.json +++ b/public/language/ro/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/ru/admin/dashboard.json b/public/language/ru/admin/dashboard.json index 137241c469..846d395675 100644 --- a/public/language/ru/admin/dashboard.json +++ b/public/language/ru/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Просм. авторизованными", "graphs.page-views-guest": "Просмотров гостями", "graphs.page-views-bot": "Просмотров ботами", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Уникальных посетителей", "graphs.registered-users": "Авторизованных пользователей", "graphs.guest-users": "Неавторизированных посетителей", diff --git a/public/language/rw/admin/dashboard.json b/public/language/rw/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/rw/admin/dashboard.json +++ b/public/language/rw/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/sc/admin/dashboard.json b/public/language/sc/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/sc/admin/dashboard.json +++ b/public/language/sc/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/sk/admin/dashboard.json b/public/language/sk/admin/dashboard.json index e979c81301..55c1d314cc 100644 --- a/public/language/sk/admin/dashboard.json +++ b/public/language/sk/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unikátny navštevníci", "graphs.registered-users": "Zarestrovaný užívatelia", "graphs.guest-users": "Guest Users", diff --git a/public/language/sl/admin/dashboard.json b/public/language/sl/admin/dashboard.json index 7d36506d8f..5504608a86 100644 --- a/public/language/sl/admin/dashboard.json +++ b/public/language/sl/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Ogledov strani-registrirani", "graphs.page-views-guest": "Ogledov strani-gosti", "graphs.page-views-bot": "Ogledov strani-robot", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Edinstveni obiskovalci", "graphs.registered-users": "Registrirani uporabniki", "graphs.guest-users": "Gostujoči uporabniki", diff --git a/public/language/sq-AL/admin/dashboard.json b/public/language/sq-AL/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/sq-AL/admin/dashboard.json +++ b/public/language/sq-AL/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/sr/admin/dashboard.json b/public/language/sr/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/sr/admin/dashboard.json +++ b/public/language/sr/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/sv/admin/dashboard.json b/public/language/sv/admin/dashboard.json index 6ad973f5f3..0be6d5866c 100644 --- a/public/language/sv/admin/dashboard.json +++ b/public/language/sv/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Unique Visitors", "graphs.registered-users": "Registered Users", "graphs.guest-users": "Guest Users", diff --git a/public/language/th/admin/dashboard.json b/public/language/th/admin/dashboard.json index 5049bb87e1..fb36057e85 100644 --- a/public/language/th/admin/dashboard.json +++ b/public/language/th/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "ยอดวิวจากผู้ลงทะเบียนแล้ว", "graphs.page-views-guest": "ยอดวิวจากผู้มาเยือน", "graphs.page-views-bot": "ยอดวิวจากบอต", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "จำนวนผู้ใช้ที่ไม่ซ้ำกัน", "graphs.registered-users": "ผู้ใช้ที่ลงทะเบียนแล้ว", "graphs.guest-users": "ผู้ใช้ที่เป็นผู้มาเยือน", diff --git a/public/language/tr/admin/dashboard.json b/public/language/tr/admin/dashboard.json index a6ef8394c2..1c40d281c9 100644 --- a/public/language/tr/admin/dashboard.json +++ b/public/language/tr/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Kayıtlı Kullanıcıların Sayfa Gösterimi", "graphs.page-views-guest": "Ziyaretçilerin Sayfa Gösterimi", "graphs.page-views-bot": "Bot Sayfa Gösterimi", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Benzersiz Ziyaretçiler", "graphs.registered-users": "Kayıtlı Kullanıcılar", "graphs.guest-users": "Misafir Kullanıcılar", diff --git a/public/language/uk/admin/dashboard.json b/public/language/uk/admin/dashboard.json index 6f8ce9fa8a..ab440180bb 100644 --- a/public/language/uk/admin/dashboard.json +++ b/public/language/uk/admin/dashboard.json @@ -75,6 +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.unique-visitors": "Унікальні відвідувачі", "graphs.registered-users": "Зареєстровані користувачі", "graphs.guest-users": "Guest Users", diff --git a/public/language/vi/admin/dashboard.json b/public/language/vi/admin/dashboard.json index 6d2a736380..0117515771 100644 --- a/public/language/vi/admin/dashboard.json +++ b/public/language/vi/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "Đã Đăng Ký Xem Trang", "graphs.page-views-guest": "Khách Xem Trang", "graphs.page-views-bot": "Bot Xem Trang", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "Khách Độc Lập", "graphs.registered-users": "Thành Viên Chính Thức", "graphs.guest-users": "Người dùng khách", diff --git a/public/language/zh-CN/admin/dashboard.json b/public/language/zh-CN/admin/dashboard.json index 2214fb1dfe..aae77c7825 100644 --- a/public/language/zh-CN/admin/dashboard.json +++ b/public/language/zh-CN/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "注册用户页面浏览量", "graphs.page-views-guest": "游客页面浏览量", "graphs.page-views-bot": "爬虫页面浏览量", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "单一访客", "graphs.registered-users": "已注册用户", "graphs.guest-users": "游客", diff --git a/public/language/zh-TW/admin/dashboard.json b/public/language/zh-TW/admin/dashboard.json index 4cc161c595..acf93cadda 100644 --- a/public/language/zh-TW/admin/dashboard.json +++ b/public/language/zh-TW/admin/dashboard.json @@ -75,6 +75,7 @@ "graphs.page-views-registered": "註冊使用者頁面瀏覽量", "graphs.page-views-guest": "訪客頁面瀏覽量", "graphs.page-views-bot": "爬蟲頁面瀏覽量", + "graphs.page-views-ap": "ActivityPub Page Views", "graphs.unique-visitors": "不重複訪客", "graphs.registered-users": "已註冊使用者", "graphs.guest-users": "Guest Users", From 020e0ad12eee643725987457a67cda67fba719a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Jul 2025 15:18:44 -0400 Subject: [PATCH 3204/4744] test: add openapi spec --- public/openapi/read/admin/analytics.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/openapi/read/admin/analytics.yaml b/public/openapi/read/admin/analytics.yaml index 508325aace..67f8ed516c 100644 --- a/public/openapi/read/admin/analytics.yaml +++ b/public/openapi/read/admin/analytics.yaml @@ -53,6 +53,10 @@ get: items: type: number pageviews:guest: + type: array + items: + type: number + pageviews:ap: type: array items: type: number \ No newline at end of file From 01f2effcedc3f8800bcd65e26fce44d1b8174da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 11 Jul 2025 15:38:21 -0400 Subject: [PATCH 3205/4744] fix: add missing ap pageview middleware --- src/routes/activitypub.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 815fd738ef..c09b7b9821 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -3,8 +3,14 @@ const helpers = require('./helpers'); module.exports = function (app, middleware, controllers) { - helpers.setupPageRoute(app, '/world', [middleware.activitypub.enabled], controllers.activitypub.topics.list); - helpers.setupPageRoute(app, '/ap', [middleware.activitypub.enabled], controllers.activitypub.fetch); + helpers.setupPageRoute(app, '/world', [ + middleware.activitypub.enabled, + middleware.activitypub.pageview, + ], controllers.activitypub.topics.list); + helpers.setupPageRoute(app, '/ap', [ + middleware.activitypub.enabled, + middleware.activitypub.pageview, + ], controllers.activitypub.fetch); /** * The following controllers only respond if the sender is making an json+activitypub style call (i.e. S2S-only) From 32e4db8ea8e18b5de6934d9aaf48e50045f4ebff Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 12 Jul 2025 09:19:22 +0000 Subject: [PATCH 3206/4744] Latest translations and fallbacks --- public/language/fi/admin/settings/activitypub.json | 10 +++++----- public/language/fi/world.json | 12 ++++++------ .../language/fr/admin/manage/user-custom-fields.json | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/public/language/fi/admin/settings/activitypub.json b/public/language/fi/admin/settings/activitypub.json index 94f9ad7822..9f4cefb5a5 100644 --- a/public/language/fi/admin/settings/activitypub.json +++ b/public/language/fi/admin/settings/activitypub.json @@ -1,18 +1,18 @@ { - "intro-lead": "What is Federation?", + "intro-lead": "Mikä on federaatio?", "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.)", - "general": "General", - "pruning": "Content Pruning", + "general": "Yleinen", + "pruning": "Sisällön karsiminen", "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": "Ota federointi käyttöön", "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.", - "probe": "Open in App", + "probe": "Avaa sovelluksessa", "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)", diff --git a/public/language/fi/world.json b/public/language/fi/world.json index 7fdb1569f2..8af989e006 100644 --- a/public/language/fi/world.json +++ b/public/language/fi/world.json @@ -1,12 +1,12 @@ { "name": "World", - "popular": "Popular topics", - "recent": "All topics", - "help": "Help", + "popular": "Suositut aiheet", + "recent": "Kaikki aiheet", + "help": "Apua", - "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.title": "Mikä tämä sivu on?", + "help.intro": "Tervetuloa omaan fediverse kulmaasi.", + "help.fediverse": "\"Fediverse\" on verkko, joka mahdollistaa sovellusten ja sivustojen keskustella keskenään ja näiden käyttäjien nähdä toisensa. Tämä foorumi on yhteydessä tähän verkkoon ja pystyy yhdistymään muun sosiaalinen verkon(\"fediverse\") kanssa. Tämä sivusto on sinun henkilökohtainen kulma fediverseä ja se sisältään pelkästään seuraamiesi käyttäjien luomia ja jakamia aiheita.", "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!", diff --git a/public/language/fr/admin/manage/user-custom-fields.json b/public/language/fr/admin/manage/user-custom-fields.json index dab10670d2..cd7ec8a52d 100644 --- a/public/language/fr/admin/manage/user-custom-fields.json +++ b/public/language/fr/admin/manage/user-custom-fields.json @@ -1,8 +1,8 @@ { - "title": "Manage Custom User Fields", - "create-field": "Create Field", - "edit-field": "Edit Field", - "manage-custom-fields": "Manage Custom Fields", + "title": "Gérer les champs utilisateur personnalisés", + "create-field": "Créer un champ", + "edit-field": "Editer un champ", + "manage-custom-fields": "Gérer les champs personnalisés", "type-of-input": "Type of input", "key": "Key", "name": "Name", From 352f4a0c35252778399fe85d590154c710908df4 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 13 Jul 2025 09:19:24 +0000 Subject: [PATCH 3207/4744] Latest translations and fallbacks --- public/language/bg/admin/dashboard.json | 2 +- public/language/fi/admin/settings/activitypub.json | 2 +- public/language/vi/admin/dashboard.json | 2 +- public/language/zh-CN/admin/dashboard.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/language/bg/admin/dashboard.json b/public/language/bg/admin/dashboard.json index 6473ec0b24..f749044a69 100644 --- a/public/language/bg/admin/dashboard.json +++ b/public/language/bg/admin/dashboard.json @@ -75,7 +75,7 @@ "graphs.page-views-registered": "Преглеждания на страниците от регистрирани потребители", "graphs.page-views-guest": "Преглеждания на страниците от гости", "graphs.page-views-bot": "Преглеждания на страниците от ботове", - "graphs.page-views-ap": "ActivityPub Page Views", + "graphs.page-views-ap": "Преглеждания на страницата от ActivityPub", "graphs.unique-visitors": "Уникални посетители", "graphs.registered-users": "Регистрирани потребители", "graphs.guest-users": "Гости", diff --git a/public/language/fi/admin/settings/activitypub.json b/public/language/fi/admin/settings/activitypub.json index 9f4cefb5a5..c14826527c 100644 --- a/public/language/fi/admin/settings/activitypub.json +++ b/public/language/fi/admin/settings/activitypub.json @@ -1,6 +1,6 @@ { "intro-lead": "Mikä on federaatio?", - "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-body": "NodeBB pystyy yhdistämään muihin NodeBB instansseihin, jotka tukevat tätä ominaisuutta. Tämä on toteutettuActivityPub protokollaa käyttäen ja, jos se on aktivoitu, NodeBB pystyy yhdistämään myös toisten sovellusten ja sivustojen kanssa jotka tukevat ActivityPub Protokollaa( esim. Mastodon, Peertube jne.) ", "general": "Yleinen", "pruning": "Sisällön karsiminen", "content-pruning": "Days to keep remote content", diff --git a/public/language/vi/admin/dashboard.json b/public/language/vi/admin/dashboard.json index 0117515771..d12d7771e2 100644 --- a/public/language/vi/admin/dashboard.json +++ b/public/language/vi/admin/dashboard.json @@ -75,7 +75,7 @@ "graphs.page-views-registered": "Đã Đăng Ký Xem Trang", "graphs.page-views-guest": "Khách Xem Trang", "graphs.page-views-bot": "Bot Xem Trang", - "graphs.page-views-ap": "ActivityPub Page Views", + "graphs.page-views-ap": "Lượt Xem Trang ActivityPub", "graphs.unique-visitors": "Khách Độc Lập", "graphs.registered-users": "Thành Viên Chính Thức", "graphs.guest-users": "Người dùng khách", diff --git a/public/language/zh-CN/admin/dashboard.json b/public/language/zh-CN/admin/dashboard.json index aae77c7825..df503c4369 100644 --- a/public/language/zh-CN/admin/dashboard.json +++ b/public/language/zh-CN/admin/dashboard.json @@ -75,7 +75,7 @@ "graphs.page-views-registered": "注册用户页面浏览量", "graphs.page-views-guest": "游客页面浏览量", "graphs.page-views-bot": "爬虫页面浏览量", - "graphs.page-views-ap": "ActivityPub Page Views", + "graphs.page-views-ap": "ActivityPub 页面浏览量", "graphs.unique-visitors": "单一访客", "graphs.registered-users": "已注册用户", "graphs.guest-users": "游客", From e5de79ff7db451b9c9517b7336b7cb94046d5fae Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 14 Jul 2025 09:19:32 +0000 Subject: [PATCH 3208/4744] Latest translations and fallbacks --- public/language/pl/admin/dashboard.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/pl/admin/dashboard.json b/public/language/pl/admin/dashboard.json index 9e65a94b2f..48bc39c7bd 100644 --- a/public/language/pl/admin/dashboard.json +++ b/public/language/pl/admin/dashboard.json @@ -75,7 +75,7 @@ "graphs.page-views-registered": "Wyświetlenia użytkowników", "graphs.page-views-guest": "Wyświetlenia gości", "graphs.page-views-bot": "Wyświetlenia botów", - "graphs.page-views-ap": "ActivityPub Page Views", + "graphs.page-views-ap": "Wyświetlenia strony ActivityPub", "graphs.unique-visitors": "Unikalni użytkownicy", "graphs.registered-users": "Zarejestrowani użytkownicy", "graphs.guest-users": "Użytkownicy-goście", From e838bb268f24e71a37b3ecec8064be39e73e1137 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:24:42 -0400 Subject: [PATCH 3209/4744] fix(deps): update dependency cron to v4.3.2 (#13546) 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 b3c3f78258..2f6a01663f 100644 --- a/install/package.json +++ b/install/package.json @@ -61,7 +61,7 @@ "connect-pg-simple": "10.0.0", "connect-redis": "9.0.0", "cookie-parser": "1.4.7", - "cron": "4.3.1", + "cron": "4.3.2", "cropperjs": "1.6.2", "csrf-sync": "4.2.1", "daemon": "1.1.0", From d8c26bec45d033b48e6ef3094d271729631dcce9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:24:51 -0400 Subject: [PATCH 3210/4744] fix(deps): update dependency webpack to v5.100.1 (#13544) 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 2f6a01663f..940d153ac9 100644 --- a/install/package.json +++ b/install/package.json @@ -147,7 +147,7 @@ "tough-cookie": "5.1.2", "undici": "^7.10.0", "validator": "13.15.15", - "webpack": "5.100.0", + "webpack": "5.100.1", "webpack-merge": "6.0.1", "winston": "3.17.0", "workerpool": "9.3.3", From 97a5d54387a174b028a74b40be67d6fae5fe682a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:25:17 -0400 Subject: [PATCH 3211/4744] chore(deps): update dependency @eslint/js to v9.31.0 (#13545) 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 940d153ac9..f309b7828a 100644 --- a/install/package.json +++ b/install/package.json @@ -161,7 +161,7 @@ "@commitlint/cli": "19.8.1", "@commitlint/config-angular": "19.8.1", "coveralls": "3.1.1", - "@eslint/js": "9.30.1", + "@eslint/js": "9.31.0", "@stylistic/eslint-plugin": "5.1.0", "eslint-config-nodebb": "1.1.9", "eslint-plugin-import": "2.32.0", From 1ad97ac19425a096b7720586b4753e61c90cdc6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 15 Jul 2025 13:02:46 -0400 Subject: [PATCH 3212/4744] refactor: closes #13547, process user uploads via batch reduce processed user count to 100 per batch --- .../4.3.0/normalize_thumbs_uploads.js | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/upgrades/4.3.0/normalize_thumbs_uploads.js b/src/upgrades/4.3.0/normalize_thumbs_uploads.js index ef12d54f81..3a33daee9f 100644 --- a/src/upgrades/4.3.0/normalize_thumbs_uploads.js +++ b/src/upgrades/4.3.0/normalize_thumbs_uploads.js @@ -89,34 +89,36 @@ module.exports = { const keys = uids.map(uid => `uid:${uid}:uploads`); const userUploadData = await db.getSortedSetsMembersWithScores(keys); - const bulkAdd = []; - const bulkRemove = []; - const promises = []; - userUploadData.forEach((userUploads, idx) => { + await Promise.all(userUploadData.map(async (allUserUploads, idx) => { const uid = uids[idx]; - if (Array.isArray(userUploads)) { - userUploads.forEach((userUpload) => { - const normalizedPath = normalizePath(userUpload.value); - if (normalizedPath !== userUpload.value) { - bulkAdd.push([`uid:${uid}:uploads`, userUpload.score, normalizedPath]); - promises.push(db.setObjectField(`upload:${md5(normalizedPath)}`, 'uid', uid)); + if (Array.isArray(allUserUploads)) { + await batch.processArray(allUserUploads, async (userUploads) => { + const bulkAdd = []; + const bulkRemove = []; + const promises = []; + userUploads.forEach((userUpload) => { + const normalizedPath = normalizePath(userUpload.value); + if (normalizedPath !== userUpload.value) { + bulkAdd.push([`uid:${uid}:uploads`, userUpload.score, normalizedPath]); + promises.push(db.setObjectField(`upload:${md5(normalizedPath)}`, 'uid', uid)); - bulkRemove.push([`uid:${uid}:uploads`, userUpload.value]); - promises.push(db.delete(`upload:${md5(userUpload.value)}`)); - } + bulkRemove.push([`uid:${uid}:uploads`, userUpload.value]); + promises.push(db.delete(`upload:${md5(userUpload.value)}`)); + } + }); + await Promise.all(promises); + await db.sortedSetRemoveBulk(bulkRemove); + await db.sortedSetAddBulk(bulkAdd); + }, { + batch: 500, }); - } - }); - - await Promise.all(promises); - await db.sortedSetRemoveBulk(bulkRemove); - await db.sortedSetAddBulk(bulkAdd); + })); progress.incr(uids.length); }, { - batch: 500, + batch: 100, }); }, }; From a08551a5e1685aaff20f3ac449ad852952e07872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 16 Jul 2025 17:42:23 -0400 Subject: [PATCH 3213/4744] refactor: add names to caches, add max to request cache --- src/activitypub/index.js | 3 +++ src/cache/lru.js | 3 +++ src/cache/ttl.js | 5 +++++ src/middleware/index.js | 1 + src/notifications.js | 1 + src/request.js | 2 ++ 6 files changed, 15 insertions(+) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 78cb3f26f6..ef69d0bdf4 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -20,14 +20,17 @@ const pubsub = require('../pubsub'); const analytics = require('../analytics'); const requestCache = ttl({ + name: 'ap-request-cache', max: 5000, ttl: 1000 * 60 * 5, // 5 minutes }); const probeCache = ttl({ + name: 'ap-probe-cache', max: 500, ttl: 1000 * 60 * 60, // 1 hour }); const probeRateLimit = ttl({ + name: 'ap-probe-rate-limit-cache', ttl: 1000 * 3, // 3 seconds }); diff --git a/src/cache/lru.js b/src/cache/lru.js index 094d3a3e93..2d46e5a5fe 100644 --- a/src/cache/lru.js +++ b/src/cache/lru.js @@ -31,6 +31,9 @@ module.exports = function (opts) { }); const lruCache = new LRUCache(opts); + if (!opts.name) { + winston.warn(`[cache/init] ${chalk.white.bgRed.bold('WARNING')} The cache name is not set. This will be required in the future.\n ${new Error('t').stack} `); + } const cache = {}; cache.name = opts.name; diff --git a/src/cache/ttl.js b/src/cache/ttl.js index 8647d0b9ac..fdc134be81 100644 --- a/src/cache/ttl.js +++ b/src/cache/ttl.js @@ -3,10 +3,15 @@ module.exports = function (opts) { const TTLCache = require('@isaacs/ttlcache'); const os = require('os'); + const winston = require('winston'); + const chalk = require('chalk'); const pubsub = require('../pubsub'); const ttlCache = new TTLCache(opts); + if (!opts.name) { + winston.warn(`[cache/init] ${chalk.white.bgRed.bold('WARNING')} The cache name is not set. This will be required in the future.\n ${new Error('t').stack} `); + } const cache = {}; cache.name = opts.name; diff --git a/src/middleware/index.js b/src/middleware/index.js index 417d8309bc..67d8e2faa0 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -23,6 +23,7 @@ const controllers = { }; const delayCache = cacheCreate({ + name: 'delay-middleware', ttl: 1000 * 60, max: 200, }); diff --git a/src/notifications.js b/src/notifications.js index df7cf51fb4..4e6e7873f3 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -22,6 +22,7 @@ const Notifications = module.exports; // ttlcache for email-only chat notifications const notificationCache = ttlCache({ + name: 'notification-email-cache', max: 1000, ttl: (meta.config.notificationSendDelay || 60) * 1000, noDisposeOnSet: true, diff --git a/src/request.js b/src/request.js index ce5e95d5bb..713c3dd6ac 100644 --- a/src/request.js +++ b/src/request.js @@ -12,6 +12,8 @@ const { version } = require('../package.json'); const plugins = require('./plugins'); const ttl = require('./cache/ttl'); const checkCache = ttl({ + name: 'request-check', + max: 1000, ttl: 1000 * 60 * 60, // 1 hour }); let allowList = new Set(); From 0fdde132082ff037da63f202c3af2459f70a8e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 16 Jul 2025 18:10:21 -0400 Subject: [PATCH 3214/4744] refactor: another missing cache name --- src/activitypub/actors.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/activitypub/actors.js b/src/activitypub/actors.js index fdaed03c2c..98fdeb74d4 100644 --- a/src/activitypub/actors.js +++ b/src/activitypub/actors.js @@ -14,6 +14,7 @@ const utils = require('../utils'); const TTLCache = require('../cache/ttl'); const failedWebfingerCache = TTLCache({ + name: 'ap-failed-webfinger-cache', max: 5000, ttl: 1000 * 60 * 10, // 10 minutes }); From 272008bb51084e688c6dc03e8df1c7c46a9f9ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 16 Jul 2025 20:23:57 -0400 Subject: [PATCH 3215/4744] refactor: add missing cache name --- src/activitypub/helpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index f7708b9bd9..ad45b67910 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -21,6 +21,7 @@ const activitypub = require('.'); const webfingerRegex = /^(@|acct:)?[\w-.]+@.+$/; const webfingerCache = ttl({ + name: 'ap-webfinger-cache', max: 5000, ttl: 1000 * 60 * 60 * 24, // 24 hours }); From 1d7c32a52f2927d59d04db9c902a0124cea856b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 17 Jul 2025 12:34:52 -0400 Subject: [PATCH 3216/4744] refactor: show both days and hours --- src/controllers/admin/info.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js index 6f63faf8a9..3c0457cd82 100644 --- a/src/controllers/admin/info.js +++ b/src/controllers/admin/info.js @@ -117,14 +117,18 @@ function getCpuUsage() { } function humanReadableUptime(seconds) { + const oneHourInSeconds = 3600; + const oneDayInSeconds = oneHourInSeconds * 24; if (seconds < 60) { return `${Math.floor(seconds)}s`; - } else if (seconds < 3600) { + } else if (seconds < oneHourInSeconds) { return `${Math.floor(seconds / 60)}m`; - } else if (seconds < 3600 * 24) { + } else if (seconds < oneDayInSeconds) { return `${Math.floor(seconds / (60 * 60))}h`; } - return `${Math.floor(seconds / (60 * 60 * 24))}d`; + const days = Math.floor(seconds / (oneDayInSeconds)); + const hours = Math.floor((seconds % (oneDayInSeconds)) / oneHourInSeconds); + return `${days}d ${hours}h`; } async function getGitInfo() { From e4a0160e085a067251339ff08229c262d90c5fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 17 Jul 2025 21:34:14 -0400 Subject: [PATCH 3217/4744] refactor: copy session/headers when building req --- src/api/helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/helpers.js b/src/api/helpers.js index 168e5539b6..7df860a569 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -35,7 +35,7 @@ exports.buildReqObject = (req, payload) => { params: req.params, method: req.method, body: payload || req.body, - session: session, + session: JSON.parse(JSON.stringify(session)), ip: req.ip, host: host, protocol: encrypted ? 'https' : 'http', @@ -44,7 +44,7 @@ exports.buildReqObject = (req, payload) => { path: referer.slice(referer.indexOf(host) + host.length), baseUrl: req.baseUrl, originalUrl: req.originalUrl, - headers: headers, + headers: { ...headers }, }; }; From 0b398bba4f0169e8eeaf4d14d44fec9d602626fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:07:44 -0400 Subject: [PATCH 3218/4744] fix(deps): update dependency webpack to v5.100.2 (#13549) 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 f309b7828a..61af2cfddd 100644 --- a/install/package.json +++ b/install/package.json @@ -147,7 +147,7 @@ "tough-cookie": "5.1.2", "undici": "^7.10.0", "validator": "13.15.15", - "webpack": "5.100.1", + "webpack": "5.100.2", "webpack-merge": "6.0.1", "winston": "3.17.0", "workerpool": "9.3.3", From 57564190f3c2fd85a057ba68a893bf896336ba29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:07:59 -0400 Subject: [PATCH 3219/4744] fix(deps): update dependency ace-builds to v1.43.2 (#13548) 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 61af2cfddd..206f1d5342 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.1", + "ace-builds": "1.43.2", "archiver": "7.0.1", "async": "3.2.6", "autoprefixer": "10.4.21", From 12b9f4c743b4247e6b2b8361251a6bec9980a2ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:22:31 -0400 Subject: [PATCH 3220/4744] fix(deps): update dependency compression to v1.8.1 (#13553) 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 206f1d5342..b87e0efe34 100644 --- a/install/package.json +++ b/install/package.json @@ -55,7 +55,7 @@ "clipboard": "2.0.11", "commander": "14.0.0", "compare-versions": "6.1.1", - "compression": "1.8.0", + "compression": "1.8.1", "connect-flash": "0.1.1", "connect-mongo": "5.1.0", "connect-pg-simple": "10.0.0", From 3f520c33eff5d2753bcfc9afaa5fbdad09055147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 18 Jul 2025 21:35:08 -0400 Subject: [PATCH 3221/4744] fix: add missing cache name --- src/middleware/uploads.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/middleware/uploads.js b/src/middleware/uploads.js index 9b434b3286..f7b016193f 100644 --- a/src/middleware/uploads.js +++ b/src/middleware/uploads.js @@ -20,6 +20,7 @@ exports.ratelimit = helpers.try(async (req, res, next) => { } if (!cache) { cache = cacheCreate({ + name: 'upload-rate-limit-cache', ttl: meta.config.uploadRateLimitCooldown * 1000, }); } From 35ca0e3b475e46adf33b58254b9ea2c0c5abab4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:03:18 -0400 Subject: [PATCH 3222/4744] fix(deps): update dependency multer to v2.0.2 (#13556) 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 b87e0efe34..91a62de796 100644 --- a/install/package.json +++ b/install/package.json @@ -94,7 +94,7 @@ "mongodb": "6.17.0", "morgan": "1.10.0", "mousetrap": "1.6.5", - "multer": "2.0.1", + "multer": "2.0.2", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.5.10", "nodebb-plugin-composer-default": "10.3.0", From 0e457f15858f341b05656bec2fa737968b923628 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:03:45 -0400 Subject: [PATCH 3223/4744] fix(deps): update dependency morgan to v1.10.1 (#13555) 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 91a62de796..62ddeabe39 100644 --- a/install/package.json +++ b/install/package.json @@ -92,7 +92,7 @@ "mime": "3.0.0", "mkdirp": "3.0.1", "mongodb": "6.17.0", - "morgan": "1.10.0", + "morgan": "1.10.1", "mousetrap": "1.6.5", "multer": "2.0.2", "nconf": "0.13.0", From 0eb0a67ae570ba5fe92059ad099aabb31a4c7a34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:04:10 -0400 Subject: [PATCH 3224/4744] fix(deps): update dependency express-session to v1.18.2 (#13554) 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 62ddeabe39..c348ae71e3 100644 --- a/install/package.json +++ b/install/package.json @@ -68,7 +68,7 @@ "diff": "8.0.2", "esbuild": "0.25.6", "express": "4.21.2", - "express-session": "1.18.1", + "express-session": "1.18.2", "express-useragent": "1.0.15", "fetch-cookie": "3.1.0", "file-loader": "6.2.0", From 1697e36f3abbcd36d31aa64a16b1dfd93cc4b016 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 13:34:52 -0400 Subject: [PATCH 3225/4744] fix(deps): update dependency esbuild to v0.25.7 (#13557) 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 c348ae71e3..fd81f70658 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.6", + "esbuild": "0.25.7", "express": "4.21.2", "express-session": "1.18.2", "express-useragent": "1.0.15", From 25c24298fb277bd98891060ce09a05415b313de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 19 Jul 2025 17:20:59 -0400 Subject: [PATCH 3226/4744] fix: closes #13558, override/extend json opts from config.json --- src/webserver.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webserver.js b/src/webserver.js index c27a4e5d4a..3cf390c329 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -239,12 +239,13 @@ function configureBodyParser(app) { } app.use(bodyParser.urlencoded(urlencodedOpts)); - const jsonOpts = nconf.get('bodyParser:json') || { + const jsonOpts = { type: [ 'application/json', 'application/ld+json', 'application/activity+json', ], + ...nconf.get('bodyParser:json'), }; app.use(bodyParser.json(jsonOpts)); } From eac3d0a043e66638cea34c78f10bb97fee32f66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 20 Jul 2025 11:57:34 -0400 Subject: [PATCH 3227/4744] fix: redis connect host/port --- src/database/redis/connection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/redis/connection.js b/src/database/redis/connection.js index 2a38cf1a79..fb2aceb33b 100644 --- a/src/database/redis/connection.js +++ b/src/database/redis/connection.js @@ -40,11 +40,11 @@ connection.connect = async function (options) { // Else, connect over tcp/ip cxn = createClient({ ...options.options, - host: redis_socket_or_host, - port: options.port, password: options.password, database: options.database, socket: { + host: redis_socket_or_host, + port: options.port, reconnectStrategy: 3000, }, }); From 54fae3b12b778f11b09f8d3150a6a9cff6f4ee44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 20 Jul 2025 13:38:31 -0400 Subject: [PATCH 3228/4744] set max on upload rate limit --- src/middleware/uploads.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/middleware/uploads.js b/src/middleware/uploads.js index f7b016193f..fdbfc3dbda 100644 --- a/src/middleware/uploads.js +++ b/src/middleware/uploads.js @@ -21,6 +21,7 @@ exports.ratelimit = helpers.try(async (req, res, next) => { if (!cache) { cache = cacheCreate({ name: 'upload-rate-limit-cache', + max: 100, ttl: meta.config.uploadRateLimitCooldown * 1000, }); } From 6a732e36166fd9ace0cd2c7692f23defea4a5fdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:00:49 -0400 Subject: [PATCH 3229/4744] fix(deps): update dependency esbuild to v0.25.8 (#13559) 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 fd81f70658..b1f4a37e9d 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.7", + "esbuild": "0.25.8", "express": "4.21.2", "express-session": "1.18.2", "express-useragent": "1.0.15", From c43c3533507854740006e2cc4589a16b5d6b9c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 21 Jul 2025 21:22:40 -0400 Subject: [PATCH 3230/4744] fix: change the client side reloginTimer to match setting when setting is changed restart timer closes #13561 --- public/src/admin/admin.js | 36 ++--------------------- public/src/admin/modules/relogin-timer.js | 36 +++++++++++++++++++++++ public/src/admin/settings.js | 7 +++-- 3 files changed, 44 insertions(+), 35 deletions(-) create mode 100644 public/src/admin/modules/relogin-timer.js diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index e0d4e49b7f..7e68c65b8c 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -9,43 +9,12 @@ require('../../scripts-admin'); app.onDomReady(); (function () { - let logoutTimer = 0; - let logoutMessage; - function startLogoutTimer() { - if (app.config.adminReloginDuration <= 0) { - return; - } - if (logoutTimer) { - clearTimeout(logoutTimer); - } - // pre-translate language string gh#9046 - if (!logoutMessage) { - require(['translator'], function (translator) { - translator.translate('[[login:logged-out-due-to-inactivity]]', function (translated) { - logoutMessage = translated; - }); - }); - } - - logoutTimer = setTimeout(function () { - require(['bootbox'], function (bootbox) { - bootbox.alert({ - closeButton: false, - message: logoutMessage, - callback: function () { - window.location.reload(); - }, - }); - }); - }, 3600000); - } - - require(['hooks', 'admin/settings'], (hooks, Settings) => { + require(['hooks', 'admin/settings', 'admin/modules/relogin-timer'], (hooks, Settings, reloginTimer) => { hooks.on('action:ajaxify.end', (data) => { updatePageTitle(data.url); setupRestartLinks(); showCorrectNavTab(); - startLogoutTimer(); + reloginTimer.start(app.config.adminReloginDuration); $('[data-bs-toggle="tooltip"]').tooltip({ animation: false, @@ -59,6 +28,7 @@ app.onDomReady(); Settings.populateTOC(); } }); + hooks.on('action:ajaxify.start', function () { require(['bootstrap'], function (boostrap) { const offcanvas = boostrap.Offcanvas.getInstance('#offcanvas'); diff --git a/public/src/admin/modules/relogin-timer.js b/public/src/admin/modules/relogin-timer.js new file mode 100644 index 0000000000..f8504b51a2 --- /dev/null +++ b/public/src/admin/modules/relogin-timer.js @@ -0,0 +1,36 @@ +import { translate } from 'translator'; +import { alert as bootboxAlert } from 'bootbox'; + +let logoutTimer = 0; +let logoutMessage; + +export function start(adminReloginDuration) { + clearTimer(); + if (adminReloginDuration <= 0) { + return; + } + + // pre-translate language string gh#9046 + if (!logoutMessage) { + translate('[[login:logged-out-due-to-inactivity]]', function (translated) { + logoutMessage = translated; + }); + } + + const timeoutMs = adminReloginDuration * 60000; + logoutTimer = setTimeout(function () { + bootboxAlert({ + closeButton: false, + message: logoutMessage, + callback: function () { + window.location.reload(); + }, + }); + }, timeoutMs); +} + +function clearTimer() { + if (logoutTimer) { + clearTimeout(logoutTimer); + } +} \ No newline at end of file diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js index 247f9646b2..f189718426 100644 --- a/public/src/admin/settings.js +++ b/public/src/admin/settings.js @@ -2,8 +2,8 @@ define('admin/settings', [ - 'uploader', 'mousetrap', 'hooks', 'alerts', 'settings', 'bootstrap', -], function (uploader, mousetrap, hooks, alerts, settings, bootstrap) { + 'uploader', 'mousetrap', 'hooks', 'alerts', 'settings', 'bootstrap', 'admin/modules/relogin-timer', +], function (uploader, mousetrap, hooks, alerts, settings, bootstrap, reloginTimer) { const Settings = {}; Settings.populateTOC = function () { @@ -217,6 +217,9 @@ define('admin/settings', [ for (const [field, value] of Object.entries(data)) { app.config[field] = value; + if (field === 'adminReloginDuration') { + reloginTimer.start(parseInt(value, 10)); + } } callback(); From 8ba230a205edd3a2149f9bec612b451b65620306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 22 Jul 2025 10:39:27 -0400 Subject: [PATCH 3231/4744] refactor: change default teaser to last-post --- install/data/defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/data/defaults.json b/install/data/defaults.json index b17bbbb135..b6ae97a3c7 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -76,7 +76,7 @@ "profile:keepAllUserImages": 0, "gdpr_enabled": 1, "allowProfileImageUploads": 1, - "teaserPost": "last-reply", + "teaserPost": "last-post", "showPostPreviewsOnHover": 1, "allowPrivateGroups": 1, "unreadCutoff": 2, From 8eedb38a99d35d3009e46937d9813943c20b502a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 22 Jul 2025 10:51:54 -0400 Subject: [PATCH 3232/4744] test: test fixes for default teaser change --- public/openapi/components/schemas/TopicObject.yaml | 7 +++++++ public/openapi/read/unread.yaml | 7 +++++++ test/topics.js | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml index b26623072e..30ba9d3a77 100644 --- a/public/openapi/components/schemas/TopicObject.yaml +++ b/public/openapi/components/schemas/TopicObject.yaml @@ -106,6 +106,9 @@ TopicObject: description: A topic identifier content: type: string + sourceContent: + type: string + nullable: true timestampISO: type: string description: An ISO 8601 formatted date string (complementing `timestamp`) @@ -118,6 +121,10 @@ TopicObject: username: type: string description: A friendly name for a given user account + displayname: + type: string + isLocal: + type: boolean userslug: type: string description: An URL-safe variant of the username (i.e. lower-cased, spaces diff --git a/public/openapi/read/unread.yaml b/public/openapi/read/unread.yaml index e916231d65..ea69f666dd 100644 --- a/public/openapi/read/unread.yaml +++ b/public/openapi/read/unread.yaml @@ -138,6 +138,9 @@ get: description: A topic identifier content: type: string + sourceContent: + type: string + nullable: true timestampISO: type: string description: An ISO 8601 formatted date string (complementing `timestamp`) @@ -150,6 +153,10 @@ get: username: type: string description: A friendly name for a given user account + displayname: + type: string + isLocal: + type: boolean userslug: type: string description: An URL-safe variant of the username (i.e. lower-cased, spaces diff --git a/test/topics.js b/test/topics.js index dcea6ccd09..bc92bbff09 100644 --- a/test/topics.js +++ b/test/topics.js @@ -2000,7 +2000,8 @@ describe('Topic\'s', () => { it('should get teasers with 2 params', (done) => { topics.getTeasers([topic1.topicData, topic2.topicData], 1, (err, teasers) => { assert.ifError(err); - assert.deepEqual([undefined, undefined], teasers); + assert(teasers[0]); + assert(teasers[1]); done(); }); }); From 1776bd1d7e732774a0a557b07d3d6a37483077a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 22 Jul 2025 10:58:17 -0400 Subject: [PATCH 3233/4744] test: fix meta test --- test/meta.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/meta.js b/test/meta.js index 77667ddbf2..93b5b443bb 100644 --- a/test/meta.js +++ b/test/meta.js @@ -257,15 +257,10 @@ describe('meta', () => { }); }); - it('should use default value if value is null', (done) => { - meta.configs.set('teaserPost', null, (err) => { - assert.ifError(err); - meta.configs.get('teaserPost', (err, value) => { - assert.ifError(err); - assert.strictEqual(value, 'last-reply'); - done(); - }); - }); + it('should use default value if value is null', async () => { + await meta.configs.set('teaserPost', null); + const value = await meta.configs.get('teaserPost'); + assert.strictEqual(value, 'last-post'); }); it('should fail if field is invalid', (done) => { From f6ed7ec21c763395704b66564b2a08cd41f830c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 22 Jul 2025 16:28:37 -0400 Subject: [PATCH 3234/4744] fix: don't translate text on admin logs page --- src/controllers/admin/logs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/admin/logs.js b/src/controllers/admin/logs.js index 51ed116eca..1fe0bb86c9 100644 --- a/src/controllers/admin/logs.js +++ b/src/controllers/admin/logs.js @@ -4,6 +4,7 @@ const validator = require('validator'); const winston = require('winston'); const meta = require('../../meta'); +const translator = require('../../translator'); const logsController = module.exports; @@ -15,6 +16,6 @@ logsController.get = async function (req, res) { winston.error(err.stack); } res.render('admin/advanced/logs', { - data: validator.escape(logs), + data: translator.escape(validator.escape(logs)), }); }; From de71cc63104660a522ebad079cbe60d26c1515d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 22 Jul 2025 16:35:55 -0400 Subject: [PATCH 3235/4744] refactor: log uid that failed --- src/activitypub/actors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activitypub/actors.js b/src/activitypub/actors.js index 98fdeb74d4..d48acda82b 100644 --- a/src/activitypub/actors.js +++ b/src/activitypub/actors.js @@ -655,7 +655,7 @@ Actors.prune = async () => { await user.deleteAccount(uid); deletionCount += 1; } catch (err) { - winston.error(err.stack); + winston.error(`Failed to delete user with uid ${uid}: ${err.stack}`); } } else { notDeletedDueToLocalContent += 1; From 8e9d38430c676cc66fe96ade6152a4ef25f53e56 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:49:52 -0400 Subject: [PATCH 3236/4744] fix(deps): update dependency mongodb to v6.18.0 (#13563) 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 b1f4a37e9d..6cc1ff3cad 100644 --- a/install/package.json +++ b/install/package.json @@ -91,7 +91,7 @@ "lru-cache": "11.1.0", "mime": "3.0.0", "mkdirp": "3.0.1", - "mongodb": "6.17.0", + "mongodb": "6.18.0", "morgan": "1.10.1", "mousetrap": "1.6.5", "multer": "2.0.2", From a8f4c5e63a93089ad334e9c927e9a0862e235394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 24 Jul 2025 10:34:37 -0400 Subject: [PATCH 3237/4744] fix: apply sanitizeSvg to regular uploads and uploads from manage uploads acp page --- src/controllers/admin/uploads.js | 44 -------------------------------- src/file.js | 40 +++++++++++++++++++++++++++++ test/files/dirty.svg | 4 +++ test/uploads.js | 9 +++++++ 4 files changed, 53 insertions(+), 44 deletions(-) create mode 100644 test/files/dirty.svg diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index ccd4261b36..0f70380695 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -4,7 +4,6 @@ const path = require('path'); const nconf = require('nconf'); const fs = require('fs'); const winston = require('winston'); -const sanitizeHtml = require('sanitize-html'); const meta = require('../../meta'); const posts = require('../../posts'); @@ -157,50 +156,11 @@ uploadsController.uploadCategoryPicture = async function (req, res, next) { return next(new Error('[[error:invalid-json]]')); } - if (uploadedFile.path.endsWith('.svg')) { - await sanitizeSvg(uploadedFile.path); - } - await validateUpload(uploadedFile, allowedImageTypes); const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`; await uploadImage(filename, 'category', uploadedFile, req, res, next); }; -async function sanitizeSvg(filePath) { - const dirty = await fs.promises.readFile(filePath, 'utf8'); - const clean = sanitizeHtml(dirty, { - allowedTags: [ - 'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop', - 'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect', - 'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern', - 'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode', - ], - allowedAttributes: { - '*': [ - // Geometry - 'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', - 'width', 'height', 'd', 'points', 'viewBox', 'transform', - - // Presentation - 'fill', 'stroke', 'stroke-width', 'opacity', - 'stop-color', 'stop-opacity', 'offset', 'style', 'class', - - // Text - 'text-anchor', 'font-size', 'font-family', - - // Misc - 'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform', - 'xmlns', 'preserveAspectRatio', - ], - }, - parser: { - lowerCaseTags: false, - lowerCaseAttributeNames: false, - }, - }); - await fs.promises.writeFile(filePath, clean); -} - uploadsController.uploadFavicon = async function (req, res, next) { const uploadedFile = req.files.files[0]; const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; @@ -296,10 +256,6 @@ uploadsController.uploadOgImage = async function (req, res, next) { async function upload(name, req, res, next) { const uploadedFile = req.files.files[0]; - if (uploadedFile.path.endsWith('.svg')) { - await sanitizeSvg(uploadedFile.path); - } - await validateUpload(uploadedFile, allowedImageTypes); const filename = name + path.extname(uploadedFile.name); await uploadImage(filename, 'system', uploadedFile, req, res, next); diff --git a/src/file.js b/src/file.js index 639cc9f58c..4c3c911950 100644 --- a/src/file.js +++ b/src/file.js @@ -7,6 +7,7 @@ const winston = require('winston'); const { mkdirp } = require('mkdirp'); const mime = require('mime'); const graceful = require('graceful-fs'); +const sanitizeHtml = require('sanitize-html'); const slugify = require('./slugify'); @@ -27,6 +28,10 @@ file.saveFileToLocal = async function (filename, folder, tempPath) { winston.verbose(`Saving file ${filename} to : ${uploadPath}`); await mkdirp(path.dirname(uploadPath)); + if (tempPath.endsWith('.svg')) { + await sanitizeSvg(tempPath); + } + await fs.promises.copyFile(tempPath, uploadPath); return { url: `/assets/uploads/${folder ? `${folder}/` : ''}${filename}`, @@ -155,4 +160,39 @@ file.walk = async function (dir) { return files.reduce((a, f) => a.concat(f), []); }; +async function sanitizeSvg(filePath) { + const dirty = await fs.promises.readFile(filePath, 'utf8'); + const clean = sanitizeHtml(dirty, { + allowedTags: [ + 'svg', 'g', 'defs', 'linearGradient', 'radialGradient', 'stop', + 'circle', 'ellipse', 'polygon', 'polyline', 'path', 'rect', + 'line', 'text', 'tspan', 'use', 'symbol', 'clipPath', 'mask', 'pattern', + 'filter', 'feGaussianBlur', 'feOffset', 'feBlend', 'feColorMatrix', 'feMerge', 'feMergeNode', + ], + allowedAttributes: { + '*': [ + // Geometry + 'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', + 'width', 'height', 'd', 'points', 'viewBox', 'transform', + + // Presentation + 'fill', 'stroke', 'stroke-width', 'opacity', + 'stop-color', 'stop-opacity', 'offset', 'style', 'class', + + // Text + 'text-anchor', 'font-size', 'font-family', + + // Misc + 'id', 'clip-path', 'mask', 'filter', 'gradientUnits', 'gradientTransform', + 'xmlns', 'preserveAspectRatio', + ], + }, + parser: { + lowerCaseTags: false, + lowerCaseAttributeNames: false, + }, + }); + await fs.promises.writeFile(filePath, clean); +} + require('./promisify')(file); diff --git a/test/files/dirty.svg b/test/files/dirty.svg new file mode 100644 index 0000000000..a948be59b2 --- /dev/null +++ b/test/files/dirty.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/uploads.js b/test/uploads.js index 3b361e36d3..4fc0e41a64 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -338,6 +338,15 @@ describe('Upload Controllers', () => { assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.png`); }); + it('should upload svg as category image after cleaning it up', async () => { + const { response, body } = await helpers.uploadFile(`${nconf.get('url')}/api/admin/category/uploadpicture`, path.join(__dirname, '../test/files/dirty.svg'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token); + assert.equal(response.statusCode, 200); + assert(Array.isArray(body)); + assert.equal(body[0].url, `${nconf.get('relative_path')}/assets/uploads/category/category-1.svg`); + const svgContents = await fs.readFile(path.join(__dirname, '../test/uploads/category/category-1.svg'), 'utf-8'); + assert.strictEqual(svgContents.includes(' \ No newline at end of file From 781a900c0fccb79d51f6f83fbfafd9d47aa0c180 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 12 Feb 2026 16:52:27 +0000 Subject: [PATCH 4155/4744] chore(i18n): fallback strings for new resources: nodebb.topic --- public/language/ar/topic.json | 5 ++- public/language/az/topic.json | 5 ++- public/language/bg/topic.json | 7 +++- public/language/bn/topic.json | 5 ++- public/language/cs/topic.json | 5 ++- public/language/da/topic.json | 5 ++- public/language/de/topic.json | 37 +++++++++-------- public/language/el/topic.json | 5 ++- public/language/en-US/topic.json | 5 ++- public/language/en-x-pirate/topic.json | 5 ++- public/language/es/topic.json | 5 ++- public/language/et/topic.json | 5 ++- public/language/fa-IR/topic.json | 5 ++- public/language/fi/topic.json | 5 ++- public/language/fr/topic.json | 57 ++++++++++++++------------ public/language/gl/topic.json | 5 ++- public/language/he/topic.json | 5 ++- public/language/hr/topic.json | 5 ++- public/language/hu/topic.json | 5 ++- public/language/hy/topic.json | 5 ++- public/language/id/topic.json | 5 ++- public/language/it/topic.json | 7 +++- public/language/ja/topic.json | 5 ++- public/language/ko/topic.json | 5 ++- public/language/lt/topic.json | 5 ++- public/language/lv/topic.json | 5 ++- public/language/ms/topic.json | 5 ++- public/language/nb/topic.json | 5 ++- public/language/nl/topic.json | 5 ++- public/language/nn-NO/topic.json | 5 ++- public/language/pl/topic.json | 7 +++- public/language/pt-BR/topic.json | 5 ++- public/language/pt-PT/topic.json | 5 ++- public/language/ro/topic.json | 5 ++- public/language/ru/topic.json | 5 ++- public/language/rw/topic.json | 5 ++- public/language/sc/topic.json | 5 ++- public/language/sk/topic.json | 5 ++- public/language/sl/topic.json | 5 ++- public/language/sq-AL/topic.json | 5 ++- public/language/sr/topic.json | 5 ++- public/language/sv/topic.json | 5 ++- public/language/th/topic.json | 5 ++- public/language/tr/topic.json | 5 ++- public/language/uk/topic.json | 5 ++- public/language/ur/topic.json | 5 ++- public/language/vi/topic.json | 5 ++- public/language/zh-CN/topic.json | 7 +++- public/language/zh-TW/topic.json | 5 ++- 49 files changed, 242 insertions(+), 95 deletions(-) diff --git a/public/language/ar/topic.json b/public/language/ar/topic.json index f030e44df2..dff6581696 100644 --- a/public/language/ar/topic.json +++ b/public/language/ar/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/az/topic.json b/public/language/az/topic.json index f8898798de..67414c4c79 100644 --- a/public/language/az/topic.json +++ b/public/language/az/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Oxunmamış yazıların linki", "thumb-image": "Mövzunun kiçik şəkli", "announcers": "Paylaşımlar", - "announcers-x": "Paylaşımlar (%1)" + "announcers-x": "Paylaşımlar (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index f2fce7da99..f80093702c 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -182,7 +182,7 @@ "composer.replying-to": "Отговор на %1", "composer.new-topic": "Нова тема", "composer.editing-in": "Редактиране на публикация в %1", - "composer.untitled-topic": "Untitled Topic", + "composer.untitled-topic": "Тема без име", "composer.uploading": "качване...", "composer.thumb-url-label": "Поставете адреса на иконка за темата", "composer.thumb-title": "Добавете иконка към тази тема", @@ -233,5 +233,8 @@ "unread-posts-link": "Връзка към непрочетените публикации", "thumb-image": "Иконка на темата", "announcers": "Споделяния", - "announcers-x": "Споделяния (%1)" + "announcers-x": "Споделяния (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/bn/topic.json b/public/language/bn/topic.json index 1069f2be1e..34f86c06f3 100644 --- a/public/language/bn/topic.json +++ b/public/language/bn/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index b3dbb3036b..dd2e4da749 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Odkaz na nepřečtené příspěvky", "thumb-image": "Miniatura tématu", "announcers": "Sdílení", - "announcers-x": "Sdílení (%1)" + "announcers-x": "Sdílení (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/da/topic.json b/public/language/da/topic.json index b498ddadee..577acf3ab6 100644 --- a/public/language/da/topic.json +++ b/public/language/da/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/de/topic.json b/public/language/de/topic.json index 51d196e5f7..e82f135a39 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -62,15 +62,15 @@ "user-moved-topic-from-ago": "%1 verschob dieses Thema von %2 %3", "user-moved-topic-from-on": "%1 verschob dieses Thema von %2 am %3", "user-shared-topic-ago": "%1 hat dieses Thema geteilt %2", - "user-shared-topic-on": "%1 shared this topic on %2", + "user-shared-topic-on": "%1 hat dieses Thema am %2 geteilt.", "user-queued-post-ago": "%1 hat Beitrag für Überprüfung markiert %3", "user-queued-post-on": "%1 hat Beitrag am %3 für Überprüfung markiert", "user-referenced-topic-ago": "%1 hat auf dieses Thema verwiesen %3", "user-referenced-topic-on": "%1 hat am %3 auf dieses Thema verwiesen", "user-forked-topic-ago": "%1 hat dieses Thema aufgespalten %3", "user-forked-topic-on": "%1 hat dieses Thema am %3 aufgespalten", - "user-crossposted-topic-ago": "%1 crossposted this topic to %2 %3", - "user-crossposted-topic-on": "%1 crossposted this topicto %2 on %3", + "user-crossposted-topic-ago": "%1 hat dieses Thema auch in %2 und %3 gepostet.", + "user-crossposted-topic-on": "%1 hat dieses Thema am %3 in %2 gepostet.", "bookmark-instructions": "Klicke hier, um zum letzten gelesenen Beitrag des Themas zurückzukehren.", "flag-post": "Diesen Post melden", "flag-user": "Diesen Benutzer melden", @@ -105,7 +105,7 @@ "thread-tools.lock": "Thema schließen", "thread-tools.unlock": "Thema öffnen", "thread-tools.move": "Thema verschieben", - "thread-tools.crosspost": "Crosspost Topic", + "thread-tools.crosspost": "Crosspost-Thema", "thread-tools.move-posts": "Beiträge verschieben", "thread-tools.move-all": "Alle verschieben", "thread-tools.change-owner": "Besitzer ändern", @@ -141,11 +141,11 @@ "bookmarks": "Lesezeichen", "bookmarks.has-no-bookmarks": "Du hast noch keine Beiträge mit Lesezeichen markiert.", "copy-permalink": "Permalink kopieren", - "go-to-original": "View Original Post", + "go-to-original": "Originalbeitrag anzeigen", "loading-more-posts": "Lade mehr Beiträge", "move-topic": "Thema verschieben", "move-topics": "Themen verschieben", - "crosspost-topic": "Cross-post Topic", + "crosspost-topic": "Cross-Post-Thema", "move-post": "Beitrag verschieben", "post-moved": "Beitrag wurde verschoben!", "fork-topic": "Thema aufspalten", @@ -167,10 +167,10 @@ "move-posts-instruction": "Klicken Sie auf die Beiträge, die Sie verschieben möchten, und geben Sie dann eine Themen-ID ein oder gehen Sie zum Zielthema", "move-topic-instruction": "Wähle die Ziel-Kategorie und klicke \"Verschieben\"", "change-owner-instruction": "Klicke auf die Beiträge, die einem anderen Benutzer zugeordnet werden sollen", - "manage-editors-instruction": "Manage the users who can edit this post below.", - "crossposts.instructions": "Select one or more categories to cross-post to. Topic(s) will be accessible from the original category and all cross-posted categories.", - "crossposts.listing": "This topic has been cross-posted to the following local categories:", - "crossposts.none": "This topic has not been cross-posted to any additional categories", + "manage-editors-instruction": "Verwalte unten die Leute, die diesen Beitrag bearbeiten können.", + "crossposts.instructions": "Wähle eine oder mehrere Kategorien aus, in denen du einen Beitrag veröffentlichen möchtest. Die Themen sind dann in der ursprünglichen Kategorie und in allen Kategorien, in denen der Beitrag veröffentlicht wurde, zugänglich.", + "crossposts.listing": "Dieses Thema wurde auch in den folgenden lokalen Kategorien gepostet:", + "crossposts.none": "Dieses Thema wurde in keine weiteren Kategorien gepostet.", "composer.title-placeholder": "Hier den Titel des Themas eingeben...", "composer.handle-placeholder": "Gib deinen Namen/Nick hier ein", "composer.hide": "Verstecken", @@ -182,7 +182,7 @@ "composer.replying-to": "Antworte auf %1", "composer.new-topic": "Neues Thema", "composer.editing-in": "Bearbeite Beitrag in %1", - "composer.untitled-topic": "Untitled Topic", + "composer.untitled-topic": "Thema ohne Titel", "composer.uploading": "Lade hoch...", "composer.thumb-url-label": "Vorschaubild-URL hier einfügen", "composer.thumb-title": "Vorschaubild zu diesem Thema hinzufügen", @@ -223,15 +223,18 @@ "last-post": "Letzter Beitrag", "go-to-my-next-post": "Zu meinem nächsten Beitrag gehen", "no-more-next-post": "Du hast keine weiteren Beiträge zu diesem Thema", - "open-composer": "Open composer", + "open-composer": "Composer öffnen", "post-quick-reply": "Schnell antworten", "navigator.index": "Beitrag %1 von %2", "navigator.unread": "%1 ungelesen", "upvote-post": "Beitrag positiv bewerten", "downvote-post": "Beitrag negativ bewerten", - "post-tools": "Post tools", - "unread-posts-link": "Unread posts link", - "thumb-image": "Topic thumbnail image", - "announcers": "Shares", - "announcers-x": "Shares (%1)" + "post-tools": "Beitrag-Tools", + "unread-posts-link": "Link zu ungelesenen Beiträgen", + "thumb-image": "Miniaturbild zum Thema", + "announcers": "Geteilt", + "announcers-x": "geteilte (1 %)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/el/topic.json b/public/language/el/topic.json index d111129021..3b6d5abe15 100644 --- a/public/language/el/topic.json +++ b/public/language/el/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/en-US/topic.json b/public/language/en-US/topic.json index ad680199ec..0e1bcdfa97 100644 --- a/public/language/en-US/topic.json +++ b/public/language/en-US/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/en-x-pirate/topic.json b/public/language/en-x-pirate/topic.json index ad680199ec..0e1bcdfa97 100644 --- a/public/language/en-x-pirate/topic.json +++ b/public/language/en-x-pirate/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/es/topic.json b/public/language/es/topic.json index a5cf78109e..072e21e832 100644 --- a/public/language/es/topic.json +++ b/public/language/es/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Enlace a entradas sin leer", "thumb-image": "Imagen miniatura del tema", "announcers": "Comparte", - "announcers-x": "Comparte (%1)" + "announcers-x": "Comparte (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/et/topic.json b/public/language/et/topic.json index a739a378c5..3c5e455644 100644 --- a/public/language/et/topic.json +++ b/public/language/et/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/fa-IR/topic.json b/public/language/fa-IR/topic.json index aa69e3167a..ce21e4587b 100644 --- a/public/language/fa-IR/topic.json +++ b/public/language/fa-IR/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "پست های خوانده نشده پیوند", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/fi/topic.json b/public/language/fi/topic.json index 3032679433..246f538d9f 100644 --- a/public/language/fi/topic.json +++ b/public/language/fi/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index 91742218e6..067f0c50dd 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -22,7 +22,7 @@ "edit": "Éditer", "delete": "Supprimer", "delete-event": "Supprimer l'événement", - "delete-event-confirm": "Êtes-vous sûr de vouloir supprimer cet événement ?", + "delete-event-confirm": "Êtes-vous sûr de vouloir supprimer cet événement ?", "purge": "Supprimer définitivement", "restore": "Restaurer", "move": "Déplacer", @@ -61,16 +61,16 @@ "user-restored-topic-on": "%1 a restauré ce sujet sur %2", "user-moved-topic-from-ago": "%1 a déplacé ce sujet de %2 %3", "user-moved-topic-from-on": "%1 a déplacé ce sujet de %2 sur %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 partagé ce sujet %2", + "user-shared-topic-on": "%1 a partagé ce sujet sur %2", "user-queued-post-ago": "%1 message En attente pour approbation %3", "user-queued-post-on": "%1 message En attente pour approbation sur %3", "user-referenced-topic-ago": "%1 a fait référence à ce sujet %3", "user-referenced-topic-on": "%1 a fait référence à ce sujet sur %3", "user-forked-topic-ago": "%1 a scindé ce sujet %3", "user-forked-topic-on": "%1 a scindé ce sujet sur %3", - "user-crossposted-topic-ago": "%1 crossposted this topic to %2 %3", - "user-crossposted-topic-on": "%1 crossposted this topicto %2 on %3", + "user-crossposted-topic-ago": "%1 a publié simultanément ce sujet sur %2 %3", + "user-crossposted-topic-on": "%1 a publié simultanément ce sujet %2 sur %3", "bookmark-instructions": "Cliquer ici pour aller au dernier message lu de ce fil.", "flag-post": "Signaler ce message", "flag-user": "Signaler cet utilisateur", @@ -86,7 +86,7 @@ "login-to-subscribe": "Veuillez vous enregistrer ou vous connecter afin de vous abonner à ce sujet.", "markAsUnreadForAll.success": "Sujet marqué comme non lu pour tout le monde.", "mark-unread": "Marquer comme non lu", - "mark-unread.success": "Sujet marqué comme non lu.", + "mark-unread.success": "Sujet marqué comme non lu", "watch": "Suivre", "unwatch": "Cesser de suivre", "watch.title": "Être notifié des nouvelles réponses dans ce sujet", @@ -95,17 +95,17 @@ "watching": "Suivi", "not-watching": "Suivre", "ignoring": "Ignoré", - "watching.description": "Me notifier les nouvelles réponses.
    Afficher le sujet dans l'onglet \"Non lu\".", - "not-watching.description": "Ne pas me notifier les nouvelles réponses.
    Afficher le sujet dans l'onglet \"Non lu\" si la catégorie n'est pas ignorée.", - "ignoring.description": "Ne pas me notifier les nouvelles réponses.
    Ne pas afficher ce sujet dans l'onglet \"Non lu\".", + "watching.description": "Me notifier les nouvelles réponses.
    Afficher le sujet dans l'onglet \"Non lus\".", + "not-watching.description": "Ne pas me notifier les nouvelles réponses.
    Afficher le sujet dans l'onglet \"Non lus\" si la catégorie n'est pas ignorée.", + "ignoring.description": "Ne pas me notifier les nouvelles réponses.
    Ne pas afficher ce sujet dans l'onglet \"Non lus\".", "thread-tools.title": "Outils pour sujets", - "thread-tools.markAsUnreadForAll": "Marquer non lu pour tous", + "thread-tools.markAsUnreadForAll": "Marquer non lus pour tous", "thread-tools.pin": "Épingler le sujet", "thread-tools.unpin": "Désépingler le sujet", "thread-tools.lock": "Verrouiller le sujet", "thread-tools.unlock": "Déverouiller le sujet", "thread-tools.move": "Déplacer le sujet", - "thread-tools.crosspost": "Crosspost Topic", + "thread-tools.crosspost": "Sujet publié simultanément", "thread-tools.move-posts": "Déplacer les messages", "thread-tools.move-all": "Déplacer tout", "thread-tools.change-owner": "Changer de propriétaire", @@ -135,7 +135,7 @@ "pin-modal-help": "Vous pouvez éventuellement définir une date d'expiration pour le(s) sujet(s) épinglé(s) ici. Vous pouvez également laisser ce champ vide pour que le sujet reste épinglé jusqu'à ce qu'il soit supprimé manuellement.", "load-categories": "Chargement des catégories en cours", "confirm-move": "Déplacer", - "confirm-crosspost": "Cross-post", + "confirm-crosspost": "Publication simultanée", "confirm-fork": "Scinder", "bookmark": "Marque-page", "bookmarks": "Marque-pages", @@ -145,7 +145,7 @@ "loading-more-posts": "Charger plus de messages", "move-topic": "Déplacer le sujet", "move-topics": "Déplacer les sujets", - "crosspost-topic": "Cross-post Topic", + "crosspost-topic": "Sujet publié simultanément", "move-post": "Déplacer", "post-moved": "Message déplacé !", "fork-topic": "Scinder le sujet", @@ -157,20 +157,20 @@ "x-posts-will-be-moved-to-y": "%1 message(s) seront déplacés vers \"%2\"", "fork-pid-count": "%1 message(s) sélectionné(s)", "fork-success": "Sujet scindé avec succès ! Cliquez ici pour aller au sujet scindé.", - "delete-posts-instruction": "Sélectionner les messages que vous souhaitez supprimer/vider", - "merge-topics-instruction": "Cliquer sur les sujets que vous voulez fusionner", + "delete-posts-instruction": "Sélectionnez les messages que vous souhaitez supprimer/vider", + "merge-topics-instruction": "Cliquez sur les sujets que vous voulez fusionner", "merge-topic-list-title": "Liste des sujets à fusionner", "merge-options": "Options de fusion", - "merge-select-main-topic": "Sélectionner le sujet principal", + "merge-select-main-topic": "Sélectionnez le sujet principal", "merge-new-title-for-topic": "Nouveau titre pour le sujet", "topic-id": "Sujet ID", - "move-posts-instruction": "Cliquer sur les articles que vous souhaitez déplacer, puis entrez un ID de sujet ou accédez au sujet cible", - "move-topic-instruction": "Sélectionner la catégorie cible puis cliquer sur déplacer", - "change-owner-instruction": "Cliquer sur les messages que vous souhaitez attribuer à un autre utilisateur.", - "manage-editors-instruction": "Manage the users who can edit this post below.", - "crossposts.instructions": "Select one or more categories to cross-post to. Topic(s) will be accessible from the original category and all cross-posted categories.", - "crossposts.listing": "This topic has been cross-posted to the following local categories:", - "crossposts.none": "This topic has not been cross-posted to any additional categories", + "move-posts-instruction": "Cliquez sur les articles que vous souhaitez déplacer, puis entrez un ID de sujet ou accédez au sujet cible", + "move-topic-instruction": "Sélectionnez la catégorie cible puis cliquer sur déplacer", + "change-owner-instruction": "Cliquez sur les messages que vous souhaitez attribuer à un autre utilisateur.", + "manage-editors-instruction": "Gérez les utilisateurs qui peuvent modifier le message ci-dessous.", + "crossposts.instructions": "Sélectionnez une ou plusieurs catégories pour publier ce sujet en simultané. Le(s) sujet(s) seront accessibles depuis la catégorie d'origine ainsi que toutes les catégories sélectionnées.", + "crossposts.listing": "Ce sujet a été publié simultanément dans les catégories locales suivantes :", + "crossposts.none": "Ce sujet n'a pas été publié simultanément dans d'autres catégories.", "composer.title-placeholder": "Entrer le titre du sujet ici…", "composer.handle-placeholder": "Entrez votre nom/identifiant ici", "composer.hide": "Cacher", @@ -182,7 +182,7 @@ "composer.replying-to": "En réponse à %1", "composer.new-topic": "Nouveau sujet", "composer.editing-in": "Modification du message dans %1", - "composer.untitled-topic": "Untitled Topic", + "composer.untitled-topic": "Sujet sans titre", "composer.uploading": "envoi en cours…", "composer.thumb-url-label": "Coller une URL de vignette du sujet", "composer.thumb-title": "Ajouter une vignette à ce sujet", @@ -207,7 +207,7 @@ "stale.create": "Créer un nouveau sujet", "stale.reply-anyway": "Répondre à ce sujet quand même", "link-back": "Re : [%1](%2)", - "diffs.title": "Historique", + "diffs.title": "Historique des modifications", "diffs.description": "Cet article a %1 révisions. Cliquer sur l'une des révisions ci-dessous pour voir le contenu du message.", "diffs.no-revisions-description": "Cet article a %1 révisions.", "diffs.current-revision": "Révision en cours", @@ -232,6 +232,9 @@ "post-tools": "Outils pour les messages", "unread-posts-link": "Lien pour les messages non lus", "thumb-image": "Vignette du sujet", - "announcers": "Partager", - "announcers-x": "Partages (%1)" + "announcers": "Partages", + "announcers-x": "Partages (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/gl/topic.json b/public/language/gl/topic.json index b0ab06e1cd..92cc657553 100644 --- a/public/language/gl/topic.json +++ b/public/language/gl/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/he/topic.json b/public/language/he/topic.json index 9541fa94e6..f285904fde 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "קישור לפוסטים שלא נקראו", "thumb-image": "תמונה ממוזערת של נושא", "announcers": "שיתופים", - "announcers-x": "שיתופים (%1)" + "announcers-x": "שיתופים (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/hr/topic.json b/public/language/hr/topic.json index 4e78840172..5c0e05436e 100644 --- a/public/language/hr/topic.json +++ b/public/language/hr/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/hu/topic.json b/public/language/hu/topic.json index dd8c01b417..0c77afc488 100644 --- a/public/language/hu/topic.json +++ b/public/language/hu/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Olvasatlan bejegyzés link", "thumb-image": "Téma bélyegkép", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/hy/topic.json b/public/language/hy/topic.json index 23b7bb178a..1ef24aba46 100644 --- a/public/language/hy/topic.json +++ b/public/language/hy/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/id/topic.json b/public/language/id/topic.json index 602935090d..50c74a8bae 100644 --- a/public/language/id/topic.json +++ b/public/language/id/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 1c26acfc63..0ea72b5f05 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -182,7 +182,7 @@ "composer.replying-to": "Rispondendo a %1", "composer.new-topic": "Nuova Discussione", "composer.editing-in": "Modifica post in %1", - "composer.untitled-topic": "Untitled Topic", + "composer.untitled-topic": "Discussione senza titolo", "composer.uploading": "caricamento...", "composer.thumb-url-label": "Incolla l'URL della miniatura per la discussione", "composer.thumb-title": "Aggiungi una miniatura a questa discussione", @@ -233,5 +233,8 @@ "unread-posts-link": "Link ai post non letti", "thumb-image": "Immagine anteprima della discussione", "announcers": "Condivisioni", - "announcers-x": "Condivisioni (%1)" + "announcers-x": "Condivisioni (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json index c697b0aeb1..fb8fdca9b1 100644 --- a/public/language/ja/topic.json +++ b/public/language/ja/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/ko/topic.json b/public/language/ko/topic.json index aaeacb42cd..223196a7ca 100644 --- a/public/language/ko/topic.json +++ b/public/language/ko/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "읽지 않은 게시물 링크", "thumb-image": "토픽 썸네일 이미지", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json index da3b54b583..948e320587 100644 --- a/public/language/lt/topic.json +++ b/public/language/lt/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/lv/topic.json b/public/language/lv/topic.json index 6a08effbd0..72b136f2a5 100644 --- a/public/language/lv/topic.json +++ b/public/language/lv/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/ms/topic.json b/public/language/ms/topic.json index 80e07b3a12..af1c8e01c4 100644 --- a/public/language/ms/topic.json +++ b/public/language/ms/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index d6b766f279..c3177f2487 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Lenke til uleste innlegg", "thumb-image": "Miniatyrbilde for emne", "announcers": "Delinger", - "announcers-x": "Delinger (%1)" + "announcers-x": "Delinger (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index cc8e3a90bc..7564fee664 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/nn-NO/topic.json b/public/language/nn-NO/topic.json index 96790cd2de..a35cbf5541 100644 --- a/public/language/nn-NO/topic.json +++ b/public/language/nn-NO/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Lenkje til uleste innlegg", "thumb-image": "Emne miniatyrbilete", "announcers": "Delingar", - "announcers-x": "Delingar (%1)" + "announcers-x": "Delingar (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index dddafd3b48..7a6b762b73 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -182,7 +182,7 @@ "composer.replying-to": "Odpowiedź na %1", "composer.new-topic": "Nowy temat", "composer.editing-in": "Edytowanie posta w %1", - "composer.untitled-topic": "Untitled Topic", + "composer.untitled-topic": "Nienazwany Temat", "composer.uploading": "wysyłanie...", "composer.thumb-url-label": "Wklej adres miniaturki tematu", "composer.thumb-title": "Dodaj miniaturkę do tego tematu", @@ -233,5 +233,8 @@ "unread-posts-link": "Link nieprzeczytanych postów", "thumb-image": "Obraz miniaturki tematu", "announcers": "Udostępnień", - "announcers-x": "Udostępnień (%1)" + "announcers-x": "Udostępnień (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/pt-BR/topic.json b/public/language/pt-BR/topic.json index 5ed317f1ef..21244f2196 100644 --- a/public/language/pt-BR/topic.json +++ b/public/language/pt-BR/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/pt-PT/topic.json b/public/language/pt-PT/topic.json index 18f52cc940..dc8eb35a40 100644 --- a/public/language/pt-PT/topic.json +++ b/public/language/pt-PT/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index b54f485a1a..badbc2b499 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Link pentru postări necitite", "thumb-image": "Imagine miniatură subiect", "announcers": "Partajări", - "announcers-x": "Partajări (%1)" + "announcers-x": "Partajări (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index bc429aab11..f42021f6c4 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/rw/topic.json b/public/language/rw/topic.json index 2958a944ba..b640897aca 100644 --- a/public/language/rw/topic.json +++ b/public/language/rw/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/sc/topic.json b/public/language/sc/topic.json index 525bece6ae..28223106ff 100644 --- a/public/language/sc/topic.json +++ b/public/language/sc/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json index ec21b76090..da85522a3b 100644 --- a/public/language/sk/topic.json +++ b/public/language/sk/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/sl/topic.json b/public/language/sl/topic.json index 7b674ec175..4f6795b5b1 100644 --- a/public/language/sl/topic.json +++ b/public/language/sl/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/sq-AL/topic.json b/public/language/sq-AL/topic.json index 28a258d57f..60cadd85f7 100644 --- a/public/language/sq-AL/topic.json +++ b/public/language/sq-AL/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json index 958cafe237..9d511aa929 100644 --- a/public/language/sr/topic.json +++ b/public/language/sr/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/sv/topic.json b/public/language/sv/topic.json index c62d9236c8..d2361ead1e 100644 --- a/public/language/sv/topic.json +++ b/public/language/sv/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/th/topic.json b/public/language/th/topic.json index 8fa13aa8b5..818384ecf6 100644 --- a/public/language/th/topic.json +++ b/public/language/th/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "ลิงก์ไปโพสต์ที่ยังไม่ได้อ่าน", "thumb-image": "รูปย่อของกระทู้", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/tr/topic.json b/public/language/tr/topic.json index e7bdb42769..9e4c6e8dd1 100644 --- a/public/language/tr/topic.json +++ b/public/language/tr/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Okunmamış iletilerin bağlantısı", "thumb-image": "Başlık önizleme görüntüsü", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/uk/topic.json b/public/language/uk/topic.json index 0a2e2d5b84..7c2ccc6659 100644 --- a/public/language/uk/topic.json +++ b/public/language/uk/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/ur/topic.json b/public/language/ur/topic.json index b60c0e1386..4ef539fdc1 100644 --- a/public/language/ur/topic.json +++ b/public/language/ur/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "غیر پڑھے ہوئے پوسٹس کا لنک", "thumb-image": "موضوع کی آئیکن", "announcers": "شیئرز", - "announcers-x": "شیئرز (%1)" + "announcers-x": "شیئرز (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index f556d11381..1e1a969616 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "Liên kết bài đăng chưa đọc", "thumb-image": "Ảnh thumbnail chủ đề", "announcers": "Chia sẻ", - "announcers-x": "Chia sẻ (%1)" + "announcers-x": "Chia sẻ (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json index 964635c7a5..d307f3f906 100644 --- a/public/language/zh-CN/topic.json +++ b/public/language/zh-CN/topic.json @@ -182,7 +182,7 @@ "composer.replying-to": "正在回复 %1", "composer.new-topic": "新主题", "composer.editing-in": "在 %1 中编辑帖子", - "composer.untitled-topic": "Untitled Topic", + "composer.untitled-topic": "无标题主题", "composer.uploading": "正在上传...", "composer.thumb-url-label": "粘贴主题缩略图网址", "composer.thumb-title": "给此主题添加缩略图", @@ -233,5 +233,8 @@ "unread-posts-link": "未读帖子链接", "thumb-image": "主题缩略图", "announcers": "分享", - "announcers-x": "分享 (%1)" + "announcers-x": "分享 (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file diff --git a/public/language/zh-TW/topic.json b/public/language/zh-TW/topic.json index e5783e84b3..aaa0ef9d53 100644 --- a/public/language/zh-TW/topic.json +++ b/public/language/zh-TW/topic.json @@ -233,5 +233,8 @@ "unread-posts-link": "未讀貼文鏈結", "thumb-image": "主題縮圖", "announcers": "Shares", - "announcers-x": "Shares (%1)" + "announcers-x": "Shares (%1)", + "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", + "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", + "guest-cta.closing": "With your input, this post could be even better 💗" } \ No newline at end of file From 7eb491367101a853b8dc5d4602426f000dc45628 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Feb 2026 12:01:54 -0500 Subject: [PATCH 4156/4744] fix: bad relative path --- public/openapi/write/admin/activitypub/rules/order.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/openapi/write/admin/activitypub/rules/order.yaml b/public/openapi/write/admin/activitypub/rules/order.yaml index 3cf931875d..75f23ac859 100644 --- a/public/openapi/write/admin/activitypub/rules/order.yaml +++ b/public/openapi/write/admin/activitypub/rules/order.yaml @@ -24,6 +24,6 @@ put: type: object properties: status: - $ref: ../../../components/schemas/Status.yaml#/Status + $ref: ../../../../components/schemas/Status.yaml#/Status response: - $ref: ../../../components/schemas/admin/rules.yaml#/RulesArray + $ref: ../../../../components/schemas/admin/rules.yaml#/RulesArray From 1598004eaa4ef34f3f4371e62bf4505dccf2ef95 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Feb 2026 12:30:19 -0500 Subject: [PATCH 4157/4744] fix: lint --- public/src/admin/settings/activitypub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/admin/settings/activitypub.js b/public/src/admin/settings/activitypub.js index 652b1bcd3d..9351f6e6c9 100644 --- a/public/src/admin/settings/activitypub.js +++ b/public/src/admin/settings/activitypub.js @@ -143,7 +143,7 @@ define('admin/settings/activitypub', [ placeholder: 'ui-state-highlight', axis: 'y', update: function () { - var rids = []; + const rids = []; tbodyEl.find('tr').each(function () { rids.push($(this).data('rid')); }); From bafd5db07ced020661118242407de1c75fbbe3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Feb 2026 12:49:41 -0500 Subject: [PATCH 4158/4744] chore: up themes --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 2d40f2a9e6..68b63cdc9f 100644 --- a/install/package.json +++ b/install/package.json @@ -108,8 +108,8 @@ "nodebb-plugin-web-push": "0.7.6", "nodebb-rewards-essentials": "1.0.2", "nodebb-theme-harmony": "2.2.8", - "nodebb-theme-lavender": "7.1.19", - "nodebb-theme-peace": "2.2.49", + "nodebb-theme-lavender": "7.1.20", + "nodebb-theme-peace": "2.2.50", "nodebb-theme-persona": "14.2.2", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", From 292e70f70a9afa8060f51d5c678eeed65f533c79 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Feb 2026 13:43:35 -0500 Subject: [PATCH 4159/4744] fix: add example value for failing schema test --- public/openapi/write/admin/activitypub/rules/order.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/public/openapi/write/admin/activitypub/rules/order.yaml b/public/openapi/write/admin/activitypub/rules/order.yaml index 75f23ac859..ef459958f1 100644 --- a/public/openapi/write/admin/activitypub/rules/order.yaml +++ b/public/openapi/write/admin/activitypub/rules/order.yaml @@ -15,6 +15,7 @@ put: description: A list of rule IDs in the preferred order. Any omitted IDs will remain in the last-known order, which may conflict with the new ordering. items: type: string + example: ["be1aee05-2d56-484e-9d83-6212d0f8a033", "81b20d59-85af-44e7-9e52-dadf6ff3c7fb"] responses: '200': description: rules successfully re-ordered From c4411423b6481ac6a8e7899e2817428284412af9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Feb 2026 14:02:49 -0500 Subject: [PATCH 4160/4744] fix: #13983, show only local categories in ACP privilege selector --- public/src/admin/manage/privileges.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index 5da36cb0d3..f5f3b689c1 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -30,6 +30,7 @@ define('admin/manage/privileges', [ Privileges.refreshPrivilegeTable(); ajaxify.updateHistory('admin/manage/privileges/' + (cid || '')); }, + localOnly: true, localCategories: ajaxify.data.categories, privilege: 'find', showLinks: true, From 26af029af0d05925e54632786afcaecc1d57f8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Feb 2026 14:22:51 -0500 Subject: [PATCH 4161/4744] https://github.com/NodeBB/NodeBB/issues/13982 --- public/src/client/topic/postTools.js | 2 +- src/views/admin/manage/categories.tpl | 2 +- src/views/admin/manage/group.tpl | 4 ++-- src/views/admin/manage/registration.tpl | 2 +- src/views/admin/manage/users.tpl | 6 +++--- .../admin/partials/categories/select-category.tpl | 2 +- .../admin/partials/category/selector-dropdown-left.tpl | 2 +- .../partials/category/selector-dropdown-right.tpl | 2 +- src/views/flags/detail.tpl | 4 ++-- src/views/partials/category/filter-dropdown-left.tpl | 2 +- src/views/partials/category/filter-dropdown-right.tpl | 2 +- src/views/partials/category/selector-dropdown-left.tpl | 2 +- .../partials/category/selector-dropdown-right.tpl | 2 +- src/views/partials/category/sort.tpl | 2 +- src/views/partials/category/tools-dropdown-left.tpl | 2 +- src/views/partials/category/tools-dropdown-right.tpl | 2 +- src/views/partials/category/watch.tpl | 2 +- src/views/partials/flags/filters.tpl | 10 +++++----- src/views/partials/tags/filter-dropdown-left.tpl | 2 +- src/views/partials/tags/filter-dropdown-right.tpl | 2 +- src/views/partials/tags/watch.tpl | 2 +- src/views/post-queue.tpl | 4 ++-- 22 files changed, 31 insertions(+), 31 deletions(-) diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 640d936f16..28538b972f 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -453,7 +453,7 @@ define('forum/topic/postTools', [ require(['chat'], function (chat) { chat.newChat(post.attr('data-uid')); }); - button.parents('.btn-group').find('.dropdown-toggle').click(); + button.parents('.dropdown').find('.dropdown-toggle').click(); return false; } diff --git a/src/views/admin/manage/categories.tpl b/src/views/admin/manage/categories.tpl index 243936577e..0519c429d3 100644 --- a/src/views/admin/manage/categories.tpl +++ b/src/views/admin/manage/categories.tpl @@ -8,7 +8,7 @@ -
    +
    diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index 3223b2a485..21fa9d5e1c 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -21,7 +21,7 @@ -
    + -
    + -
    +
    {{{ if @first }}}{invites.username}{{{ end }}} {invites.invitations.email} {invites.invitations.username} -
    +
    +
    @@ -37,25 +37,23 @@ {./name}
    {./percentFull}%{{{if ./length}}}{./length}{{{else}}}{./itemCount}{{{end}}} + {./percentFull}%{{{if ./length}}}{./length}{{{else}}}{./itemCount}{{{end}}} {{{ if (./name == "post") }}}
    - - - - + +
    {{{ else }}} {{{if ./max}}}{./max}{{{else}}}{./maxSize}{{{end}}} {{{ end }}}
    {./hits}{./misses}{./hitRatio}{./hitsPerSecond}{./ttl}{./hits}{./misses}{./hitRatio}{./hitsPerSecond}{./ttl}
    From 6e4e02a68bb56eed1bc0c6bd01adc61a4eb8646b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:02:40 -0500 Subject: [PATCH 4171/4744] fix(deps): update dependency qs to v6.14.2 (#13978) 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 bd76ddf18d..0712b722b7 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", - "qs": "6.14.1", + "qs": "6.14.2", "redis": "5.10.0", "rimraf": "6.1.2", "rss": "1.2.2", From 1020092b978e8e6a5b8ffcefb4187ea3da9e3949 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:02:52 -0500 Subject: [PATCH 4172/4744] fix(deps): update dependency webpack to v5.105.2 (#13986) 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 0712b722b7..983970bf01 100644 --- a/install/package.json +++ b/install/package.json @@ -151,7 +151,7 @@ "tough-cookie": "6.0.0", "undici": "^7.10.0", "validator": "13.15.26", - "webpack": "5.105.1", + "webpack": "5.105.2", "webpack-merge": "6.0.1", "winston": "3.19.0", "workerpool": "10.0.1", From b0f2feadf48964e438a2ae9d663c56821248e62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Feb 2026 21:26:40 -0500 Subject: [PATCH 4173/4744] refactor: shorter check --- public/src/modules/helpers.common.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index a33e7af8c7..6a09a32869 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -374,11 +374,7 @@ module.exports = function (utils, Benchpress, relative_path) { } function shouldHideReplyContainer(post) { - if (post.replies.count <= 0 || post.replies.hasSingleImmediateReply) { - return true; - } - - return false; + return post.replies.count <= 0 || post.replies.hasSingleImmediateReply; } function humanReadableNumber(number, toFixed = 1) { From 0b7df274c3ff973fc761c9b454f48a26f492f29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Feb 2026 22:49:55 -0500 Subject: [PATCH 4174/4744] fix: unbans not triggering if user data is loaded wit 'banned' property only this was happening because of `fieldsToRemove` running before unban logic and clearing out 'banned:expire' field to undefined --- src/user/data.js | 73 +++++++++++++++++++++++------------------------- test/user.js | 10 +++++++ 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/user/data.js b/src/user/data.js index 425379f7a2..80a9b5d9bf 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -118,31 +118,22 @@ module.exports = function (User) { }; function ensureRequiredFields(fields, fieldsToRemove) { - function addField(field) { - if (!fields.includes(field)) { - fields.push(field); - fieldsToRemove.push(field); - } - } - if (fields.length && !fields.includes('uid')) { fields.push('uid'); } - if (fields.includes('picture')) { - addField('uploadedpicture'); - } - - if (fields.includes('status')) { - addField('lastonline'); - } - - if (fields.includes('banned') && !fields.includes('banned:expire')) { - addField('banned:expire'); - } - - if (fields.includes('username') && !fields.includes('fullname')) { - addField('fullname'); + const requiredFields = { + picture: 'uploadedpicture', + status: 'lastonline', + banned: 'banned:expire', + 'banned:expire': 'banned', + username: 'fullname', + }; + for (const [key, field] of Object.entries(requiredFields)) { + if (fields.includes(key) && !fields.includes(field)) { + fields.push(field); + fieldsToRemove.push(field); + } } } @@ -246,7 +237,7 @@ module.exports = function (User) { } if (user.hasOwnProperty('email')) { - user.email = validator.escape(user.email ? user.email.toString() : ''); + user.email = validator.escape(String(user.email || '')); } if (!user.uid && !activitypub.helpers.isUri(user.uid)) { @@ -280,19 +271,6 @@ module.exports = function (User) { user.status = User.getStatus(user); } - for (let i = 0; i < fieldsToRemove.length; i += 1) { - user[fieldsToRemove[i]] = undefined; - } - - // User Icons - if (requestedFields.includes('picture') && user.username && user.uid !== 0 && !meta.config.defaultAvatar) { - if (!iconBackgrounds.includes(user['icon:bgColor'])) { - const nameAsIndex = Array.from(user.username).reduce((cur, next) => cur + next.charCodeAt(), 0); - user['icon:bgColor'] = iconBackgrounds[nameAsIndex % iconBackgrounds.length]; - } - user['icon:text'] = (user.username[0] || '').toUpperCase(); - } - if (user.hasOwnProperty('joindate')) { user.joindateISO = utils.toISOString(user.joindate); } @@ -305,7 +283,18 @@ module.exports = function (User) { user.muted = user.mutedUntil > Date.now(); } - if (user.hasOwnProperty('banned') || user.hasOwnProperty('banned:expire')) { + user.isLocal = utils.isNumber(user.uid); + + // User Icons + if (requestedFields.includes('picture') && user.username && user.uid !== 0 && !meta.config.defaultAvatar) { + if (!iconBackgrounds.includes(user['icon:bgColor'])) { + const nameAsIndex = Array.from(user.username).reduce((cur, next) => cur + next.charCodeAt(), 0); + user['icon:bgColor'] = iconBackgrounds[nameAsIndex % iconBackgrounds.length]; + } + user['icon:text'] = (user.username[0] || '').toUpperCase(); + } + + if (user.hasOwnProperty('banned') && user.hasOwnProperty('banned:expire')) { const result = User.bans.calcExpiredFromUserData(user); user.banned = result.banned; const unban = result.banned && result.banExpired; @@ -316,9 +305,17 @@ module.exports = function (User) { user.banned = false; } } - - user.isLocal = utils.isNumber(user.uid); }); + + // remove fields that were added just for processing + fieldsToRemove.forEach((field) => { + users.forEach((user) => { + if (user) { + user[field] = undefined; + } + }); + }); + if (unbanUids.length) { await User.bans.unban(unbanUids, '[[user:info.ban-expired]]'); } diff --git a/test/user.js b/test/user.js index fd90f9c6de..b2ad5970a8 100644 --- a/test/user.js +++ b/test/user.js @@ -1377,6 +1377,16 @@ describe('User', () => { assert(result); assert.strictEqual(result.topicData.title, 'banned topic'); }); + + it('should unban user properly if only "banned" field is requested', async () => { + const testUid = await User.create({ username: 'bannedUser3' }); + await User.bans.ban(testUid, Date.now() + 2000); + assert.strictEqual(await db.isSortedSetMember('users:banned', testUid), true); + await setTimeout(3000); + await User.getUserFields(testUid, ['uid', 'banned']); // loading their data unbans the user + assert.strictEqual(await db.isSortedSetMember('users:banned', testUid), false); + }); + }); describe('Digest.getSubscribers', () => { From 0e2a42d547ab1736fb4f92c727500051a4f6d49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Feb 2026 23:22:10 -0500 Subject: [PATCH 4175/4744] test: fix spec --- public/openapi/components/schemas/Chats.yaml | 4 ++++ public/openapi/components/schemas/TopicObject.yaml | 6 ++++-- public/openapi/components/schemas/UserObject.yaml | 4 ++++ public/openapi/read/unread.yaml | 2 ++ public/openapi/read/user/userslug/chats/roomid.yaml | 2 ++ public/openapi/write/categories/cid/moderator/uid.yaml | 4 ++++ src/user/data.js | 2 +- 7 files changed, 21 insertions(+), 3 deletions(-) diff --git a/public/openapi/components/schemas/Chats.yaml b/public/openapi/components/schemas/Chats.yaml index 036b937158..3d4339c083 100644 --- a/public/openapi/components/schemas/Chats.yaml +++ b/public/openapi/components/schemas/Chats.yaml @@ -108,6 +108,10 @@ MessageObject: `icon:text` for the user's auto-generated icon example: "#f44336" + banned_until: + type: number + description: A UNIX timestamp representing the moment a ban will be lifted + example: 0 banned_until_readable: type: string description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned" diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml index 64c47f0daa..605c32de85 100644 --- a/public/openapi/components/schemas/TopicObject.yaml +++ b/public/openapi/components/schemas/TopicObject.yaml @@ -60,8 +60,6 @@ TopicObject: signature: type: string nullable: true - banned: - type: number status: type: string icon:text: @@ -76,6 +74,10 @@ TopicObject: `icon:text` for the user's auto-generated icon example: "#f44336" + banned: + type: number + banned_until: + type: number banned_until_readable: type: string required: diff --git a/public/openapi/components/schemas/UserObject.yaml b/public/openapi/components/schemas/UserObject.yaml index 5dc0da6bf4..61d98b1f80 100644 --- a/public/openapi/components/schemas/UserObject.yaml +++ b/public/openapi/components/schemas/UserObject.yaml @@ -681,6 +681,10 @@ UserObjectACP: lastonlineISO: type: string example: '2020-03-27T20:30:36.590Z' + banned_until: + type: number + description: A UNIX timestamp representing the moment a ban will be lifted + example: 0 banned_until_readable: type: string description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned" diff --git a/public/openapi/read/unread.yaml b/public/openapi/read/unread.yaml index 77e9ec44f6..6ae0e8500e 100644 --- a/public/openapi/read/unread.yaml +++ b/public/openapi/read/unread.yaml @@ -109,6 +109,8 @@ get: `icon:text` for the user's auto-generated icon example: "#f44336" + banned_until: + type: number banned_until_readable: type: string required: diff --git a/public/openapi/read/user/userslug/chats/roomid.yaml b/public/openapi/read/user/userslug/chats/roomid.yaml index 73c4a62da9..448f350a42 100644 --- a/public/openapi/read/user/userslug/chats/roomid.yaml +++ b/public/openapi/read/user/userslug/chats/roomid.yaml @@ -124,6 +124,8 @@ get: `icon:text` for the user's auto-generated icon example: "#f44336" + banned_until: + type: number banned_until_readable: type: string deleted: diff --git a/public/openapi/write/categories/cid/moderator/uid.yaml b/public/openapi/write/categories/cid/moderator/uid.yaml index 3e7223d1d7..bbba82c7c6 100644 --- a/public/openapi/write/categories/cid/moderator/uid.yaml +++ b/public/openapi/write/categories/cid/moderator/uid.yaml @@ -79,6 +79,10 @@ put: type: number description: A Boolean representing whether a user is banned or not example: 0 + banned_until: + type: number + description: A UNIX timestamp representing the moment a ban will be lifted + example: 0 banned_until_readable: type: string description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned" diff --git a/src/user/data.js b/src/user/data.js index 80a9b5d9bf..d45e4a6259 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -275,7 +275,7 @@ module.exports = function (User) { user.joindateISO = utils.toISOString(user.joindate); } - if (user.hasOwnProperty('lastonline')) { + if (user.hasOwnProperty('lastonline') && (!requestedFields.length || requestedFields.includes('lastonline')) && !fieldsToRemove.includes('lastonline')) { user.lastonlineISO = utils.toISOString(user.lastonline) || user.joindateISO; } From a84464cffbcf8310ed3176e8a50ee977e5a96104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Feb 2026 09:45:56 -0500 Subject: [PATCH 4176/4744] chore: up themes --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 983970bf01..3094b5b4aa 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.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.9", + "nodebb-theme-harmony": "2.2.10", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", - "nodebb-theme-persona": "14.2.3", + "nodebb-theme-persona": "14.2.4", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", "nprogress": "0.2.0", From 8c8782fd242e9192ea51608dab6da706353f9aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Feb 2026 10:17:34 -0500 Subject: [PATCH 4177/4744] fix: #13990, don't blindly set `user` field on notification objects that don't have a "from" property --- install/package.json | 4 ++-- src/notifications.js | 13 ++++++++----- test/notifications.js | 37 ++++++++++++++++++++++++++++--------- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/install/package.json b/install/package.json index 3094b5b4aa..b6acf95bd8 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.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.10", + "nodebb-theme-harmony": "2.2.11", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", - "nodebb-theme-persona": "14.2.4", + "nodebb-theme-persona": "14.2.5", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", "nprogress": "0.2.0", diff --git a/src/notifications.js b/src/notifications.js index 374aec988c..a47860887a 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -86,7 +86,7 @@ Notifications.getMultiple = async function (nids) { const keys = nids.map(nid => `notifications:${nid}`); const notifications = await db.getObjects(keys); - const userKeys = notifications.map(n => n && n.from); + const userKeys = notifications.filter(n => n && n.from).map(n => n.from); let [usersData, categoriesData] = await Promise.all([ User.getUsersFields(userKeys, ['username', 'userslug', 'picture']), categories.getCategoriesFields(userKeys, ['cid', 'name', 'slug', 'picture']), @@ -105,8 +105,10 @@ Notifications.getMultiple = async function (nids) { return userData; }); + // from can be either uid or cid + const userMap = new Map(userKeys.map((from, index) => [from, usersData[index]])); - notifications.forEach((notification, index) => { + notifications.forEach((notification) => { if (notification) { intFields.forEach((field) => { if (notification.hasOwnProperty(field)) { @@ -123,8 +125,9 @@ Notifications.getMultiple = async function (nids) { if (notification.bodyLong) { notification.bodyLong = utils.stripHTMLTags(notification.bodyLong, ['img', 'p', 'a']); } - - notification.user = usersData[index]; + if (userMap.has(notification.from)) { + notification.user = userMap.get(notification.from); + } if (notification.user && notification.from) { notification.image = notification.user.picture || null; if (notification.user.username === '[[global:guest]]') { @@ -166,7 +169,7 @@ Notifications.create = async function (data) { const oldNotif = await db.getObject(`notifications:${data.nid}`); if ( oldNotif && - parseInt(oldNotif.pid, 10) === parseInt(data.pid, 10) && + String(oldNotif.pid, 10) === String(data.pid, 10) && parseInt(oldNotif.importance, 10) > parseInt(data.importance, 10) ) { return null; diff --git a/test/notifications.js b/test/notifications.js index 0fdcc7f117..33d03fadcf 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -19,15 +19,8 @@ describe('Notifications', () => { let uid; let notification; - before((done) => { - user.create({ username: 'poster' }, (err, _uid) => { - if (err) { - return done(err); - } - - uid = _uid; - done(); - }); + before(async () => { + uid = await user.create({ username: 'poster' }); }); it('should fail to create notification without a nid', (done) => { @@ -59,6 +52,32 @@ describe('Notifications', () => { }); }); + it('should create a notification with a custom icon', async () => { + const nid = 'custom-icon-notification'; + await notifications.create({ + nid: nid, + bodyShort: 'Notification with custom icon', + icon: 'fa-solid fa-bell', + }); + const notifData = await notifications.get(nid); + assert.strictEqual(notifData.user, undefined); + assert.strictEqual(notifData.icon, 'fa-solid fa-bell'); + }); + + it('should create a notification with a user icon/bgColor', async () => { + const uid = await user.create({ username: 'iconuser' }); + const nid = 'user-icon-notification'; + await notifications.create({ + nid: nid, + bodyShort: 'Notification with user icon', + from: uid, + }); + const notifData = await notifications.get(nid); + assert.strictEqual(notifData.icon, undefined); + assert.strictEqual(notifData.user['icon:text'], 'I'); + assert.strictEqual(notifData.user['icon:bgColor'], '#3f51b5'); + }); + it('should return null if pid is same and importance is lower', (done) => { notifications.create({ bodyShort: 'bodyShort', From a8c68ddc65c446fcda641eef174c835e2a6c8609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Feb 2026 10:39:54 -0500 Subject: [PATCH 4178/4744] test: fix redis, from was string in map, but int in notif object --- src/notifications.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index a47860887a..12ed99f7d5 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -106,7 +106,7 @@ Notifications.getMultiple = async function (nids) { return userData; }); // from can be either uid or cid - const userMap = new Map(userKeys.map((from, index) => [from, usersData[index]])); + const userMap = new Map(userKeys.map((from, index) => [String(from), usersData[index]])); notifications.forEach((notification) => { if (notification) { @@ -125,8 +125,9 @@ Notifications.getMultiple = async function (nids) { if (notification.bodyLong) { notification.bodyLong = utils.stripHTMLTags(notification.bodyLong, ['img', 'p', 'a']); } - if (userMap.has(notification.from)) { - notification.user = userMap.get(notification.from); + const fromUser = userMap.get(String(notification.from)); + if (fromUser !== undefined) { + notification.user = fromUser; } if (notification.user && notification.from) { notification.image = notification.user.picture || null; From 3756a8fe6cc7038b3536eb63a0bfd2ddfd4dc2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Feb 2026 11:29:16 -0500 Subject: [PATCH 4179/4744] refactor: updateTags to modern js --- public/src/ajaxify.js | 72 +++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 48 deletions(-) diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index cc8a56800b..679273da14 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -298,70 +298,46 @@ ajaxify.widgets = { render: render }; ajaxify.updateTitle = updateTitle; function updateTags() { - const metaWhitelist = ['title', 'description', /og:.+/, /article:.+/, 'robots'].map(function (val) { - return new RegExp(val); - }); + const metaWhitelist = ['title', 'description', /og:.+/, /article:.+/, 'robots'].map(val => new RegExp(val)); const linkWhitelist = ['canonical', 'alternate', 'up']; // Delete the old meta tags - Array.prototype.slice - .call(document.querySelectorAll('head meta')) - .filter(function (el) { - const name = el.getAttribute('property') || el.getAttribute('name'); - return metaWhitelist.some(function (exp) { - return !!exp.test(name); - }); - }) - .forEach(function (el) { - document.head.removeChild(el); - }); + document.querySelectorAll('head meta').forEach(el => { + const name = el.getAttribute('property') || el.getAttribute('name') || ''; + if (metaWhitelist.some(exp => exp.test(name))) { + el.remove(); + } + }); // Add new meta tags - ajaxify.data._header.tags.meta - .filter(function (tagObj) { - const name = tagObj.name || tagObj.property; - return metaWhitelist.some(function (exp) { - return !!exp.test(name); - }); - }).forEach(async function (tagObj) { + ajaxify.data._header.tags.meta.forEach(async (tagObj) => { + const name = tagObj.name || tagObj.property; + if (metaWhitelist.some(exp => exp.test(name))) { if (tagObj.content) { tagObj.content = await translator.translate(tagObj.content); } const metaEl = document.createElement('meta'); - Object.keys(tagObj).forEach(function (prop) { - metaEl.setAttribute(prop, tagObj[prop]); - }); + Object.keys(tagObj).forEach(prop => metaEl.setAttribute(prop, tagObj[prop])); document.head.appendChild(metaEl); - }); - + } + }); // Delete the old link tags - Array.prototype.slice - .call(document.querySelectorAll('head link')) - .filter(function (el) { - const name = el.getAttribute('rel'); - return linkWhitelist.some(function (item) { - return item === name; - }); - }) - .forEach(function (el) { - document.head.removeChild(el); - }); + document.querySelectorAll('head link').forEach(el => { + const name = el.getAttribute('rel'); + if (linkWhitelist.some(item => item === name)) { + el.remove(); + } + }); // Add new link tags - ajaxify.data._header.tags.link - .filter(function (tagObj) { - return linkWhitelist.some(function (item) { - return item === tagObj.rel; - }); - }) - .forEach(function (tagObj) { + ajaxify.data._header.tags.link.forEach(async (tagObj) => { + if (linkWhitelist.some(item => item === tagObj.rel)) { const linkEl = document.createElement('link'); - Object.keys(tagObj).forEach(function (prop) { - linkEl.setAttribute(prop, tagObj[prop]); - }); + Object.keys(tagObj).forEach(prop => linkEl.setAttribute(prop, tagObj[prop])); document.head.appendChild(linkEl); - }); + } + }); } ajaxify.end = function (url, tpl_url) { From 1ca9841ce5c1283b68b4f3a5acb93efa33bdc657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Feb 2026 12:26:54 -0500 Subject: [PATCH 4180/4744] fix: dont call getInbox for /recent make sure there are no dupes if called --- src/topics/sorted.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/topics/sorted.js b/src/topics/sorted.js index f0bb2ac731..1d4da1f772 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -74,7 +74,7 @@ module.exports = function (Topics) { } async function getInbox(tids, params) { - if (params.cids && !params.cids.includes('-1')) { + if (!Array.isArray(params.cids) || !params.cids.includes('-1')) { return tids; } @@ -97,7 +97,7 @@ module.exports = function (Topics) { inbox = await db[method](`uid:${params.uid}:inbox`, 0, meta.config.recentMaxTopics - 1); } - return tids.concat(inbox); + return _.uniq(tids.concat(inbox)); } function sortToSet(sort) { From 25f866cac13a8abb8d49aa8ab01ae4f7571c26a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:27:29 -0500 Subject: [PATCH 4181/4744] chore(deps): update postgres docker tag to v18.2 (#13987) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose-pgsql.yml | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose-pgsql.yml b/docker-compose-pgsql.yml index 1bc3da2515..9f4db32813 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:18.1-alpine + image: postgres:18.2-alpine restart: unless-stopped environment: POSTGRES_USER: nodebb diff --git a/docker-compose.yml b/docker-compose.yml index 053a1311c1..0faafb8f0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: - redis postgres: - image: postgres:18.1-alpine + image: postgres:18.2-alpine restart: unless-stopped environment: POSTGRES_USER: nodebb From ff292f7dee5890b1711b3347b2baad94d77e045e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:29:07 -0500 Subject: [PATCH 4182/4744] fix(deps): update dependency nodebb-plugin-composer-default to v10.3.16 (#13991) 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 b6acf95bd8..269553edb1 100644 --- a/install/package.json +++ b/install/package.json @@ -97,7 +97,7 @@ "multer": "2.0.2", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.6.1", - "nodebb-plugin-composer-default": "10.3.15", + "nodebb-plugin-composer-default": "10.3.16", "nodebb-plugin-dbsearch": "6.3.5", "nodebb-plugin-emoji": "6.0.5", "nodebb-plugin-emoji-android": "4.1.1", From 71d4a6fc4e98117d203f93b083a4804bdecaf718 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:29:23 -0500 Subject: [PATCH 4183/4744] fix(deps): update dependency sortablejs to v1.15.7 (#13985) 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 269553edb1..cf85ac1842 100644 --- a/install/package.json +++ b/install/package.json @@ -140,7 +140,7 @@ "socket.io": "4.8.3", "socket.io-client": "4.8.3", "@socket.io/redis-adapter": "8.3.0", - "sortablejs": "1.15.6", + "sortablejs": "1.15.7", "spdx-license-list": "6.11.0", "terser-webpack-plugin": "5.3.16", "textcomplete": "0.18.2", From efd322737d2b683441568b7c588d8ef8c2d2fadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Feb 2026 20:13:59 -0500 Subject: [PATCH 4184/4744] moved to harmony --- public/scss/skins.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/public/scss/skins.scss b/public/scss/skins.scss index c1a18b0a47..4a330c521f 100644 --- a/public/scss/skins.scss +++ b/public/scss/skins.scss @@ -2,11 +2,6 @@ // brite text-secondary is white :/ .skin-brite { - .btn { - // this removes the 3px left margin on buttons in brite skin - // without button in the sidebar ontopic.tpl aren't properly - margin-left: 0px; - } .text-secondary { color: var(--bs-secondary-color) !important; } From bb9033af02d5ebbd96c9c9c926291487f601a842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Feb 2026 21:42:41 -0500 Subject: [PATCH 4185/4744] fix: wrong wrapping of route --- src/routes/activitypub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index c09b7b9821..6c13fea365 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -50,7 +50,7 @@ module.exports = function (app, middleware, controllers) { app.get('/topic/:tid/:slug?', [...middlewares, middleware.assert.topic], helpers.tryRoute(controllers.activitypub.actors.topic)); app.get('/category/:cid/inbox', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.getInbox)); - app.post('/category/:cid/inbox', [...inboxMiddlewares, middleware.assert.category, ...inboxMiddlewares], helpers.tryRoute(controllers.activitypub).postInbox); + app.post('/category/:cid/inbox', [...inboxMiddlewares, middleware.assert.category, ...inboxMiddlewares], helpers.tryRoute(controllers.activitypub.postInbox)); app.get('/category/:cid/outbox', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.getCategoryOutbox)); app.post('/category/:cid/outbox', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.postOutbox)); app.get('/category/:cid/:slug?', [...middlewares, middleware.assert.category], helpers.tryRoute(controllers.activitypub.actors.category)); From 705a151a6bbdc9305eaaadb3e193a160eda054c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 14 Feb 2026 17:14:30 -0500 Subject: [PATCH 4186/4744] add text-tabular utility --- public/scss/generics.scss | 1 + public/src/admin/advanced/cache.js | 4 ++-- src/views/admin/advanced/cache.tpl | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/public/scss/generics.scss b/public/scss/generics.scss index f2c50ee44e..e76b8d0989 100644 --- a/public/scss/generics.scss +++ b/public/scss/generics.scss @@ -13,6 +13,7 @@ .text-md { font-size: 1.125rem!important; } // 18px on harmony .text-sm { font-size: 0.875rem!important; } // 14px on harmony .text-xs { font-size: 0.75rem!important; } // 12px on harmony +.text-tabular { font-variant-numeric: tabular-nums!important; } .overscroll-behavior-contain { overscroll-behavior: contain; diff --git a/public/src/admin/advanced/cache.js b/public/src/admin/advanced/cache.js index ce11ea6581..d8463da487 100644 --- a/public/src/admin/advanced/cache.js +++ b/public/src/admin/advanced/cache.js @@ -36,8 +36,8 @@ define('admin/advanced/cache', ['alerts'], function (alerts) { const rows = tbody.find('tr').toArray(); // Toggle sort direction - const ascending = !!$(this).data('asc'); - $(this).data('asc', !ascending); + const ascending = $(this).data('sort') === 'asc'; + $(this).data('sort', !ascending ? 'asc' : 'desc'); // Remove sort indicators from all headers table.find('th i').addClass('invisible'); diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl index 17e226665e..a0efd3ff3e 100644 --- a/src/views/admin/advanced/cache.tpl +++ b/src/views/admin/advanced/cache.tpl @@ -14,19 +14,19 @@ - - - - - - - - - + + + + + + + + + - + {{{ each caches }}} - + {{{ each info }}} From 63199ea75c6bb2481d7d65f20daf84a2bca8c3e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:55:27 -0500 Subject: [PATCH 4209/4744] fix(deps): update dependency redis to v5.11.0 (#13996) 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 184deb7def..cbef4a9111 100644 --- a/install/package.json +++ b/install/package.json @@ -125,7 +125,7 @@ "progress-webpack-plugin": "1.0.16", "prompt": "1.3.0", "qs": "6.15.0", - "redis": "5.10.0", + "redis": "5.11.0", "rimraf": "6.1.3", "rss": "1.2.2", "rtlcss": "4.3.0", From 5df2b8b7857a74f99f5098d5e016a94f1748c45b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 11 Feb 2026 15:02:29 -0500 Subject: [PATCH 4210/4744] feat: quick create on world page This commit removes title requirement checks in NodeBB and updates the topic creation logic so that incoming topic creation API requests without a title just generate a title (like they already do for incoming AP content.) --- public/language/en-GB/topic.json | 1 + public/src/client/world.js | 7 +++++-- public/src/modules/quickreply.js | 14 +++++++++++++- src/controllers/activitypub/topics.js | 7 ++++++- src/routes/write/topics.js | 2 +- src/topics/create.js | 12 +++++++++++- 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index d7533b80f3..ac69995c89 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -254,6 +254,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", diff --git a/public/src/client/world.js b/public/src/client/world.js index adf6925e57..8d7a11b927 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -3,8 +3,10 @@ define('forum/world', [ 'forum/infinitescroll', 'search', 'sort', 'hooks', 'alerts', 'api', 'bootbox', 'helpers', 'forum/category/tools', - 'translator', -], function (infinitescroll, search, sort, hooks, alerts, api, bootbox, helpers, categoryTools, translator) { + 'translator', 'quickreply', +], function (infinitescroll, search, sort, hooks, + alerts, api, bootbox, helpers, categoryTools, + translator, quickreply) { const World = {}; $(window).on('action:ajaxify.start', function () { @@ -14,6 +16,7 @@ define('forum/world', [ World.init = function () { app.enterRoom('world'); categoryTools.init($('#world-feed')); + quickreply.init(); sort.handleSort('categoryTopicSort', 'world'); diff --git a/public/src/modules/quickreply.js b/public/src/modules/quickreply.js index 3293bb3275..602c1ace05 100644 --- a/public/src/modules/quickreply.js +++ b/public/src/modules/quickreply.js @@ -69,6 +69,18 @@ define('quickreply', [ handle: undefined, content: replyMsg, }; + let replyRoute = '/topics'; + switch(ajaxify.data.template.name) { + case 'topic': + replyData.tid = ajaxify.data.tid; + replyRoute = `/topics/${ajaxify.data.tid}`; + break; + + case 'world': + replyData.cid = '-1'; + break; + } + const replyLen = replyMsg.length; if (replyLen < parseInt(config.minimumPostLength, 10)) { return alerts.error('[[error:content-too-short, ' + config.minimumPostLength + ']]'); @@ -78,7 +90,7 @@ define('quickreply', [ ready = false; element.val(''); - api.post(`/topics/${ajaxify.data.tid}`, replyData, function (err, data) { + api.post(replyRoute, replyData, function (err, data) { ready = true; if (err) { element.val(replyMsg); diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 11cc8368f4..d780f954c7 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -7,6 +7,7 @@ const user = require('../../user'); const topics = require('../../topics'); const posts = require('../../posts'); const categories = require('../../categories'); +const privileges = require('../../privileges'); const translator = require('../../translator'); const pagination = require('../../pagination'); const utils = require('../../utils'); @@ -24,7 +25,10 @@ controller.list = async function (req, res) { const start = Math.max(0, (page - 1) * topicsPerPage); const stop = start + topicsPerPage - 1; - const userSettings = await user.getSettings(req.uid); + const [userSettings, userPrivileges] = await Promise.all([ + user.getSettings(req.uid), + privileges.categories.get('-1', req.uid), + ]); const targetUid = await user.getUidByUserslug(req.query.author); let cidQuery = { uid: req.uid, @@ -40,6 +44,7 @@ controller.list = async function (req, res) { const data = await categories.getCategoryById(cidQuery); delete data.children; data.sort = req.query.sort; + data.privileges = userPrivileges; let tids; let topicCount; diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index a1c7810558..107fdac8bd 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -10,7 +10,7 @@ const { setupApiRoute } = routeHelpers; module.exports = function () { const middlewares = [middleware.ensureLoggedIn]; - setupApiRoute(router, 'post', '/', [middleware.checkRequired.bind(null, ['cid', 'title', 'content'])], controllers.write.topics.create); + setupApiRoute(router, 'post', '/', [middleware.checkRequired.bind(null, ['cid', 'content'])], controllers.write.topics.create); setupApiRoute(router, 'get', '/:tid', [], controllers.write.topics.get); setupApiRoute(router, 'post', '/:tid', [middleware.checkRequired.bind(null, ['content']), middleware.assert.topic], controllers.write.topics.reply); setupApiRoute(router, 'delete', '/:tid', [...middlewares], controllers.write.topics.purge); diff --git a/src/topics/create.js b/src/topics/create.js index 56fbe49830..f4a5ac8b04 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const winston = require('winston'); +const tokenizer = require('sbd'); const db = require('../database'); const utils = require('../utils'); @@ -102,9 +103,17 @@ module.exports = function (Topics) { privileges.users.isAdministrator(uid), ]); - data.title = String(data.title).trim(); data.tags = data.tags || []; data.content = String(data.content || '').trimEnd(); + + if (data.title) { + data.title = String(data.title).trim(); + } else { + const sentences = tokenizer.sentences(data.content, { sanitize: true, newline_boundaries: true }); + data.title = sentences.shift(); + data.generatedTitle = 1; + } + if (!isAdmin) { Topics.checkTitle(data.title); } @@ -259,6 +268,7 @@ module.exports = function (Topics) { Topics.addParentPosts([postData], uid), Topics.syncBacklinks(postData), Topics.markAsRead([tid], uid), + activitypub.notes.syncUserInboxes(tid, uid), ]); // Returned data is a superset of post summary data From 4bf0f61ea37ee8a272d5037441584cee863fb26e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Feb 2026 11:41:19 -0500 Subject: [PATCH 4211/4744] fix: lint, unused class --- src/views/admin/settings/activitypub.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/admin/settings/activitypub.tpl b/src/views/admin/settings/activitypub.tpl index 130e4ca8fc..1fbebbf6df 100644 --- a/src/views/admin/settings/activitypub.tpl +++ b/src/views/admin/settings/activitypub.tpl @@ -57,7 +57,7 @@ - + {{{ each rules }}} + @@ -25,7 +26,8 @@ - + + {{{ end }}} From 8e050353319a7cfc89e2098cba1a08a6b37ade1d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 11 Mar 2026 02:33:04 +0000 Subject: [PATCH 4564/4744] chore(i18n): fallback strings for new resources: nodebb.admin-advanced-jobs --- public/language/ar/admin/advanced/jobs.json | 3 ++- public/language/az/admin/advanced/jobs.json | 3 ++- public/language/bg/admin/advanced/jobs.json | 3 ++- public/language/bn/admin/advanced/jobs.json | 3 ++- public/language/cs/admin/advanced/jobs.json | 3 ++- public/language/da/admin/advanced/jobs.json | 3 ++- public/language/de/admin/advanced/jobs.json | 3 ++- public/language/el/admin/advanced/jobs.json | 3 ++- public/language/en-US/admin/advanced/jobs.json | 3 ++- public/language/en-x-pirate/admin/advanced/jobs.json | 3 ++- public/language/es/admin/advanced/jobs.json | 3 ++- public/language/et/admin/advanced/jobs.json | 3 ++- public/language/fa-IR/admin/advanced/jobs.json | 3 ++- public/language/fi/admin/advanced/jobs.json | 3 ++- public/language/fr/admin/advanced/jobs.json | 3 ++- public/language/gl/admin/advanced/jobs.json | 3 ++- public/language/he/admin/advanced/jobs.json | 3 ++- public/language/hr/admin/advanced/jobs.json | 3 ++- public/language/hu/admin/advanced/jobs.json | 3 ++- public/language/hy/admin/advanced/jobs.json | 3 ++- public/language/id/admin/advanced/jobs.json | 3 ++- public/language/it/admin/advanced/jobs.json | 3 ++- public/language/ja/admin/advanced/jobs.json | 3 ++- public/language/ko/admin/advanced/jobs.json | 3 ++- public/language/lt/admin/advanced/jobs.json | 3 ++- public/language/lv/admin/advanced/jobs.json | 3 ++- public/language/ms/admin/advanced/jobs.json | 3 ++- public/language/nb/admin/advanced/jobs.json | 3 ++- public/language/nl/admin/advanced/jobs.json | 3 ++- public/language/nn-NO/admin/advanced/jobs.json | 3 ++- public/language/pl/admin/advanced/jobs.json | 3 ++- public/language/pt-BR/admin/advanced/jobs.json | 3 ++- public/language/pt-PT/admin/advanced/jobs.json | 3 ++- public/language/ro/admin/advanced/jobs.json | 3 ++- public/language/ru/admin/advanced/jobs.json | 3 ++- public/language/rw/admin/advanced/jobs.json | 3 ++- public/language/sc/admin/advanced/jobs.json | 3 ++- public/language/sk/admin/advanced/jobs.json | 3 ++- public/language/sl/admin/advanced/jobs.json | 3 ++- public/language/sq-AL/admin/advanced/jobs.json | 3 ++- public/language/sr/admin/advanced/jobs.json | 3 ++- public/language/sv/admin/advanced/jobs.json | 3 ++- public/language/th/admin/advanced/jobs.json | 3 ++- public/language/tr/admin/advanced/jobs.json | 3 ++- public/language/uk/admin/advanced/jobs.json | 3 ++- public/language/ur/admin/advanced/jobs.json | 3 ++- public/language/vi/admin/advanced/jobs.json | 3 ++- public/language/zh-CN/admin/advanced/jobs.json | 3 ++- public/language/zh-TW/admin/advanced/jobs.json | 3 ++- 49 files changed, 98 insertions(+), 49 deletions(-) diff --git a/public/language/ar/admin/advanced/jobs.json b/public/language/ar/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/ar/admin/advanced/jobs.json +++ b/public/language/ar/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/az/admin/advanced/jobs.json b/public/language/az/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/az/admin/advanced/jobs.json +++ b/public/language/az/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/bg/admin/advanced/jobs.json b/public/language/bg/admin/advanced/jobs.json index fe2f4bbe32..43378082d5 100644 --- a/public/language/bg/admin/advanced/jobs.json +++ b/public/language/bg/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "График", "next-run": "Следващо изпълнение", "last-duration": "Последна продължителност", - "running": "В процес на изпълнение" + "running": "В процес на изпълнение", + "active": "Active" } \ No newline at end of file diff --git a/public/language/bn/admin/advanced/jobs.json b/public/language/bn/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/bn/admin/advanced/jobs.json +++ b/public/language/bn/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/cs/admin/advanced/jobs.json b/public/language/cs/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/cs/admin/advanced/jobs.json +++ b/public/language/cs/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/da/admin/advanced/jobs.json b/public/language/da/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/da/admin/advanced/jobs.json +++ b/public/language/da/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/de/admin/advanced/jobs.json b/public/language/de/admin/advanced/jobs.json index d3939d8555..f786aa43b8 100644 --- a/public/language/de/admin/advanced/jobs.json +++ b/public/language/de/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Zeitplan", "next-run": "Nächster Lauf", "last-duration": "Letzte Dauer", - "running": "Laufend" + "running": "Laufend", + "active": "Active" } \ No newline at end of file diff --git a/public/language/el/admin/advanced/jobs.json b/public/language/el/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/el/admin/advanced/jobs.json +++ b/public/language/el/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/en-US/admin/advanced/jobs.json b/public/language/en-US/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/en-US/admin/advanced/jobs.json +++ b/public/language/en-US/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/advanced/jobs.json b/public/language/en-x-pirate/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/en-x-pirate/admin/advanced/jobs.json +++ b/public/language/en-x-pirate/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/es/admin/advanced/jobs.json b/public/language/es/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/es/admin/advanced/jobs.json +++ b/public/language/es/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/et/admin/advanced/jobs.json b/public/language/et/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/et/admin/advanced/jobs.json +++ b/public/language/et/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/fa-IR/admin/advanced/jobs.json b/public/language/fa-IR/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/fa-IR/admin/advanced/jobs.json +++ b/public/language/fa-IR/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/fi/admin/advanced/jobs.json b/public/language/fi/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/fi/admin/advanced/jobs.json +++ b/public/language/fi/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/fr/admin/advanced/jobs.json b/public/language/fr/admin/advanced/jobs.json index 965f1e0b3e..42a1e1f567 100644 --- a/public/language/fr/admin/advanced/jobs.json +++ b/public/language/fr/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Planification", "next-run": "Prochaine exécution", "last-duration": "Durée précédente", - "running": "En cours" + "running": "En cours", + "active": "Active" } \ No newline at end of file diff --git a/public/language/gl/admin/advanced/jobs.json b/public/language/gl/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/gl/admin/advanced/jobs.json +++ b/public/language/gl/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/he/admin/advanced/jobs.json b/public/language/he/admin/advanced/jobs.json index 9b699783be..5dcbe5bd7b 100644 --- a/public/language/he/admin/advanced/jobs.json +++ b/public/language/he/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "לוח זמנים", "next-run": "ריצה הבאה", "last-duration": "זמן ביצוע אחרון", - "running": "בהרצה" + "running": "בהרצה", + "active": "Active" } \ No newline at end of file diff --git a/public/language/hr/admin/advanced/jobs.json b/public/language/hr/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/hr/admin/advanced/jobs.json +++ b/public/language/hr/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/hu/admin/advanced/jobs.json b/public/language/hu/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/hu/admin/advanced/jobs.json +++ b/public/language/hu/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/hy/admin/advanced/jobs.json b/public/language/hy/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/hy/admin/advanced/jobs.json +++ b/public/language/hy/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/id/admin/advanced/jobs.json b/public/language/id/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/id/admin/advanced/jobs.json +++ b/public/language/id/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/it/admin/advanced/jobs.json b/public/language/it/admin/advanced/jobs.json index 0ada99550f..6ff20a0785 100644 --- a/public/language/it/admin/advanced/jobs.json +++ b/public/language/it/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Programma", "next-run": "Prossima esecuzione", "last-duration": "Ultima durata", - "running": "In esecuzione" + "running": "In esecuzione", + "active": "Active" } \ No newline at end of file diff --git a/public/language/ja/admin/advanced/jobs.json b/public/language/ja/admin/advanced/jobs.json index 58a18e06d6..2cf132d0d6 100644 --- a/public/language/ja/admin/advanced/jobs.json +++ b/public/language/ja/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "スケジュール", "next-run": "次を実行", "last-duration": "前回の所要時間", - "running": "実行中" + "running": "実行中", + "active": "Active" } \ No newline at end of file diff --git a/public/language/ko/admin/advanced/jobs.json b/public/language/ko/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/ko/admin/advanced/jobs.json +++ b/public/language/ko/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/lt/admin/advanced/jobs.json b/public/language/lt/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/lt/admin/advanced/jobs.json +++ b/public/language/lt/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/lv/admin/advanced/jobs.json b/public/language/lv/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/lv/admin/advanced/jobs.json +++ b/public/language/lv/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/ms/admin/advanced/jobs.json b/public/language/ms/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/ms/admin/advanced/jobs.json +++ b/public/language/ms/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/nb/admin/advanced/jobs.json b/public/language/nb/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/nb/admin/advanced/jobs.json +++ b/public/language/nb/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/nl/admin/advanced/jobs.json b/public/language/nl/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/nl/admin/advanced/jobs.json +++ b/public/language/nl/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/nn-NO/admin/advanced/jobs.json b/public/language/nn-NO/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/nn-NO/admin/advanced/jobs.json +++ b/public/language/nn-NO/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/pl/admin/advanced/jobs.json b/public/language/pl/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/pl/admin/advanced/jobs.json +++ b/public/language/pl/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/pt-BR/admin/advanced/jobs.json b/public/language/pt-BR/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/pt-BR/admin/advanced/jobs.json +++ b/public/language/pt-BR/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/pt-PT/admin/advanced/jobs.json b/public/language/pt-PT/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/pt-PT/admin/advanced/jobs.json +++ b/public/language/pt-PT/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/ro/admin/advanced/jobs.json b/public/language/ro/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/ro/admin/advanced/jobs.json +++ b/public/language/ro/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/ru/admin/advanced/jobs.json b/public/language/ru/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/ru/admin/advanced/jobs.json +++ b/public/language/ru/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/rw/admin/advanced/jobs.json b/public/language/rw/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/rw/admin/advanced/jobs.json +++ b/public/language/rw/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/sc/admin/advanced/jobs.json b/public/language/sc/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/sc/admin/advanced/jobs.json +++ b/public/language/sc/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/sk/admin/advanced/jobs.json b/public/language/sk/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/sk/admin/advanced/jobs.json +++ b/public/language/sk/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/sl/admin/advanced/jobs.json b/public/language/sl/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/sl/admin/advanced/jobs.json +++ b/public/language/sl/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/sq-AL/admin/advanced/jobs.json b/public/language/sq-AL/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/sq-AL/admin/advanced/jobs.json +++ b/public/language/sq-AL/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/sr/admin/advanced/jobs.json b/public/language/sr/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/sr/admin/advanced/jobs.json +++ b/public/language/sr/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/sv/admin/advanced/jobs.json b/public/language/sv/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/sv/admin/advanced/jobs.json +++ b/public/language/sv/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/th/admin/advanced/jobs.json b/public/language/th/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/th/admin/advanced/jobs.json +++ b/public/language/th/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/tr/admin/advanced/jobs.json b/public/language/tr/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/tr/admin/advanced/jobs.json +++ b/public/language/tr/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/uk/admin/advanced/jobs.json b/public/language/uk/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/uk/admin/advanced/jobs.json +++ b/public/language/uk/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/ur/admin/advanced/jobs.json b/public/language/ur/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/ur/admin/advanced/jobs.json +++ b/public/language/ur/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/vi/admin/advanced/jobs.json b/public/language/vi/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/vi/admin/advanced/jobs.json +++ b/public/language/vi/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/advanced/jobs.json b/public/language/zh-CN/admin/advanced/jobs.json index 6e70b3518a..9a145abf51 100644 --- a/public/language/zh-CN/admin/advanced/jobs.json +++ b/public/language/zh-CN/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "调度", "next-run": "下次执行", "last-duration": "上次持续时间", - "running": "执行中" + "running": "执行中", + "active": "Active" } \ No newline at end of file diff --git a/public/language/zh-TW/admin/advanced/jobs.json b/public/language/zh-TW/admin/advanced/jobs.json index 764b7d2b9f..896b07930a 100644 --- a/public/language/zh-TW/admin/advanced/jobs.json +++ b/public/language/zh-TW/admin/advanced/jobs.json @@ -4,5 +4,6 @@ "schedule": "Schedule", "next-run": "Next Run", "last-duration": "Last Duration", - "running": "Running" + "running": "Running", + "active": "Active" } \ No newline at end of file From c3c9d89b6a15d4512993d93abec318f079df58aa Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 11 Mar 2026 09:07:41 +0000 Subject: [PATCH 4565/4744] Latest translations and fallbacks --- public/language/bg/admin/advanced/jobs.json | 2 +- public/language/de/admin/advanced/jobs.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/bg/admin/advanced/jobs.json b/public/language/bg/admin/advanced/jobs.json index 43378082d5..dc0c0314a9 100644 --- a/public/language/bg/admin/advanced/jobs.json +++ b/public/language/bg/admin/advanced/jobs.json @@ -5,5 +5,5 @@ "next-run": "Следващо изпълнение", "last-duration": "Последна продължителност", "running": "В процес на изпълнение", - "active": "Active" + "active": "Активни" } \ No newline at end of file diff --git a/public/language/de/admin/advanced/jobs.json b/public/language/de/admin/advanced/jobs.json index f786aa43b8..73530000f9 100644 --- a/public/language/de/admin/advanced/jobs.json +++ b/public/language/de/admin/advanced/jobs.json @@ -5,5 +5,5 @@ "next-run": "Nächster Lauf", "last-duration": "Letzte Dauer", "running": "Laufend", - "active": "Active" + "active": "Aktiv" } \ No newline at end of file From ac483152e986c2cc61dbe768bdcd2d3ea3c4a20e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 11 Mar 2026 10:11:29 -0400 Subject: [PATCH 4566/4744] fix: derped handleBack in world.js --- public/src/client/world.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/world.js b/public/src/client/world.js index 7644639938..ddecba0ead 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -48,7 +48,7 @@ define('forum/world', [ } handleBack.init((after, handleBackCb) => { - loadTopicsAfter(after, 1, (payload, callback) => { + loadTopicsAfter(after, undefined, 1, (payload, callback) => { app.parseAndTranslate(ajaxify.data.template.name, 'posts', payload, function (html) { const listEl = document.getElementById('world-feed'); $(listEl).append(html); From 330106e8d44aa01f417e5787a9ed5a00ae650ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 11 Mar 2026 11:10:12 -0400 Subject: [PATCH 4567/4744] feat: add partial query help in acp manage users increase bounce timeout --- public/language/en-GB/admin/manage/users.json | 1 + public/src/admin/manage/users.js | 2 +- src/views/admin/manage/users.tpl | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json index b669dcca11..9b46ea0cc2 100644 --- a/public/language/en-GB/admin/manage/users.json +++ b/public/language/en-GB/admin/manage/users.json @@ -40,6 +40,7 @@ "250-per-page": "250 per page", "500-per-page": "500 per page", + "search.help": "Use "*" to make partial searches, for example "*query"", "search.uid": "By User ID", "search.uid-placeholder": "Enter a user ID to search", "search.username": "By User Name", diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index 95c2877e5b..e3d9f24db6 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -666,7 +666,7 @@ define('admin/manage/users', [ page: 1, }); } - $('#user-search').on('keyup', utils.debounce(doSearch, 250)); + $('#user-search').on('input', utils.debounce(doSearch, 500)); $('#user-search-by').on('change', doSearch); } diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index 6e094d4c35..53fae59f16 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -6,8 +6,10 @@
    + - + +
    + + + +
    +

    + [[admin/settings/general:screenshot.help]] +

    +

    From e3ba38f2a230f3a710cc84ba4e8ce0e7d1361467 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 12 Mar 2026 18:28:58 +0000 Subject: [PATCH 4609/4744] chore(i18n): fallback strings for new resources: nodebb.admin-settings-general --- public/language/ar/admin/settings/general.json | 4 +++- public/language/az/admin/settings/general.json | 4 +++- public/language/bg/admin/settings/general.json | 4 +++- public/language/bn/admin/settings/general.json | 4 +++- public/language/cs/admin/settings/general.json | 4 +++- public/language/da/admin/settings/general.json | 4 +++- public/language/de/admin/settings/general.json | 4 +++- public/language/el/admin/settings/general.json | 4 +++- public/language/en-US/admin/settings/general.json | 4 +++- public/language/en-x-pirate/admin/settings/general.json | 4 +++- public/language/es/admin/settings/general.json | 4 +++- public/language/et/admin/settings/general.json | 4 +++- public/language/fa-IR/admin/settings/general.json | 4 +++- public/language/fi/admin/settings/general.json | 4 +++- public/language/fr/admin/settings/general.json | 4 +++- public/language/gl/admin/settings/general.json | 4 +++- public/language/he/admin/settings/general.json | 4 +++- public/language/hr/admin/settings/general.json | 4 +++- public/language/hu/admin/settings/general.json | 4 +++- public/language/hy/admin/settings/general.json | 4 +++- public/language/id/admin/settings/general.json | 4 +++- public/language/it/admin/settings/general.json | 4 +++- public/language/ja/admin/settings/general.json | 4 +++- public/language/ko/admin/settings/general.json | 4 +++- public/language/lt/admin/settings/general.json | 4 +++- public/language/lv/admin/settings/general.json | 4 +++- public/language/ms/admin/settings/general.json | 4 +++- public/language/nb/admin/settings/general.json | 4 +++- public/language/nl/admin/settings/general.json | 4 +++- public/language/nn-NO/admin/settings/general.json | 4 +++- public/language/pl/admin/settings/general.json | 4 +++- public/language/pt-BR/admin/settings/general.json | 4 +++- public/language/pt-PT/admin/settings/general.json | 4 +++- public/language/ro/admin/settings/general.json | 4 +++- public/language/ru/admin/settings/general.json | 4 +++- public/language/rw/admin/settings/general.json | 4 +++- public/language/sc/admin/settings/general.json | 4 +++- public/language/sk/admin/settings/general.json | 4 +++- public/language/sl/admin/settings/general.json | 4 +++- public/language/sq-AL/admin/settings/general.json | 4 +++- public/language/sr/admin/settings/general.json | 4 +++- public/language/sv/admin/settings/general.json | 4 +++- public/language/th/admin/settings/general.json | 4 +++- public/language/tr/admin/settings/general.json | 4 +++- public/language/uk/admin/settings/general.json | 4 +++- public/language/ur/admin/settings/general.json | 4 +++- public/language/vi/admin/settings/general.json | 4 +++- public/language/zh-CN/admin/settings/general.json | 4 +++- public/language/zh-TW/admin/settings/general.json | 4 +++- 49 files changed, 147 insertions(+), 49 deletions(-) diff --git a/public/language/ar/admin/settings/general.json b/public/language/ar/admin/settings/general.json index 43e7f5a38c..ad5fba74ae 100644 --- a/public/language/ar/admin/settings/general.json +++ b/public/language/ar/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "الكلمات الدليله للموقع", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "صورة", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "رفع", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/az/admin/settings/general.json b/public/language/az/admin/settings/general.json index 6747b5b431..ec84fda492 100644 --- a/public/language/az/admin/settings/general.json +++ b/public/language/az/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Sayt təsviri", "keywords": "Saytın açar sözləri", "keywords-placeholder": "İcmanızı təsvir edən açar sözlər, vergüllə ayrılmış", - "logo-and-icons": "Saytın loqosu və ikonaları", + "logo-and-icons": "Media & Branding", "logo.image": "Şəkil", "logo.image-placeholder": "Forumun başlığında göstəriləcək loqoya gedən yol", "logo.upload": "Yüklə", @@ -35,6 +35,8 @@ "touch-icon.help": "Tövsiyə olunan ölçü və format: 512x512, yalnız PNG formatı. Əgər toxunma ikonu göstərilməyibsə, NodeBB favikondan istifadə etməyə qayıdacaq.", "maskable-icon": "Maskalana bilən (Ev ekranı) ikonu", "maskable-icon.help": "Tövsiyə olunan ölçü və format: 512x512, yalnız PNG formatı. Əgər maskalana bilən ikona göstərilməyibsə, NodeBB yenidən Touch Icon-a düşəcək.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Çıxış linklər", "outgoing-links.warning-page": "Gedən linklər xəbərdarlıq səhifəsindən istifadə edin", "search": "Axtarış", diff --git a/public/language/bg/admin/settings/general.json b/public/language/bg/admin/settings/general.json index 9c84b5413d..433f5d7ceb 100644 --- a/public/language/bg/admin/settings/general.json +++ b/public/language/bg/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Описание на уеб сайта", "keywords": "Ключови думи на уеб сайта", "keywords-placeholder": "Ключови думи, описващи общността Ви. Трябва да бъдат разделени със запетаи.", - "logo-and-icons": "Лого и иконки на уеб сайта", + "logo-and-icons": "Media & Branding", "logo.image": "Изображение", "logo.image-placeholder": "Път до логото, което да бъде показано в заглавната част на форума", "logo.upload": "Качване", @@ -35,6 +35,8 @@ "touch-icon.help": "Препоръчителен размер и формат: 512x512, само във формат „PNG“. Ако не е посочена иконка за сензорен екран, NodeBB ще използва иконката на уеб сайта.", "maskable-icon": "Маскируема иконка (за начален екран)", "maskable-icon.help": "Препоръчителен размер и формат: 512x512, само във формат „PNG“. Ако не е посочена маскируема иконка, NodeBB ще използва иконката за сензорен екран.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Изходящи връзки", "outgoing-links.warning-page": "Показване на предупредителна страница при щракване върху външни връзки", "search": "Търсене", diff --git a/public/language/bn/admin/settings/general.json b/public/language/bn/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/bn/admin/settings/general.json +++ b/public/language/bn/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/cs/admin/settings/general.json b/public/language/cs/admin/settings/general.json index 3a35e73236..a5dcb3ef1e 100644 --- a/public/language/cs/admin/settings/general.json +++ b/public/language/cs/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Popis stránky", "keywords": "Klíčová slova pro stránky", "keywords-placeholder": "Klíčová slova popisující vaši komunitu, odděleno čárkou", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Obrázek", "logo.image-placeholder": "Cesta k logu, aby mohlo být zobrazeno v hlavičce fóra", "logo.upload": "Nahrát", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Odchozí odkazy", "outgoing-links.warning-page": "Použít stránku s upozorněním při odchozích odkazech", "search": "Search", diff --git a/public/language/da/admin/settings/general.json b/public/language/da/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/da/admin/settings/general.json +++ b/public/language/da/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/de/admin/settings/general.json b/public/language/de/admin/settings/general.json index 975e47f559..278b7fbf52 100644 --- a/public/language/de/admin/settings/general.json +++ b/public/language/de/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Seitenbeschreibung", "keywords": "Forum Schlüsselworte", "keywords-placeholder": "Schlüsselworte, die ihre Community beschreiben, mit Komma getrennt", - "logo-and-icons": "Website-Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Bild", "logo.image-placeholder": "Pfad zu einem Logo, welches im Header des Forums angezeigt werden soll", "logo.upload": "Hochladen", @@ -35,6 +35,8 @@ "touch-icon.help": "Empfohlene Größe und Format: 512x512, nur PNG-Format. Wenn kein Touch-Symbol angegeben wird, verwendet NodeBB wieder das Favicon.", "maskable-icon": "Maskierbares (Start-Bildschirm) Symbol", "maskable-icon.help": "Empfohlene Größe und Format: 512x512, nur PNG-Format. Wenn kein maskierbares Icon angegeben wird, greift NodeBB auf das Touch-Symbol zurück.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Ausgehende Links", "outgoing-links.warning-page": "Warnseite für ausgehende links verwenden", "search": "Suche", diff --git a/public/language/el/admin/settings/general.json b/public/language/el/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/el/admin/settings/general.json +++ b/public/language/el/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/en-US/admin/settings/general.json b/public/language/en-US/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/en-US/admin/settings/general.json +++ b/public/language/en-US/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/en-x-pirate/admin/settings/general.json b/public/language/en-x-pirate/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/en-x-pirate/admin/settings/general.json +++ b/public/language/en-x-pirate/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/es/admin/settings/general.json b/public/language/es/admin/settings/general.json index 09a6c8295f..e76f554aae 100644 --- a/public/language/es/admin/settings/general.json +++ b/public/language/es/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Palabras Clave (keywords) del Sitio", "keywords-placeholder": "Palabras Clave (keywords) que describen tu comunidad, separadas por comas", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Imagen", "logo.image-placeholder": "Ruta al logo que se mostrará en la cabecera del foro", "logo.upload": "Subir", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Enlaces a sitios externos", "outgoing-links.warning-page": "Usar Página de Advertencia para Enlaces a Sitios Externos", "search": "Search", diff --git a/public/language/et/admin/settings/general.json b/public/language/et/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/et/admin/settings/general.json +++ b/public/language/et/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/fa-IR/admin/settings/general.json b/public/language/fa-IR/admin/settings/general.json index e8b37f08e5..c401841a15 100644 --- a/public/language/fa-IR/admin/settings/general.json +++ b/public/language/fa-IR/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "جستجو", diff --git a/public/language/fi/admin/settings/general.json b/public/language/fi/admin/settings/general.json index 3f71acedf1..dc341d2e0c 100644 --- a/public/language/fi/admin/settings/general.json +++ b/public/language/fi/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Sivuston avainsanat", "keywords-placeholder": "Yhteisöäsi kuvaavat avainsanat pilkuin eroteltuina.", - "logo-and-icons": "Sivuston logo ja kuvakkeet", + "logo-and-icons": "Media & Branding", "logo.image": "Kuva", "logo.image-placeholder": "Foorumin otsakkeessa näytettävän logon sijainti.", "logo.upload": "Lataa", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Peitetttävä (aloitussivun) kuvake", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/fr/admin/settings/general.json b/public/language/fr/admin/settings/general.json index 6da6d17130..2122df070a 100644 --- a/public/language/fr/admin/settings/general.json +++ b/public/language/fr/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Description du site", "keywords": "Mots-clés du site", "keywords-placeholder": "Mots-clés décrivant votre communauté, séparés par des virgules", - "logo-and-icons": "Logo & Icônes du site", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Chemin vers un logo à afficher dans l'en-tête du forum", "logo.upload": "Téléverser", @@ -35,6 +35,8 @@ "touch-icon.help": "Taille et format recommandés : 512x512, format PNG uniquement. Si aucune icône d'accueil n'est spécifiée, le favicon NodeBB sera visible.", "maskable-icon": "Icône masquable (écran d'accueil)", "maskable-icon.help": "Taille et format recommandés : 512x512, format PNG uniquement. Si aucune icône masquable n'est spécifiée, le favicon NodeBB sera visible.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Liens sortants", "outgoing-links.warning-page": "Utiliser la page d'avertissement pour liens sortants", "search": "Rechercher", diff --git a/public/language/gl/admin/settings/general.json b/public/language/gl/admin/settings/general.json index 23438c4828..7a25eb12a4 100644 --- a/public/language/gl/admin/settings/general.json +++ b/public/language/gl/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/he/admin/settings/general.json b/public/language/he/admin/settings/general.json index 3d0cfbd4ba..1ab09d5bd4 100644 --- a/public/language/he/admin/settings/general.json +++ b/public/language/he/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "תיאור האתר", "keywords": "מילות מפתח של האתר", "keywords-placeholder": "מילות מפתח המתארות את הקהילה שלך, מופרדות באמצעות פסיקים", - "logo-and-icons": "לוגו אתר ואייקונים", + "logo-and-icons": "Media & Branding", "logo.image": "תמונה", "logo.image-placeholder": "נתב ללוגו שיראה בכותרת הפורום", "logo.upload": "העלאה", @@ -35,6 +35,8 @@ "touch-icon.help": "סמליל דף אינטרנט מופיע כאשר מישהו מסמן את דף האינטרנט שלך או מוסיף את דף האינטרנט שלך למסך הבית שלו, גודל ותבנית מומלצים: 512x512, תבנית PNG בלבד. אם לא הוגדר סמליל דף אינטרנט, NodeBB יחזור להשתמש בסמליל הפבאייקון.", "maskable-icon": "סמליל הניתן להסוואה (במסך הבית)", "maskable-icon.help": "סמליל הניתן להסוואה מופיע בדף הבית של הסוללרי, זהו תמונה אטומה עם מעט ריפוד שהיישום דף הבית שלך יוכל לחתוך אחר כך לצורה ולגודל הרצוי. עדיף לא להסתמך על צורה מסוימת, מכיוון שהצורה שנבחרה בסופו של דבר יכולה להשתנות לפי סוגי מסך בית ופלטפורמה. גודל ותבנית מומלצים: 512x512, תבנית PNG בלבד. אם לא הוגדר אייקון הניתן להסוואה, NodeBB יחזור להשתמש בסמליל דף האינטרנט.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "קישורים חיצוניים", "outgoing-links.warning-page": "שימוש בדף האזהרה לקישורים יוצאים", "search": "חיפוש", diff --git a/public/language/hr/admin/settings/general.json b/public/language/hr/admin/settings/general.json index d21861149a..72f42c022c 100644 --- a/public/language/hr/admin/settings/general.json +++ b/public/language/hr/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Ključne riječi", "keywords-placeholder": "Ključne riječi koje opisuju Vašu zajednicu, odvojeni zarezom", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Slika", "logo.image-placeholder": "Putanja logotipa za zaglavlje foruma", "logo.upload": "Učitaj", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Odlazne poveznice", "outgoing-links.warning-page": "Koristi upozorenje za odlazne poveznice", "search": "Search", diff --git a/public/language/hu/admin/settings/general.json b/public/language/hu/admin/settings/general.json index 29839979b5..eb71f531b7 100644 --- a/public/language/hu/admin/settings/general.json +++ b/public/language/hu/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Weboldal kulcsszavak", "keywords-placeholder": "A közösségedet leíró kulcsszavak, vesszővel elválasztva", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Kép", "logo.image-placeholder": "A logó elérési útvonala, amit a fórum fejlécében fogunk megjeleníteni", "logo.upload": "Feltöltés", @@ -35,6 +35,8 @@ "touch-icon.help": "Ajánlott méret és formátum: 512x512, csak PNG formátum. Ha nincs beállítva, a NodeBB a favicon-t fogja használni.", "maskable-icon": "Maszkolható (főképernyő) ikon", "maskable-icon.help": "Ajánlott méret és formátum: 512x512, csak PNG formátum. Ha nincs beállítva, a NodeBB a favicon-t fogja használni", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Kimenő linkek", "outgoing-links.warning-page": "Kimenő link figyelmeztető oldal használata", "search": "Keresés", diff --git a/public/language/hy/admin/settings/general.json b/public/language/hy/admin/settings/general.json index ba5240156d..d42a299463 100644 --- a/public/language/hy/admin/settings/general.json +++ b/public/language/hy/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Կայքի հիմնաբառեր", "keywords-placeholder": "Ձեր համայնքը նկարագրող հիմնաբառեր՝ բաժանված ստորակետերով", - "logo-and-icons": "Կայքի Լոգո և պատկերանշաններ", + "logo-and-icons": "Media & Branding", "logo.image": "Նկար ", "logo.image-placeholder": "Ճանապարհ դեպի լոգո՝ ֆորումի վերնագրում ցուցադրելու համար", "logo.upload": "Վերբեռնել", @@ -35,6 +35,8 @@ "touch-icon.help": "Առաջարկվող չափը և ձևաչափը՝ 512x512, միայն PNG ձևաչափ: Եթե որևէ հպման պատկերակ նշված չէ, NodeBB-ը կվերադառնա ֆավիկոնի օգտագործմանը:", "maskable-icon": "Դիմակելի (հիմնական էկրան) պատկերակ", "maskable-icon.help": "Առաջարկվող չափը և ձևաչափը՝ 512x512, միայն PNG ձևաչափ: Եթե ոչ մի դիմակավոր պատկերակ նշված չէ, NodeBB-ը կվերադառնա Touch Icon-ին:", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Ելքային հղումներ", "outgoing-links.warning-page": "Օգտագործեք ելքային հղումների նախազգուշացման էջը", "search": "Որոնում", diff --git a/public/language/id/admin/settings/general.json b/public/language/id/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/id/admin/settings/general.json +++ b/public/language/id/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/it/admin/settings/general.json b/public/language/it/admin/settings/general.json index 6b1fea08ae..ebb00fe63f 100644 --- a/public/language/it/admin/settings/general.json +++ b/public/language/it/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Descrizione sito", "keywords": "Parole chiave del sito", "keywords-placeholder": "Parole chiave che descrivono la vostra comunità, separate da virgole", - "logo-and-icons": "Logo e icone del sito", + "logo-and-icons": "Media & Branding", "logo.image": "Immagine", "logo.image-placeholder": "Percorso del logo da visualizzare sull'intestazione del forum", "logo.upload": "Carica", @@ -35,6 +35,8 @@ "touch-icon.help": "Dimensioni e formato consigliati: 512x512, solo formato PNG. Se non è specificata alcuna icona touch, NodeBB tornerà a utilizzare la favicon.", "maskable-icon": "Icona Mascherabile (Schermata Iniziale)", "maskable-icon.help": "Dimensioni e formato consigliati: 512x512, solo formato PNG. Se non è specificata alcuna icona mascherabile, NodeBB tornerà a utilizzare l'Icona Touch.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Link in uscita", "outgoing-links.warning-page": "Usa pagina di avviso per i link in uscita", "search": "Cerca", diff --git a/public/language/ja/admin/settings/general.json b/public/language/ja/admin/settings/general.json index 2d79a2bc35..f3f623f970 100644 --- a/public/language/ja/admin/settings/general.json +++ b/public/language/ja/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "サイトのキーワード", "keywords-placeholder": "あなたのコミュニティを記述するキーワード、カンマ区切り", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "画像", "logo.image-placeholder": "フォーラムのヘッダーに表示するロゴのパス", "logo.upload": "アップロード", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "外部サイトへのリンク", "outgoing-links.warning-page": "送信リンクの警告ページを使用", "search": "Search", diff --git a/public/language/ko/admin/settings/general.json b/public/language/ko/admin/settings/general.json index 0756494d50..a2f98a64bf 100644 --- a/public/language/ko/admin/settings/general.json +++ b/public/language/ko/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "사이트 키워드", "keywords-placeholder": "커뮤니티를 설명하는 키워드, 쉼표로 구분", - "logo-and-icons": "사이트 로고 & 아이콘", + "logo-and-icons": "Media & Branding", "logo.image": "이미지", "logo.image-placeholder": "포럼 헤더에 표시할 로고의 경로", "logo.upload": "업로드", @@ -35,6 +35,8 @@ "touch-icon.help": "권장 크기 및 형식: 512x512, PNG 형식만. 터치 아이콘을 지정하지 않은 경우 NodeBB는 파비콘을 사용합니다.", "maskable-icon": "Maskable (홈 화면) 아이콘", "maskable-icon.help": "권장 크기 및 형식: 512x512, PNG 형식만. 마스크 가능 아이콘을 지정하지 않은 경우 NodeBB는 터치 아이콘을 사용합니다.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "외부로 나가는 링크", "outgoing-links.warning-page": "외부 링크 경고 페이지 사용", "search": "검색", diff --git a/public/language/lt/admin/settings/general.json b/public/language/lt/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/lt/admin/settings/general.json +++ b/public/language/lt/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/lv/admin/settings/general.json b/public/language/lv/admin/settings/general.json index fabfbb824e..2d306b6508 100644 --- a/public/language/lv/admin/settings/general.json +++ b/public/language/lv/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Foruma atslēgvārdi", "keywords-placeholder": "Atslēgvārdi, kas apraksta forumu, atdalīti ar komatu", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Bilde", "logo.image-placeholder": "Ceļš uz logo, ko parādītu foruma galvenē", "logo.upload": "Augšupielādēt", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Izejošās saites", "outgoing-links.warning-page": "Lietot izejošo saišu brīdinājuma lapu", "search": "Search", diff --git a/public/language/ms/admin/settings/general.json b/public/language/ms/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/ms/admin/settings/general.json +++ b/public/language/ms/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/nb/admin/settings/general.json b/public/language/nb/admin/settings/general.json index 17d717b787..15d189e51f 100644 --- a/public/language/nb/admin/settings/general.json +++ b/public/language/nb/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Nøkkelord for nettstedet", "keywords-placeholder": "Nøkkelord som beskriver fellesskapet ditt, kommaseparert", - "logo-and-icons": "Logo og ikoner for nettstedet", + "logo-and-icons": "Media & Branding", "logo.image": "Bilde", "logo.image-placeholder": "Sti til et logo som vises i forumets topptekst", "logo.upload": "Last opp", @@ -35,6 +35,8 @@ "touch-icon.help": "Anbefalt størrelse og format: 512x512, kun PNG-format. Hvis ingen berøringsikon er spesifisert, brukes favicon som reserve.", "maskable-icon": "Maskerbart (Hjem-skjerm) ikon", "maskable-icon.help": "Anbefalt størrelse og format: 512x512, kun PNG-format. Hvis ingen maskerbart ikon er spesifisert, brukes berøringsikon som reserve.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Utgående lenker", "outgoing-links.warning-page": "Bruk varslingsside for utgående lenker", "search": "Søk", diff --git a/public/language/nl/admin/settings/general.json b/public/language/nl/admin/settings/general.json index a5828eaa84..4a450b7aff 100644 --- a/public/language/nl/admin/settings/general.json +++ b/public/language/nl/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Trefwoorden", "keywords-placeholder": "Trefwoorden die uw community beschrijven, kommagescheiden", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Afbeelding", "logo.image-placeholder": "Pad naar een logo om te tonen op de forum header", "logo.upload": "Uploaden", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Uitgaande links", "outgoing-links.warning-page": "Gebruik waarschuwingspagina voor uitgaande links", "search": "Search", diff --git a/public/language/nn-NO/admin/settings/general.json b/public/language/nn-NO/admin/settings/general.json index 2157dd08fb..c32ff58019 100644 --- a/public/language/nn-NO/admin/settings/general.json +++ b/public/language/nn-NO/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Nøkkelord", "keywords-placeholder": "Skriv inn nøkkelord, skilde med komma", - "logo-and-icons": "Logo og ikon", + "logo-and-icons": "Media & Branding", "logo.image": "Bilet-URL for logo", "logo.image-placeholder": "https://din-nettstad.no/logo.png", "logo.upload": "Last opp logo", @@ -35,6 +35,8 @@ "touch-icon.help": "Bruk touch-ikonet for å vise på mobile einingar.", "maskable-icon": "Maskerbart ikon", "maskable-icon.help": "Maskerbare ikon vert brukt for å tilpasse webappen til ulike skjermstorleikar.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Utgåande lenkjer", "outgoing-links.warning-page": "Advarselside for utgåande lenkjer", "search": "Søk", diff --git a/public/language/pl/admin/settings/general.json b/public/language/pl/admin/settings/general.json index ed26bb21bf..083be75a77 100644 --- a/public/language/pl/admin/settings/general.json +++ b/public/language/pl/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Opis strony", "keywords": "Słowa kluczowe strony", "keywords-placeholder": "Słowa kluczowe opisujące społeczność, oddzielone przecinkami", - "logo-and-icons": "Logo i ikony strony", + "logo-and-icons": "Media & Branding", "logo.image": "Obraz", "logo.image-placeholder": "Ścieżka do logo, które ma być wyświetlane w nagłówku forum", "logo.upload": "Prześlij", @@ -35,6 +35,8 @@ "touch-icon.help": "Rekomendowana wielkość: 512x512, tylko format PNG. Jeśli nie ustalono ikony dotykowej, użyta zostanie favikona.", "maskable-icon": "Ikona ekranu głównego", "maskable-icon.help": "Rekomendowana wielkość: 512x512, tylko format PNG. Jeśli nie ustalono tej ikony, użyta zostanie ikona dotykowa.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Odnośniki wychodzące", "outgoing-links.warning-page": "Używaj strony ostrzegawczej o odnośnikach wychodzących", "search": "Szukaj", diff --git a/public/language/pt-BR/admin/settings/general.json b/public/language/pt-BR/admin/settings/general.json index d59c71f9f5..c96b724a56 100644 --- a/public/language/pt-BR/admin/settings/general.json +++ b/public/language/pt-BR/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Palavras-chave do Site", "keywords-placeholder": "Palavras-chave descrevendo sua comunidade, separadas por vírgula", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Imagem", "logo.image-placeholder": "Caminho de URL do logotipo para mostrar no cabeçalho do fórum", "logo.upload": "Enviar", @@ -35,6 +35,8 @@ "touch-icon.help": "Tamanho e formato recomendados: 512x512, somente formato PNG. Se nenhum ícone para touch for especificado, o NodeBB usará o seu próprio favicon.", "maskable-icon": "Ícone Mascarável (de Tela Inicial)", "maskable-icon.help": "Tamanho e formato recomendados: 512x512, somente formato PNG. Se nenhum ícone mascarável for especificado, o NodeBB usará o seu próprio Ícone para Touch.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Links Externos", "outgoing-links.warning-page": "Habilitar Página de Aviso de Links Externos", "search": "Search", diff --git a/public/language/pt-PT/admin/settings/general.json b/public/language/pt-PT/admin/settings/general.json index 8e7e464640..394753a971 100644 --- a/public/language/pt-PT/admin/settings/general.json +++ b/public/language/pt-PT/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Palavras-chave do Site", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Imagem", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Enviar", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Links Externos", "outgoing-links.warning-page": "Utilizar a página de aviso para links externos", "search": "Search", diff --git a/public/language/ro/admin/settings/general.json b/public/language/ro/admin/settings/general.json index ebebb10fa5..3d4dd933d3 100644 --- a/public/language/ro/admin/settings/general.json +++ b/public/language/ro/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Descrierea site-ului", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/ru/admin/settings/general.json b/public/language/ru/admin/settings/general.json index 21aceec945..cfeaca388e 100644 --- a/public/language/ru/admin/settings/general.json +++ b/public/language/ru/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Ключевые слова для сайта", "keywords-placeholder": "Укажите через запятую ключевые слова, описывающие ваше сообщество", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Логотип в шапке сайта", "logo.image-placeholder": "Путь к файлу логотипа ", "logo.upload": "Загрузить", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Внешние ссылки", "outgoing-links.warning-page": "Предупреждать, когда пользователь переходит по внешним ссылкам", "search": "Поиск", diff --git a/public/language/rw/admin/settings/general.json b/public/language/rw/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/rw/admin/settings/general.json +++ b/public/language/rw/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/sc/admin/settings/general.json b/public/language/sc/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/sc/admin/settings/general.json +++ b/public/language/sc/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/sk/admin/settings/general.json b/public/language/sk/admin/settings/general.json index 2dfddf133e..2223015b5a 100644 --- a/public/language/sk/admin/settings/general.json +++ b/public/language/sk/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Kľúčové slová pre stránky", "keywords-placeholder": "Kľúčové slová popisujúce Vašu komunitu, oddelené čiarkou", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Obrázok", "logo.image-placeholder": "Cesta k logu, aby mohlo byť zobrazené v hlavičke fóra", "logo.upload": "Nahrať", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Odchádzajúce odkazy", "outgoing-links.warning-page": "Použiť stránku s upozornením pri odchádzajúcich odkazoch", "search": "Search", diff --git a/public/language/sl/admin/settings/general.json b/public/language/sl/admin/settings/general.json index ea11d557c7..fec7da1605 100644 --- a/public/language/sl/admin/settings/general.json +++ b/public/language/sl/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Ključne besede spletnega mesta", "keywords-placeholder": "Ključne besede, ki opisujejo vašo skupnost, ločene z vejicami", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Slika", "logo.image-placeholder": "Pot do logotipa za prikaz v glavi foruma", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Odhodne povezave", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Išči", diff --git a/public/language/sq-AL/admin/settings/general.json b/public/language/sq-AL/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/sq-AL/admin/settings/general.json +++ b/public/language/sq-AL/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/sr/admin/settings/general.json b/public/language/sr/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/sr/admin/settings/general.json +++ b/public/language/sr/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/sv/admin/settings/general.json b/public/language/sv/admin/settings/general.json index d56c819745..0ee921d831 100644 --- a/public/language/sv/admin/settings/general.json +++ b/public/language/sv/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/th/admin/settings/general.json b/public/language/th/admin/settings/general.json index a0f6cc7655..85299636dd 100644 --- a/public/language/th/admin/settings/general.json +++ b/public/language/th/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Keywords", "keywords-placeholder": "Keywords describing your community, comma-separated", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Image", "logo.image-placeholder": "Path to a logo to display on forum header", "logo.upload": "Upload", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Outgoing Links", "outgoing-links.warning-page": "Use Outgoing Links Warning Page", "search": "Search", diff --git a/public/language/tr/admin/settings/general.json b/public/language/tr/admin/settings/general.json index e553bcdf41..27df764c91 100644 --- a/public/language/tr/admin/settings/general.json +++ b/public/language/tr/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Site Anahtar Kelimeler", "keywords-placeholder": "Topluluğunuzu tanımlayan anahtar kelimeler, virgülle-ayrılmış", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Görsel", "logo.image-placeholder": "Forum başlığında görüntülenecek bir logo yolu", "logo.upload": "Yükle", @@ -35,6 +35,8 @@ "touch-icon.help": "Önerilen Boyut: 512x512. Önerilen format: PNG. Simge belirtilmezse varsayılan olarak favicon kullanılır.", "maskable-icon": "Maskelenebilir (Ana Ekran) Simgesi", "maskable-icon.help": "Önerilen boyut ve format: 512x512, PNG formatı. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Harici Bağlantılar", "outgoing-links.warning-page": "Dışarı giden bağlantılar için uyarı sayfası kullan", "search": "Arama", diff --git a/public/language/uk/admin/settings/general.json b/public/language/uk/admin/settings/general.json index 96962dfe01..76f2cfabe9 100644 --- a/public/language/uk/admin/settings/general.json +++ b/public/language/uk/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "Ключові слова сайту", "keywords-placeholder": "Ключові слова, що описують вашу спільноту, розділені комами", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "Зображення", "logo.image-placeholder": "Шлях до логотипу для відображення в шапці форуму", "logo.upload": "Завантажити", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Зовнішні посилання", "outgoing-links.warning-page": "Використовувати сторінку попередження про зовнішній перехід", "search": "Search", diff --git a/public/language/ur/admin/settings/general.json b/public/language/ur/admin/settings/general.json index 51e14ab67f..b2594add22 100644 --- a/public/language/ur/admin/settings/general.json +++ b/public/language/ur/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "ویب سائٹ کی تفصیل", "keywords": "ویب سائٹ کے کلیدی الفاظ", "keywords-placeholder": "آپ کی کمیونٹی کی وضاحت کرنے والے کلیدی الفاظ، کوموں سے الگ کیے گئے۔", - "logo-and-icons": "ویب سائٹ کا لوگو اور آئیکنز", + "logo-and-icons": "Media & Branding", "logo.image": "تصویر", "logo.image-placeholder": "فورم کے ہیڈر میں دکھائے جانے والے لوگو کا پاتھ", "logo.upload": "اپ لوڈ", @@ -35,6 +35,8 @@ "touch-icon.help": "تجویز کردہ سائز اور فارمیٹ: 512x512، صرف PNG فارمیٹ میں۔ اگر ٹچ آئیکن بیان نہیں کیا گیا تو NodeBB ویب سائٹ کے فیویکن کا استعمال کرے گا۔", "maskable-icon": "ماسک ایبل آئیکن (ہوم اسکرین کے لیے)", "maskable-icon.help": "تجویز کردہ سائز اور فارمیٹ: 512x512، صرف PNG فارمیٹ میں۔ اگر ماسک ایبل آئیکن بیان نہیں کیا گیا تو NodeBB ٹچ آئیکن کا استعمال کرے گا۔", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "باہر جانے والے لنکس", "outgoing-links.warning-page": "باہری لنکس پر کلک کرنے پر تنبیہی صفحہ دکھائیں", "search": "تلاش", diff --git a/public/language/vi/admin/settings/general.json b/public/language/vi/admin/settings/general.json index a45f6a551f..5907a11dc6 100644 --- a/public/language/vi/admin/settings/general.json +++ b/public/language/vi/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Mô Tả Trang", "keywords": "Từ Khóa Trang", "keywords-placeholder": "Các từ khóa mô tả cộng đồng của bạn, được phân tách bằng dấu phẩy", - "logo-and-icons": "Lô-gô & Biểu Tượng Trang", + "logo-and-icons": "Media & Branding", "logo.image": "Ảnh", "logo.image-placeholder": "Đường dẫn đến biểu trưng để hiển thị phần đầu diễn đàn", "logo.upload": "Tải lên", @@ -35,6 +35,8 @@ "touch-icon.help": "Kích cỡ và định dạng được đề xuất: 512x512, chỉ định dạng PNG. Nếu không có biểu tượng cảm ứng nào, NodeBB sẽ quay trở lại sử dụng favicon.", "maskable-icon": "Biểu tượng có thể che được (Màn Hình Trang Chủ)", "maskable-icon.help": "Kích thước và định dạng nên là: 512x512, chỉ định dạng PNG. Nếu không có biểu tượng có thể che được nào được chỉ định, NodeBB sẽ trở lại Biểu Tượng Chạm.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "Liên Kết Đi", "outgoing-links.warning-page": "Sử Dụng Trang Cảnh Báo Liên Kết Đi", "search": "Tìm kiếm", diff --git a/public/language/zh-CN/admin/settings/general.json b/public/language/zh-CN/admin/settings/general.json index cd6cc33bac..489ebf012b 100644 --- a/public/language/zh-CN/admin/settings/general.json +++ b/public/language/zh-CN/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "站点描述", "keywords": "站点关键字", "keywords-placeholder": "描述您的社区的关键字(以逗号分隔)", - "logo-and-icons": "网站徽标与图标", + "logo-and-icons": "Media & Branding", "logo.image": "图像", "logo.image-placeholder": "要在论坛标题上显示的 Logo 的路径", "logo.upload": "上传", @@ -35,6 +35,8 @@ "touch-icon.help": "推荐的尺寸和格式:512x512,仅限PNG格式。如果没有指定触摸图标,NodeBB将回退到站点图标。", "maskable-icon": "可遮蔽(主屏)图标", "maskable-icon.help": "推荐的尺寸和格式:512x512,仅限PNG格式。如果没有指定可遮蔽图标,NodeBB将回退到触摸图标。", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "站外链接", "outgoing-links.warning-page": "使用站外链接警告页", "search": "搜索", diff --git a/public/language/zh-TW/admin/settings/general.json b/public/language/zh-TW/admin/settings/general.json index 992143a90b..a438f177ee 100644 --- a/public/language/zh-TW/admin/settings/general.json +++ b/public/language/zh-TW/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Site Description", "keywords": "網站關鍵字", "keywords-placeholder": "描述您的社區的關鍵字,以逗號分隔", - "logo-and-icons": "Site Logo & Icons", + "logo-and-icons": "Media & Branding", "logo.image": "圖檔", "logo.image-placeholder": "要在論壇標題上顯示的 Logo 的路徑", "logo.upload": "上傳", @@ -35,6 +35,8 @@ "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", "maskable-icon": "Maskable (Homescreen) Icon", "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "screenshot": "Screenshot", + "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", "outgoing-links": "站外連結", "outgoing-links.warning-page": "使用站外連結警告頁", "search": "Search", From 09c54127de83d326e7ef6c2398da6cf0025a6ee2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Mar 2026 14:31:43 -0400 Subject: [PATCH 4610/4744] fix: screenshot fallback --- public/images/screenshot-default.png | Bin 0 -> 42804 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/images/screenshot-default.png diff --git a/public/images/screenshot-default.png b/public/images/screenshot-default.png new file mode 100644 index 0000000000000000000000000000000000000000..cd231ecb2a64f3e3136f45426ea18554ac6a4350 GIT binary patch literal 42804 zcmd43WmH>X8?IS{7BB8lyafV9io09!;!+%ny9QdUxD+qNwYY16;#wSnyIb&tAeqhg zoipqFnKf&E%$)fP3E6AEJMa5E*L^*Nt18Q4p_8FMdGZ8HUQSB=$&;t;Po6wSe}xMC z1R+n8_vFc&C-PF_nqEdn%P4LHyJVHQ?o|P|n&W_Es z_!DmQ$67&l4V&CZqXXdctG;y75y9}YjU4NB$BBd8tk=8_6yP@7vG%#!U6BB&yR)-6 z!p&x)N!&1zO?Ron9uIe7m$;&nsh!1`$l?AzP+6*HE#WV|h=@qCi{)q{E{5gU*Px)F zj(tP_@0K_B=pe)v zmg%%<4S%^?vOXhZ(IYXF#he&#bvnaaa6gD4W;fRJSC*2(HEu#RE=hdnbTMn6ubhOg zxHA@*muD6SYA=OUm?0`_osZ!1Uf#ZR;012$r7EhSL}ruuNa*VA=hvuia(O zwyD)ZK)pLY$~Lg3Jx9qBZ)kNyiW9J1VlhThQJux|;Npfjp4K}|U}6xl2er1gKHL@+ zM%$yVCY3_Y%p!TbE_R|r^J;5rA7b_vep4PmeitA!-+Pl0feb5<9v&`TtuSc3z$XQ) zkB4iERW>Y3q0!S>1#+1bl}2ro$-03r&Z?b8E5oSSEB>wdB92J=lv4wbJD^e(l`f_` z(`XCzq*cAJhkM^U@Fkzm<$h(k6q+!a7A%()S7(v_u#$XL>Q+{6S8(73JIM$g*)s`R z-{1bd9KAbgT=l|uB$Y!g=gY@GKHD)K<@(t+o2aMf=Muvn8O}RL{M;qR#2E{X4gAf$ zZw!Ar-22e@T<-a=+nTvV#<^S{&OB#`B#ws5{tOG#1eP<>QWSW9Xz^n4yOX81qNi%L zK7@79B}rbk&JNqNopp5-LM^fSi5{8Eme_t%Fpyyygbza{ZRiVkin6&vG{WV;s-OSWTaBP`$K zr%@I)TIY5=i&kycD*=6QK(rr|Lv)hKCa7ii(mA+a>>2D0iPuY*zam`xq`%*>`_Y9Kgd>>$#E-RB5^<4a;bS zz4>^cxuC}xsG{c=qL4WC6{Dh;>FIof2JgMF*<|=!lQ%gk7NrTAh)qcS$+$GKwb53) zCxUcpb;UCZPr8*e=;soxxl5#y%R8gGSA9_=6y~RcJw#@v(RFNG7E094vD~=0zg39* zaHxf4GD1JrkxzV%I+KU&c6Q6!#hmf;^V=x0snfqq{JqE92RnwKaQO6m_G&+qs?|EK z^VJx*%^z0Ri>t|KDEIHfS>>IEXqD^6rZ+m$1*{0Qh%IZK4%$shevkQE4;5QhE+_jm zQLAs2-U(Yi)69Fdp0asIQ%E4#X3E2hCbcR<(7M}D~$6C z3hr+aOf-YZxyy`Yk;2YHZmb41N=T5I?Ia~LXn;k#@&+R-$#&LcD-{jF)odFzg4gDW zY=3~oeFLkt+wFg5YJ8QZ$>llAZ7!F^HZTZ6C0!*K${yPli~tUTMinx@8th8>+T{)?gI%@O*WFn4J&U0^Mb>~2!y>Z1+txk?clht^366H%< zjN|S}8`+PPN`tA@1P8e^YqKSj-dWIa@W}K?nlQN@HxhTg)^Z|i{^!iQ&U9{h(>AVX zzlwx}yk)_RurQGUfMsS+U+rwG#OA!*?JR45J_^F(^Vjqd3rz-U2|Et5`f zr}-t4qk#nrRehN?Hq`fZ_aaOU@in(kJbwX$&T>91?5)Hr@F}Q_f(D%`Q7)QSX73yq zCH^dZ_l-0oI))Pfa_l$xz zVgRVAjH#wxu3qf2tLA(G{Ru!CRn7@|9EK3H8)e7Z@@iJ zA*R!F;bG*wF%9R4R;=#X;|@B<6N6L^cZqDe7Qbz>C@N7jT~4d6UuhZ~Ru8mx3oVqf zRKhY{!Dy=Wlb+TKO*FK!G3GbRoU5O@BU9Kp9H!s*L=qEES&jCIT8(u=AjHisma^$u zqI9l(nBN+%&SzLyfwQFpx)j|CJL&FaVCKwM`K~upnyd61?_tF;k;Q1KX|BQ9J$tMK z0+%&t^PmKd4g%26`|Qd9>*lRiR^0|XK~Lz6OK%u1CFxX=Y{t(S)iUdKu|@y04F)21 z{cur_bJHhDw&3m^cxn zfk(Me7nqPhr&YQ5m9TVnqwk-77$FzqP$Jtm9?(xZJ#ONFEZ9RE&aC}h@D(}lCgPvI z&$onkl0KT{2DqP?KZ)hbAt{;67J)rYdK10e|gbh;o0jKzgbVOSQ$^`QGoQ z`I2C+#%Eq4Un$-o7P?cuSnbelY4FESx<3w|Ng>Qs8hsO}g>adakJu~w-XKI@I|s3Q zeBq=2hq9`;_!Z`#pnQb8pqueezrKHP3Yl#h;zBZAN$hb~jnQ^{8x6HLXu}N0XR0B6 zpT=n^_H+7o)jceO?5BG3EgZh*%c5QO^(pwFlpy-6&2=|Sr_L=zD{M03;N+wNn({{H zy)Wuyp}hV4?6IKdM1`y8QnM>$2a>GNYFtU>vun2;1MYe|$yC%$-*6wG?vby^14UVwFC{C z54zgznrkrl^NY`2Zoze8N|Z12oWaUP#hE*qKo0nGV>;bR~T zU7z({#NI^iK1x@&Y-~N|yB|#be^v`^tnnv`2swh-O|+z#k5Q$Zzd;A3PlelhPTKH( zuC*f9o|a6PRb~mB4v4k=3^-o@Rzq02dA!(Yy?9`CcM6saSmb#oBX_9i&+cB5sCUzX zMP5)&SnV?Cci;RzP{r-bD%sT+$n+CFFZin#*a41;ic0z3ckF{AXuOs}ZL+=d&XTu% z=M{AMISR&B(D#1w$5WRDJkh2oJ6-CfsrY;3?Qi>57Mn6yogw1p zmTG^hKpH7hG4PWgUr0#kz14&1i2fc-*z#j~8A!||IM#YOH?81`wY-pqPUdi|Ac-g2 zQ3W_qwHi5WM}J zGk)3#>hG@CXToAcgR%+xY$Br5<>KCm@RaL1*t|oXnekqqwBUYNx4K`t21;7BAfE+{ zH=yb34Bxet`&-2FLjAQb4lY;#mmQ90DVeYS{As^+3$Y)&-B)NlsRuNE{6*G_f$9B@ zuK7VUL7vK~3MrcM;I@Ec$dJ!|;QT{_J*8jd&ZtCT)IO+E5n|q~@S#qyTpPw#tlJjR zZuufhDU+1Tw5nq)ozJ2rRQJy7tjfKc8T|2?8nY}1_}pu2IDK&yib=uwEWN=_GB+5l z+W8-+{koshX~?H$R7|RvTNt%#9Pmtp4WN8C5PL-jpDg$o zWPz~@_YGK<5nWrjBL8N&tbkEs-aO(J_5kBg)Lt&3Raa zy8%Q@USnMF$pLPQ6n8sWQAAE5av6))5>EDw?osz|VMlXEl^9xGDr{xImu<6B@mH8g z^eaL??ET@c^}`0S{I|)D)R~>y$lENZ?Sl{Y8FDPWRrdYVdxB134_1GjxSq+hRCqv7 z5BqAg+8pSPGwtT@WVw;m;qB%xjCZf?$E*A)1E9Rg>TY}HRzj{?biw9%GqiNwIX`KF=k2mERr$mlZj^lfi%TuvR@^ZdIXL&k5g`~GJ z@sjslxu+4==wZIhRn1>-(h20O>Re1R1Ql+uI zk`ABn^?sGCgNtoYe65{1YJlLZ*LnsUdltEtIbn$Wj*F_USsMsO*}vwrM1qIDqJ5!h zi$Ws;-DeQ_j)c*?eFzU4TS0l4?bLo_yz&`#k?%}b|ABz2lX$XWvEJsJoi68h?^Coi z`BwXWWeS0FUN(b2=4o^IbTXJqBwQ5DE{JG*yon-whtA!&W=;z9JBT{^ggy>D(->#9 zwv+}LqiN*~bd>cASZ>Vi!v1O8v+Cad5o-zCcr_W*ztv5)e|Ue3@B%v2yHi*4`{ohU zqyj<~NLlv)2Y#KcN?+o$5`$VQDj_dxSN@{foawH~Crc zr_;gRjdcV44y{L-4jeq(5~plKi*B|~?AIy8&nY${S;Zo{mr{TmBKyb zTrSpVMn^WjDB|km+q0XC10`u)J_ol6GA@$d&3+ZtY9GoBrFafWpUa~aQX7q;*^~P9 zynY)DNj(Z`e81W4A0OQAeXqpY{k+%C79{n5si$F>jc8gn^4QNR6kQKb#)t^F(!jL& zKSY@)Z>_E8ogO3!e$gzCTB$I+^6X}Y2Tw>OX;s-ml;3KU(C12pM_mxJn+%*zG`ZbG zBr7sD>Wm%KeP}1qjC5T8v7O3iL6$2U8^drBFg4#C6Qfr3c|u(&lSgBWwDBpp&R3^_ z<~CQh(tly`)V=dAm2<^092GcFjHOQ32c_Y5`=$^hw0H3Q^M?vOkdW{7k|m1OMb8Zw*U-rf7*KHZ*wo7Qv9MWj)88{hnvXdyT$3CKpR^H_|g|+Ub_Zt_Oz`Gnl9we zU1Rkxw=zI3Qsw&l2zIgsKI*3Bi+_6c5DXj|Vr|Y)o29xp9!GPj{O*Bli*IsO4u@a0 zxAw2qP9ZTnvV#fyUSd;?=-r!4Ij{(L0P9@Z0W zCRfR+#5kZdD}#<)&~!584|^<(tUBEse3Hu?ru>>8!c-e%5nbr#h$iRFA^)RCptM## z4;5f+Cq?(ROtggloe*o`wbNDb>-*+rY7uRZJrZi871R}PQ~`?Q3gs-%(|P_UO#zb? zl0@o{2)H@K`1BJA2K@lV`AX6UUmWF5*Z~{dXw@vO$24um+nLJ`XA${}>yaX(Ff9e* zIIGjCpNWVmA-Qe>uMXJ!9x@ARg}QzDp97Rx1CFxVL2>@@;=A34`(wj4tobC-uX|`~ zac~yCd<0Qg^7uKG?(lA0&M!6>oH6}m;Nx$C15Uv+Pq2+|mhmTETk{7=((~oBKmTs9 z`kd|IfZd-RvL{Oeo;GVeJaj5%^%Lh=q_)+n@Jddv7(%?=nQ(l4%=O zJx}@`o_`;2)b^B$Am~Z3*h5O}wF+qSpbBNljltmpOZC&7&Y?(S7h3pBA+%N>f;C&*@r zYHSfi&An(=ktOlLT6a3r>HH+hsTW@LwP77ROw+qWWTKNQmReMN^_%1xPd4xMv1k}T zW^)z$8z6N6?S zN;G_Wu{>1wOE9YU!Of1n;OW+&2EF_*l9>`ssZomg%WXs7haQoAz5V%r=MOW7Jk}U> zhKEEsD}7T%V{rgv)x4{qaM>O)xf!{CiDR~Sy?S|c}sGCi9({9D-+}!0!Glc^xZgaj^Ao~yYsy6|c2l9a4U?edo%QkZsH~}>Ps6Xg?Y#gJz zLL#dXX2)n|kzqz3|74Si@YZOOMr@;>%!8kIwfC@xm$$c#ZI%DBUKj;`O*?XMsz~RL zISQL$bBXY4m-c(kP2M`h4U_q3V&~3pN}}C4&+TCVXGr>FDw7D(j(eztv(YfQx|oo5ATsI_+OTM(utf%!Dj&6qA*7YU~&smi~kg3?{7iGe*a~ zHwhHG)f;GDY2ZE)+{LQV3fg=EE`Yxv8@oKbzGQc`y zq9e9cYwNK%I;DC3nyJCKn3V8Ho4+>CXJ!adySg(1S+jmH!yl&B(@ZIbx0?vlE2OF- zzXA&yXyS119nQFt|)zh;-_}x_f zp$gkG)4s589kEpXKekIxFH}Cp)63J^&9_LhOChci(7-pJPc49?D~TmpHV}S>%c7-( zNi9s}yxBAB0z4{aXOr7NSNU|I&aSXSAYelMxbKHpG_J4!kKIa1r(vBF4OTlq>POSao$?*ur`T+X%y<`hd<5Ehu4%? zFVQNq109bM(eRzaK!hL;l~7NVu~nYdzb|jK^5a4;;&9C_Yqx#nP(b|(L-RTIEa{!Z zTDz&iUk!Q~6q5gBiE+Jp{>S#Lza z?Zr{^1p;Pq)DfNDkn1-;`Brn!mO{u)ik+LDZ@Wv3?OEX+rv}Wvxu$@1H~zBAPdkSd zsGEb0E$<@Cm86zZf}AUfeXzS1M?SzelFD-K)ACtl8Gf7oAEbd#+D;PB-;x*?G3159xYC(o1iqV zKJ%clpT8E2xdTsg{ua~2ITy`yskStTlVjnRj@Uv>?gxyTt_1dy@HBO7dsY~<6%unz z=0@Q$ptrz|ips6?3RpnHRBbQ!ICw|DrBoWx07W$-ZBlPzT>*pmpG zRyEl50u;yjcGS@AczRU5=PJFy+Lmo#O}`O$vmdijDKjoYv*BtAXLWOHKj6LXwO~)#HigNrW9WiUO%XaXXZTR6*DRGQCQoi`a<+)uJ)$Kc(y*iZIne}1rf+80Yz2#pu5lfgRB z^DN0U|DP_vgHv=xl8E{{CY4Z+jFvp<3+g*}!G~t(6CV5d!gU)*Z7b_WlQ=QQ<@U&j zqKR#$d|!Fys%v|Fgu8oRxY=(dyJ$7}ZN2vaBK8yVZx#4Z?}TWQ+JtXGpF?PCZTq9L z@|VH$wHD&nWxDZnzFO&Ahf!nHykAIir7sTq60_s~R2>0!x3%c6 zu(!RK{F0o!zf&&gw6XQ>(&PAxx1nXI$HBss<>}xVR;lYqO94Tgvog>Y8?+*hz=l48 zYfp~J>f9?D_s0n-7wa2dAckfRPfsVzmWdl;YL6UAW!^Jp4YX%+1;lrB^vCJ>&F;34 z@LN7R-~E@j>+zR!foxO1%8L|0aBh5JZ0~nL27wdZ%ay=cc04C z`6JeYUcPZy;^RS{+M#5Xi*g^XC5XP>XZry30JgLCEdPUq92sXP~rZ-T~VQ!8-&T*TcDvT6kwE#mGct8G;jho^qx`k8=ZP%d*i zVT(&BK6fJ8r-SVYTCG9JhDFjigU zyMw9aE2pV55e<0$%VLj>DJRMAP3BXh%}B*tSW8GgyZ6PZtG8|f-^L`1dY_wn95)VX zSiSkgOuBiYKl;YZ6sUO`qd@YJiY?mFY-Y9%F~h&7ke?sA3P=uSjr-Gsl|tjR(xVDm zI{dL9E6>N9CCSZ)_ISmmuB+VwlG#2<1gZEu$rsd)X?u7m)!v&Z4X6UOZ?izw01mo0 zQJ+^3#u>_D*dgxjj!NvuxkJC(U%Va!TJ7Ge!Kx_bDjTc*YUJy+*BO7}e8mTPWmca`soT%oJ4Sz#s)X{uGi-$&!2ba?&VBuOaQT`q>_Ip*mZG#)qk{#? zd#icinhd|!dVkO4f&avRaG4t)7B#_&`4GN8#u2xL5-`n3Wz<1b6B{hqF8q&{i z8A)N5*7(LiVS}~ZJl5< zZZ0(YD2aLrO{h;ARLG7wC%uLouwCjCJ_TkxXK1vSSoE4^{QDSd<_k<^vf zVWmzG{>(u#nQI_QjFgT`j?}#RAx}2;HLsCD`JTmyaxf1(c;qti-QDCu{QOsGjDR2o zpmJ5%8t_2LdeJ@-*X9Wo@wwW|oth{()U|v&w1tZ1zc0egNT^#2Pz^n6ygiq*wap77 zCbmI0pR}`g=A6Tmgr)8VjDTyPS)xj1HJTy0z-{9eAcx_65-2S&%4FE^q2yC%SMTtt z*_4bpKn5b*uYO($NOBpr)Gf563EV?)&_!snYQ5&s4XNms>pqgrKvW%83oYV?NxKjf zwY^Owd`_oTMsLgq;;7thKQ*#ypTc}z1HTl6Z&AdN_PJmd$jUP20C(n)bR=j00Mjc| zAvV-GD*pqEqN%wh2iRMQ*w0+KkEG(NyjUiRsO@i&Oz!2Z3ST-MV zv8=2KTl#WgGb|dgE{UHxI+xYIkl0EbX0WyqaTB$mvjSv5zOhcpJp7gUeNL3U|AqV0 zV}axfy?s#JAiw&c%g)*^!=~@#BF&*-LCA2uuQ3}cby^^6S{&P;qIShz3pbh|n)o4U zBks4QC*plf7U1m4*jSHuv%hsewhr!J{21iymWi2RVEyNBmut9jX<_<*@3QqyckZt5 zqr~zWCHMZzN?q@jN9sB+AM5%R~dvJ`<@DjPggBp_UeBhQxCN{XTL z=&TRl+4RQo3Os1RL>uuISPPsiA=zBGGwskrqr5}tEqq>)K8kp8^)TmM>yi?ryPIwf zy1({XK8wdesD+93#U~Fyvk2@Ye&Kx?CX~hg@gPy|C;cFks8NH&=*oW#Esx{*Qk)l9 zF-nQf0ms;l4{f+~p~oD(I3|w#;b9^#QH@_Gv+DoGDNE|_o-R=%e7+poJvBVM(~3_Y zR*6$tFHObz*c>difMu!i7K_CV!2-T=B8>lGt&ZqBvN?_&*fv~yGmfwQ?IuamI16zu zSE^VAan+l*9h%D6n?QDdfS%}y)o`1P$HfD`v?X9s zC;1*rF~jt?6>ATRiK*!G*tb~h_DYQGab6Tjp)tfIbrfSX`3s*GPo0#Eu(!FNPebx* zciBGomeiAsLuP7{CN>!#L;Zm)c9mf4_9|e3|9b_1X80L$(gwdy4}5D_!*@-|_W~dT z=}^GbV>{m-q};1N7WCNY%#(fW`-W{xXUo1z(!ZQ8(_)Z#Y5YGw3&jLLEOWOI{|=7! zgHIEV$4l=qWi>@lB|00#n&B%Vdv=sgAivc7G0@UANV;NxgH99S~*r0FJ~d zVpXs)ib@a{2+g}nwNI?4tJpSru3PD%f`XpoF(`d~t7a#fLCgA|Z_ayo;3>muGeb7; zMA1nifCdLF*%Lu}8aiw1X^%pfY zwUF<6%v&{DTR1-|N9mcy`MH0oicSgxs6sOm+7EY0>z!HuPPCr7(o$mjA67T=&|1eBz@J=WuZ2Mlv&O@X6kkS(wj40PS{G3JMCPtNU;f;;ENeMMX?Jj*H(w ziymu$v*88UG8?_okxR`kxiT|Dz_vBEE5MUAbDm`T-sg^fnRt<>0=t0~1w94xvL!E% z?(zEo1cSMgp4)0QUTXJg|M5$GGD69o%#>Xvy^}+O+?IQG`UHuz2PG5zzFnKkB-jr!Us2Ac|`=EW6rg~ z*nyOgaAjpBjFN>YnJqQ?1=@zb$9ApNm@@z;&Hw!vRBDAt_>cx)p*+P6VV^=ZUIZ){ z0H*>lkPyE=N^C{wp&u0t22Xgdy|zPwi>SW+#_!Ai{LOFOm%FQTvC&bqxE?{%32AeE zsd%a@=z2}D;BgQGsWi@$%W>rJuQP^G0mZ=Ulg>pJ3#i@LR`R2f?2t$8wrD8OAQ{wBZT<0a*TYgHHK1a516~uZ zupT!7Nd48Zexn%R);>$>Ua=<;Phztt!`h$NWZE2$?>Ku@r*Ha+?hOD+p;PUH)c~Cf zw*{=>gsJ@4X@z?M_GRu$L7_ltv0L7xw-t+szrNgp<$+QtGf&! zt%4R)&N`Ygh&KOrc0W>kKn$w?G?(pQ4{{W9Q6gs3jiC#m?oKFDiGOeMY-=c<5{rCV zdMAqX=)vfU3|VZr_FpmVCWDTadprKZa_~F}4&HT8j8yIuV9|OTiE#g7gA-IG!mh=d z`JMYWQa^$Y$Qx2BC&!w6p&|wbxwMi9x8hH2C7F@4QD#XDu8bpT{DiJM>|KTBi)VWP z4#4@u0{&hYO-}2wC1>&gleuI-Z(LI(_=hICB^R?4{!Du(bg@#Xdhf{am)|Ax$aRKr zC+zUZJN0JKpj;_n^^cZ5SiBh!7&1izuv4tr29gqtL`3cVB}$Pvr2{o3qQb)ddJ(CYf!GfBQCyak$nB-ep^GKZz0a zJXsI|m1>rJ^>6dhvHV81`c9({(}zl)U3#g>*)2C&i30C2i1;c}VRG#W+*XW~wc(7n zi~?JVLB;WO>lLlU4=wkY+L;}F7?qw*&QVdC)ds5X)*xLvzWYy;jhm<$ZznR> z4!J&8X<}0SP~PZzQogIM$$;ig~yv5r)7|pxg5_6(+2XIj0Pe`rOZ%i z#T47BYak}j$Ko75ACJ#-I_7!;WDnwpME_1TrusZkfOvPG?J_hfKnkyCnFMFrS&>WcR!{Jm{Iu40`)N1?YXPBA*(*0Y%5hc3!lDW z>Bpb-lgTK> zXx9_F@Wx((N4QXY4}q~4l(vD zv6hr2(M~7tA05X0nC(FRB1g0SXeOGIMeb_D_r*>!KpB|fnbBPv`{!k`a3|!y8r7ub zDdm61j2d)L=x#msy%OuxdjK)<_V9sQv&4uPm>H;v(%02?n#_1d*jNvoQD*mj|5_|n z3+n?0c%p{bWui#t>g^@NZVe^2_G@>Wybp=`VzSB%^P1a-!gZ$@_o1P(r}DrA0X0An zO3P_XN_!zd$6z=IS}m|I-2w8CDi6z`GBE==(P8w!&+rBz*JGym;C_4ebhq?mQ4fB_Jk;6=t}jw zagk!9#(0nPpU#7?;z1^4el}*UpRFg9nh+aUhzIY96XBKjww#*ES*rSBw@?w;wCS_@ z$jj-xlpKFVkZ>olf%b0oCeuWVB(dUrGzT6{rzg<#a3gY>4Oc%h$_~(nuQ76jshKey zoQMBub~WjnMx(Baj$?MCu`uqi;Zpv^SCF6|btWxY@r(bxEn>?FqaA?4Yg@mk9wdge zPUT@1x1-DTGgflH_;7*7tK7_88XatQ<2`u($xU7XqI~SOAtxuXpwbY?4xA?yLz&>0 zdz{!obuzBoBjKjf82c@%`>2Esqmd56wnMz-<;AXK zJAQmxh|Ot|G9~ptm%_g_7qF_6bIDeiZC}$w-3(I>FEnd0RgLZYLq#lm74n4Z{tuJm zkuO=Wa3`P!C^eVqs|YT5m;^{Es8RtTWgYk7oxh+vdB758bNW=A zP$-v7%0M7Kj7VbB3{i@28GCV>72I%F?ur zDbc5IzQmwXQThHNC0szGLP@%#LfIxmo&`joV}Ax7;0Y`pw~z>V5j7G~rq@hs(CQJG zFcr{OH_<4GLJ*WQApr^NN>pO~LpG6^=YxpwRgKo&PywLeSOuK*< z&hfcwl|JgB&Cs2!*?OUja5S?c8wnC^WCR7(lk@D)87GgBx~EB62SFtpa1FL$fWn|z zd;r1XdmP3Y``K%p7GS-iXN4Q8eVQS3k0apWJhO&1_XBNiBY^?!5n*oIEHr(y;Qt!u z(9^{*Y~w}3CC8+a9u9~Bdxj>ntj1f8pxi%|9zlhG1&^xNXcy{;PBgBSs#JpujDtOn zIkMhLBv(u$A7t+x>EIIjRJxwth%7E25tKh~IT=4CF{@$HeaEixHaZgZBB5+WWN2^| zD!p^y+UyWHCo46nOB$7nq#T7I#9 z6bp%w(;#gpg!7t~VSyXk#9Mt%1OJn64*@sJGOTZ}QL~~JxvkF=DhLx$dR_E6)7)qd zv#3P<1A!TUnF=171d&1&sv;Uj^T1H0>DQt>Hu;SQKPp&k-YKPWek7(Gwo;J(#Vx;q z1MGVabWKAl&+^b$mERWi;>+1Mb^n|L$7*+$lcZCoG=|2rxi@P)S| zI8JHIxMqzvz=Y^Me5)(RFX^X%1Cyus{$i;h5R#TY<@Q~XxX*&y_Yq_iyt_3>;qvmb zzG?M54!g9O+RJs+WvsCs%kp8Jt<)>QH{Y7mDl?L0f|-xR8KsEkf6UNxRN?izJZS%fux}T zO)kv(%=W&ud;iD^dr~d# zgwZF*%`l0oauMGkm4U5rHetjI--IvCF4R259jLkWHe3O$g+@KUK7kT#J-hCFj>t7gLuS4)abrTIj~oWfy~FaO zvQ_~}4XU<7#Y60tD6s*rlE#$AZJDQ&3lA2Pd~K(%4*mezUq*`!tO8U+VmJ#Ujpab! zpL_YiLEc=;gvu|T^6Oba4b8VmI)P)M@87Au>4VRwt0~Y-vmKK21(IR#fBKjU0q^F} zSZBECWtLgxPY-|V&$(aN#PzcPsg-%n-{j*CGk`)lUtAvtP%9G1LIY{IPPd56#f6!U z$JaZm{jT(W340njHc&8m*x6GKfredn1}xugen-u!@+7DEi54{bYITK|^)Cp4jhzoqAY4R|cs^BZVc%E7k-{>4l1N0;4X>zMdB+Cq$6>9cgFH~3oq z{QmeVk~$q$BP50{CsaHJXg+<`){o}g2!m3PN*@qp6>{KDF({bZKSkZTS$Vme*>p&s zYKCL9P~RUglqLazeH8I{{_;cw=iKvgK%C&$d#v^v8@#oC3Z$^LW(z+S&rGwv!SohYVLr}7jT7|RhjU3*VMpJ;tAGE;d#`__ ztK)!v>N5RZ^4ktiB0kyN>Q3>prF)9s!e?T-U$)?LRC8z$Ml<;M`d7q$w8y2DwsQicrtJ>f6AF0M-B8kDQ>fpBm;bdz_+j@jsi>Dot#isq#r1 zw51f8jEZ_zZ6X4AS+W143>qt)qDUvp(ubc!@u^Wd-*qZ_wZYKN+x{aLZUnJ25UvRR zPZxmu@o)D$(mjNBD%g_b(_MQvp=fNpsDr%5axe;T@4=U6S^x3e!T|vw4dM%(gs4*DM5)uh&adE;VobN}4`Iwd#V)|lHt2dCL}`%XPV$y^`@yM1cas!XnNMOc6nS$R3(-XSVJ&+T0nUh0J$I9|B{^MnK44ELV22;!gQ%t}s z@3jL_tI=HjU2Kng1XA+!<4*y~-x>u{^~^~}h*6rGxiyT#bJYO}-<7EpfWz@#!9TVo z=}6J5xHx7ACM`>oi+rJIXG^6HI2Qc6(HoKNHPPjPtN>xQSJ|HVmk=52yu=EE4tK9{ zWdZYjLZ2H}jZ-B2oQ{f!*h}bSh{npsUexsiD)l(Pd77~HqQSQ~8$i;(H(6~T*U@XSTxTF4iVzV^h0=l?AC}o`Y0)kdF@F^&LMjM;=v@#DehU zyamWhwdsuBBc-l3?VT=4OkvkwP)D`tE!V3xGLFS(Y7p@@cpyFvKRxd`<-Z`-^{;&V z9|S-Y0!;W(k@ML10%qsh_HhWn(Q=KBe*y)rS~ip)2u;9Q;+Zl=Zvu?K2-r_|a;KZ2 z4`5c!lOA7AZ$KIxxwB_u zjG+wo4+`Cw$U~ky<93;Q(-T8DY|MWPgefi-3I`$aI&B`$8<6ez_AnpRfJ{f36O z8@HiXaqNx`SnWW_@MZYsI-^xC_03bV*~;iB(#b<;YlnX{7J1_uJY3`S@_e$W1X6B@ z4}3U!jgWZ8ODNcHycYIkx>$=e1OsgL4%UK|?t~b51ddqHEsD7&Qb4{2pd2)gLs9_9 zo3O(18R~x30!X|suRqrLZ}RDH4W#Fp*X9nWO)Rk^K??2OUp9)0S6+X}g)P`E zbH3#b#iI|h^`MZYK&@&G^LE-Tjx$qlwfsQA=SBkHHHg_{u{h;_2BR-BA38z=;61LD zo~iB!L~O#o(b_BGqU2DGxLaS#FnmhUU4`GCUe%CCQ?OwE(G+Ao|1RV=@TB~OT|DqF zBZf0SV0@V^j%Zw2(P=X0&y|Ue@F)*x(4_4nSp^#SYUc~B=}LX_cN+BFtFkKAA@3H$ zaD`i9*junjzvW7!ucrszoB<;^kAsthLn-2E>I`+Xp5#1_N|A6eKoESX9x>IUz4ArVs;Q~z^ZxlKKJX4J*>rh`PPM5`+GwU=<*M$`=i`6r zyktMl9~?3k>3&8=wl8`9eGNpx%FpK)7s)S(Sj97ijB@~Ipc6IAK&cFv88H-Wkd7cg z3g=BY0h(U9DlnFqy=gMJECkF1);XNv*H{gIxjE8&r;+_-8(MegGQwuq5-5J}yAK0W zGJ4A~!!UsB(c1r<=5syyLzM?uzqaFmO>=T`lKS4F#X<6!Mi;MozTCrgBt^m_N}d@L z-5>iNLz0)ZSZKio%o}}Ve+7(b1ai=Y?(r3A*5FxxB;y@-iU<2#?0}+31XM-!Vb3bTh5SGGF>qvFf&?12*9_)NYU0>1_ldD7dCT(G-A>e4Zz{ zT9+;(y!P`IySLd-+padGCH9ZzFMNzFhLcIpHhY5-5)wMV@2v;q2nJGK5_Yf@iSqXK z`2vCdI`O18(ZU+WKM_y2D7+*7FE5M%6V%6DK8P%Z052bqv(LFrOGVTrC+p~83 zAYoh#RISLD>iY`4p9<7b;Qv+q%HIN0TSVrk@`rXu13Ae=nFcm3Sd=!+WDO6`Q^pd} z|5=O6@wxA9L?|CGzHu$YT=>!Iw_T#;h5B1+8%LKJ-`h~61Mp^(Aq6Pz|5f$4R7s!5 zoOyfw5r-Mwj~9M&#j>R-AAfM@R?XGH#e#gbbH9<}lX7f;LFaDTyRt2>K zvt_2&ODNWm!jOUjvtb}^t7EW@<4{Pmrtj~8A0___m`?r%B$kv0x0q7@0`&P@Fl)_6 z^FdTWFD`6jA=Xnh-eH|g5(fu~TEFWG`(r3_iC?;85`YZk^q1?uX#O9({bg8`UHtD2 ztCUCy(kVztmvl*YN;lFi4Kjd~N|$s?Nq3iYN=bK0!_Y%NYp(y^&v75e{bs+}_w~w4 zJqBmyJlDC_H$K0feCCBu51lms<@p8@7<5SS7=KJXa(~9(OrnvE)gDDSu6N`*?}jIJ zuM>&*xfweMD+DRiQJk52!!yIN#gT2w{cFZ@xwz~;`SJ4c@AADS;BYoEJ`hLU2C3N@ z{GzmK=Q83Bhp;pPxG#)GOG)q=1?xWIsgJ=S|EL%VBztMQBRxl~t-OY5=zbv;dOpW_ zeza|I-E6K^a1GDpdCNC7<}&tMMZir69*0GA=a1iNV7Wc-Jl|6c=}_TG0afb+=!Ww) zs4x^?Aa&TA$bMOB_A{zy6s2WOIf?TX1yq_^v|fC#`_3Nu`6_mq9>#oSB8nZYXULgB zMe5&Q^`g&c7K-Z1eEO2Wmz5Q8ymJyaZxf?g=-N!HoS=@dpa~!YFd09?orUu8T-AHy z$Dg@*&Uc&ihd4i0;xvutIB4!`X}>jH{MGy}8X(&AZhx{U@_(fPqZutRhxYxB1hP!1 zMPI6gDTABL&~SNYZ(cgkP}j)&alRaH=LRFSwujTb6Bh(k|ELwp(J(FzOKHoG)gCV* zv^IH6vq+#G#}zz`Z=9{JiG$~xG~?00hP1W(8)L5nKCBxo;;$1lVL6_aoy0#pj+Q0A z)s-@Be{mh)*RD0&5{wN)vhG>nWMLMiT#EAXcAGI3G3zF@e>!-);F1x{4HIh~HGf2M zizM!_+)p~+ol|^7CW8Dk9bAojWuVJ4y`(!qj5DD@$m5Xq-ZB6&Q-4=3*e1F%BGN#A z{H`G1KR~&u0z?zS)+NJUgR+;d35dmEf}Xjt2b4xG=?UIo{D~1h3QBhr!Bt@3aeX6N zP3=czsBD;B_s)q-0#nFW`JWP~wHgd2pb|JR8z1gt*y2jmYS0WLA*iBdP6tP&zfKan z2|pY}cfZPK2o?g+4;se)i74u`xkM@OI?&>@$3L4G^f0r7!4|Xnz7%b(>M=e-XJ5sU|msSYwg< zQ6l+bt={*EW!e!=IbU^YC;MSxH8%k-gm>plwF>gxS6U&vm@^J!_L0f`W+^jA<>O7} zobv3*m_*r>7DB#OK(fH061qj7oR*8}T(Qxz1c}J=B%*(;`T<%;M>nnJX*Hu?IuabD zDEpj1SUh1hRnQGtEiab6Eh~|e&@^W>qy2r{*Nd7hTG423MdHI}KO7{*f9s@AgU{zj z>kWpO<4hTU+1p5RfoxH`8rZXhzKpZ?pK&%D>g?}1ebc!J-h6B9n46QOo9O3zy`3~Z zMZcX$x-p#0snZKP(D80M{!MvT>B0kcjRgkHy-7cXwv)q`27O`I1^EeQ6_zXhH}xjA z&S4cEncrL^fn4@<)cfYqvS~GL&XxnlidU~nM3_*n8&<4Uo^5$G;vdNQ}Xm38utfxtK8`HDGp{G(dLLvA$Hx4oP zR)ztQemidlIMD-61YrVX-9TYPEoKvfk4Lk{j=4%?00zH`TzCe$)1g;oX1}Km?JZ zxd8>juaTrVD;l(Th&4WaO6eCjAcO{V0q)DYMl2L8a;cMb$vdRX;mseYY*oE#x_e+} z)s^MjT3bt%O&MPV5%F4SJR<;`1kPvH*|IdOx806XhaUWe5|MBg1;7%CUa$Aa+l$JD zaj$OTn~}7Q^R_Qr zF&j3cUThY3)D6w;(`*hmJ_zz&_{=(yXaubLUW5YfOzEz!Q0U61{T01F#6$ZNug{-t z=C(YF)JwiNwy_w`Ny%&{+kK#2d*`5{Wr;haV6@8>&9addFuLsHpbrHL*Z+K-rU~5l zj~oV2*OHa@8 z40T^_b8AQ|so6Lt@=SKdEW$>k0<;rKcWBUq?#9b>t1&^~aN0PQop?X=y| zk`u5RWDkkt_Wgc6M8pkx48X_veIG9Xx>4B#FRBi|Y}?7?ra4!#J1xrCGZTyPkLDJb z%nfq!y6$`@-ye);*xByC#syV)i%b*``1evb0&X*lrqx>&Dd<2DAr)r|2#VIZ`@!0> ziQ0wlDNLYqAsx~dH2arkn&pU@ObgKt=5TV)s1mv`2V-)!*U@6Iao^_mHtPR_L`o7r z4D|I@PkM)(1NW2_@e!mwoW9DdTQ+mwHx4njb#q9YWiMq)oS!HVL4<|dJTFhO1d;iKiUgOefDoBKG8lP>vk@MFlDq1Y<>4JtgM+W6t9&VuV5;EzZ{)=_*rKfgbnCG=8I zO_`*Rc_$lNv9!z)q4aUc- zCo@xOIdO(P+AeA(z|Dj`CBxCVz)65EwH*TRJ@u_WEk{UM=PjW=b$! zwi5JHvTe%p3aL-Ce~tt|nA*Xadd~$}yz22cr4ONXVx&Dns|L9~qsLklkiT!PoG^BC zgo}pZszHQa-P(mM76^r)uJDwUW2IpXJOqBbpkHTBeI%UFYv;Wrcci-cgx>c0V6m0J z?_e>oGq_C@et_OZ&&Uu(G`jQjC$C}H3^}TIToLU=Ji+c^muo=I6g1Z(ui&37HTrHD zi1DthRZXcL-%{*PwI0Dh*r6Q^tuG96JP*hm%69Tk_Vb103~u28&tjE6hkZR zc1*wuDzO!r0Ti89KeG4dJL7POkaiUW58hItpzL5NAF4hu6Vwnk%3z_u0ZrFV3J_cb zo6s*HS-!sD_~IuBvCIPbMNmk{zD-Y;>H2gN^VCWf2}XmMDJu51C2(}U+2a^k`pyy| zIFbldSw;3#TKm0A~=oz9m2TOIKF>(|LpJe*y@N-18Y> zgfW1x0mMYL`tepRzw?7>S%fmMMc7%WPW?Jw6<^GC_l#T|Re86_VX=Zm|7Vl&U`;Zt z$=DZ{Rad*Y!|&0Qhu^~vq3>;E61y!o!1R@Ocw8;}gcAyp!(oZQI{^oFZh)}?xPq%O zr2P)S(+U;=@kM-A1Ao9kd8F|6cuLmO`1L7F@wtSI7!K2W`XfW#wDluH{V-gQnN{4k zM@rcZ0zKKD*$wqF{x8Lz`+GKQ-*z1>Q8%;WRPDBm@?9;)mfQ(cCwsIRS%~$)_GWte zD_^}NB(ypo9~n`Ki7aJ@Dd@F3aVljBht-r>kC)FV%VY>1aJ+i_^5mS14fj=FL_pk! zo$gF~0r!skc~85ecR1@84Go?gvtG1Oyj1I2GpdiIW~qM$Nt-A|m9Vhl{OR!4-(L z(R-izzq9X5H@>Md8v!b$oH4ratBiKf?A||{;X)f)MCv^+Nk2l8zvw@eYHC+7fhPak z-~YyAXlT>0)_b;uo_)r?hdo)C^!F3=1F}6AH+k6iT z8B{&4v}BSeBz;ggEN_SvNqAK7Ou<2A@=CnLb2pM+K@-m~_-Xy?ERki*3P%tV6 ziqI;#pG1)2maBwSU01zcos7wWIy#q2FjFv!unP@DQT`*hKndAaTvH&4seO&J+oh|5C(PV?4pcsSQJk)S&$e|9Cyp&WMB6} zLck_92a8--4A8%R9sL1Fu(hi5Ge4u^Vh^W)O1tK8rI&VFNKVF&m|Q{hWALz?R+ah43QjQd+su!9LW zG7xwJIGD4|kFQxCy)+c@*9j?9J58at<`M`ja<{ z!C^KQ*-UnkrlA3x<<^ut@Cw{1NhYZP&tWxzQU7 zU>rVFGnyk8A!ZJr>x0(ri#wpo73%z`;x*EGDtvQGF@;2LVorDtGJCzC-1QQdFsMX_ zGspZaUuJEG@L%Z`m%07sil2y7zJk$4G5YS*@m8%_Z-K&-SZH-!oz$W3ltyB)gXZ=Y zqgJI)ANf=Sh(9Fe&Bv;JCE1v-exk!Ql5T2=z2fRoGQt`aqiM% zp1Kj(?)L;7#q${kpdFWuu1k4h44oh}Win4)Kl)h*T*P;O&7gViP65Fu0#9g?q9~qX zDYNEcdhl`az3k0zJS7~bO#?>_- zRZkvUN4;J5C)lSQO>Mk!R2L`opBqp3hcQ+~wXC5FWEIH$q(G6St+%KayK&F}7p$CB7;g+kssShK(&_s?Fw zB(zGXr>Uw`%73uz*I!`VcO)Te5mR&V<|vG<5XH zJE#8cI*%@r1H-{ zru>CCz^2THJ7P88;F;=_|G>zK|1lNw;b3H$4{$Fgmma=toWO+>Fs%RMD8KuUqx^s2 zugir8j^5WM=5xOUFpc_m^ywcbe-omBpR4+@7w!oCU--AtXm${+Zy_C<%VT>!k_+F! zsCSU7E9#{uNcmQ0vtKc#e{|;*18+Zr5M`zB>iFdI&dZ>dlXu0X=DP<0KVH!lzNQ9{ zX`0@S?N%$wJEin@L(DcG@|j4RUAczp0N=ywyA#yQ_4w;cN5?ZOAKrw>NEt3if1Nr& zZX6Z|Z@1M={>-Wo)q48QX8I6#c0qa9t7lhNeGh@{rpFI~o_FW;EiKD%I2rKL#arjO zREo?_f|kT_^RlvDmpokX94t3euo^bKk$fQyN+rlkO|C&Xf#N?#XLcuAu|PA7cx&V8 zcroAyy}?h2K4gE+<0UvteE~k%N05}LNo;|n8?NlzG z7jVsYJ5$4>f*>Iw;W;X*>SH>mw0ns@2Zd}LZN#)U2W%ZBFlso$BJeo=8%j$3(Rbh_ z$iz~k+Gv&s91s2Ynv;W&A$&_9;BnY9WjizV5DQJ^FbQuwY?h#G3p@rUgVj`9^*nyg zR}85LoA?vxxX8|QHHfIUb_G@DsMISuks>ObL*F}8aLNyb`hpPt*M*(6J+NbM=^o_T zhxW~#6}_kj4~UFKzVQ}oQ;1Q%`j3^p4LW7h-_h8gK2`Mj8|Z1&=c}J6>eU2pr}EgO z$@ez@e8b&Auw-9wc3E1_pL}z_{n3lDJfOglo8?O8YX*{ODO4co01G_wP^JwS{x8!DcZOD*W`R|7?vL z2_`vzfb^qaZ(kHejBtAKJc_+8x7GrWAd%6X{a?++HXj1v;}!VRPPIuL^_}Cy^Fp<} zNs&c$t%4SKrrZe%#ddqv>?fhEkB|4fH=W)SSJ- zk@e!U$it+(M(`=$-&`dmip(p|$F;!EErR)gEgs|Bu~k{S$EEtnN{8wkzyTEVML(#@ z;Wxan?KiSGLss`O-S1!@s}8&2etz}wd$)i$C6f}^bN@RFP*O74O-eSG?_8DrCY#4< z){0cbi$Uy`hI%XLAAo>J(bcQJ)xu{rYT5v0T(RqA&uXF zYzLzc2?OCxdU5e*<-!(2?>5oQr{bRg=3y!b;&obT=CV-y>@fm4jwDQZ^xf z1qJamR{9u@j+W&ku^@5EzT!GpPGFus8mMX>&C#kpVt0AiH zyi=g9>+g6+Tg_^U17TlsuZrMz-;E@xcPNfaQ0+o_qg<8-gRUWfxY#!h+nK^kRs%L^ zjV}yr=)#HW)*|j!CP=s&3 zTII~G1kSOqT~{pmbcpyX%5PX?2-BNC$tsebUR+4lkC2T6F3N=`Duk~P+s#HpdWJ%v zD6Vjy$FakHU41HtD%0tH&zwOb0x47d`V5~%wLNo_n>e1~N7!yArD}=K$*O!TB|_{> zag^EhY1ipr^|NS?n_WMbS;(MYQ8ib=tj&%i>75_&LKwdY5-#(+YDn!Iv# zj4njzn?lTTc|1>?|2g&aj1>-6PD@k!Pmy4S`Obbc}z|3*NQ zSr0d#BX+RbVdEd1&4he$TpBP5bvXx7x<9GhFV5x`Tn~zhkh>h#`U8~3v>5g@|D+a< zO~uGvf!1`<=P9>Tsa~j1Q25M9im;BxIXiZ`PC5>?b4Ao{U7Uj`QC!@oxh9j+A_t7! z-hJvPv{6{;GE=#ddQ%ZhQxA7@f4c-92B&O`bn$xwl%JU|73G<~I>l>V27b;!%V9J#8pHeR!~o z`AzDKKL^ zM-Q4gFnalEORhvj+ciMh<4c$#2&((y1bz)Ji<+-DbxEp3r!>%tx0%%F7_Z+}wmK1y zWAi^*NWlZfO%0k1+@uDW35`eQ4GWIx>^B;P>IG`G=|WE0_u3vWV~+lGdDnlNQvwMR z^(nO@eZESI>z{1xsqfs7>yBpe@(1n5>s}o#`F$~5YX0dKW`SLI{mbc9CGbb?Q`ouy zL3J3q!R`>3ZvwdRCJdAP0gjuqsPKUd2Zx8_t+sSr#oUnB5|RPc$5|$xmj_1o;_V|a zJWUpzta99~-}evqC|uS(pAP6-k`Rp)>X*)q{;k(5>P-^Pvi_x&^gJeOzWfL%NjJ3) z6C^6FO5zs{C#F(apuvHi@uA&X{-QAg$*4`RvC`SPyO?kwBqr^3$OmaG!oz=wU(O*L z!F|dZ#vo+U;Bwq1{MP)g#yMgvi({$`iXP|)*cLYHG3uAj6L0CFB#h<>(1g(h1aeb^ zInSvkC`39R@Awdfd`pDOHG0D;yktFh`@2cKM3SS`^3_9R zJiY~qhjEilQRAa}Cl6B^xNyiyHC5&q^%((u;%kubL$K5AvnN#@+M7-|1_@68EYjN7XVfEPPNzefy zC^kCUfvzxxY9A*q@P2~*o zrN;j#MTD)sn)u_sIqL*OJ1MsnS*u-{#0po8R1S|094>C!IQLv34s!ba8-BO_yk4C= zv{I+88(6lR_XUnL*1^}-`$syfAc8n{;jY!NkvdNFwtjL}>WVGz0=>+)Q_YCpp1?)a zd{q*TWCYXc=Lf}YsjKDej(V3NM;p?yiWfg#HQjlgQJMCl_s1}kl-;>0Y7F-VutH{9 z=)W$VeKn&!3*h6-5xV~VwhU6a>SDEkSr=ctnp4hd=yt8D;j@-!*n*2+R|mbnXIloa z^5RiKaFIB@A`-#w?JaT*XSR$U5|!1U3^!~Dqwd~(LbAkC z&`<^ok0FWzVoPp<_xJv?%L$KFwlw}T>7YF+G4%vqnZ$IZj=Sp$yYWSt_7Ci+ZAlKz z)|?i_qtvEWRp>MfYCW38Wb;#e4wIj1*+g2JB2qT4!w94Tm1(c;nw({Wws*v27>44c zFW-dZ?FTD9ggYRW|mf3onCZDN!jL9E@0~kaH;@UbajA9I=PcI?zQV? z5D1eO*^s`iqQT&(RxKN~wiA@^borNdeKYeT3|C^xjW}*KY`0DT#Hw!4NsF2cx4bEB zm(dMc&I_6*!iRrPx;=8)PLX3#+>_<+Yxh++b84Q1Xbj;~0YLxFw#@4RMC!s&w>Z6A zo}AB1W3qA_v~L$Cq7}Zz_~fDqXX|BJ+x>b!O)u&{$4IW zhn;-d6QC>`i})Oqdmg`;-_2y@oohhPf=>JI$Ms zu|4`ckS@3%b%4a821%y}$!^&Wm1S+Kx2rgy@K4#23V6xgi^}t)t{n{YGvo!4Zrom7 zqrG$dE2`KXGvqojfu0PkG%a(6Rs}zTCRy~#LYPD&1J{&y|AC@TF+MISBARxVZu(c7 za8(QR!CaJN99I7Z$dvAd2&G1fx^3@{g!I_4>)-mO(e(+Cn^o(A;y2JrvS zx}F}?;Oajlft|c700MNlEZXlcUZ(N8;DZN8(a9LHo2}{W>R4Gn=s;c(~K2)J1wr*IjMxs`f5IK`cG$!w@Sb~nL%?vUN%Se&8-};YTh7yo!3c|3}O41*B zZHp-2dK3xf9u$Y@s-@>E#_?!mu$o*SlEA@_X>#{)PR!01cy0 z-4z7!b6@D}Fr$Sj#*C2&L1dDSD6VtJ*gS*yp~P=C*sH!6B;Y-F!4RfIsWi!13TH7U z_{wQyN&?xSdkhDUwH==Lpo8$jS5x6i$$Ulou|E*95VJ{#gTu!MT(9~G4KT{1D(V6n~O~cff%G2(0 z&j+0eO*`mBn*ARA$B_=INV}lAxuERbm!@pXW_~Ms!MqgXd;&tX37VYV7=g-6+AXA@ zlEdCaZ$7NiG|Yh`4Fu~*kRmK(cu!Z?K{vGwIxtR~FjTi%Vm|ih zX4>z?eg;}gr6A0v@?p!|I*{og0ST(cCH+l5?gK;Ig2!=@oFqfKV<@gvbuY#w`h28; zcTwGt-FRcW!Urt{cG~<7x}VL5uA7{4Gz_LaIA3reYEK6x$aM&ae1FE@`JPRa&9>QM zftt!Q57s+$db|H;Z!bCe83e!Rwuq$MP8%A6(cpOXx^!@kO9^oYby8ysshY|;woE;OD;dT;dqxa?HZZ(_Z^yp1$0J(@4v3p$|>}bJJ zxOvyvMWd13-dR8Zh$kqlMy;LY``kahs(as>tS^5PJM!kSJrbLW1VF%SnYDm%2l20xt7R^~H+GRi6$fxCZO&R~k{kiq{J`T|gQs z_PYZTTZQeYu!1uQ1`5jUTO(;JO4xdA;*8#KM*Yjfg*vg`a*kh#hK&|vK)g5s?icNf zoWr~Qlbc$8*^?75ie1w#`QZ+pPdhSrknvV}R%}V;-VTD(e;9(&QDXfhqrZ&q(W+eC zMF(Z$OkxDj9ggy4de~hzwSoI8=~hV;8+Z&9Gu2vWq|MPpt$R6!2!IqTZmt&K=Uu(6 z$^(AK6PEIvjTCN--f&XlHG3amG?p#hLbv1FSGSL&z!?xe6>aY%zu%zNPvo;c*=m@d z7A_Esy1~jaUhd%`czeaSNUPKXR>6yGACT$>g-x95d}UQuB3#mNxV0SRLIC{HAkw+& zpJ18=K%>^)w3e4sfUs0J04{V6GN1yYZqAYB!+*Zijh0XKU;;~F6Hgb)w?g}mCMMY# zj2fZp=6(Q^gM#%awWnS^rPt6a=8fQ;m6RQwv{EDpY*XsX~hfV~=Q z9k*y~pRVxeImhB`=~Ae@#cnPaFL6;6ub$dj?+0kdpiw}Le7PSyS@=QHQ@N4z2CyVQ zJ$Ag&*457Xv=&g5I?1n7^_^FKKhqu-Jl-6 zFfuxdh43|8qTteAr9tH>Iytwl&yn*=B739#_qP@wNg-6-LukuGiB!;KA;|ej$echg zJDRsQ^`8ucNHT%o}s(x-Z%Pkp%vpr*uY!8d|Wx)TTQp2(v=D% zaQYVq71fiGK54R~`FiURh?h&(vhc_F24KszoO(v&N9+|JwSt3U*X@!3I{8@N&O(OZ zJqowuQsh5HlH$;v^(_&nyRP@84#f&GvUs(C$m|9r7;4!c2I1{Y2;%Q;qI*6WjU4g+?~B>)~iTCkmLb5J)cKCKY)w zr_h!e6@>-9yD3@!=rX4{ma6+a-6Ijth8rcgls0dKQ|8cCQaD;1ek>S%M>j`-pkb)G!e z!X8>!r{gQsHcjS~LUsldCJ>>PZ$dZ6FAYym`Dq(9rqMqk$-rM@AN+9zZ%X6>95x%= z4{05i+j8o}<{I*z&kjM80zlTi$W~`d^04z4mf4`q_ zxR-f>%`G-{??qV2y7yVHyy6!k?go*z5m@|Z)It`T+;9zuSpLoSXsR?)c_gWNo}LDS zunq#9#&00OtnQ^~3T=Y?|7@E}{R?5#AE0R+Tu8k61Om9;n}hKmB2Hw#^Mjd>BF#)2 z(SeQXWT##QcX#qhTj*a6(#Pj$$5GpMV%cjLwaV5C%F;UpCB;7U-cm2-7R53QeL5Xp z&tshGPRBgdExs)H^`7W@=?W$t>_K5P^J!l2C$Ac8*_q)OoW0o}jJ4dO2<`b$4M zjrfDtbtf4L7KP}1jrAl^i#h>0dHED!h2;%$%1cgWQGGf0X zt%3xboFE_%4NPwP9_|(zwoBKK^e3?gzCed7Jlr8lK;tUazh|7UwGJ81Y|`n98|Al~ z+868*>l#1vCvi4A)Ikmm_jtyxhR~h$3Me)TG zX2UQ%U#oeU*7oR6V1O?PfT4e1jefcLCmroi3b#5Xo%_*Tfd#|hs5v^zo%5Gpb+WDj z$49Lw+7+J3#G}K>T`-AP*J)~pkz`y7?YQF7P0A>kL?QW##6Q(TJ{IjeEkrKakSZ6^ zp@w~Svni6}gWQ_y$r03qUl8fNW2JlH%t6`lrrNE2Fj_Q%k$9v#{?PAwv^YuFEx;!B z;Boo>+pjPR!bntU%f}%Xzr87=h-yr-S*=GRbA9)NAbJ}qOV=_xaf$Jqxm9_E%OipB z;5fJ&815SWcXMs4$ME?h2k9TAcio3g$ju|7r@r@wrL>YMpTMRvteJidv z?Dc?3Nomai6B83&kGK+Wi?K#cF_SIDai)0t)L#^OaVGug6I&W7nXxs|XPz@Cj>|&G z_hEn9z|p_^WOjD39V|e>NjzVk!Fu{s^pg_yiNg^U{5AVYtw1EWD0Yt_SlroV_Cd-Lu%*mh2^wJQ(X(@}8{fE7-e)sdHfd463HDj=p$CwwLmsQiESBEzR74R!znrK9f$}H{&d!} zshlJ!FiCuUX#-gE!o~%>h9-h4Hwv;qmlD>dV4c?f2^HkJIvd8jqM1T(%Y59Sc#ld; z$$|`B(v8`Q#sB)_IlYs<^}4~o3;^jQUutnf0aNSxcCZ64jyddm?ci?f3O+N=t^l1Y zwC8{Qz0sh!g2&%rw4zFt1UnpgSMbP-A|fJ^6XovYBsC6B7k5WsTL2h(@l6B2MnK8e z#KhYF>&0G@+yCP=+}iCsLq1skzZ0TKAT!l@?0zsGn3ZMBCH3o#$A_!WxA@GyRv)K3 z2xk8G=KwOA{sJDyZ}g^_Y&hgpR9*&%i-im8P9xH?SFxxR(RyHE)z~bnKMEGYHl58d za723u{?}*N=IOdIZ4V|zAmn^~^b2>~JJ)$@RK^tQbF0{*0pYY9)iV7K!lL1)e=&%J zk;xY7;k&_3LGH2zB?&VYLI`cQnWiO}&It_H=I7P|0Kk3udf()^HJ3F6Qs}%&q$DcHR=r;eKFMBko8?u!Ey}=~? z3O~Zy@8ee}lRpw{|BKiA-k%2UWVL4Aj!xL&(Qs#9)R%7s1Zv-!^M4`A+0FX`ha2P` z#5Fum{;0!WEcjo*=-HtFFLk}R8sk~3fVi>oONcP6lTUKi)}HoX-7D;5v_D9hvoPzBiC`8H1jCR!WRxXnH2cI;|cQrgN5(^i%=XprtsB_Vaoh_AX6<* zF0fifqHkR)^ZlwO({tGtGXuE3;| z0C<`q(_XvznNGEt=Hnlr9?IRlqky!R>O|VKQb5xM|W_M zjDuGlm4SxbDC(}xwU2^ug01WRBIV%f?e@v55}o>|+pPMXAP3GXkoP`20BLOf8!Z@A z;t^Cnbc;n%0cOFI3PNyMmzH8CdXPXEN$i%JS+!Y|6YOMuLpu;AN8v|2GH`PJ4Ki?R zhxeHDq-3h+shvBsWo|)GS}LlIHczOI4z2hEHTU>D#nXDTEdksuwRq3vIR$RHdkX2`1H_6fn$&3v07;(FtqJ; z=SE%=NaMr`CC_Oj5kebU!f8Sb_k=>=OqP=+0`Dej{ZEM@;6DS}6t|#G{ zg`#vUb@m|_J_PKd$7;+4-~fs1vn>ruI)j2oVZX@T>VNC!$<+*Zn#=}Y^!Z$V%{Zm< z;t5^Zm&dx?V+0M3+qjKbF*@hKM?#+o3d2ttAqAn`NR zE*(h}1(w<3E5k}p!^116OW&G+ys^1;#M5~0we%I+VSzdFFG#Vp z*N>w7)YQ*AC<4{xnVFbO%#(5bJ04^{CaW8S^h{8p`U+3*>fUnXcO-Hv+Gx`#65q^I zh^*IclT4XgTU&QIpl33r2E5Vstdp+Z|<-Bl^>J<0#x zi>wm(zcSk?+`}#zLbn89UJd#yh#2ice?#UlTRaW^3Lf*%BCvRdCVQd@qrNUY`j>f- zb(A$$aL3;QZHtQEzFq56JByh(V)T(07e+SSzSxD`Uf{XXx2gy|)ZJR!HS?p2#2ZB7 za$EoO=}7_%E*QITVK;!2hlhM9C=M+JlYzzyEP3KOw6b-{08%*1FOwXWEt~UP?{RZi zWXy4l?c&T7#lv)F`9A4kNPl*Ie!Y9GRq9G}_r=sp@SlbA&~;cMyIHqYi}!X;#+#*F zoPeG%pRkmvVk)i31NMb82?^qh9#E_v%29v*)#E6$l-J3RnHY~e`>5vAeh+GzZ_Qd5 zO6JJAIb$Ul$T~YayL#m#5W}RSmV=kc0z*FE4Y5!pmV)VX^v+nWs$87qptzgfmI)!c@t~W@_mKq9s@q*P0?mxTn0EjS{^{0ch z64N~N<{*>R;AA&V$nPm!mwH^RJJEu~Q?`z;-e$X@2!eUe82d9lWB%Bzfwc1Y|CqGV z1qAd!Hd9L@E~hs*Qj2`KS=@l?Pjrx8?R@rAVqqBiKu{vs9~HlBJ>SOT!uRq#c6ax~ z!ItbO2{@h%ys)}h!sVUK?Tpx#o`M;RA*}#utkU?TYV6B=y?ALdCwDJhHtia1oNk@{ z4cG7{eliV1oF1?ULIF1BBWQ+rKCIL1ao>mn$CAMb2*99kR$5r!%_*u$qHav1u9dQR0L<^7au72M@$!tfZFePp&lzoKZC|NugAlBfqVjAwo1pH z)m%&AxO~EZ_HtFcEeQM(dA9gQfE2h&xx1oUNR3S`$*)xsKD(ED(?!{SkKiKvC3)m= zETTfmsO5N_FGC66Ew}S#fS5W9K;emp=*a9xb4?@|`1v9>myLc;T;;Un`0&9ZbTD4e zVl*idNsk1?)s6O@4sE5&l`wqb7uYq6g5!UYT9a_WpR_=b`z@5^ zXs&Zm!F!91-II=p+Qxr<+r@~DtNEMIT4QotG6KWi%xNv-+Lb9sxyeLvb&7S z@s(%5-Zewb)U;OGmy77umWEatV9K z>*ZJoDv3-`>t+h_Co%zB?=ZA}wOGse^=VT=px>Cc8SC`n5UVTxPZ~(e|LTM)9bI)WAf_$xaI36kHa9rMiB|uEcnWu4CPOv|A(`Q% zoKL5UHKcZ9dvX@@|BbN0)`>x3pc~%n*m%@kGFeN1w5`6$#hj)eFPs!~E3Eki9-Hu? z!haJEleh=|SV0HKrnj!5G6glR25;N!tkp~PMGfBT_1gyncf}v{#6^A8n=RY`bRLVS zebBf@jSqnBSqvUt^|ak;E@=qyzI~2_wkdsP5up)JvsowEe0iX?1bKM+00NC@DO?WU zYpnnNS{eFdb9E&09bimUJj_Oa?ApQu;{&lYU1Kq(7pWaw_QYWH6>eLbuQq5@mKn68 zG8R+!C-Pu{7kHw#P;qq^*W6#OeQ%rehZlGwh{(wB@GQV1yZ|eaSZ_GdSnGZiyp4^K zTEE`=XQ4ta7BM()p+VGOb~eZtbIShUpk&??NK{}qfDdBHeY+wfMRh31$Uo(_UKCZm zld7At-rXbRcryCnQ#SwFAPV3gx)`yrrh?}{kCm4%Peu9a3hOt}mM6Uj3|MWZO1^@4 z!;FU;aa)FK4mscFXRmrACXeNZ6ia*Q0gAe`*DbQA z%=G~e+m4g z@A=;Q{eI8=<9_cw=l8w$ub*@LX zS#u9Cl@~SU>Qx^l-Q20V!$L1H2q*yofjdB>N{@BRd&Fk5EF@GHIL);+^i=ZVrM}DK zJLi1YHXI>*qio9<%xT}D8{Tg_QWOoe2#tD!ya&M*USO076@h}7*iS1>( z=wo#%?P~uJGccPTG)xv}WJ04=gH|mbYJ$*XGmt-)Zl1s-W@`s?C!8v7{0D6KFg!wH2-?p8D+3eRlx1 z{4!n`rc!%#Q(`z+^SN!EI&s#I#s;?73II*D4P3!<1wdXldOn(61AOEjh}3Dz zRy#>AA&1wuTRTX^+*@DMi>%5s z)u|mxlF_`RVsJx{sl*%1y@f_$V$l>4YkVR;K)cag|L1dHYG6J;@IGy2a6MzF(it1L zdwxUh5K#}fq48lt_Eo3oA*5rzepe=^W+`=cMVA|+B>RGp&x57Q6DH8qFW8=N=ur(} zaU~w*yVvM4_%9zQK+V*hJo{D|NUFE>cBe=8yNy){Si+-2utW_IKNBuURuS>Y?pc+` z@iHFbiw3Q7DnuQ3tk5(qr)(u3%A z^}y$TTI%BKeC(Rgk=!j)33c|sD35{Bhx!RBNqC8eL7IEw?QGLBPs@I@Tmgx@#%c~o z0sYx^5)#(`A>TjI)u|)2oaPRPA8gW#ZtBt6elouhvLBW^`~P`k2x*i3f2GW zkJk@@O5zZ(N*C>@c}zk~^IVGP-XiF4N?k*-z52CArM6RP@BS~h1h{brcZihi?siMv& zI6R#2^*C@Y;7vZ|M?@~6c>`nfH*%fFwJ16zz%fj^Nu$wpzeFF4U`aoujzl8q7H9W! zxS0Hf?(y-XO^h2z=a-Kf;7l&che0BxDY2^n%A>z%UjKwb6p5L5R)iWpdDC72lx|IFhL!6_}mBEX&!6EKM4L;am0J>Obxj7%9Ma zroh?0&2{FnRjf1=vYfz~0EJI@<9?Q+C38#T{n#st*-;*sHW$)A-Foy0ax_of2TYHJ z-#~U5(?wMU7@t|4rWa@Je>vawLvcWXFHm)@4A?FPtKCpSUlWKAq3?)&9{ZRr zH;tiA<$k81q;vo)Z5e9s&S{rs4zD8OwDsqwpxmv>xu@m%Fq80V>g%9bp(fot`;-{h z#gq66^lGnwe@P5RZ9ekBWtuIEj1@J%WAL}0Z8sLp2GF0 zA%~@2N~4PH<*RcoE!pY;PO{X}@tSbD6JADLx~v+1Qz^h7xy}pXXKWk;dLIg)8sOG} zp{tfGEWlwO3$5-C;auk+zxLZ6RFuzOQ@ z1&KQ~2%Znm+sWmee(rY`n@@oBZ6#wWZv-Hc3dfkd(+(A9B#M0HRqv2AMmihg6ITFH z27HvjUJyC_V#HP=69~a@G73P@X;W%j*mC@Oh4E)&)PgBvBT|R1CIv$D9I)M+=44nY zZC!1Jfo9UZTDNq_1Pzy6fGDk3+NqPjfnWnZ*>D|vJ@r66!oGU?-S?C)!*3$%-+WVh z!jT7o^|q=|uL&D(D0|6b9Dpd{-$aCVjOMPa^t({$XWE1A0oFVe|SRf zVk^ACd#WF%aMGM0D8Rqi#DIPNJ)v9x78GwwpJw$hw*AJ0gW4KUtnnK1E=7c-{Latd zgVC6ew6K1b6`ELxf@vTQGFxeh?|ph!za0&}oS8J*WRtn^c27vr;!0C#Z9L>sKwtPr~vN zW>w3-QeFX5C`sZ7$@xj?5*f{QS#vzwHQ3B;%+iU!)A(ANyF`$qV3}Gu7?@TL*eE`T z5-+`Rh6*Vg6~1z8uP9lOz|1kpHsJa8Q-wPdDB7&LZYMH}$?iv0XKIPPY)gCXb=1zz zF2`owwdT$O7l`z5tC&hSL9;%IYWS7V)0 z*5Vn)2Fn>2Yzfx-2fI!#GoDM~;3Us*GLwM-zzVJpWf)J9EMT1?%tD~q6{zc;aZz2s ztQ+Er0$2CRa%pCi{`un%tgF13`|-Ay5T)VnEA0#?Y@I|deFmO7maT$99r&~56H8-t zv7j_AKGXgVdJR(Ukd=;47mF}ThtyM#F_~QPHIRgL#=%^BC1DO!W-c9_wd!9MeYT?*>mh>!9ojb?>1x>H zkpFVcnMsog;MBPdMp1Q1i8!T&k3@OOWj2Q z)U9ex7z>LLn8m5_3kXcPS*E(u30=>{m0IEt?es(w^IJi+g{h+i@w?BW^yetRem@;Q z+SfH5t!Pn~f8cw?NB6qXLJ*G0K@kj$i(A#kZm9EsUDS(TqkT$R5^=xcF-9!2Cvp)? zNs9UIopCkec5FceV~NC%dY#*CEwQjGYd*m9>Iiy)$@V^HeQdWixzY}#i63zk;wDdI z$lS~(m|nzzR@R@-`}+sb!w5OYxUmcVOK9w^xL-qqqamgDJXu~&HSMYLy)Di}DQo`m zloKUO$@ReY7R9G#On#~=ixt4UBc8)S-{o#)x04&`v+aw@FT?!YF4E~dz~obLqe*Dc z6Z3#Y=R234b)`lhob8wz$vyvGg*ru`BluuI#M}FVCccz&h}-7!NLY)9^#x^yV8KpR zy}BBuyc?jC3iMg&VW;;#CHC@tySF;4vR=G+rP<`~{ZIb|jCm%TadJ-`G3yIgL348e z0+FZPaCU|R?XbY458wh?W@ayVMf!*9mXEz2t3MxkH2t5LGw?S}bd!IA4m&bE3d+eD z3Zx$Z%NMX4M4gI#f^R1`?JG%FJY>1rZf(me8XO@l^$+B``lY30WT?WMN%zq1F3&g+ zO`FwUv;~J-hHo*|sXM!@<#1=R6l}JTPZk4Fsq%>CA8zWVL@oSwb7j&`@M50zx%U*+-+6JV7UkdZ;+MyNzsB03-k4hn2$Nd|SrSJ;U8zVj1We@tT~7p_ ztGI5365f%{%=rcJjomp0?8-O)$PH}isH{8f6g?gQsB3esM?Sl4$WAyt_6IMH^lJT` z7l*#~jHHfE$n}HH0BDQ%yW3aa5BNqU>biJwf8t<2&VXP6&^sBbp0LSKCPYjzRsel zc{yizJh`m}1Oi~6{{9yZiqo$_Dtw#oJ3@~oIb6Pcdqy!Akx!QgFoE;!$&>j=MqaphiZZ7xvUqOunQ2r9Mk$WQ}r0K5e|&Y~96?qnwa&Sga{Xg+5CF4y_*C5VN=Jh|SwXWNT| zLc$RqQ2&=)l9uqDM<5=gr_M<45tjWfmt?DkX*OazV4p zO8CjaTW`r^k%4H_ER`b>obS08<~Y~`42lQb8!I!aqwB9f!1(o4`Atg?XX-SZrg}*v zNAEq`k;ftu8@)v zPmo4nM52_`ZNteNrx(>XEa+iIh_#X4p`lB@EfPc>0SpLde*50fNF6=5{&h!0HvjIg z7n}z5>V$#PxM)mDZ{Spsb)`irEGv!?)yD`z#@3Ig^Oix1>)Jrm%)1ZmP0|oGXTm~O zr0mKWr8U6%#`Jz%PvyCrLqiu@7g&Xb4G-+jmYN{I)rcF|D$Dxm<1P%@r?fMpa#Q3U zJNt7DMx40P0ahsie*Tk>kEs+Lv&AQ51ch31-!WrgrwlbT6ogC&zhYp*9Te6+$+(|zB9F3gS*r{72;Tuq-p?HieM6RZl%`Gaxm5TR)} zEk@35X!Kjym!wd(%gyWlZ)$dLs3(tb7^;YIQh;L^`px6l4e)pU~O0|*2<3b?z`NHLW@J_`vI7_n3{kEr-J1*l>5B0 z`b(7ukk3nMkCPOaB_!8-9&B$%qjlcsCMU7EcYc&B#-8zs@%TYY+PROsZ@yOTA{heV zMJ1bmePp$-pPW@&x4P5RYS=ki?PEH6<5KaOU{!1K7=_`!IUKVmU-&Qj{kCr}ReI%< Svmkj7_%S$dqEo7UIrwjN8u(5C literal 0 HcmV?d00001 From 4fef1b4f9d68f479b9258c08390d1053fa14907e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Mar 2026 15:50:33 -0400 Subject: [PATCH 4611/4744] increase analytics 404 --- src/meta/errors.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/meta/errors.js b/src/meta/errors.js index bae26c2acc..7b623fe5f0 100644 --- a/src/meta/errors.js +++ b/src/meta/errors.js @@ -74,8 +74,10 @@ Errors.log404 = function (route) { if (!route) { return; } - route = route.slice(0, 512).replace(/\/$/, ''); // remove trailing slashes + analytics.increment('errors:404'); + + route = route.slice(0, 512).replace(/\/$/, ''); // remove trailing slashes const segments = route.split('/'); const containsUUID = segments.some((segment) => { const cleanSegment = segment.split('.')[0]; @@ -84,7 +86,7 @@ Errors.log404 = function (route) { if (containsUUID) { return; } - analytics.increment('errors:404'); + counters[route] = counters[route] || 0; counters[route] += 1; }; From 6147a4d0821f601d600b2d59b1653c6433866113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Mar 2026 22:02:03 -0400 Subject: [PATCH 4612/4744] fix: merged chat notifications if all the messages are from the same user usernames.length ends up being 1 so need to use default translation string and not -dual/-triple/-multiple variants --- src/notifications.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index d06108e8ca..e38ccd215f 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -519,7 +519,9 @@ Notifications.merge = async function (notifications) { differentiators.forEach((differentiator) => { function typeFromLength(items) { - if (items.length === 2) { + if (items.length <= 1) { + return ''; + } else if (items.length === 2) { return 'dual'; } else if (items.length === 3) { return 'triple'; @@ -553,7 +555,7 @@ Notifications.merge = async function (notifications) { const type = typeFromLength(usernames); const isMultiple = type === 'multiple'; const txArgs = [ - `${mergeId}-${type}`, + `${mergeId}${type ? '-type' : ''}`, ...usernames.slice(0, usernames.length <= 3 ? 3 : 2), ...(isMultiple ? [usernames.length - 2] : []), notifObj.roomIcon, @@ -574,7 +576,7 @@ Notifications.merge = async function (notifications) { const type = typeFromLength(usernames); const isMultiple = type === 'multiple'; const txArgs = [ - `${mergeId}-${type}`, + `${mergeId}${type ? '-type' : ''}`, ...usernames.slice(0, usernames.length <= 3 ? 3 : 2), ...(isMultiple ? [usernames.length - 2] : []), utils.decodeHTMLEntities(notifObj.topicTitle || ''), From 40fecd01f35d3f972ec36f09470522026ca6d0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Mar 2026 22:04:48 -0400 Subject: [PATCH 4613/4744] fix: type --- src/notifications.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index e38ccd215f..0a108ff03b 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -555,7 +555,7 @@ Notifications.merge = async function (notifications) { const type = typeFromLength(usernames); const isMultiple = type === 'multiple'; const txArgs = [ - `${mergeId}${type ? '-type' : ''}`, + `${mergeId}${type ? `-${type}` : ''}`, ...usernames.slice(0, usernames.length <= 3 ? 3 : 2), ...(isMultiple ? [usernames.length - 2] : []), notifObj.roomIcon, @@ -576,7 +576,7 @@ Notifications.merge = async function (notifications) { const type = typeFromLength(usernames); const isMultiple = type === 'multiple'; const txArgs = [ - `${mergeId}${type ? '-type' : ''}`, + `${mergeId}${type ? `-${type}` : ''}`, ...usernames.slice(0, usernames.length <= 3 ? 3 : 2), ...(isMultiple ? [usernames.length - 2] : []), utils.decodeHTMLEntities(notifObj.topicTitle || ''), From f2bca332621541b22562a7a3c1681a53a770a4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Mar 2026 22:13:38 -0400 Subject: [PATCH 4614/4744] perf: move out nconf.get and isClientScript regex --- src/controllers/404.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/404.js b/src/controllers/404.js index 412676c2e7..1688b80a7a 100644 --- a/src/controllers/404.js +++ b/src/controllers/404.js @@ -10,6 +10,9 @@ const activitypub = require('../activitypub'); const middleware = require('../middleware'); const helpers = require('../middleware/helpers'); +const relativePath = nconf.get('relative_path'); +const isClientScript = new RegExp(`^${relativePath}\\/assets\\/src\\/.+\\.js(\\?v=\\w+)?$`); + const error404Icons = [ 'fa-hippo', 'fa-cat', 'fa-otter', 'fa-dog', 'fa-cow', 'fa-fish', @@ -17,9 +20,6 @@ const error404Icons = [ ]; exports.handle404 = helpers.try(async (req, res) => { - const relativePath = nconf.get('relative_path'); - const isClientScript = new RegExp(`^${relativePath}\\/assets\\/src\\/.+\\.js(\\?v=\\w+)?$`); - if (plugins.hooks.hasListeners('action:meta.override404')) { return plugins.hooks.fire('action:meta.override404', { req: req, From 59dd22ca2e8bb00dd8ec57ab4d96af5347cb4f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Mar 2026 22:04:48 -0400 Subject: [PATCH 4615/4744] fix: type --- src/notifications.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index fd0d01ce55..ef7e813a95 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -549,7 +549,7 @@ Notifications.merge = async function (notifications) { const type = typeFromLength(usernames); const isMultiple = type === 'multiple'; const txArgs = [ - `${mergeId}-${type}`, + `${mergeId}${type ? `-${type}` : ''}`, ...usernames.slice(0, usernames.length <= 3 ? 3 : 2), ...(isMultiple ? [usernames.length - 2] : []), notifObj.roomIcon, @@ -570,7 +570,7 @@ Notifications.merge = async function (notifications) { const type = typeFromLength(usernames); const isMultiple = type === 'multiple'; const txArgs = [ - `${mergeId}-${type}`, + `${mergeId}${type ? `-${type}` : ''}`, ...usernames.slice(0, usernames.length <= 3 ? 3 : 2), ...(isMultiple ? [usernames.length - 2] : []), utils.decodeHTMLEntities(notifObj.topicTitle || ''), From 4d55ee0a55d95ecc5e41339d4f5c64550c08e4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Mar 2026 22:13:38 -0400 Subject: [PATCH 4616/4744] perf: move out nconf.get and isClientScript regex --- src/controllers/404.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/controllers/404.js b/src/controllers/404.js index bed1a085e3..03407e084c 100644 --- a/src/controllers/404.js +++ b/src/controllers/404.js @@ -9,12 +9,17 @@ const plugins = require('../plugins'); const activitypub = require('../activitypub'); const middleware = require('../middleware'); const helpers = require('../middleware/helpers'); -const { secureRandom } = require('../utils'); + +const relativePath = nconf.get('relative_path'); +const isClientScript = new RegExp(`^${relativePath}\\/assets\\/src\\/.+\\.js(\\?v=\\w+)?$`); + +const error404Icons = [ + 'fa-hippo', 'fa-cat', 'fa-otter', + 'fa-dog', 'fa-cow', 'fa-fish', + 'fa-dragon', 'fa-horse', 'fa-dove', +]; exports.handle404 = helpers.try(async (req, res) => { - const relativePath = nconf.get('relative_path'); - const isClientScript = new RegExp(`^${relativePath}\\/assets\\/src\\/.+\\.js(\\?v=\\w+)?$`); - if (plugins.hooks.hasListeners('action:meta.override404')) { return plugins.hooks.fire('action:meta.override404', { req: req, @@ -62,16 +67,12 @@ exports.send404 = helpers.try(async (req, res) => { bodyClass: helpers.buildBodyClass(req, res), }); } - const icons = [ - 'fa-hippo', 'fa-cat', 'fa-otter', - 'fa-dog', 'fa-cow', 'fa-fish', - 'fa-dragon', 'fa-horse', 'fa-dove', - ]; + await middleware.buildHeaderAsync(req, res); res.render('404', { path: validator.escape(path), title: '[[global:404.title]]', bodyClass: helpers.buildBodyClass(req, res), - icon: icons[secureRandom(0, icons.length - 1)], + icon: error404Icons[Math.floor(Math.random() * error404Icons.length)], }); }); From 26bb60effcc485cd72eaa55088486ff9b280d11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 12 Mar 2026 22:02:03 -0400 Subject: [PATCH 4617/4744] fix: merged chat notifications if all the messages are from the same user usernames.length ends up being 1 so need to use default translation string and not -dual/-triple/-multiple variants --- src/notifications.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/notifications.js b/src/notifications.js index ef7e813a95..197ea656cc 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -515,7 +515,9 @@ Notifications.merge = async function (notifications) { differentiators.forEach((differentiator) => { function typeFromLength(items) { - if (items.length === 2) { + if (items.length <= 1) { + return ''; + } else if (items.length === 2) { return 'dual'; } else if (items.length === 3) { return 'triple'; From f1d311454177b357d8b78c010ca34f66fbb9515b Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 13 Mar 2026 09:07:41 +0000 Subject: [PATCH 4618/4744] Latest translations and fallbacks --- public/language/bg/admin/manage/users.json | 2 +- public/language/bg/admin/settings/general.json | 6 +++--- public/language/de/admin/manage/users.json | 2 +- public/language/it/admin/advanced/jobs.json | 2 +- public/language/it/admin/manage/users.json | 2 +- public/language/vi/admin/advanced/jobs.json | 6 +++--- public/language/vi/admin/manage/privileges.json | 2 +- public/language/vi/admin/manage/users.json | 2 +- public/language/vi/admin/menu.json | 2 +- public/language/vi/admin/settings/activitypub.json | 4 ++-- public/language/vi/aria.json | 2 +- public/language/vi/error.json | 2 +- public/language/vi/world.json | 4 ++-- public/language/zh-CN/admin/advanced/jobs.json | 2 +- public/language/zh-CN/admin/manage/users.json | 2 +- 15 files changed, 21 insertions(+), 21 deletions(-) diff --git a/public/language/bg/admin/manage/users.json b/public/language/bg/admin/manage/users.json index 898aa08316..b68fa15b10 100644 --- a/public/language/bg/admin/manage/users.json +++ b/public/language/bg/admin/manage/users.json @@ -40,7 +40,7 @@ "250-per-page": "250 на страница", "500-per-page": "500 на страница", - "search.help": "Use "*" to make partial searches, for example "*query"", + "search.help": "Използвайте "*" за частични търсения. например: "*дума"", "search.uid": "По потребителски идентификатор", "search.uid-placeholder": "Въведете потребителски идентификатор, който да потърсите", "search.username": "По име на потребител", diff --git a/public/language/bg/admin/settings/general.json b/public/language/bg/admin/settings/general.json index 433f5d7ceb..74b12dd3e2 100644 --- a/public/language/bg/admin/settings/general.json +++ b/public/language/bg/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Описание на уеб сайта", "keywords": "Ключови думи на уеб сайта", "keywords-placeholder": "Ключови думи, описващи общността Ви. Трябва да бъдат разделени със запетаи.", - "logo-and-icons": "Media & Branding", + "logo-and-icons": "Медийни материали", "logo.image": "Изображение", "logo.image-placeholder": "Път до логото, което да бъде показано в заглавната част на форума", "logo.upload": "Качване", @@ -35,8 +35,8 @@ "touch-icon.help": "Препоръчителен размер и формат: 512x512, само във формат „PNG“. Ако не е посочена иконка за сензорен екран, NodeBB ще използва иконката на уеб сайта.", "maskable-icon": "Маскируема иконка (за начален екран)", "maskable-icon.help": "Препоръчителен размер и формат: 512x512, само във формат „PNG“. Ако не е посочена маскируема иконка, NodeBB ще използва иконката за сензорен екран.", - "screenshot": "Screenshot", - "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", + "screenshot": "Екранна снимка", + "screenshot.help": "Препоръчителен размер и формат: между 320px и 3480px, само JPG и PNG. Ако няма зададена екранна снимка, NodeBB ще използва такава по подразбиране.", "outgoing-links": "Изходящи връзки", "outgoing-links.warning-page": "Показване на предупредителна страница при щракване върху външни връзки", "search": "Търсене", diff --git a/public/language/de/admin/manage/users.json b/public/language/de/admin/manage/users.json index acf5de647c..a3965798a8 100644 --- a/public/language/de/admin/manage/users.json +++ b/public/language/de/admin/manage/users.json @@ -40,7 +40,7 @@ "250-per-page": "250 pro Seite", "500-per-page": "500 pro Seite", - "search.help": "Use "*" to make partial searches, for example "*query"", + "search.help": "Benutz "*" um nach Teilen von Wörtern zu suchen, zum Beispiel "*anfrage"", "search.uid": "Nach Benutzer-ID", "search.uid-placeholder": "Gib eine Benutzer-ID ein um danach zu suchen", "search.username": "Nach Nutzernamen", diff --git a/public/language/it/admin/advanced/jobs.json b/public/language/it/admin/advanced/jobs.json index 6ff20a0785..110381511f 100644 --- a/public/language/it/admin/advanced/jobs.json +++ b/public/language/it/admin/advanced/jobs.json @@ -5,5 +5,5 @@ "next-run": "Prossima esecuzione", "last-duration": "Ultima durata", "running": "In esecuzione", - "active": "Active" + "active": "Attivo" } \ No newline at end of file diff --git a/public/language/it/admin/manage/users.json b/public/language/it/admin/manage/users.json index f657537395..3c00762d4f 100644 --- a/public/language/it/admin/manage/users.json +++ b/public/language/it/admin/manage/users.json @@ -40,7 +40,7 @@ "250-per-page": "250 per pagina", "500-per-page": "500 per pagina", - "search.help": "Use "*" to make partial searches, for example "*query"", + "search.help": "Usa "*" per effettuare ricerche parziali, ad esempio "*richiesta"", "search.uid": "Da ID Utente", "search.uid-placeholder": "Inserisci l'ID utente da cercare", "search.username": "Da Nome Utente", diff --git a/public/language/vi/admin/advanced/jobs.json b/public/language/vi/admin/advanced/jobs.json index 896b07930a..7cd625a139 100644 --- a/public/language/vi/admin/advanced/jobs.json +++ b/public/language/vi/admin/advanced/jobs.json @@ -1,7 +1,7 @@ { - "jobs": "Jobs", - "job-name": "Job Name", - "schedule": "Schedule", + "jobs": "Công việc", + "job-name": "Tên Công Việc", + "schedule": "Lên lịch", "next-run": "Next Run", "last-duration": "Last Duration", "running": "Running", diff --git a/public/language/vi/admin/manage/privileges.json b/public/language/vi/admin/manage/privileges.json index 6b56cdc2d5..7183f47ff4 100644 --- a/public/language/vi/admin/manage/privileges.json +++ b/public/language/vi/admin/manage/privileges.json @@ -29,7 +29,7 @@ "access-topics": "Truy Cập Chủ Đề", "create-topics": "Tạo Chủ Đề", "reply-to-topics": "Trả Lời Chủ Đề", - "crosspost-topics": "Cross-post Topics", + "crosspost-topics": "Chủ Đề Đăng Chéo", "schedule-topics": "Lên Lịch Chủ Đề", "tag-topics": "Gắn Thẻ Chủ Đề", "edit-posts": "Chỉnh Sửa Bài Đăng", diff --git a/public/language/vi/admin/manage/users.json b/public/language/vi/admin/manage/users.json index 17cd5611b2..349d33d288 100644 --- a/public/language/vi/admin/manage/users.json +++ b/public/language/vi/admin/manage/users.json @@ -40,7 +40,7 @@ "250-per-page": "250 mỗi trang", "500-per-page": "500 mỗi trang", - "search.help": "Use "*" to make partial searches, for example "*query"", + "search.help": "Dùng "*" thực hiện tìm kiếm một phần, ví dụ "*query"", "search.uid": "Bởi ID Người Dùng", "search.uid-placeholder": "Nhập ID người dùng để tìm", "search.username": "Theo Tên Người Dùng", diff --git a/public/language/vi/admin/menu.json b/public/language/vi/admin/menu.json index 0bf7c1c273..bf999805fb 100644 --- a/public/language/vi/admin/menu.json +++ b/public/language/vi/admin/menu.json @@ -78,7 +78,7 @@ "advanced/logs": "Nhật ký", "advanced/errors": "Lỗi", "advanced/cache": "Bộ nhớ đệm", - "advanced/jobs": "Jobs", + "advanced/jobs": "Công việc", "development/logger": "Ghi nhật ký", "development/info": "Thông tin", diff --git a/public/language/vi/admin/settings/activitypub.json b/public/language/vi/admin/settings/activitypub.json index 7dff454a67..ed7b3b8643 100644 --- a/public/language/vi/admin/settings/activitypub.json +++ b/public/language/vi/admin/settings/activitypub.json @@ -49,6 +49,6 @@ "content.outgoing": "Ra ngoài", "content.summary-limit": "Số lượng ký tự sau đó bản tóm tắt sẽ được tạo.", "content.summary-limit-help": "Khi nội dung được phân phối vượt quá số lượng ký tự này, tóm tắt được tạo ra, bao gồm tất cả các câu hoàn chỉnh trước giới hạn này. (Mặc định: 500)", - "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string": "Dấu phân cách Ghi chú/Bài đăng", + "content.break-string-help": "Dấu phân cách này có thể được người dùng thành thạo chèn thủ công khi soạn thảo chủ đề mới. Nó hướng dẫn NodeBB sử dụng nội dung cho đến thời điểm đó như một phần của tóm tắt. Nếu chuỗi này không được sử dụng, thì phương án dự phòng dựa trên số lượng ký tự sẽ được áp dụng. (Mặc định: [...])" } \ No newline at end of file diff --git a/public/language/vi/aria.json b/public/language/vi/aria.json index b82f396907..71fa3f06de 100644 --- a/public/language/vi/aria.json +++ b/public/language/vi/aria.json @@ -6,5 +6,5 @@ "user-watched-tags": "Thẻ người dùng đã xem", "delete-upload-button": "Xóa nút tải lên", "group-page-link-for": "Liên kết trang nhóm cho %1", - "show-crossposts": "Show Cross-posts" + "show-crossposts": "Hiển thị Bài Đăng Chéo" } \ No newline at end of file diff --git a/public/language/vi/error.json b/public/language/vi/error.json index bf230db126..1d4e90a668 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -1,6 +1,6 @@ { "invalid-data": "Dữ Liệu Không Hợp Lệ", - "invalid-config-field-value": "Invalid value for config field \"%1\": %2", + "invalid-config-field-value": "Giá trị không hợp lệ cho trường cấu hình \"%1\": %2", "invalid-json": "JSON không hợp lệ", "wrong-parameter-type": "Giá trị của loại %3 được mong đợi cho thuộc tính `%1`, nhưng thay vào đó, %2 đã được nhận", "required-parameters-missing": "Các thông số bắt buộc bị thiếu trong lệnh gọi API này: %1", diff --git a/public/language/vi/world.json b/public/language/vi/world.json index 337b188306..77e8e377d4 100644 --- a/public/language/vi/world.json +++ b/public/language/vi/world.json @@ -22,6 +22,6 @@ "onboard.how": "Trong thời gian chờ đợi, bạn có thể nhấp vào các nút tắt ở trên cùng để xem diễn đàn này biết thêm những gì và bắt đầu khám phá một số nội dung mới!", "category-search": "Tìm danh mục...", - "see-more": "See more", - "see-less": "See less" + "see-more": "Xem nhiều hơn", + "see-less": "Xem ít hơn" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/advanced/jobs.json b/public/language/zh-CN/admin/advanced/jobs.json index 9a145abf51..7159567c23 100644 --- a/public/language/zh-CN/admin/advanced/jobs.json +++ b/public/language/zh-CN/admin/advanced/jobs.json @@ -5,5 +5,5 @@ "next-run": "下次执行", "last-duration": "上次持续时间", "running": "执行中", - "active": "Active" + "active": "启用" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/users.json b/public/language/zh-CN/admin/manage/users.json index 5acff6becc..d62f412fc7 100644 --- a/public/language/zh-CN/admin/manage/users.json +++ b/public/language/zh-CN/admin/manage/users.json @@ -40,7 +40,7 @@ "250-per-page": "每页250", "500-per-page": "每页500", - "search.help": "Use "*" to make partial searches, for example "*query"", + "search.help": "使用 "*" 进行部分搜索,例如 "*query"", "search.uid": "通过用户 ID", "search.uid-placeholder": "输入用户 ID 以搜索", "search.username": "通过用户名", From 38a1da4609c0ba2409efd2fc89e577953cd16b3a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 12:09:25 -0400 Subject: [PATCH 4619/4744] fix: imagesLoaded integration for handleBack in world.js --- public/src/client/world.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/public/src/client/world.js b/public/src/client/world.js index ddecba0ead..ca9fda9187 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -3,10 +3,10 @@ define('forum/world', [ 'forum/infinitescroll', 'search', 'sort', 'hooks', 'alerts', 'api', 'bootbox', 'helpers', 'forum/category/tools', - 'translator', 'quickreply', 'handleBack', + 'translator', 'quickreply', 'handleBack', 'imagesloaded', ], function (infinitescroll, search, sort, hooks, alerts, api, bootbox, helpers, categoryTools, - translator, quickreply, handleBack) { + translator, quickreply, handleBack, imagesLoaded) { const World = {}; World.init = function () { @@ -52,11 +52,13 @@ define('forum/world', [ app.parseAndTranslate(ajaxify.data.template.name, 'posts', payload, function (html) { const listEl = document.getElementById('world-feed'); $(listEl).append(html); - html.find('.timeago').timeago(); - handleImages(); - handleShowMoreButtons(); - callback(); - handleBackCb(); + imagesLoaded(listEl, () => { + html.find('.timeago').timeago(); + handleImages(); + handleShowMoreButtons(); + callback(); + handleBackCb(); + }); }); }); }, { container: '#world-feed' }); From e2131d1d2e1c6f14cb8867ac7e22840da3f4c63f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 12:41:55 -0400 Subject: [PATCH 4620/4744] refactor: /world sorting logic to always use topics/sorted logic - New params for getSortedTopics (includeRemote, followingOnly) - Ability to show latest (followers only) or latest (all), ?all query param to discriminate - World now always shows posts from the local forum - Popular sort will be followers-only + local --- public/language/en-GB/world.json | 3 +- src/controllers/activitypub/topics.js | 30 +++++++++----------- src/topics/sorted.js | 40 +++++++++++++++++---------- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/public/language/en-GB/world.json b/public/language/en-GB/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/en-GB/world.json +++ b/public/language/en-GB/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 6aaae596af..702893cac8 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -2,7 +2,6 @@ const _ = require('lodash'); -const db = require('../../database'); const meta = require('../../meta'); const user = require('../../user'); const topics = require('../../topics'); @@ -20,8 +19,8 @@ controller.list = async function (req, res) { const { topicsPerPage } = await user.getSettings(req.uid); let { page, after } = req.query; page = parseInt(page, 10) || 1; - let start = Math.max(0, (page - 1) * topicsPerPage); - let stop = start + topicsPerPage - 1; + const start = Math.max(0, (page - 1) * topicsPerPage); + const stop = start + topicsPerPage - 1; const [userSettings, userPrivileges] = await Promise.all([ user.getSettings(req.uid), @@ -50,27 +49,24 @@ controller.list = async function (req, res) { if (req.query.sort === 'popular') { cidQuery = { ...cidQuery, - cids: ['-1'], sort: 'posts', term: req.query.term || 'day', + includeRemote: true, + followingOnly: !req.query.all || !parseInt(req.query.all, 10), }; delete cidQuery.cid; ({ tids, topicCount } = await topics.getSortedTopics(cidQuery)); tids = tids.slice(start, stop !== -1 ? stop + 1 : undefined); } else { - if (after) { - // Update start/stop with values inferred from `after` - const set = await categories.buildTopicsSortedSet(cidQuery); - const index = await db.sortedSetRevRank(set, decodeURIComponent(after)); - if (index && start - index < 1) { - const count = stop - start; - start = index + 1; - stop = start + count; - } - } - - tids = await categories.getTopicIds({ ...cidQuery, start, stop }); - topicCount = await categories.getTopicCount(cidQuery); + cidQuery = { + ...cidQuery, + term: req.query.term, + includeRemote: true, + followingOnly: !req.query.all || !parseInt(req.query.all, 10), + }; + delete cidQuery.cid; + ({ tids, topicCount } = await topics.getSortedTopics(cidQuery)); + tids = tids.slice(start, stop !== -1 ? stop + 1 : undefined); } data.topicCount = topicCount; diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 1d4da1f772..6756fbfe38 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -46,7 +46,8 @@ module.exports = function (Topics) { let tids; if (params.term !== 'alltime') { if (params.sort === 'posts') { - tids = await getTidsWithMostPostsInTerm(params.cids, params.uid, params.term); + const { cids, uid, term, includeRemote, followingOnly } = params; + tids = await getTidsWithMostPostsInTerm({ cids, uid, term, includeRemote, followingOnly }); } else { const cids = await getCids(params.cids, params.uid); tids = await Topics.getLatestTidsFromSet( @@ -74,17 +75,18 @@ module.exports = function (Topics) { } async function getInbox(tids, params) { - if (!Array.isArray(params.cids) || !params.cids.includes('-1')) { + if (!params.includeRemote) { return tids; } let inbox; + const set = params.followingOnly ? `uid:${params.uid}:inbox` : 'cid:-1:tids'; if (params.term !== 'alltime') { const method = params.sort === 'old' ? 'getSortedSetRangeByScore' : 'getSortedSetRevRangeByScore'; inbox = await db[method]( - `uid:${params.uid}:inbox`, + set, 0, 1000, '+inf', @@ -94,7 +96,7 @@ module.exports = function (Topics) { const method = params.sort === 'old' ? 'getSortedSetRange' : 'getSortedSetRevRange'; - inbox = await db[method](`uid:${params.uid}:inbox`, 0, meta.config.recentMaxTopics - 1); + inbox = await db[method](set, 0, meta.config.recentMaxTopics - 1); } return _.uniq(tids.concat(inbox)); @@ -115,33 +117,41 @@ module.exports = function (Topics) { return 'topics:recent'; } - async function getCids(cids, uid) { + async function getCids(cids, uid, includeRemote) { if (Array.isArray(cids)) { cids = await privileges.categories.filterCids('topics:read', cids, uid); } else { cids = await categories.getCidsByPrivilege('categories:cid', uid, 'topics:read'); cids = cids.filter(cid => cid !== -1); } + + if (includeRemote) { + const remoteCids = await db.getObjectValues('handle:cid'); + cids = [-1, ...cids, ...remoteCids]; + } return cids; } - async function getTidsWithMostPostsInTerm(cids, uid, term) { - cids = await getCids(cids, uid); - const pids = await db.getSortedSetRevRangeByScore( - cids.map(cid => `cid:${cid}:pids`), + async function getTidsWithMostPostsInTerm({ cids, uid, term, includeRemote, followingOnly }) { + cids = await getCids(cids, uid, includeRemote); + const sets = cids.map(cid => `cid:${cid}:tids`); + if (followingOnly && sets.includes('cid:-1:tids')) { + sets.splice(sets.indexOf('cid:-1:tids'), 1, `uid:${uid}:inbox`); + } + const tids = await db.getSortedSetRevRangeByScore( + sets, 0, 1000, '+inf', Date.now() - Topics.getSinceFromTerm(term) ); - const postObjs = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['tid']); const tidToCount = {}; - postObjs.forEach((post) => { - tidToCount[post.tid] = tidToCount[post.tid] || 0; - tidToCount[post.tid] += 1; + tids.forEach((tid) => { + tidToCount[tid] = tidToCount[tid] || 0; + tidToCount[tid] += 1; }); - return _.uniq(postObjs.map(post => String(post.tid))) + return _.uniq(tids) .sort((t1, t2) => tidToCount[t2] - tidToCount[t1]); } @@ -200,7 +210,7 @@ module.exports = function (Topics) { } async function sortTids(tids, params) { - if (params.term === 'alltime' && !params.cids && !params.tags.length && params.filter !== 'watched' && !params.floatPinned) { + if (params.term === 'alltime' && !params.cids && !params.tags.length && params.filter !== 'watched' && !params.floatPinned && !params.includeRemote) { return tids; } From 702f7c6251323fbf5525e6350c2610c4ba972115 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 13 Mar 2026 16:48:21 +0000 Subject: [PATCH 4621/4744] chore(i18n): fallback strings for new resources: nodebb.world --- public/language/ar/world.json | 3 ++- public/language/az/world.json | 3 ++- public/language/bg/world.json | 3 ++- public/language/bn/world.json | 3 ++- public/language/cs/world.json | 3 ++- public/language/da/world.json | 3 ++- public/language/de/world.json | 3 ++- public/language/el/world.json | 3 ++- public/language/en-US/world.json | 3 ++- public/language/en-x-pirate/world.json | 3 ++- public/language/es/world.json | 3 ++- public/language/et/world.json | 3 ++- public/language/fa-IR/world.json | 3 ++- public/language/fi/world.json | 3 ++- public/language/fr/world.json | 3 ++- public/language/gl/world.json | 3 ++- public/language/he/world.json | 3 ++- public/language/hr/world.json | 3 ++- public/language/hu/world.json | 3 ++- public/language/hy/world.json | 3 ++- public/language/id/world.json | 3 ++- public/language/it/world.json | 3 ++- public/language/ja/world.json | 3 ++- public/language/ko/world.json | 3 ++- public/language/lt/world.json | 3 ++- public/language/lv/world.json | 3 ++- public/language/ms/world.json | 3 ++- public/language/nb/world.json | 3 ++- public/language/nl/world.json | 3 ++- public/language/nn-NO/world.json | 3 ++- public/language/pl/world.json | 3 ++- public/language/pt-BR/world.json | 3 ++- public/language/pt-PT/world.json | 3 ++- public/language/ro/world.json | 3 ++- public/language/ru/world.json | 3 ++- public/language/rw/world.json | 3 ++- public/language/sc/world.json | 3 ++- public/language/sk/world.json | 3 ++- public/language/sl/world.json | 3 ++- public/language/sq-AL/world.json | 3 ++- public/language/sr/world.json | 3 ++- public/language/sv/world.json | 3 ++- public/language/th/world.json | 3 ++- public/language/tr/world.json | 3 ++- public/language/uk/world.json | 3 ++- public/language/ur/world.json | 3 ++- public/language/vi/world.json | 3 ++- public/language/zh-CN/world.json | 3 ++- public/language/zh-TW/world.json | 3 ++- 49 files changed, 98 insertions(+), 49 deletions(-) diff --git a/public/language/ar/world.json b/public/language/ar/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/ar/world.json +++ b/public/language/ar/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/az/world.json b/public/language/az/world.json index bf503c1e9e..f4101247d6 100644 --- a/public/language/az/world.json +++ b/public/language/az/world.json @@ -1,6 +1,7 @@ { "name": "Dünya", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/bg/world.json b/public/language/bg/world.json index cec323447a..0f78ca89f7 100644 --- a/public/language/bg/world.json +++ b/public/language/bg/world.json @@ -1,6 +1,7 @@ { "name": "Свят", - "latest": "Най-нови", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Популярни (за деня)", "popular-week": "Популярни (за седмицата)", "popular-month": "Популярни (за месеца)", diff --git a/public/language/bn/world.json b/public/language/bn/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/bn/world.json +++ b/public/language/bn/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/cs/world.json b/public/language/cs/world.json index 03b928bc9e..5d64fc0acd 100644 --- a/public/language/cs/world.json +++ b/public/language/cs/world.json @@ -1,6 +1,7 @@ { "name": "Svět", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/da/world.json b/public/language/da/world.json index 854bd7f7fa..ed22d19f06 100644 --- a/public/language/da/world.json +++ b/public/language/da/world.json @@ -1,6 +1,7 @@ { "name": "Verden", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/de/world.json b/public/language/de/world.json index aae1add79b..95e16612b9 100644 --- a/public/language/de/world.json +++ b/public/language/de/world.json @@ -1,6 +1,7 @@ { "name": "Welt", - "latest": "Letzte", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Beliebt (Tag)", "popular-week": "Beliebt (Woche)", "popular-month": "Beliebt (Monat)", diff --git a/public/language/el/world.json b/public/language/el/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/el/world.json +++ b/public/language/el/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/en-US/world.json b/public/language/en-US/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/en-US/world.json +++ b/public/language/en-US/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/en-x-pirate/world.json b/public/language/en-x-pirate/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/en-x-pirate/world.json +++ b/public/language/en-x-pirate/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/es/world.json b/public/language/es/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/es/world.json +++ b/public/language/es/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/et/world.json b/public/language/et/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/et/world.json +++ b/public/language/et/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/fa-IR/world.json b/public/language/fa-IR/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/fa-IR/world.json +++ b/public/language/fa-IR/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/fi/world.json b/public/language/fi/world.json index 013c363443..87d193efa9 100644 --- a/public/language/fi/world.json +++ b/public/language/fi/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/fr/world.json b/public/language/fr/world.json index b419024fc9..fb839a8ce8 100644 --- a/public/language/fr/world.json +++ b/public/language/fr/world.json @@ -1,6 +1,7 @@ { "name": "Monde", - "latest": "Dernier", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Populaires (Jour)", "popular-week": "Populaires (Semaine)", "popular-month": "Populaires (Mois)", diff --git a/public/language/gl/world.json b/public/language/gl/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/gl/world.json +++ b/public/language/gl/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/he/world.json b/public/language/he/world.json index df588a6375..9235d136c3 100644 --- a/public/language/he/world.json +++ b/public/language/he/world.json @@ -1,6 +1,7 @@ { "name": "עולם", - "latest": "אחרונים", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "פופולרי (יומי)", "popular-week": "פופולרי (שבועי)", "popular-month": "פופולרי (חודשי)", diff --git a/public/language/hr/world.json b/public/language/hr/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/hr/world.json +++ b/public/language/hr/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/hu/world.json b/public/language/hu/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/hu/world.json +++ b/public/language/hu/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/hy/world.json b/public/language/hy/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/hy/world.json +++ b/public/language/hy/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/id/world.json b/public/language/id/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/id/world.json +++ b/public/language/id/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/it/world.json b/public/language/it/world.json index 7cf33e04dd..859fdbedcd 100644 --- a/public/language/it/world.json +++ b/public/language/it/world.json @@ -1,6 +1,7 @@ { "name": "Mondo", - "latest": "Ultimo", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popolare (Giorno)", "popular-week": "Popolare (Settimana)", "popular-month": "Popolare (Mese)", diff --git a/public/language/ja/world.json b/public/language/ja/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/ja/world.json +++ b/public/language/ja/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/ko/world.json b/public/language/ko/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/ko/world.json +++ b/public/language/ko/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/lt/world.json b/public/language/lt/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/lt/world.json +++ b/public/language/lt/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/lv/world.json b/public/language/lv/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/lv/world.json +++ b/public/language/lv/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/ms/world.json b/public/language/ms/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/ms/world.json +++ b/public/language/ms/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/nb/world.json b/public/language/nb/world.json index 0b31cac31a..bd39706af9 100644 --- a/public/language/nb/world.json +++ b/public/language/nb/world.json @@ -1,6 +1,7 @@ { "name": "Verden", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/nl/world.json b/public/language/nl/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/nl/world.json +++ b/public/language/nl/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/nn-NO/world.json b/public/language/nn-NO/world.json index 5b7d186b92..45ae2a8b3e 100644 --- a/public/language/nn-NO/world.json +++ b/public/language/nn-NO/world.json @@ -1,6 +1,7 @@ { "name": "Verda", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/pl/world.json b/public/language/pl/world.json index e2c8e8c1f3..62f0daf537 100644 --- a/public/language/pl/world.json +++ b/public/language/pl/world.json @@ -1,6 +1,7 @@ { "name": "Świat", - "latest": "Najnowsze", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popularne (Dziś)", "popular-week": "Popularne (W Tygodniu)", "popular-month": "Popularne (W Miesiącu)", diff --git a/public/language/pt-BR/world.json b/public/language/pt-BR/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/pt-BR/world.json +++ b/public/language/pt-BR/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/pt-PT/world.json b/public/language/pt-PT/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/pt-PT/world.json +++ b/public/language/pt-PT/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/ro/world.json b/public/language/ro/world.json index 1527ca76f5..dc182ccab2 100644 --- a/public/language/ro/world.json +++ b/public/language/ro/world.json @@ -1,6 +1,7 @@ { "name": "Lumea", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/ru/world.json b/public/language/ru/world.json index 46950ee50b..e83f4ee497 100644 --- a/public/language/ru/world.json +++ b/public/language/ru/world.json @@ -1,6 +1,7 @@ { "name": "Мир", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/rw/world.json b/public/language/rw/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/rw/world.json +++ b/public/language/rw/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/sc/world.json b/public/language/sc/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/sc/world.json +++ b/public/language/sc/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/sk/world.json b/public/language/sk/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/sk/world.json +++ b/public/language/sk/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/sl/world.json b/public/language/sl/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/sl/world.json +++ b/public/language/sl/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/sq-AL/world.json b/public/language/sq-AL/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/sq-AL/world.json +++ b/public/language/sq-AL/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/sr/world.json b/public/language/sr/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/sr/world.json +++ b/public/language/sr/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/sv/world.json b/public/language/sv/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/sv/world.json +++ b/public/language/sv/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/th/world.json b/public/language/th/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/th/world.json +++ b/public/language/th/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/tr/world.json b/public/language/tr/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/tr/world.json +++ b/public/language/tr/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/uk/world.json b/public/language/uk/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/uk/world.json +++ b/public/language/uk/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/ur/world.json b/public/language/ur/world.json index ed5793dce1..df3cc4f334 100644 --- a/public/language/ur/world.json +++ b/public/language/ur/world.json @@ -1,6 +1,7 @@ { "name": "جهان", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", diff --git a/public/language/vi/world.json b/public/language/vi/world.json index 77e8e377d4..7e8d1181c8 100644 --- a/public/language/vi/world.json +++ b/public/language/vi/world.json @@ -1,6 +1,7 @@ { "name": "Thế giới", - "latest": "Mới nhất", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Phổ biến (Ngày)", "popular-week": "Phổ biến (Tuần)", "popular-month": "Phổ biến (Tháng)", diff --git a/public/language/zh-CN/world.json b/public/language/zh-CN/world.json index 94a04f3224..db61c5c92d 100644 --- a/public/language/zh-CN/world.json +++ b/public/language/zh-CN/world.json @@ -1,6 +1,7 @@ { "name": "世界", - "latest": "最新", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "热门(按天)", "popular-week": "热门(按周)", "popular-month": "热门(按月)", diff --git a/public/language/zh-TW/world.json b/public/language/zh-TW/world.json index 427135352d..58e6526fbb 100644 --- a/public/language/zh-TW/world.json +++ b/public/language/zh-TW/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest", + "latest": "Latest (Following)", + "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", "popular-month": "Popular (Month)", From 1af835641e65fa813e99ec47bff5b4aef19f1c09 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 13:19:29 -0400 Subject: [PATCH 4622/4744] fix: restored popular calculation behaviour that was broken by e2131d1d2e1c6f14cb8867ac7e22840da3f4c63f, removed followingOnly arg passing for popular --- src/topics/sorted.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 6756fbfe38..a37d249cb0 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -46,8 +46,8 @@ module.exports = function (Topics) { let tids; if (params.term !== 'alltime') { if (params.sort === 'posts') { - const { cids, uid, term, includeRemote, followingOnly } = params; - tids = await getTidsWithMostPostsInTerm({ cids, uid, term, includeRemote, followingOnly }); + const { cids, uid, term, includeRemote } = params; + tids = await getTidsWithMostPostsInTerm({ cids, uid, term, includeRemote }); } else { const cids = await getCids(params.cids, params.uid); tids = await Topics.getLatestTidsFromSet( @@ -132,26 +132,24 @@ module.exports = function (Topics) { return cids; } - async function getTidsWithMostPostsInTerm({ cids, uid, term, includeRemote, followingOnly }) { + async function getTidsWithMostPostsInTerm({ cids, uid, term, includeRemote }) { cids = await getCids(cids, uid, includeRemote); - const sets = cids.map(cid => `cid:${cid}:tids`); - if (followingOnly && sets.includes('cid:-1:tids')) { - sets.splice(sets.indexOf('cid:-1:tids'), 1, `uid:${uid}:inbox`); - } - const tids = await db.getSortedSetRevRangeByScore( + const sets = cids.map(cid => `cid:${cid}:pids`); + const pids = await db.getSortedSetRevRangeByScore( sets, 0, 1000, '+inf', Date.now() - Topics.getSinceFromTerm(term) ); + const postObjs = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['tid']); const tidToCount = {}; - tids.forEach((tid) => { - tidToCount[tid] = tidToCount[tid] || 0; - tidToCount[tid] += 1; + postObjs.forEach((post) => { + tidToCount[post.tid] = tidToCount[post.tid] || 0; + tidToCount[post.tid] += 1; }); - return _.uniq(tids) + return _.uniq(postObjs.map(post => String(post.tid))) .sort((t1, t2) => tidToCount[t2] - tidToCount[t1]); } From 10859455801682469082fab7158ac5d048110ba9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 13:21:14 -0400 Subject: [PATCH 4623/4744] fix: bump harmony --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index c7250d637c..507f5c5a8d 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.52", + "nodebb-theme-harmony": "2.2.53", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", "nodebb-theme-persona": "14.2.28", From ff1e1b9200c118795d9ff1bd1cd8b8f51107756c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 13:25:32 -0400 Subject: [PATCH 4624/4744] test: exclude uploadScreenshot from routeMap parsing test --- test/controllers-admin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/controllers-admin.js b/test/controllers-admin.js index 154fec69a8..c7498eefee 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -629,6 +629,7 @@ describe('Admin Controllers', () => { 'uploadfavicon', 'uploadTouchIcon', 'uploadMaskableIcon', + 'uploadScreenshot', 'uploadlogo', 'uploadOgImage', 'uploadDefaultAvatar', @@ -643,7 +644,7 @@ describe('Admin Controllers', () => { await privileges.admin.give([privileges.admin.routeMap[route]], uid); ({ response: res } = await request.get(`${nconf.get('url')}/api/admin/${route}`, requestOpts)); - assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.statusCode, 200, `${route} returned ${res.statusCode} instead of 200`); await privileges.admin.rescind([privileges.admin.routeMap[route]], uid); } From 1aa5ca88522c062ab3c522723185243993d226bb Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 13:52:19 -0400 Subject: [PATCH 4625/4744] fix: restore guest access to /world, default to latest(all) --- public/src/client/world.js | 23 +++++++++++++++-------- src/controllers/activitypub/topics.js | 4 ++++ src/routes/activitypub.js | 1 - 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/public/src/client/world.js b/public/src/client/world.js index ca9fda9187..b22813162b 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -36,14 +36,21 @@ define('forum/world', [ const sortOptionsEl = document.getElementById('sort-options'); if (sortLabelEl && sortOptionsEl) { const params = new URLSearchParams(window.location.search); - if (params.get('sort') === 'popular') { - translator.translate(`[[world:popular-${params.get('term')}]]`, function (translated) { - sortLabelEl.innerText = translated; - }); - } else { - translator.translate('[[world:latest]]', function (translated) { - sortLabelEl.innerText = translated; - }); + switch(params.get('sort')) { + case 'popular': { + translator.translate(`[[world:popular-${params.get('term')}]]`, function (translated) { + sortLabelEl.innerText = translated; + }); + break; + } + + default: { + console.log('here!'); + translator.translate(`[[world:latest${params.get('all') === '1' ? '-all' : ''}]]`, function (translated) { + sortLabelEl.innerText = translated; + }); + break; + } } } diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 702893cac8..8785fecc14 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -16,6 +16,10 @@ const helpers = require('../helpers'); const controller = module.exports; controller.list = async function (req, res) { + if (!req.uid && !req.query.sort && !req.query.all) { + return helpers.redirect(res, '/world?all=1', false); + } + const { topicsPerPage } = await user.getSettings(req.uid); let { page, after } = req.query; page = parseInt(page, 10) || 1; diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 00d8b1a125..6c13fea365 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -6,7 +6,6 @@ module.exports = function (app, middleware, controllers) { helpers.setupPageRoute(app, '/world', [ middleware.activitypub.enabled, middleware.activitypub.pageview, - middleware.ensureLoggedIn, ], controllers.activitypub.topics.list); helpers.setupPageRoute(app, '/ap', [ middleware.activitypub.enabled, From 58da90361f8bab5adc91b0a1aa82e59b87686e50 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 13:57:34 -0400 Subject: [PATCH 4626/4744] fix: debug log --- public/src/client/world.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/src/client/world.js b/public/src/client/world.js index b22813162b..6e63b85a23 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -45,7 +45,6 @@ define('forum/world', [ } default: { - console.log('here!'); translator.translate(`[[world:latest${params.get('all') === '1' ? '-all' : ''}]]`, function (translated) { sortLabelEl.innerText = translated; }); From d1e1a0082d9555cca3e453890932ca01f0e85156 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Mar 2026 14:42:48 -0400 Subject: [PATCH 4627/4744] fix: long-press support for topicSelect, #14045 --- public/src/client/category/tools.js | 9 ++++-- public/src/modules/topicSelect.js | 46 +++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/public/src/client/category/tools.js b/public/src/client/category/tools.js index 58a257f293..bd15ff459b 100644 --- a/public/src/client/category/tools.js +++ b/public/src/client/category/tools.js @@ -9,11 +9,16 @@ define('forum/category/tools', [ 'api', 'bootbox', 'alerts', -], function (topicSelect, threadTools, components, api, bootbox, alerts) { + 'bootstrap', +], function (topicSelect, threadTools, components, api, bootbox, alerts, bootstrap) { const CategoryTools = {}; CategoryTools.init = function (containerEl) { - topicSelect.init(updateDropdownOptions, containerEl); + topicSelect.init(updateDropdownOptions, () => { + const toggleEl = document.querySelector('.thread-tools button'); + const dropdown = new bootstrap.Dropdown(toggleEl); + dropdown.show(); + }, containerEl); handlePinnedTopicSort(); diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js index 50d38c4ba7..54cffc4465 100644 --- a/public/src/modules/topicSelect.js +++ b/public/src/modules/topicSelect.js @@ -7,13 +7,14 @@ define('topicSelect', ['components'], function (components) { let topicsContainer; - TopicSelect.init = function (onSelect, containerEl) { + TopicSelect.init = function (onSelect, onLongPress, containerEl) { topicsContainer = containerEl || $('[component="category"]'); topicsContainer.on('selectstart', '[component="topic/select"]', function (ev) { ev.preventDefault(); }); - topicsContainer.on('click', '[component="topic/select"]', function (ev) { + let isLongPress = false; + const click = function (ev) { const select = $(this); const topicEl = select.parents('[component="category/topic"]'); if (ev.shiftKey) { @@ -28,6 +29,47 @@ define('topicSelect', ['components'], function (components) { if (typeof onSelect === 'function') { onSelect(); } + }; + topicsContainer.on('click', '[component="topic/select"]', function (ev) { + if (isLongPress) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + return false; + } + + click.call(this, ev); + }); + + // Long press + let longPressTimeout; + const start = function (ev) { + if (ev.type === 'touchstart') { + ev.preventDefault(); + } + isLongPress = false; + longPressTimeout = setTimeout(() => { + isLongPress = true; + click.call(this, ev); + if (navigator.vibrate) { + navigator.vibrate(50); + } + const topicEl = this.closest('[component="category/topic"]'); + if (topicEl.classList.contains('selected')) { + onLongPress(); + } + }, 500); + }; + const cancel = () => { + clearTimeout(longPressTimeout); + }; + topicsContainer.on('mousedown', '[component="topic/select"]', start); + topicsContainer.on('touchstart', '[component="topic/select"]', start); + topicsContainer.on('mouseup', '[component="topic/select"]', cancel); + topicsContainer.on('mouseleave', '[component="topic/select"]', cancel); + topicsContainer.on('touchend', '[component="topic/select"]', cancel); + topicsContainer.on('touchcancel', '[component="topic/select"]', cancel); + topicsContainer.on('contextmenu', (e) => { + e.preventDefault(); }); }; From 533ae69c460e40e3ed43f8a94fb3829513865bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Mar 2026 18:42:50 -0400 Subject: [PATCH 4628/4744] feat: allow 3 profile pics (#14092) * feat: allow 3 profile pics * test: fix notification test * test: fix user picture test * test: relative_path fixes * fix: relative_path getting saved in when updating profile pic --- public/src/modules/accounts/picture.js | 65 +++----- src/api/users.js | 11 +- src/socket.io/user/picture.js | 19 ++- .../4.10.0/user-profile-pictures-zset.js | 23 +++ src/user/data.js | 3 +- src/user/delete.js | 1 + src/user/picture.js | 150 +++++++++++------- src/views/modals/change-picture.tpl | 54 +++---- test/notifications.js | 3 +- test/user.js | 3 +- 10 files changed, 194 insertions(+), 138 deletions(-) create mode 100644 src/upgrades/4.10.0/user-profile-pictures-zset.js diff --git a/public/src/modules/accounts/picture.js b/public/src/modules/accounts/picture.js index 0bec1a24a9..651ce7deaa 100644 --- a/public/src/modules/accounts/picture.js +++ b/public/src/modules/accounts/picture.js @@ -27,7 +27,12 @@ define('accounts/picture', [ icon: { text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor'] }, defaultAvatar: ajaxify.data.defaultAvatar, allowProfileImageUploads: ajaxify.data.allowProfileImageUploads, - iconBackgrounds: ajaxify.data.iconBackgrounds, + iconBackgrounds: ajaxify.data.iconBackgrounds.map((color) => { + return { + color, + selected: color === ajaxify.data['icon:bgColor'], + }; + }), user: { uid: ajaxify.data.uid, username: ajaxify.data.username, @@ -55,9 +60,8 @@ define('accounts/picture', [ }, }); - modal.on('shown.bs.modal', updateImages); - modal.on('click', '.list-group-item', function selectImageType() { - modal.find('.list-group-item').removeClass('active'); + modal.on('click', '[component="profile/picture/button"]', function selectImageType() { + modal.find('[component="profile/picture/button"]').removeClass('active'); $(this).addClass('active'); }); @@ -69,34 +73,17 @@ define('accounts/picture', [ handleImageUpload(modal); - function updateImages() { - // Check to see which one is the active picture - if (!ajaxify.data.picture) { - modal.find('[data-type="default"]').addClass('active'); - } else { - modal.find('.list-group-item img').each(function () { - if (this.getAttribute('src') === ajaxify.data.picture) { - $(this).parents('.list-group-item').addClass('active'); - } - }); - } - - // Update avatar background colour - const iconbgEl = modal.find(`[data-bg-color="${ajaxify.data['icon:bgColor']}"]`); - if (iconbgEl.length) { - iconbgEl.addClass('selected'); - } else { - modal.find('[data-bg-color="transparent"]').addClass('selected'); - } - } - function saveSelection() { - const type = modal.find('.list-group-item.active').attr('data-type'); + const activeBtn = modal.find('[component="profile/picture/button"].active'); + const type = activeBtn.attr('data-type'); + const picture = activeBtn.find('img').attr('src'); const iconBgColor = modal.find('[data-bg-color].selected').attr('data-bg-color') || 'transparent'; - changeUserPicture(type, iconBgColor).then(() => { + api.put(`/users/${ajaxify.data.theirid}/picture`, { + type, picture, iconBgColor, + }).then(() => { Picture.updateHeader( - type === 'default' ? '' : modal.find('.list-group-item.active img').attr('src'), + type === 'default' ? '' : picture, iconBgColor ); ajaxify.refresh(); @@ -158,13 +145,6 @@ define('accounts/picture', [ } } - function onRemoveComplete() { - if (ajaxify.data.uploadedpicture === ajaxify.data.picture) { - ajaxify.refresh(); - Picture.updateHeader(); - } - } - modal.find('[data-action="upload"]').on('click', function () { modal.modal('hide'); @@ -217,21 +197,24 @@ define('accounts/picture', [ }); modal.find('[data-action="remove-uploaded"]').on('click', function () { + const removeBtn = $(this); + const removePicture = removeBtn.attr('data-url'); socket.emit('user.removeUploadedPicture', { uid: ajaxify.data.theirid, + picture: removePicture, }, function (err) { - modal.modal('hide'); if (err) { return alerts.error(err); } - onRemoveComplete(); + removeBtn.parent().remove(); + if (removePicture === ajaxify.data.picture) { + modal.modal('hide'); + ajaxify.refresh(); + Picture.updateHeader(); + } }); }); } - function changeUserPicture(type, bgColor) { - return api.put(`/users/${ajaxify.data.theirid}/picture`, { type, bgColor }); - } - return Picture; }); diff --git a/src/api/users.js b/src/api/users.js index 369a692cc0..b962968882 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -2,7 +2,7 @@ const path = require('path'); const fs = require('fs').promises; - +const nconf = require('nconf'); const validator = require('validator'); const winston = require('winston'); @@ -627,7 +627,14 @@ usersAPI.changePicture = async (caller, data) => { if (type === 'default') { picture = ''; } else if (type === 'uploaded') { - picture = await user.getUserField(data.uid, 'uploadedpicture'); + const cleanPath = data.picture.replace(new RegExp(`^${nconf.get('relative_path')}`), ''); + const isUserPicture = await user.isUserUploadedPicture(data.uid, cleanPath); + if (isUserPicture) { + await user.setUserField(data.uid, 'uploadedpicture', cleanPath); + picture = cleanPath; + } else { + picture = ''; + } } else if (type === 'external' && url) { picture = validator.escape(url); } else { diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js index 828dca61f8..d63f28e61c 100644 --- a/src/socket.io/user/picture.js +++ b/src/socket.io/user/picture.js @@ -1,5 +1,9 @@ 'use strict'; +const validator = require('validator'); +const nconf = require('nconf'); + +const db = require('../../database'); const user = require('../../user'); const plugins = require('../../plugins'); @@ -10,7 +14,7 @@ module.exports = function (SocketUser) { } await user.isAdminOrSelf(socket.uid, data.uid); // 'keepAllUserImages' is ignored, since there is explicit user intent - const userData = await user.removeProfileImage(data.uid); + const userData = await user.removeProfileImage(data.uid, data.picture); plugins.hooks.fire('action:user.removeUploadedPicture', { callerUid: socket.uid, uid: data.uid, @@ -23,27 +27,29 @@ module.exports = function (SocketUser) { throw new Error('[[error:invalid-data]]'); } - const [list, userObj] = await Promise.all([ + const [list, userObj, userPictures] = await Promise.all([ plugins.hooks.fire('filter:user.listPictures', { uid: data.uid, pictures: [], }), user.getUserData(data.uid), + db.getSortedSetRevRange(`uid:${data.uid}:profile:pictures`, 0, 2), ]); - if (userObj.uploadedpicture) { + userPictures.forEach((picture) => { list.pictures.push({ type: 'uploaded', - url: userObj.uploadedpicture, + url: `${nconf.get('relative_path')}${picture}`, text: '[[user:uploaded-picture]]', }); - } + }); // Normalize list into "user object" format list.pictures = list.pictures.map(({ type, url, text }) => ({ type, username: text, - picture: url, + picture: validator.escape(String(url)), + selected: url === userObj.picture, })); list.pictures.unshift({ @@ -51,6 +57,7 @@ module.exports = function (SocketUser) { 'icon:text': userObj['icon:text'], 'icon:bgColor': userObj['icon:bgColor'], username: '[[user:default-picture]]', + selected: !userObj.picture, }); return list.pictures; diff --git a/src/upgrades/4.10.0/user-profile-pictures-zset.js b/src/upgrades/4.10.0/user-profile-pictures-zset.js new file mode 100644 index 0000000000..98ce8f9e4f --- /dev/null +++ b/src/upgrades/4.10.0/user-profile-pictures-zset.js @@ -0,0 +1,23 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Add uid::profile:pictures zset', + timestamp: Date.UTC(2026, 2, 13), + method: async function () { + const { progress } = this; + await batch.processSortedSet('users:joindate', async (uids) => { + const userData = await db.getObjects(uids.map(uid => `user:${uid}`)); + const now = Date.now(); + const bulkAdd = userData.filter(u => u && u.uploadedpicture) + .map(u => ([`uid:${u.uid}:profile:pictures`, now, u.uploadedpicture])); + await db.sortedSetAddBulk(bulkAdd); + progress.incr(uids.length); + }, { + batch: 500, + progress, + }); + }, +}; diff --git a/src/user/data.js b/src/user/data.js index d45e4a6259..fbe9fd75da 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -376,7 +376,8 @@ module.exports = function (User) { const _iconBackgrounds = [ '#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#009688', '#1b5e20', '#33691e', '#827717', '#e65100', '#ff5722', - '#795548', '#607d8b', + '#795548', '#607d8b', '#00bcd4', '#ffc107', '#8bc34a', '#9e9e9e', + '#004d40', '#ad1457', ]; const data = await plugins.hooks.fire('filter:user.iconBackgrounds', { iconBackgrounds: _iconBackgrounds }); diff --git a/src/user/delete.js b/src/user/delete.js index 9deecd001b..3de2127ed0 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -134,6 +134,7 @@ module.exports = function (User) { `uid:${uid}:flag:pids`, `uid:${uid}:sessions`, `uid:${uid}:shares`, + `uid:${uid}:profile:images`, `invitation:uid:${uid}`, ]; diff --git a/src/user/picture.js b/src/user/picture.js index 9e7eaf6d00..db135b463f 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -52,7 +52,10 @@ module.exports = function (User) { const filename = `${data.uid}-profilecover-${Date.now()}${extension}`; const uploadData = await image.uploadImage(filename, `profile/uid-${data.uid}`, picture); - await deleteCurrentPicture(data.uid, 'cover:url'); + if (!meta.config['profile:keepAllUserImages']) { + await deletePicture(data.uid, 'cover:url'); + } + await User.setUserField(data.uid, 'cover:url', uploadData.url); if (data.position) { @@ -87,30 +90,11 @@ module.exports = function (User) { throw new Error('[[error:invalid-image-extension]]'); } - const normalizedPath = await convertToPNG(userPhoto.path); - const isNormalized = userPhoto.path !== normalizedPath; - - await image.resizeImage({ - path: normalizedPath, - type: isNormalized ? 'image/png' : userPhoto.type, - width: meta.config.profileImageDimension, - height: meta.config.profileImageDimension, + return await storeUserUploadedPicture(data.callerUid, data.uid, { + path: userPhoto.path, + type: userPhoto.type, + extension, }); - - const filename = generateProfileImageFilename(data.uid, extension); - const uploadedImage = await image.uploadImage(filename, `profile/uid-${data.uid}`, { - uid: data.uid, - path: normalizedPath, - name: 'profileAvatar', - }); - - await deleteCurrentPicture(data.uid, 'uploadedpicture'); - await User.updateProfile(data.callerUid, { - uid: data.uid, - uploadedpicture: uploadedImage.url, - picture: uploadedImage.url, - }, ['uploadedpicture', 'picture']); - return uploadedImage; }; // uploads image data in base64 as profile picture @@ -133,40 +117,67 @@ module.exports = function (User) { } picture.path = await image.writeImageDataToTempFile(data.imageData); - const normalizedPath = await convertToPNG(picture.path); - const isNormalized = picture.path !== normalizedPath; - picture.path = normalizedPath; - await image.resizeImage({ + + return await storeUserUploadedPicture(data.callerUid, data.uid, { path: picture.path, - type: isNormalized ? 'image/png' : type, - width: meta.config.profileImageDimension, - height: meta.config.profileImageDimension, + type, + extension, }); - - const filename = generateProfileImageFilename(data.uid, extension); - const uploadedImage = await image.uploadImage(filename, `profile/uid-${data.uid}`, picture); - - await deleteCurrentPicture(data.uid, 'uploadedpicture'); - await User.updateProfile(data.callerUid, { - uid: data.uid, - uploadedpicture: uploadedImage.url, - picture: uploadedImage.url, - }, ['uploadedpicture', 'picture']); - return uploadedImage; } finally { await file.delete(picture.path); } }; - async function deleteCurrentPicture(uid, field) { - if (meta.config['profile:keepAllUserImages']) { - return; + async function storeUserUploadedPicture(callerUid, updateUid, picture) { + const { type, extension } = picture; + const normalizedPath = await convertToPNG(picture.path); + const isNormalized = picture.path !== normalizedPath; + + await image.resizeImage({ + path: normalizedPath, + type: isNormalized ? 'image/png' : type, + width: meta.config.profileImageDimension, + height: meta.config.profileImageDimension, + }); + + const filename = generateProfileImageFilename(updateUid, extension); + const uploadedImage = await image.uploadImage(filename, `profile/uid-${updateUid}`, { + uid: updateUid, + path: picture.path, + name: 'profileAvatar', + }); + + await User.updateProfile(callerUid, { + uid: updateUid, + uploadedpicture: uploadedImage.url, + picture: uploadedImage.url, + }, ['uploadedpicture', 'picture']); + + const zsetKey = `uid:${updateUid}:profile:pictures`; + + if (!meta.config['profile:keepAllUserImages']) { + // if we are not keeping all images, only keep most recent 3 + const imagesToKeep = 3; + const previousImages = await db.getSortedSetRevRangeWithScores(zsetKey, 0, -1); + const toDeleteImages = previousImages.filter((imagePath, index) => index >= imagesToKeep - 1) + .map(image => image.value); + const toRemove = [ + ...toDeleteImages.map(imagePath => ([zsetKey, imagePath])), + ]; + + await db.sortedSetRemoveBulk(toRemove); + toDeleteImages.forEach((imagePath) => { + if (imagePath && !imagePath.startsWith('http')) { + file.delete(imagePath); + } + }); } - await deletePicture(uid, field); + await db.sortedSetAdd(zsetKey, Date.now(), uploadedImage.url); + return { url: uploadedImage.url }; } async function deletePicture(uid, field) { - const uploadPath = await getPicturePath(uid, field); + const uploadPath = await getPicturePathFromUserField(uid, field); if (uploadPath) { await file.delete(uploadPath); } @@ -207,31 +218,56 @@ module.exports = function (User) { await db.deleteObjectFields(`user:${data.uid}`, ['cover:url', 'cover:position']); }; - User.removeProfileImage = async function (uid) { + // this function expects a path without nconf.get('relative_path) prepended + User.isUserUploadedPicture = async (uid, picture) => { + return await db.isSortedSetMember(`uid:${uid}:profile:pictures`, picture); + }; + + User.removeProfileImage = async function (uid, picture) { const userData = await User.getUserFields(uid, ['uploadedpicture', 'picture']); - await deletePicture(uid, 'uploadedpicture'); - await User.setUserFields(uid, { - uploadedpicture: '', - // if current picture is uploaded picture, reset to user icon - picture: userData.uploadedpicture === userData.picture ? '' : userData.picture, - }); + if (!picture) { + picture = userData.uploadedpicture; + } + // picture has relative_path prepended, db entries don't have it, so remove it + const cleanPath = picture.replace(new RegExp(`^${nconf.get('relative_path')}`), ''); + const isUserPicture = await User.isUserUploadedPicture(uid, cleanPath); + if (isUserPicture) { + const path = getPicturePath(uid, picture); + await Promise.all([ + path && !path.startsWith('http') ? file.delete(path) : null, + db.sortedSetRemove(`uid:${uid}:profile:pictures`, cleanPath), + ]); + if (picture === userData.picture) { + // if deleting current uploaded picture, reset to user icon + await User.setUserFields(uid, { + uploadedpicture: '', + picture: '', + }); + } + } + return userData; }; User.getLocalCoverPath = async function (uid) { - return getPicturePath(uid, 'cover:url'); + return await getPicturePathFromUserField(uid, 'cover:url'); }; User.getLocalAvatarPath = async function (uid) { - return getPicturePath(uid, 'uploadedpicture'); + return await getPicturePathFromUserField(uid, 'uploadedpicture'); }; - async function getPicturePath(uid, field) { + async function getPicturePathFromUserField(uid, field) { const value = await User.getUserField(uid, field); + return getPicturePath(uid, value); + } + + function getPicturePath(uid, value) { if (!value || !value.startsWith(`${nconf.get('relative_path')}/assets/uploads/profile/uid-${uid}`)) { return false; } const filename = value.split('/').pop(); return path.join(nconf.get('upload_path'), `profile/uid-${uid}`, filename); } + }; diff --git a/src/views/modals/change-picture.tpl b/src/views/modals/change-picture.tpl index f0c16744dc..390f4e95d4 100644 --- a/src/views/modals/change-picture.tpl +++ b/src/views/modals/change-picture.tpl @@ -1,43 +1,39 @@ +
    + + +
    From aeb530433fc9449b02b2042dc811f1788cb170c5 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 16 Mar 2026 19:35:14 +0000 Subject: [PATCH 4645/4744] chore(i18n): fallback strings for new resources: nodebb.admin-settings-activitypub --- public/language/ar/admin/settings/activitypub.json | 3 ++- public/language/az/admin/settings/activitypub.json | 3 ++- public/language/bg/admin/settings/activitypub.json | 3 ++- public/language/bn/admin/settings/activitypub.json | 3 ++- public/language/cs/admin/settings/activitypub.json | 3 ++- public/language/da/admin/settings/activitypub.json | 3 ++- public/language/de/admin/settings/activitypub.json | 3 ++- public/language/el/admin/settings/activitypub.json | 3 ++- public/language/en-US/admin/settings/activitypub.json | 3 ++- public/language/en-x-pirate/admin/settings/activitypub.json | 3 ++- public/language/es/admin/settings/activitypub.json | 3 ++- public/language/et/admin/settings/activitypub.json | 3 ++- public/language/fa-IR/admin/settings/activitypub.json | 3 ++- public/language/fi/admin/settings/activitypub.json | 3 ++- public/language/fr/admin/settings/activitypub.json | 3 ++- public/language/gl/admin/settings/activitypub.json | 3 ++- public/language/he/admin/settings/activitypub.json | 3 ++- public/language/hr/admin/settings/activitypub.json | 3 ++- public/language/hu/admin/settings/activitypub.json | 3 ++- public/language/hy/admin/settings/activitypub.json | 3 ++- public/language/id/admin/settings/activitypub.json | 3 ++- public/language/it/admin/settings/activitypub.json | 3 ++- public/language/ja/admin/settings/activitypub.json | 3 ++- public/language/ko/admin/settings/activitypub.json | 3 ++- public/language/lt/admin/settings/activitypub.json | 3 ++- public/language/lv/admin/settings/activitypub.json | 3 ++- public/language/ms/admin/settings/activitypub.json | 3 ++- public/language/nb/admin/settings/activitypub.json | 3 ++- public/language/nl/admin/settings/activitypub.json | 3 ++- public/language/nn-NO/admin/settings/activitypub.json | 3 ++- public/language/pl/admin/settings/activitypub.json | 3 ++- public/language/pt-BR/admin/settings/activitypub.json | 3 ++- public/language/pt-PT/admin/settings/activitypub.json | 3 ++- public/language/ro/admin/settings/activitypub.json | 3 ++- public/language/ru/admin/settings/activitypub.json | 3 ++- public/language/rw/admin/settings/activitypub.json | 3 ++- public/language/sc/admin/settings/activitypub.json | 3 ++- public/language/sk/admin/settings/activitypub.json | 3 ++- public/language/sl/admin/settings/activitypub.json | 3 ++- public/language/sq-AL/admin/settings/activitypub.json | 3 ++- public/language/sr/admin/settings/activitypub.json | 3 ++- public/language/sv/admin/settings/activitypub.json | 3 ++- public/language/th/admin/settings/activitypub.json | 3 ++- public/language/tr/admin/settings/activitypub.json | 3 ++- public/language/uk/admin/settings/activitypub.json | 3 ++- public/language/ur/admin/settings/activitypub.json | 3 ++- public/language/vi/admin/settings/activitypub.json | 3 ++- public/language/zh-CN/admin/settings/activitypub.json | 3 ++- public/language/zh-TW/admin/settings/activitypub.json | 3 ++- 49 files changed, 98 insertions(+), 49 deletions(-) diff --git a/public/language/ar/admin/settings/activitypub.json b/public/language/ar/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/ar/admin/settings/activitypub.json +++ b/public/language/ar/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/az/admin/settings/activitypub.json b/public/language/az/admin/settings/activitypub.json index c528187a81..d68d9dbdaf 100644 --- a/public/language/az/admin/settings/activitypub.json +++ b/public/language/az/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/bg/admin/settings/activitypub.json b/public/language/bg/admin/settings/activitypub.json index d2e19f1aa2..4923a53f7f 100644 --- a/public/language/bg/admin/settings/activitypub.json +++ b/public/language/bg/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Брой знаци, след които се създава обобщение", "content.summary-limit-help": "Когато дадено съдържание е федерирано в посока извън този форум и превишава този брой знаци, ще се създаде обобщение, което се състои от всички завършени изречения до ограничението. (по подразбиране: 500)", "content.break-string": "Разделител на съдържанието", - "content.break-string-help": "Този разделител може да бъде поставен ръчно от потребителите с по-високи правомощия, когато създават нови теми. Той казва на NodeBB да използва съдържанието до така посоченото място като част от обобщението. Ако този разделител не бъде използван, се прилага зададеният брой на знаците. (по подразбиране: [...])" + "content.break-string-help": "Този разделител може да бъде поставен ръчно от потребителите с по-високи правомощия, когато създават нови теми. Той казва на NodeBB да използва съдържанието до така посоченото място като част от обобщението. Ако този разделител не бъде използван, се прилага зададеният брой на знаците. (по подразбиране: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/bn/admin/settings/activitypub.json b/public/language/bn/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/bn/admin/settings/activitypub.json +++ b/public/language/bn/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/cs/admin/settings/activitypub.json b/public/language/cs/admin/settings/activitypub.json index 111cdfa472..7cbf3281c2 100644 --- a/public/language/cs/admin/settings/activitypub.json +++ b/public/language/cs/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/da/admin/settings/activitypub.json b/public/language/da/admin/settings/activitypub.json index f5726b8b58..49819c963e 100644 --- a/public/language/da/admin/settings/activitypub.json +++ b/public/language/da/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/de/admin/settings/activitypub.json b/public/language/de/admin/settings/activitypub.json index ca3af062c9..bc119ed32b 100644 --- a/public/language/de/admin/settings/activitypub.json +++ b/public/language/de/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Anzahl der Zeichen, nach der eine Zusammenfassung erstellt wird", "content.summary-limit-help": "Wenn Inhalte rausgeschickt werden, die diese Zeichenanzahl überschreiten, wird eine Zusammenfassung erstellt, die alle vollständigen Sätze vor dieser Grenze enthält. (Standard: 500)", "content.break-string": "Notiz/Artikel-Trennzeichen", - "content.break-string-help": "\nDieses Trennzeichen kann von Power-Usern beim Erstellen neuer Themen manuell eingefügt werden. Es sagt NodeBB, dass der Inhalt bis zu dieser Stelle als Teil der Zusammenfassungverwendet werden soll. Wenn diese Zeichenfolge nicht benutzt wird, greift die Standardregel für die Zeichenanzahl. (Standard: [...])" + "content.break-string-help": "\nDieses Trennzeichen kann von Power-Usern beim Erstellen neuer Themen manuell eingefügt werden. Es sagt NodeBB, dass der Inhalt bis zu dieser Stelle als Teil der Zusammenfassungverwendet werden soll. Wenn diese Zeichenfolge nicht benutzt wird, greift die Standardregel für die Zeichenanzahl. (Standard: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/el/admin/settings/activitypub.json b/public/language/el/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/el/admin/settings/activitypub.json +++ b/public/language/el/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/en-US/admin/settings/activitypub.json b/public/language/en-US/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/en-US/admin/settings/activitypub.json +++ b/public/language/en-US/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/activitypub.json b/public/language/en-x-pirate/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/en-x-pirate/admin/settings/activitypub.json +++ b/public/language/en-x-pirate/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/es/admin/settings/activitypub.json b/public/language/es/admin/settings/activitypub.json index 7dca33aad3..033001a12f 100644 --- a/public/language/es/admin/settings/activitypub.json +++ b/public/language/es/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/et/admin/settings/activitypub.json b/public/language/et/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/et/admin/settings/activitypub.json +++ b/public/language/et/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/activitypub.json b/public/language/fa-IR/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/fa-IR/admin/settings/activitypub.json +++ b/public/language/fa-IR/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/fi/admin/settings/activitypub.json b/public/language/fi/admin/settings/activitypub.json index c43363e641..678232c912 100644 --- a/public/language/fi/admin/settings/activitypub.json +++ b/public/language/fi/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/fr/admin/settings/activitypub.json b/public/language/fr/admin/settings/activitypub.json index 566f2e7e00..2fde051d4e 100644 --- a/public/language/fr/admin/settings/activitypub.json +++ b/public/language/fr/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Nombre de caractères déclenchant la génération d’un résumé", "content.summary-limit-help": "Lorsque le contenu fédéré dépasse ce nombre de caractères, un résumé est généré à partir de toutes les phrases complètes situées avant cette limite. (Par défaut : 500)", "content.break-string": "Séparateur note/article", - "content.break-string-help": "Ce séparateur peut être inséré manuellement par les utilisateurs avancés lors de la création de nouveaux sujets. Il indique à NodeBB d’utiliser le contenu jusqu’à cet emplacement comme résumé. Si cette chaîne n’est pas utilisée, la limite de caractères par défaut s’applique. (Par défaut : [...])" + "content.break-string-help": "Ce séparateur peut être inséré manuellement par les utilisateurs avancés lors de la création de nouveaux sujets. Il indique à NodeBB d’utiliser le contenu jusqu’à cet emplacement comme résumé. Si cette chaîne n’est pas utilisée, la limite de caractères par défaut s’applique. (Par défaut : [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/gl/admin/settings/activitypub.json b/public/language/gl/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/gl/admin/settings/activitypub.json +++ b/public/language/gl/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/he/admin/settings/activitypub.json b/public/language/he/admin/settings/activitypub.json index a2add15547..cf39104619 100644 --- a/public/language/he/admin/settings/activitypub.json +++ b/public/language/he/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "מכסת תווים שמעבר לה ייווצר תקציר", "content.summary-limit-help": "כאשר תוכן המופץ החוצה חורג ממכסת תווים זו, ייווצר תקצירהמורכב מכל המשפטים השלמים שלפני המגבלה. (ברירת מחדל: 500)", "content.break-string": "מפריד הערת סיכום/מאמר", - "content.break-string-help": "משתמשים מתקדמים יכולים להזין את המפריד הזה ידנית בעת כתיבת נושאים חדשים. הוא מורה ל-NodeBB להשתמש בתוכן שעד לנקודה זו כחלק מה- תקציר . אם לא נעשה שימוש במחרוזת זו, תופעל חלופת ספירת התווים. (ברירת מחדל: [...] )" + "content.break-string-help": "משתמשים מתקדמים יכולים להזין את המפריד הזה ידנית בעת כתיבת נושאים חדשים. הוא מורה ל-NodeBB להשתמש בתוכן שעד לנקודה זו כחלק מה- תקציר . אם לא נעשה שימוש במחרוזת זו, תופעל חלופת ספירת התווים. (ברירת מחדל: [...] )", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/hr/admin/settings/activitypub.json b/public/language/hr/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/hr/admin/settings/activitypub.json +++ b/public/language/hr/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/hu/admin/settings/activitypub.json b/public/language/hu/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/hu/admin/settings/activitypub.json +++ b/public/language/hu/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/hy/admin/settings/activitypub.json b/public/language/hy/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/hy/admin/settings/activitypub.json +++ b/public/language/hy/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/id/admin/settings/activitypub.json b/public/language/id/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/id/admin/settings/activitypub.json +++ b/public/language/id/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/it/admin/settings/activitypub.json b/public/language/it/admin/settings/activitypub.json index 792edca984..320400e725 100644 --- a/public/language/it/admin/settings/activitypub.json +++ b/public/language/it/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Numero di caratteri dopo il quale sarà generato un riepilogo", "content.summary-limit-help": "Quando il contenuto federato supera questo limite di caratteri, sarà generato un riepilogo che comprende tutte le frasi complete precedenti a tale limite. (Predefinito: 500)\n ", "content.break-string": "Delimitatore di nota/articolo", - "content.break-string-help": "Questo delimitatore può essere inserito manualmente dagli utenti esperti durante la composizione di nuove discussioni. Indica a NodeBB di usare il contenuto fino a quel punto come parte del riepilogo. Se questa stringa non viene usata, si applica il conteggio dei caratteri di riserva. (Predefinito: [...])" + "content.break-string-help": "Questo delimitatore può essere inserito manualmente dagli utenti esperti durante la composizione di nuove discussioni. Indica a NodeBB di usare il contenuto fino a quel punto come parte del riepilogo. Se questa stringa non viene usata, si applica il conteggio dei caratteri di riserva. (Predefinito: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/ja/admin/settings/activitypub.json b/public/language/ja/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/ja/admin/settings/activitypub.json +++ b/public/language/ja/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/ko/admin/settings/activitypub.json b/public/language/ko/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/ko/admin/settings/activitypub.json +++ b/public/language/ko/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/lt/admin/settings/activitypub.json b/public/language/lt/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/lt/admin/settings/activitypub.json +++ b/public/language/lt/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/lv/admin/settings/activitypub.json b/public/language/lv/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/lv/admin/settings/activitypub.json +++ b/public/language/lv/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/ms/admin/settings/activitypub.json b/public/language/ms/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/ms/admin/settings/activitypub.json +++ b/public/language/ms/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/nb/admin/settings/activitypub.json b/public/language/nb/admin/settings/activitypub.json index 312603a4ff..63597bafc7 100644 --- a/public/language/nb/admin/settings/activitypub.json +++ b/public/language/nb/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/nl/admin/settings/activitypub.json b/public/language/nl/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/nl/admin/settings/activitypub.json +++ b/public/language/nl/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/nn-NO/admin/settings/activitypub.json b/public/language/nn-NO/admin/settings/activitypub.json index 016130ebb9..c573484437 100644 --- a/public/language/nn-NO/admin/settings/activitypub.json +++ b/public/language/nn-NO/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/pl/admin/settings/activitypub.json b/public/language/pl/admin/settings/activitypub.json index 746b544e8c..66f6baa799 100644 --- a/public/language/pl/admin/settings/activitypub.json +++ b/public/language/pl/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/activitypub.json b/public/language/pt-BR/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/pt-BR/admin/settings/activitypub.json +++ b/public/language/pt-BR/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/activitypub.json b/public/language/pt-PT/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/pt-PT/admin/settings/activitypub.json +++ b/public/language/pt-PT/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/ro/admin/settings/activitypub.json b/public/language/ro/admin/settings/activitypub.json index 79dfbd9e30..f4d9a3d629 100644 --- a/public/language/ro/admin/settings/activitypub.json +++ b/public/language/ro/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/ru/admin/settings/activitypub.json b/public/language/ru/admin/settings/activitypub.json index be8f21ea1a..11d3ce79b5 100644 --- a/public/language/ru/admin/settings/activitypub.json +++ b/public/language/ru/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/rw/admin/settings/activitypub.json b/public/language/rw/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/rw/admin/settings/activitypub.json +++ b/public/language/rw/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/sc/admin/settings/activitypub.json b/public/language/sc/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/sc/admin/settings/activitypub.json +++ b/public/language/sc/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/sk/admin/settings/activitypub.json b/public/language/sk/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/sk/admin/settings/activitypub.json +++ b/public/language/sk/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/sl/admin/settings/activitypub.json b/public/language/sl/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/sl/admin/settings/activitypub.json +++ b/public/language/sl/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/activitypub.json b/public/language/sq-AL/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/sq-AL/admin/settings/activitypub.json +++ b/public/language/sq-AL/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/sr/admin/settings/activitypub.json b/public/language/sr/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/sr/admin/settings/activitypub.json +++ b/public/language/sr/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/sv/admin/settings/activitypub.json b/public/language/sv/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/sv/admin/settings/activitypub.json +++ b/public/language/sv/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/th/admin/settings/activitypub.json b/public/language/th/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/th/admin/settings/activitypub.json +++ b/public/language/th/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/tr/admin/settings/activitypub.json b/public/language/tr/admin/settings/activitypub.json index a4be3b7879..707c485bf2 100644 --- a/public/language/tr/admin/settings/activitypub.json +++ b/public/language/tr/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/uk/admin/settings/activitypub.json b/public/language/uk/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/uk/admin/settings/activitypub.json +++ b/public/language/uk/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/ur/admin/settings/activitypub.json b/public/language/ur/admin/settings/activitypub.json index 34869aa058..fea9adb3a6 100644 --- a/public/language/ur/admin/settings/activitypub.json +++ b/public/language/ur/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/vi/admin/settings/activitypub.json b/public/language/vi/admin/settings/activitypub.json index 4343357b7d..e1f3cd8804 100644 --- a/public/language/vi/admin/settings/activitypub.json +++ b/public/language/vi/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Số lượng ký tự sau đó bản tóm tắt sẽ được tạo.", "content.summary-limit-help": "Khi nội dung được phân phối vượt quá số lượng ký tự này, tóm tắt được tạo ra, bao gồm tất cả các câu hoàn chỉnh trước giới hạn này. (Mặc định: 500)", "content.break-string": "Dấu phân cách Ghi chú/Bài đăng", - "content.break-string-help": "Dấu phân cách này có thể được người dùng thành thạo chèn thủ công khi soạn thảo chủ đề mới. Nó hướng dẫn NodeBB sử dụng nội dung cho đến thời điểm đó như một phần của tóm tắt. Nếu chuỗi này không được sử dụng, thì phương án dự phòng dựa trên số lượng ký tự sẽ được áp dụng. (Mặc định: [...])" + "content.break-string-help": "Dấu phân cách này có thể được người dùng thành thạo chèn thủ công khi soạn thảo chủ đề mới. Nó hướng dẫn NodeBB sử dụng nội dung cho đến thời điểm đó như một phần của tóm tắt. Nếu chuỗi này không được sử dụng, thì phương án dự phòng dựa trên số lượng ký tự sẽ được áp dụng. (Mặc định: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/activitypub.json b/public/language/zh-CN/admin/settings/activitypub.json index 24d31ff887..75308dab20 100644 --- a/public/language/zh-CN/admin/settings/activitypub.json +++ b/public/language/zh-CN/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "生成摘要后的字符计数", "content.summary-limit-help": "当联合输出内容超过此字符限制时,将生成 摘要 ,包含该限制前的所有完整句子。(默认值:500)", "content.break-string": "注释/文章分隔符", - "content.break-string-help": "此分隔符可由高级用户在撰写新主题时手动插入。它指示 NodeBB 将该分隔符之前的内容作为摘要的一部分。若未使用此字符串,则采用字符计数备用方案。(默认值:[...])" + "content.break-string-help": "此分隔符可由高级用户在撰写新主题时手动插入。它指示 NodeBB 将该分隔符之前的内容作为摘要的一部分。若未使用此字符串,则采用字符计数备用方案。(默认值:[...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/activitypub.json b/public/language/zh-TW/admin/settings/activitypub.json index 9718851b7d..3a6e0f137a 100644 --- a/public/language/zh-TW/admin/settings/activitypub.json +++ b/public/language/zh-TW/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file From 779a372fc5b3ab56cb464cdfe5d9b0e276cc1c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 16 Mar 2026 20:31:24 -0400 Subject: [PATCH 4646/4744] test: add missing selectedCategory to world.yaml fix url of plugins that are missing it in plugin.json and look for repository.url for backup --- public/openapi/read/world.yaml | 3 +++ src/plugins/data.js | 1 + 2 files changed, 4 insertions(+) diff --git a/public/openapi/read/world.yaml b/public/openapi/read/world.yaml index 450d49e362..d2cbe0b332 100644 --- a/public/openapi/read/world.yaml +++ b/public/openapi/read/world.yaml @@ -31,6 +31,9 @@ get: properties: bookmarks: type: number + selectedCategory: + $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject + nullable: true showThumbs: type: boolean showTopicTools: diff --git a/src/plugins/data.js b/src/plugins/data.js index ba6e319e78..0078fa34b8 100644 --- a/src/plugins/data.js +++ b/src/plugins/data.js @@ -55,6 +55,7 @@ Data.loadPluginInfo = async function (pluginPath) { pluginData.description = packageData.description; pluginData.version = packageData.version; pluginData.repository = packageData.repository; + pluginData.url = pluginData.url || pluginData?.repository?.url || ''; pluginData.nbbpm = packageData.nbbpm; pluginData.path = pluginPath; } catch (err) { From 19bb37cae8f80307b17ae3eac3a2b75362610949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 16 Mar 2026 20:39:37 -0400 Subject: [PATCH 4647/4744] lint: remove unused --- public/src/client/header/notifications.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/src/client/header/notifications.js b/public/src/client/header/notifications.js index 6d833ccbd6..71c50ea726 100644 --- a/public/src/client/header/notifications.js +++ b/public/src/client/header/notifications.js @@ -5,7 +5,6 @@ define('forum/header/notifications', function () { notifications.prepareDOM = function () { const notifTrigger = $('[component="notifications"] [data-bs-toggle="dropdown"]'); - const listEl = document.querySelector('[component="notifications/list"]'); notifTrigger.on('show.bs.dropdown', async (ev) => { const notifications = await app.require('notifications'); From d458eadb6628998ae5ced5cba53677a8f61967b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 16 Mar 2026 21:07:44 -0400 Subject: [PATCH 4648/4744] add active class to selected filter show no-notifs if there are no unreads --- install/package.json | 2 +- public/src/client/header/notifications.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index ee1a0c15d6..fa702393fb 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.58", + "nodebb-theme-harmony": "2.2.59", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", "nodebb-theme-persona": "14.2.32", diff --git a/public/src/client/header/notifications.js b/public/src/client/header/notifications.js index 71c50ea726..94a23c9ecd 100644 --- a/public/src/client/header/notifications.js +++ b/public/src/client/header/notifications.js @@ -24,7 +24,8 @@ define('forum/header/notifications', function () { dropdownEl.on('click', '[data-filter]', (e) => { const filter = e.target.getAttribute('data-filter'); - + dropdownEl.find('[data-filter]').removeClass('active'); + e.target.classList.add('active'); if (filter === 'unread') { listEl.get(0).querySelectorAll('[data-nid]:not(.unread)').forEach((e) => { e.classList.toggle('hidden', true); @@ -34,6 +35,8 @@ define('forum/header/notifications', function () { e.classList.toggle('hidden', false); }); } + const visibleNotifCount = dropdownEl.find('[data-nid]:not(.hidden)').length; + dropdownEl.find('.no-notifs').toggleClass('hidden', visibleNotifCount !== 0); }); }); From d5f4a3702067d0e6f176e71fde5c287df875a39b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:11:13 -0400 Subject: [PATCH 4649/4744] fix(deps): update dependency tough-cookie to v6.0.1 (#14097) 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 fa702393fb..0238672e62 100644 --- a/install/package.json +++ b/install/package.json @@ -149,7 +149,7 @@ "timeago": "1.6.7", "tinycon": "0.6.8", "toobusy-js": "0.5.1", - "tough-cookie": "6.0.0", + "tough-cookie": "6.0.1", "undici": "^7.10.0", "validator": "13.15.26", "webpack": "5.105.4", From 73b023b430d8dcf5aaa92647286009e100f89315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Mar 2026 10:42:35 -0400 Subject: [PATCH 4650/4744] chore: up composer --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 4a9cfa33a8..3fad170317 100644 --- a/install/package.json +++ b/install/package.json @@ -97,7 +97,7 @@ "multer": "2.1.1", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.6.1", - "nodebb-plugin-composer-default": "10.3.27", + "nodebb-plugin-composer-default": "10.3.28", "nodebb-plugin-dbsearch": "6.4.1", "nodebb-plugin-emoji": "6.0.6", "nodebb-plugin-emoji-android": "4.1.1", From 3825c755e9bafed066aa305a641b47b6cddfb929 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:58:20 -0400 Subject: [PATCH 4651/4744] chore(deps): update dependency jsdom to v29 (#14100) 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 4d576dc99e..ee7cbba54c 100644 --- a/install/package.json +++ b/install/package.json @@ -172,7 +172,7 @@ "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", - "jsdom": "28.1.0", + "jsdom": "29.0.0", "lint-staged": "16.3.3", "mocha": "11.7.5", "mocha-lcov-reporter": "1.3.0", From 3765fb37f0407ca4138a67e14e8091becdcb5814 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:58:40 -0400 Subject: [PATCH 4652/4744] fix(deps): update dependency lru-cache to v11.2.7 (#14096) 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 ee7cbba54c..6019d38cdc 100644 --- a/install/package.json +++ b/install/package.json @@ -89,7 +89,7 @@ "jsonwebtoken": "9.0.3", "lodash": "4.17.23", "logrotate-stream": "0.2.9", - "lru-cache": "11.2.6", + "lru-cache": "11.2.7", "mime": "3.0.0", "mkdirp": "3.0.1", "mongodb": "7.1.0", From c26bfddfe180e66a36ccafac7a0b9ec1ac5ad235 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:58:55 -0400 Subject: [PATCH 4653/4744] fix(deps): update dependency esbuild to v0.27.4 (#14090) 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 6019d38cdc..2060ba6afd 100644 --- a/install/package.json +++ b/install/package.json @@ -67,7 +67,7 @@ "csrf-sync": "4.2.1", "daemon": "1.1.0", "diff": "8.0.3", - "esbuild": "0.27.3", + "esbuild": "0.27.4", "express": "4.22.1", "express-session": "1.19.0", "express-useragent": "2.1.0", From 06c3b88b1bb5590d333440403b57338022f22f2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:59:51 -0400 Subject: [PATCH 4654/4744] chore(deps): update commitlint monorepo to v20.5.0 (#14098) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 2060ba6afd..ddf847714e 100644 --- a/install/package.json +++ b/install/package.json @@ -162,8 +162,8 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@commitlint/cli": "20.4.4", - "@commitlint/config-angular": "20.4.4", + "@commitlint/cli": "20.5.0", + "@commitlint/config-angular": "20.5.0", "coveralls": "3.1.1", "@eslint/js": "10.0.1", "@stylistic/eslint-plugin": "5.10.0", From 902533db0659866fda51428a99320b8a6ab6e4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Mar 2026 11:56:31 -0400 Subject: [PATCH 4655/4744] fix: delete cid::privilegeMask on category.purge --- src/categories/delete.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/categories/delete.js b/src/categories/delete.js index 5f96e32676..3575107336 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -52,6 +52,7 @@ module.exports = function (Categories) { `cid:${cid}:uid:watch:state`, `cid:${cid}:children`, `cid:${cid}:tag:whitelist`, + `cid:${cid}:privilegeMask`, `${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, ]); const privilegeList = await privileges.categories.getPrivilegeList(); From 27b0fbe68593a16ea8ebe7f9a4ed2dbcb2b70b62 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 17 Mar 2026 12:06:43 -0400 Subject: [PATCH 4656/4744] fix: only show category selector on quickreply on /world --- install/package.json | 4 ++-- public/src/modules/quickreply.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/install/package.json b/install/package.json index ddf847714e..42f19561a3 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.59", + "nodebb-theme-harmony": "2.2.60", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", - "nodebb-theme-persona": "14.2.32", + "nodebb-theme-persona": "14.2.33", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.2", "nprogress": "0.2.0", diff --git a/public/src/modules/quickreply.js b/public/src/modules/quickreply.js index f081c59588..3a9573fb7b 100644 --- a/public/src/modules/quickreply.js +++ b/public/src/modules/quickreply.js @@ -19,7 +19,7 @@ define('quickreply', [ return; } - if ($('[component="topic/quickreply/container"] [component="category-selector"]')) { + if (opts?.body?.cid && $('[component="topic/quickreply/container"] [component="category-selector"]')) { categorySelector.init($('[component="category-selector"]'), { privilege: 'topics:create', selectedCategory: ajaxify.data.selectedCategory, @@ -28,6 +28,7 @@ define('quickreply', [ opts.body.cid = category.cid; }, }); + $('[component="topic/quickreply/container"] [component="topic/quickreply/category-selector"').removeClass('hidden'); } const qrDraftId = ajaxify.data.tid ? `qr:draft:tid:${ajaxify.data.tid}` : `qr:draft:cid:${opts?.body?.cid || -1}`; From 35c03e5c8e71221975a14773504cddf1a2f77308 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 17 Mar 2026 12:11:24 -0400 Subject: [PATCH 4657/4744] fix: issue where initial quickcreate post wouldn't go to the right cid --- public/src/client/world.js | 2 +- src/controllers/api.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/src/client/world.js b/public/src/client/world.js index 61492a5d9f..ec2c4ef497 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -14,7 +14,7 @@ define('forum/world', [ quickreply.init({ route: '/topics', body: { - cid: ajaxify.data.cid, + cid: config.activitypub.worldDefaultCid || ajaxify.data.cid, }, }); diff --git a/src/controllers/api.js b/src/controllers/api.js index 4fd83dcd5b..ba49ccb29c 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -103,6 +103,7 @@ apiController.loadConfig = async function (req) { }, activitypub: { probe: meta.config.activitypubEnabled && meta.config.activitypubProbe, + worldDefaultCid: meta.config.activitypubWorldDefaultCid, }, }; From 168b17e828f7d7c5dc8d553402e145243e872504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Mar 2026 12:11:28 -0400 Subject: [PATCH 4658/4744] chore: up mentions --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 3fad170317..8b4c8802f2 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.2.3", "nodebb-plugin-markdown": "13.2.4", - "nodebb-plugin-mentions": "4.8.17", + "nodebb-plugin-mentions": "4.8.18", "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.6", "nodebb-rewards-essentials": "1.0.2", From 4a01d55f6ad76c1a83e1818770fca21d20c09c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Mar 2026 12:29:22 -0400 Subject: [PATCH 4659/4744] chore: up develop --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index f32497d288..1215fbb396 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.60", + "nodebb-theme-harmony": "2.2.61", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", "nodebb-theme-persona": "14.2.33", From efaf8eb996da1220d944e4472f3f28c5a1f57688 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 17 Mar 2026 12:42:45 -0400 Subject: [PATCH 4660/4744] fix: schema fix for new api config value --- public/openapi/read/config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/openapi/read/config.yaml b/public/openapi/read/config.yaml index 16d2043667..273cf56076 100644 --- a/public/openapi/read/config.yaml +++ b/public/openapi/read/config.yaml @@ -184,4 +184,6 @@ get: type: object properties: probe: - type: number \ No newline at end of file + type: number + worldDefaultCid: + type: string \ No newline at end of file From 7c65471ba41f3d79c91ccea5e386aee27bb88e1c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 17 Mar 2026 12:51:14 -0400 Subject: [PATCH 4661/4744] fix: close notif drawer on item click, fix crash in module --- public/src/modules/notifications.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 70dc54e3bb..599644848e 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -43,13 +43,13 @@ define('notifications', [ app.parseAndTranslate('partials/notifications_list', { notifications }, function (html) { notifList.html(html); notifList.off('click').on('click', '[component="notifications/item/link"]', function (ev) { - const notifEl = $(this); + const notifEl = $(this).parents('[data-nid]'); if (scrollToPostIndexIfOnPage(notifEl)) { ev.stopPropagation(); ev.preventDefault(); - if (triggerEl) { - triggerEl.dropdown('toggle'); - } + } + if (triggerEl) { + triggerEl.dropdown('toggle'); } const unread = notifEl.hasClass('unread'); @@ -61,6 +61,7 @@ define('notifications', [ }); components.get('notifications').on('click', '.mark-all-read', () => { Notifications.markAllRead(); + triggerEl.dropdown('toggle'); }); components.get('notifications').on('click', `[href="${config.relative_path}/notifications"]`, () => { triggerEl.dropdown('toggle'); From bea46026389ac5127cca7bc31b3de3d602a31d25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:11:50 -0400 Subject: [PATCH 4662/4744] chore(deps): update dependency lint-staged to v16.4.0 (#14099) 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 1215fbb396..abb4fd3946 100644 --- a/install/package.json +++ b/install/package.json @@ -173,7 +173,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "29.0.0", - "lint-staged": "16.3.3", + "lint-staged": "16.4.0", "mocha": "11.7.5", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From 483a63c67b5919f6ef56daaecad77d9f0ce7c79d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 18 Mar 2026 09:07:37 +0000 Subject: [PATCH 4663/4744] Latest translations and fallbacks --- public/language/bg/admin/settings/activitypub.json | 2 +- public/language/bg/admin/settings/post.json | 4 ++-- public/language/bg/login.json | 2 +- public/language/bg/world.json | 14 +++++++------- public/language/de/admin/settings/activitypub.json | 2 +- public/language/de/world.json | 12 ++++++------ 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/public/language/bg/admin/settings/activitypub.json b/public/language/bg/admin/settings/activitypub.json index 4923a53f7f..089529287a 100644 --- a/public/language/bg/admin/settings/activitypub.json +++ b/public/language/bg/admin/settings/activitypub.json @@ -51,5 +51,5 @@ "content.summary-limit-help": "Когато дадено съдържание е федерирано в посока извън този форум и превишава този брой знаци, ще се създаде обобщение, което се състои от всички завършени изречения до ограничението. (по подразбиране: 500)", "content.break-string": "Разделител на съдържанието", "content.break-string-help": "Този разделител може да бъде поставен ръчно от потребителите с по-високи правомощия, когато създават нови теми. Той казва на NodeBB да използва съдържанието до така посоченото място като част от обобщението. Ако този разделител не бъде използван, се прилага зададеният брой на знаците. (по подразбиране: [...])", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.world-default-cid": "Идентификатор на категория по подразбиране при съставяне на страница в „Света“" } \ No newline at end of file diff --git a/public/language/bg/admin/settings/post.json b/public/language/bg/admin/settings/post.json index f39ea34f3c..d937bcddd2 100644 --- a/public/language/bg/admin/settings/post.json +++ b/public/language/bg/admin/settings/post.json @@ -29,7 +29,7 @@ "restrictions.stale-help": "Ако дадена тема е определена като „стара“, то потребителите, които се опитат да пишат в нея, ще получат предупредително съобщение. (0 = изключено)", "timestamp": "Време", "timestamp.cut-off": "Използване на дата след (в брой дни)", - "timestamp.cut-off-help": "Датите и времената ще бъдат показвани относително (напр. „преди 3 часа“ или „преди 5 дни“), и преведени на множество\n\\t\\t\\t\\t\\tезици. След определено време, този текст ще започне да показва самите дата и час, според езика на потребителя\n\\t\\t\\t\\t\\t(напр. „5 ноември 2016 15:30“).
    (По подразбиране: 30, тоест един месец). Ако зададете 0, винаги ще се изписват дати, а ако оставите полето празно, времето ще бъде винаги относително.", + "timestamp.cut-off-help": "Датите и времената ще бъдат показвани относително (напр. „преди 3 часа“ или „преди 5 дни“), и преведени на множество\n\t\t\t\t\tезици. След определено време, този текст ще започне да показва самите дата и час, според езика на потребителя\n\t\t\t\t\t(напр. „5 ноември 2016 15:30“).
    (По подразбиране: 30, тоест един месец). Ако зададете 0, винаги ще се изписват дати, а ако оставите полето празно, времето ще бъде винаги относително.", "timestamp.necro-threshold": "Мъртва граница (в дни)", "timestamp.necro-threshold-help": "Между публикациите ще бъде показано съобщение, ако времето между тях е по-дълго от мъртвата граница. (По подразбиране: 7, или една седмица). Задайте 0 за изключване.
    ", "timestamp.topic-views-interval": "Интервал за увеличаване на броя на преглеждания на темите (в минути)", @@ -51,7 +51,7 @@ "signature.hide-duplicates": "Скриване на дублираните подписи в темите", "signature.max-length": "Максимална дължина на подписите", "composer": "Настройки за съставянето", - "composer-help": "Следващите настройки определят функционалностите и/или вида на елемента за съставяне на\n\\t\\t\\t\\tпубликация, който се използва от потребителите, когато те създават нови теми или отговорят в съществуващи.", + "composer-help": "Следващите настройки определят функционалностите и/или вида на елемента за съставяне на\n\t\t\t\tпубликация, който се използва от потребителите, когато те създават нови теми или отговорят в съществуващи.", "composer.show-help": "Показване на раздела „Помощ“", "composer.enable-plugin-help": "Позволяване на добавките да добавят съдържание в раздела за помощ", "composer.custom-help": "Персонализиран текст за помощ", diff --git a/public/language/bg/login.json b/public/language/bg/login.json index 4914291c5c..1dd0452064 100644 --- a/public/language/bg/login.json +++ b/public/language/bg/login.json @@ -6,7 +6,7 @@ "alternative-logins": "Други начини за вписване", "failed-login-attempt": "Неуспешно вписване", "login-successful": "Вие влязохте успешно!", - "dont-have-account": "Нямате регистрация?", + "dont-have-account": "Нямате акаунт?", "logged-out-due-to-inactivity": "Вие излязохте автоматично от администраторския контролен панел, поради бездействие.", "caps-lock-enabled": "Главните букви са включени" } \ No newline at end of file diff --git a/public/language/bg/world.json b/public/language/bg/world.json index 6e332f3fe8..6ca101eab0 100644 --- a/public/language/bg/world.json +++ b/public/language/bg/world.json @@ -1,8 +1,8 @@ { "name": "Свят", - "latest": "Latest", - "latest-local": "Latest (Local)", - "latest-all": "Последни (всички)", + "latest": "Най-нови", + "latest-local": "Най-нови (локални)", + "latest-all": "Най-нови (всички)", "popular-day": "Популярни (за деня)", "popular-week": "Популярни (за седмицата)", "popular-month": "Популярни (за месеца)", @@ -18,10 +18,10 @@ "help.federating": "По същия начин, ако потребители извън този форум започнат да следват Вас, тогава Вашите публикации ще започнат да се появяват в техните приложения и уеб сайтове.", "help.next-generation": "Това е новото поколение социална мрежа. Започнете да допринасяте още днес!", - "onboard.title": "A world of content at your fingertips…", - "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", - "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", - "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", + "onboard.title": "Свят изпълнен със съдържание, на една ръка разстояние…", + "onboard.what": "Гледайте на това като на глобална опашка за откриване на съдържание. Тук се събират интересни дискусии от различни места в мрежата и от други общности – на едно място.", + "onboard.why": "Можете да разглеждате препоръчаните популярни теми, но най-добрият начин да ползвате тази функционалност, е да я нагласите по свой вкус. Ако си създадете акаунт, ще можете да следвате определени хора или теми – така ще филтрирате шума и ще виждате само това, което Ви интересува.", + "onboard.how": "Готови? Създайте си акаунт и започнете да следвате другите, да получавате известия когато хората Ви отговарят, както и да запазвате любимите си открития.", "category-search": "Търсене на категория…", "see-more": "Вижте повече", diff --git a/public/language/de/admin/settings/activitypub.json b/public/language/de/admin/settings/activitypub.json index bc119ed32b..98ea96fe66 100644 --- a/public/language/de/admin/settings/activitypub.json +++ b/public/language/de/admin/settings/activitypub.json @@ -51,5 +51,5 @@ "content.summary-limit-help": "Wenn Inhalte rausgeschickt werden, die diese Zeichenanzahl überschreiten, wird eine Zusammenfassung erstellt, die alle vollständigen Sätze vor dieser Grenze enthält. (Standard: 500)", "content.break-string": "Notiz/Artikel-Trennzeichen", "content.break-string-help": "\nDieses Trennzeichen kann von Power-Usern beim Erstellen neuer Themen manuell eingefügt werden. Es sagt NodeBB, dass der Inhalt bis zu dieser Stelle als Teil der Zusammenfassungverwendet werden soll. Wenn diese Zeichenfolge nicht benutzt wird, greift die Standardregel für die Zeichenanzahl. (Standard: [...])", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.world-default-cid": "Standard-Kategorie-ID für den "Welt" Seiten-Editor" } \ No newline at end of file diff --git a/public/language/de/world.json b/public/language/de/world.json index f03c9cd773..ba57a0e92d 100644 --- a/public/language/de/world.json +++ b/public/language/de/world.json @@ -1,7 +1,7 @@ { "name": "Welt", - "latest": "Latest", - "latest-local": "Latest (Local)", + "latest": "Neueste", + "latest-local": "Neueste (Lokal)", "latest-all": "Neueste (Alle)", "popular-day": "Beliebt (Tag)", "popular-week": "Beliebt (Woche)", @@ -18,10 +18,10 @@ "help.federating": "Wenn Leute außerhalb dieses Forums anfangen, dirzu folgen, werden deine Beiträge auch in diesen Apps und auf diesen Websites angezeigt.", "help.next-generation": "Das ist die nächste Generation der sozialen Medien, leg noch heute los!", - "onboard.title": "A world of content at your fingertips…", - "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", - "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", - "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", + "onboard.title": "Eine Welt voller Inhalte, immer griffbereit…", + "onboard.what": "Betrachte das als deinen weltweiten Entdeckungs-Feed. Hier findest du interessante Diskussionen aus dem gesamten Internet und anderen Communities – alles an einem Ort.", + "onboard.why": "Du kannst sehen, was gerade im Trend liegt, am besten nutzt du diesen Feed, indem du ihn ganz nach deinen Wünschen gestaltest. Wenn du ein Konto erstellst, kannst du bestimmten Creators und Themen folgen, um den Ballast auszusortieren und nur das zu sehen, was dir wichtig ist.", + "onboard.how": "Bist du bereit, loszulegen? Erstelle ein Konto, um anderen zu folgen, Benachrichtigungen zu erhalten, wenn dir ein Mensch antwortet, und deine Lieblingsfunde zu speichern.", "category-search": "Such eine Kategorie...", "see-more": "Mehr sehen", From 11b98583b17bba11eb12608c325caae5d630d992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Mar 2026 08:48:46 -0400 Subject: [PATCH 4664/4744] check triggerEl in callbacks --- public/src/modules/notifications.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index 599644848e..1e68cb724a 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -48,9 +48,8 @@ define('notifications', [ ev.stopPropagation(); ev.preventDefault(); } - if (triggerEl) { - triggerEl.dropdown('toggle'); - } + + triggerEl?.dropdown('toggle'); const unread = notifEl.hasClass('unread'); if (!unread) { @@ -61,10 +60,10 @@ define('notifications', [ }); components.get('notifications').on('click', '.mark-all-read', () => { Notifications.markAllRead(); - triggerEl.dropdown('toggle'); + triggerEl?.dropdown('toggle'); }); components.get('notifications').on('click', `[href="${config.relative_path}/notifications"]`, () => { - triggerEl.dropdown('toggle'); + triggerEl?.dropdown('toggle'); }); Notifications.handleUnreadButton(notifList); From 7e2c7db3b216605dea9d96f6e09c903767147273 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Mar 2026 09:10:10 -0400 Subject: [PATCH 4665/4744] fix: schema fix for new api config value --- public/openapi/read/admin/config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/openapi/read/admin/config.yaml b/public/openapi/read/admin/config.yaml index a58405fb84..044c0b87a1 100644 --- a/public/openapi/read/admin/config.yaml +++ b/public/openapi/read/admin/config.yaml @@ -153,6 +153,8 @@ get: properties: probe: type: number + worldDefaultCid: + type: string acpLang: type: string openOutgoingLinksInNewTab: From 895997b2b873aea1b320173ec3620d110195e8d2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Mar 2026 09:58:23 -0400 Subject: [PATCH 4666/4744] docs: wrong type for worldDefaultCid --- public/openapi/read/admin/config.yaml | 2 +- public/openapi/read/config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/openapi/read/admin/config.yaml b/public/openapi/read/admin/config.yaml index 044c0b87a1..457e63fcc9 100644 --- a/public/openapi/read/admin/config.yaml +++ b/public/openapi/read/admin/config.yaml @@ -154,7 +154,7 @@ get: probe: type: number worldDefaultCid: - type: string + type: number acpLang: type: string openOutgoingLinksInNewTab: diff --git a/public/openapi/read/config.yaml b/public/openapi/read/config.yaml index 273cf56076..f4ae1dfe87 100644 --- a/public/openapi/read/config.yaml +++ b/public/openapi/read/config.yaml @@ -186,4 +186,4 @@ get: probe: type: number worldDefaultCid: - type: string \ No newline at end of file + type: number \ No newline at end of file From 58d3aa77cd0f4383a699433461e24dacdb6887ef Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Mar 2026 10:28:54 -0400 Subject: [PATCH 4667/4744] feat: add /world as a potential home page route --- src/controllers/helpers.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 7101170357..3f635a4a80 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -423,6 +423,10 @@ helpers.getHomePageRoutes = async function (uid) { route: 'categories', name: 'Categories', }, + { + route: 'world', + name: 'World', + }, { route: 'unread', name: 'Unread', From cc606677cb76356d3270dec0badf7efe9c109488 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Mar 2026 11:09:08 -0400 Subject: [PATCH 4668/4744] fix: cold load redirect should only affect guests --- src/controllers/accounts/profile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index 21f6e6a191..93be1d2b4e 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -24,7 +24,7 @@ profileController.get = async function (req, res, next) { return next(); } - if (meta.config.activitypubEnabled && !res.locals.isAPI && !utils.isNumber(userData.uid)) { + if (!req.loggedIn && meta.config.activitypubEnabled && !res.locals.isAPI && !utils.isNumber(userData.uid)) { return helpers.redirect(res, `/outgoing?url=${encodeURIComponent(userData.uid)}`); } From 44e78e4775204ae00b0176932eb9bd239b276f34 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Mar 2026 12:13:50 -0400 Subject: [PATCH 4669/4744] fix: sync follow counts on local and remote follows, #14105 --- src/activitypub/helpers.js | 2 +- src/activitypub/inbox.js | 19 +++++++++-------- src/controllers/activitypub/index.js | 12 ++++------- src/user/follow.js | 31 +++++++++++++++++++++------- 4 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index a6dae62caf..3b6e7549aa 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -473,7 +473,7 @@ Helpers.generateCollection = async ({ set, method, count, page, perPage, url }) } else if (set) { method = method.bind(null, set); } - count = count || await db.sortedSetCard(set); + count = count ?? await db.sortedSetCard(set); const pageCount = Math.max(1, Math.ceil(count / perPage)); let items = []; let paginate = true; diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index d0ee3976a4..cd14579362 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -517,11 +517,12 @@ inbox.follow = async (req) => { } const now = Date.now(); - await db.sortedSetAdd(`followersRemote:${id}`, now, actor); - await db.sortedSetAdd(`followingRemote:${actor}`, now, id); // for following backreference (actor pruning) - - const followerRemoteCount = await db.sortedSetCard(`followersRemote:${id}`); - await user.setUserField(id, 'followerRemoteCount', followerRemoteCount); + await Promise.all([ + db.sortedSetAdd(`followersRemote:${id}`, now, actor), + db.sortedSetAdd(`followingRemote:${actor}`, now, id), // for following backreference (actor pruning) + user.syncFollowCounts(id, false, true), + user.syncFollowCounts(actor, true, false), + ]); activitypub.actors._followerCache.del(id); await user.onFollow(actor, id); @@ -600,8 +601,8 @@ inbox.accept = async (req) => { db.sortedSetAdd(`followingRemote:${id}`, timestamp, actor), db.sortedSetAdd(`followersRemote:${actor}`, timestamp, id), // for followers backreference and notes assertion checking ]); - const followingRemoteCount = await db.sortedSetCard(`followingRemote:${id}`); - await user.setUserField(id, 'followingRemoteCount', followingRemoteCount); + await user.syncFollowCounts(id, true, false); + await user.syncFollowCounts(actor, false, true); } else if (localType === 'category') { if (!await db.isSortedSetMember(`followRequests:cid.${id}`, actor)) { if (await db.isSortedSetMember(`cid:${id}:following`, actor)) return; // already following @@ -649,9 +650,9 @@ inbox.undo = async (req) => { await Promise.all([ db.sortedSetRemove(`followersRemote:${id}`, actor), db.sortedSetRemove(`followingRemote:${actor}`, id), + user.syncFollowCounts(id, false, true), + user.syncFollowCounts(actor, true, false), ]); - const followerRemoteCount = await db.sortedSetCard(`followerRemote:${id}`); - await user.setUserField(id, 'followerRemoteCount', followerRemoteCount); notifications.rescind(`follow:${id}:uid:${actor}`); activitypub.actors._followerCache.del(id); break; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 086e90933b..b7eaa334c8 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -66,10 +66,8 @@ Controller.fetch = async (req, res, next) => { }; Controller.getFollowing = async (req, res) => { - const { followingCount, followingRemoteCount } = await user.getUserFields(req.params.uid, ['followingCount', 'followingRemoteCount']); - const totalItems = parseInt(followingCount || 0, 10) + parseInt(followingRemoteCount || 0, 10); - - const count = totalItems; + const followingCount = await user.getUserField(req.params.uid, 'followingCount'); + const count = parseInt(followingCount, 10); const collection = await activitypub.helpers.generateCollection({ method: user.getFollowing.bind(null, req.params.uid), count, @@ -92,10 +90,8 @@ Controller.getFollowing = async (req, res) => { }; Controller.getFollowers = async (req, res) => { - const { followerCount, followerRemoteCount } = await user.getUserFields(req.params.uid, ['followerCount', 'followerRemoteCount']); - const totalItems = parseInt(followerCount || 0, 10) + parseInt(followerRemoteCount || 0, 10); - - const count = totalItems; + const followerCount = await user.getUserField(req.params.uid, 'followerCount'); + const count = parseInt(followerCount, 10); const collection = await activitypub.helpers.generateCollection({ method: user.getFollowers.bind(null, req.params.uid), count, diff --git a/src/user/follow.js b/src/user/follow.js index 7e561d07e5..d14251d38c 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -58,15 +58,32 @@ module.exports = function (User) { ]); } - const [followingCount, followingRemoteCount, followerCount, followerRemoteCount] = await db.sortedSetsCard([ - `following:${uid}`, `followingRemote:${uid}`, `followers:${theiruid}`, `followersRemote:${theiruid}`, - ]); - await Promise.all([ - User.setUserField(uid, 'followingCount', followingCount + followingRemoteCount), - User.setUserField(theiruid, 'followerCount', followerCount + followerRemoteCount), - ]); + User.syncFollowCounts(uid, true, false); + User.syncFollowCounts(theiruid, false, true); } + User.syncFollowCounts = async function (uid, following, followers) { + const sets = []; + const property = []; + if (following) { + property.push('followingCount'); + sets.push(`following:${uid}`, `followingRemote:${uid}`); + } + if (followers) { + property.push('followerCount'); + sets.push(`followers:${uid}`, `followersRemote:${uid}`); + }; + + const values = await db.sortedSetsCard(sets); + const payload = property.reduce((payload, cur, idx) => { + const sum = values[idx * 2] + values[(idx * 2) + 1]; + payload[cur] = sum; + return payload; + }, {}); + + await User.setUserFields(uid, payload); + }; + User.getFollowing = async function (uid, start, stop) { return await getFollow(uid, 'following', start, stop); }; From fa7c1a522bc98d6fab15b9dc3c5308c1de196e67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:50:13 -0400 Subject: [PATCH 4670/4744] fix(deps): update dependency nodemailer to v8.0.3 (#14104) 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 abb4fd3946..9243ba4190 100644 --- a/install/package.json +++ b/install/package.json @@ -113,7 +113,7 @@ "nodebb-theme-peace": "2.2.57", "nodebb-theme-persona": "14.2.33", "nodebb-widget-essentials": "7.0.43", - "nodemailer": "8.0.2", + "nodemailer": "8.0.3", "nprogress": "0.2.0", "passport": "0.7.0", "passport-http-bearer": "1.0.1", From f51e1b2a28eaf5a7defb56bc9b3c5011044fb448 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 10:32:57 -0400 Subject: [PATCH 4671/4744] fix(deps): update dependency cronstrue to v3.14.0 (#14107) 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 9243ba4190..c31cbf4889 100644 --- a/install/package.json +++ b/install/package.json @@ -62,7 +62,7 @@ "connect-redis": "9.0.0", "cookie-parser": "1.4.7", "cron": "4.4.0", - "cronstrue": "3.13.0", + "cronstrue": "3.14.0", "cropperjs": "1.6.2", "csrf-sync": "4.2.1", "daemon": "1.1.0", From ebe709da89b356f5f4bfdace77e46d451c286580 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Mar 2026 11:36:28 -0400 Subject: [PATCH 4672/4744] fix: call syncfollowcounts on unfollow as well --- src/activitypub/out.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/activitypub/out.js b/src/activitypub/out.js index 45d902fb05..5242e6df6d 100644 --- a/src/activitypub/out.js +++ b/src/activitypub/out.js @@ -471,7 +471,8 @@ Out.undo.follow = enabledCheck(async (type, id, actor) => { db.sortedSetRemove(`followingRemote:${id}`, actor), db.sortedSetRemove(`followRequests:uid.${id}`, actor), db.sortedSetRemove(`followersRemote:${actor}`, id), - db.decrObjectField(`user:${id}`, 'followingRemoteCount'), + user.syncFollowCounts(id, true, false), + user.syncFollowCounts(actor, false, true), ]); } else if (type === 'cid') { await Promise.all([ From 8ca34e74bd7715315f0cd76943e1044e9e0a134f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Mar 2026 12:02:26 -0400 Subject: [PATCH 4673/4744] fix: improve idempotency of ap test --- test/activitypub/actors.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/activitypub/actors.js b/test/activitypub/actors.js index e659713de3..e185984c1b 100644 --- a/test/activitypub/actors.js +++ b/test/activitypub/actors.js @@ -737,6 +737,10 @@ describe('Pruning', () => { }); describe('Users', () => { + before(async function () { + this.current = await activitypub.actors.prune(); + }); + it('should do nothing if the user is newer than the prune cutoff', async () => { const { id: uid } = helpers.mocks.person(); await activitypub.actors.assert([uid]); @@ -762,7 +766,7 @@ describe('Pruning', () => { assert(result.counts.deleted >= 1); }); - it('should do nothing if the user has some content (e.g. a topic)', async () => { + it('should do nothing if the user has some content (e.g. a topic)', async function () { const { cid } = await categories.create({ name: utils.generateUUID() }); const { id: uid } = helpers.mocks.person(); const { id, note } = helpers.mocks.note({ @@ -776,7 +780,7 @@ describe('Pruning', () => { const result = await activitypub.actors.prune(); assert.strictEqual(result.counts.deleted, 0); - assert.strictEqual(result.counts.preserved, 1); + assert.strictEqual(result.counts.preserved, this.current.counts.preserved + 1); assert.strictEqual(result.counts.missing, 0); }); }); From e9da60db3bb3553f6cb8c633e8907d3b88cebbdf Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 19 Mar 2026 16:20:32 +0000 Subject: [PATCH 4674/4744] chore: incrementing version number - v4.10.0 --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index c31cbf4889..9e90dc0dac 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.9.2", + "version": "4.10.0", "homepage": "https://www.nodebb.org", "repository": { "type": "git", @@ -204,4 +204,4 @@ "url": "https://github.com/barisusakli" } ] -} +} \ No newline at end of file From c480df9e9c8ad5b54bbb7ce77c5d1e087ef76bb7 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 19 Mar 2026 16:20:32 +0000 Subject: [PATCH 4675/4744] chore: update changelog for v4.10.0 --- CHANGELOG.md | 231 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6f7ec88f..8a6be3f690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,234 @@ +#### v4.10.0 (2026-03-19) + +##### Chores + +* **deps:** + * update dependency lint-staged to v16.4.0 (#14099) (bea46026) + * update commitlint monorepo to v20.5.0 (#14098) (06c3b88b) + * update dependency jsdom to v29 (#14100) (3825c755) + * update commitlint monorepo to v20.4.4 (#14088) (02f8ea2c) + * update dependency lint-staged to v16.3.3 (#14075) (ac45b719) + * update dependency @stylistic/eslint-plugin to v5.10.0 (#14065) (9e2c6b67) + * update docker/build-push-action action to v7 (#14066) (6d8c4493) + * update docker/metadata-action action to v6 (#14067) (73b5bce5) + * update docker/setup-buildx-action action to v4 (#14060) (d7de8cf6) + * update docker/login-action action to v4 (#14054) (8c15096f) + * update dependency lint-staged to v16.3.2 (#14048) (ddd6db0f) + * update commitlint monorepo to v20.4.3 (#14047) (07881cbf) + * update dependency lint-staged to v16.3.1 (#14029) (2e4ee9f1) + * update dependency globals to v17.4.0 (#14035) (32864460) + * update github artifact actions (#14027) (aec68c6b) + * update postgres docker tag to v18.3 (#14023) (b69dbc38) + * update dependency nyc to v18 (#14011) (dc1ce5e1) +* up develop (4a01d55f) +* up mentions (168b17e8) +* up composer (73b023b4) +* up harmony (ec4e87ff) +* incrementing version number - v4.9.2 (e6846052) +* update changelog for v4.9.2 (2c00b137) +* incrementing version number - v4.9.1 (72e44c86) +* incrementing version number - v4.9.0 (3fdd1bef) +* incrementing version number - v4.8.1 (713ae0c0) +* incrementing version number - v4.8.0 (3fac737a) +* incrementing version number - v4.7.2 (cd419d8a) +* incrementing version number - v4.7.1 (afb88805) +* incrementing version number - v4.7.0 (e82d40f8) +* incrementing version number - v4.6.3 (9fc5b0f3) +* incrementing version number - v4.6.2 (f98747db) +* incrementing version number - v4.6.1 (f47aa678) +* incrementing version number - v4.6.0 (ee395bc5) +* incrementing version number - v4.5.2 (ad2da639) +* incrementing version number - v4.5.1 (69f4b61f) +* incrementing version number - v4.5.0 (f05c5d06) +* incrementing version number - v4.4.6 (074043ad) +* incrementing version number - v4.4.5 (6f106923) +* incrementing version number - v4.4.4 (d323af44) +* incrementing version number - v4.4.3 (d354c2eb) +* incrementing version number - v4.4.2 (55c510ae) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* incrementing version number - v4.0.3 (2b65c735) +* 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.admin-settings-activitypub (aeb53043) + * fallback strings for new resources: nodebb.world (e9063a63) + * fallback strings for new resources: nodebb.world (61414fa4) + * fallback strings for new resources: nodebb.world (702f7c62) + * fallback strings for new resources: nodebb.admin-settings-general (e3ba38f2) + * fallback strings for new resources: nodebb.admin-manage-users (a46f0136) + * fallback strings for new resources: nodebb.admin-advanced-jobs (8e050353) + * fallback strings for new resources: nodebb.admin-menu (5a8a1661) + * fallback strings for new resources: nodebb.error (a17cd6c7) + * fallback strings for new resources: nodebb.world (4d44e913) + * fallback strings for new resources: nodebb.admin-dashboard (49a21a1f) + * fallback strings for new resources: nodebb.admin-dashboard (858d84ff) + +##### Documentation Changes + +* wrong type for worldDefaultCid (895997b2) + +##### New Features + +* add /world as a potential home page route (58d3aa77) +* add category selector to /world quick composer (2f5021e5) +* ability to show only local posts in /world (44e65b8d) +* #14094, notification drawer UX improvements (6c01a5d8) +* allow 3 profile pics (#14092) (533ae69c) +* screenshot upload in ACP, send fallback brand icons in manifest, serve assets for richer PWA install UI (75a6dfff) +* category group actor outbox, #14083 (b317cdd3) +* new ap mocks, now publishing user outboxes (f848393e) +* show cronjobs in acp (#14068) (3c0a6540) +* redirect cold requests to remote resources to their canonical source, #14043 (2b12f8b5) +* include alt text in image/attachment property federating out (ca5aee10) + +##### Bug Fixes + +* improve idempotency of ap test (8ca34e74) +* call syncfollowcounts on unfollow as well (ebe709da) +* sync follow counts on local and remote follows, #14105 (44e78e47) +* cold load redirect should only affect guests (cc606677) +* schema fix for new api config value (7e2c7db3) +* close notif drawer on item click, fix crash in module (7c65471b) +* schema fix for new api config value (efaf8eb9) +* issue where initial quickcreate post wouldn't go to the right cid (35c03e5c) +* only show category selector on quickreply on /world (27b0fbe6) +* delete cid::privilegeMask on category.purge (902533db) +* bump themes (3aa8d5ba) +* removing topic tools/checkbox from /world for guests, reword guest CTA in /world (53286625) +* add back 'after' query param handling in /world that was removed accidentally (67a93da5) +* restrict contextmenu preventDefault to the checkbox only (2eb0964d) +* long-press support for topicSelect, #14045 (d1e1a008) +* debug log (58da9036) +* restore guest access to /world, default to latest(all) (1aa5ca88) +* bump harmony (10859455) +* restored popular calculation behaviour that was broken by e2131d1d2e1c6f14cb8867ac7e22840da3f4c63f, removed followingOnly arg passing for popular (1af83564) +* imagesLoaded integration for handleBack in world.js (38a1da46) +* merged chat notifications if all the messages are from the same user (26bb60ef) +* type (59dd22ca) +* type (40fecd01) +* merged chat notifications if all the messages are from the same user (6147a4d0) +* screenshot fallback (09c54127) +* buildRecipients to handle if local uids are passed in followers (a8f081c0) +* update Like/Dislike to have addressees in activity (c8e349ca) +* accidental hardcoded cid (464bc275) +* skip AP cache on context processing methods (a3ee7447) +* skip AP cache on context processing methods (10e4d579) +* cache key (9eea12ec) +* cache key (74fa77dd) +* missing orderedItems on category outbox index retrieval (8496e1ef) +* #14084, fix tags not getting properly removed from topics (0a94cecb) +* group badge on group details page (52e8ede8) +* return digest header only if it is set to something (aka not null) (1ad9ce5f) +* missing page parseInt (9978af59) +* dont add self username when clicking reply (9fcaad38) +* notifs (ed3a3672) +* also use tx.compile in chat notif merge (6d22e33a) +* publish id with user outbox, fixes #13478 (c08a45a5) +* merge with txArgs (e1d0e2a0) +* merged notification translations (34b68109) +* derped handleBack in world.js (ac483152) +* syntax error on undefined value (6b3b3e7e) +* filter out image attachments from remote data if they are already embedded in content (40b8544f) +* update thumbs loading logic to always include post attachments as part of thumbs (prior: was controlled by thumbsOnly flag orshowPostUploadsAsThumbnail setting) (c2d190e1) +* #14072, world to call thumbs with thumbsOnly filter (f1976168) +* promises in groups.leave (f826e629) +* #14071, duplicate items loaded via IS on /world (d29f1fbd) +* #14043, cold-load redirect should only affect guests (5a7316b1) +* hacking handleBack module to work with world page (971c8603) +* bump web-push (27e12a28) +* patch translateKey to wrap arguments in first string isolate (FSI) and Pop Directional Isolate (PDI) characters (59f19ba4) +* add missing db call (4d1d1c86) +* #14061, world.js show more buttons on infinite scroll (0aead782) +* update clamp-fade to use mask-image, add background to btn-link on Brite skin (9bc1b400) +* #14046, sneak in a mention to the community in mocked replies (b8ef027c) +* world page 'see more' bugs (cc733631) +* #13239, unescape custom user field values (9e69b9ad) +* restore `preview` as it is now supported by BridyFed (8a371d23) +* missing done (9604a0cd) +* bump harmony, #14042 (b02cdaa9) +* #14042, adopt plugin-feed's show-more/less logic/scss (43f2951a) +* update minimum title length default to zero to allow title-less topics via composer (6bfe3cd0) +* parent cid (f567d970) +* skip parsing of duplicate emoji tags (363cad29) +* **deps:** + * update dependency cronstrue to v3.14.0 (#14107) (f51e1b2a) + * update dependency nodemailer to v8.0.3 (#14104) (fa7c1a52) + * update dependency esbuild to v0.27.4 (#14090) (c26bfddf) + * update dependency lru-cache to v11.2.7 (#14096) (3765fb37) + * update dependency tough-cookie to v6.0.1 (#14097) (d5f4a370) + * update dependency nodebb-theme-peace to v2.2.57 (#14076) (cd08a5e4) + * update dependency nodemailer to v8.0.2 (#14077) (add3c651) + * update dependency satori to v0.25.0 (#14037) (817c38b9) + * update dependency postcss to v8.5.8 (#14051) (942619db) + * update dependency pg to v8.20.0 (#14058) (9cc2c2f9) + * update dependency pg-cursor to v2.19.0 (#14059) (045a7073) + * update dependency terser-webpack-plugin to v5.3.17 (#14052) (4410d884) + * update dependency multer to v2.1.1 (#14050) (de22b7a9) + * update dependency webpack to v5.105.4 (#14053) (3dc3b2e2) + * update dependency fs-extra to v11.3.4 (#14049) (cfb6145e) + * update dependency satori to v0.19.3 (#14036) (250911b7) + * update dependency nodebb-plugin-emoji to v6.0.6 (#14034) (7434103c) + * update dependency sitemap to v9.0.1 (#14028) (f97484c2) + * update dependency webpack to v5.105.3 (#14022) (38787a2d) + * update dependency multer to v2.1.0 (#14024) (9b65e316) + * update dependency pg-cursor to v2.18.0 (#14026) (54810cfd) + * update dependency pg to v8.19.0 (#14025) (badb57f2) + * update dependency autoprefixer to v10.4.27 (#14021) (054b4aa6) + +##### Other Changes + +* remove unused (19bb37ca) +* jobs.json (e1b6e617) +* remove unused (d6d3116e) +* remove unused (b50a10df) + +##### Performance Improvements + +* switch to set, remove parseFloat in redis (09de6fb9) +* move out nconf.get and isClientScript regex (4d55ee0a) +* move out nconf.get and isClientScript regex (f2bca332) +* make a single round trip for set(s)Remove (bcbb7bc4) +* cache groups:createtime (380d9895) + +##### Refactors + +* /world sorting logic to always use topics/sorted logic (e2131d1d) +* move to data (36bf3f16) +* get rid of cleanupUids use missing set (6569ea51) +* remove async.series, use batch.processSortedSet (894248e6) +* get rid of helper function (1cc77343) +* switch to cursor (1c7daf0d) +* use set (d9344140) +* pass in cid to rename/remove (fe4a22fb) +* remove admin.themes.getInstalled (92d72f67) + +##### Tests + +* add missing selectedCategory to world.yaml (779a372f) +* exclude uploadScreenshot from routeMap parsing test (ff1e1b92) +* make tests happy (08bed89b) +* fix test maybe (25f6088f) +* add one more topic to tag test (e01cb104) +* cleaner user.delete test (215d6440) +* set minimumtitlelength for test (7429b5d4) +* added test to ensure that Likes do not get processed when privilege is rescinded (8cba65cd) +* break apart inbox handling tests to its own file in test/activitypub (06e0bd6a) +* add debug test to see if failing test is due to race condition (e3119c76) +* fix spec (6dd9f734) + #### v4.9.2 (2026-03-11) ##### Chores From 5ed7eb6461163cef47f6cece7f489793dff1e602 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 20 Mar 2026 09:07:39 +0000 Subject: [PATCH 4676/4744] Latest translations and fallbacks --- .../it/admin/settings/activitypub.json | 2 +- .../language/it/admin/settings/general.json | 4 +- public/language/it/world.json | 14 +- public/language/ja/admin/admin.json | 22 +-- .../language/ja/admin/advanced/database.json | 4 +- public/language/ja/user.json | 166 +++++++++--------- public/language/ja/users.json | 10 +- 7 files changed, 111 insertions(+), 111 deletions(-) diff --git a/public/language/it/admin/settings/activitypub.json b/public/language/it/admin/settings/activitypub.json index 320400e725..2a37d00c8a 100644 --- a/public/language/it/admin/settings/activitypub.json +++ b/public/language/it/admin/settings/activitypub.json @@ -51,5 +51,5 @@ "content.summary-limit-help": "Quando il contenuto federato supera questo limite di caratteri, sarà generato un riepilogo che comprende tutte le frasi complete precedenti a tale limite. (Predefinito: 500)\n ", "content.break-string": "Delimitatore di nota/articolo", "content.break-string-help": "Questo delimitatore può essere inserito manualmente dagli utenti esperti durante la composizione di nuove discussioni. Indica a NodeBB di usare il contenuto fino a quel punto come parte del riepilogo. Se questa stringa non viene usata, si applica il conteggio dei caratteri di riserva. (Predefinito: [...])", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.world-default-cid": "ID predefinito della categoria per "Mondo" editor di pagine" } \ No newline at end of file diff --git a/public/language/it/admin/settings/general.json b/public/language/it/admin/settings/general.json index ebb00fe63f..8eb63538df 100644 --- a/public/language/it/admin/settings/general.json +++ b/public/language/it/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Descrizione sito", "keywords": "Parole chiave del sito", "keywords-placeholder": "Parole chiave che descrivono la vostra comunità, separate da virgole", - "logo-and-icons": "Media & Branding", + "logo-and-icons": "Media e Branding", "logo.image": "Immagine", "logo.image-placeholder": "Percorso del logo da visualizzare sull'intestazione del forum", "logo.upload": "Carica", @@ -36,7 +36,7 @@ "maskable-icon": "Icona Mascherabile (Schermata Iniziale)", "maskable-icon.help": "Dimensioni e formato consigliati: 512x512, solo formato PNG. Se non è specificata alcuna icona mascherabile, NodeBB tornerà a utilizzare l'Icona Touch.", "screenshot": "Screenshot", - "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", + "screenshot.help": "Dimensione e formato consigliati: tra 320px e 3480px, solo formato JPG e PNG. Se non viene specificato uno screenshot, NodeBB userà uno screenshot generico", "outgoing-links": "Link in uscita", "outgoing-links.warning-page": "Usa pagina di avviso per i link in uscita", "search": "Cerca", diff --git a/public/language/it/world.json b/public/language/it/world.json index 3af26b4563..a26996f6c9 100644 --- a/public/language/it/world.json +++ b/public/language/it/world.json @@ -1,8 +1,8 @@ { "name": "Mondo", - "latest": "Latest", - "latest-local": "Latest (Local)", - "latest-all": "Latest (All)", + "latest": "Ultimo", + "latest-local": "Ultimo (Locale)", + "latest-all": "Ultimo (Tutti)", "popular-day": "Popolare (Giorno)", "popular-week": "Popolare (Settimana)", "popular-month": "Popolare (Mese)", @@ -18,10 +18,10 @@ "help.federating": "Allo stesso modo, se gli utenti esterni a questo forum iniziano a seguirti, i tuoi post inizieranno ad apparire anche su quelle app e quei siti web.", "help.next-generation": "Questa è la prossima generazione di social media, inizia a contribuire oggi!", - "onboard.title": "A world of content at your fingertips…", - "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", - "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", - "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", + "onboard.title": "Un mondo di contenuti a portata di mano…", + "onboard.what": "Consideralo come il tuo feed globale di scoperte. Riunisce in un unico posto discussioni interessanti da tutto il web e da altre comunità.", + "onboard.why": "Mentre puoi sfogliare ciò che è di tendenza ora, il modo migliore per usare questo feed è farlo tuo. Creando un account, puoi seguire creatori e discussioni specifiche per filtrare i contenuti superflui e vedere solo ciò che conta per te.", + "onboard.how": "Pronto a tuffarti? Crea un account per iniziare a seguire gli altri, ricevere notifiche quando qualcuno ti risponde e salvare i tuoi contenuti preferiti.", "category-search": "Trova una categoria...", "see-more": "Vedi di più", diff --git a/public/language/ja/admin/admin.json b/public/language/ja/admin/admin.json index e3a5942b3e..bf90ee8f25 100644 --- a/public/language/ja/admin/admin.json +++ b/public/language/ja/admin/admin.json @@ -4,15 +4,15 @@ "acp-title": "%1| NodeBB管理画面", "settings-header-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", - "min": "Min:", - "max": "Max:", - "view": "View", - "edit": "Edit", - "add": "Add", - "select-icon": "Select Icon" + "changes-saved": "変更を保存しました", + "changes-saved-message": "NodeBBの設定変更が保存されました", + "changes-not-saved": "変更が保存されませんでした", + "changes-not-saved-message": "NodeBBが変更の保存中に問題が発生しました。(%1)", + "save-changes": "変更を保存", + "min": "最小:", + "max": "最大:", + "view": "表示", + "edit": "編集", + "add": "追加", + "select-icon": "アイコンを選択" } \ No newline at end of file diff --git a/public/language/ja/admin/advanced/database.json b/public/language/ja/admin/advanced/database.json index f1377df5f7..6393c7d006 100644 --- a/public/language/ja/admin/advanced/database.json +++ b/public/language/ja/admin/advanced/database.json @@ -22,7 +22,7 @@ "mongo.bytes-out": "バイトアウト", "mongo.num-requests": "リクエスト数", "mongo.raw-info": "MongoDBのRaw情報", - "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + "mongo.unauthorized": "NodeBBはMongoDBデータベースから関連する統計をクエリできませんでした。NodeBBが使用するユーザーに「admin」データベースの「clusterMonitor」ロールが含まれていることを確認してください。", "redis": "Redis", "redis.version": "Redisのバージョン", @@ -47,6 +47,6 @@ "redis.raw-info": "RedisのRaw情報", "postgres": "Postgres", - "postgres.version": "PostgreSQL Version", + "postgres.version": "PostgreSQLバージョン", "postgres.raw-info": "Postgres のRaw情報" } diff --git a/public/language/ja/user.json b/public/language/ja/user.json index 2b03a9bb65..f9884cbafd 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -1,33 +1,33 @@ { - "user-menu": "User menu", + "user-menu": "ユーザーメニュー", "banned": "BANされた", - "unbanned": "Unbanned", - "muted": "Muted", - "unmuted": "Unmuted", + "unbanned": "BAN解除", + "muted": "ミュート中", + "unmuted": "ミュート解除", "offline": "オフライン", - "deleted": "削除されました", + "deleted": "削除済み", "username": "ユーザー名", "joindate": "参加日", "postcount": "投稿数", "email": "メール", "confirm-email": "メールアドレスを確認", "account-info": "アカウント情報", - "admin-actions-label": "Administrative Actions", + "admin-actions-label": "管理操作", "ban-account": "BANアカウント", "ban-account-confirm": "本当にこのユーザーをBANしますか?", - "unban-account": "禁止アカウント解除します", - "mute-account": "Mute Account", - "unmute-account": "Unmute Account", - "delete-account": "アカウント削除します", - "delete-account-as-admin": "Delete Account", - "delete-content": "Delete Account Content", - "delete-all": "Delete Account and Content", - "delete-account-confirm": "Are you sure you want to anonymize your posts and delete your account?
    This action is irreversible and you will not be able to recover any of your data

    Enter your password to confirm that you wish to destroy this account.", - "delete-this-account-confirm": "Are you sure you want to delete this account while leaving its contents behind?
    This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

    ", - "delete-account-content-confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
    This action is irreversible and you will not be able to recover any data

    ", - "delete-all-confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
    This action is irreversible and you will not be able to recover any data

    ", - "account-deleted": "アカウントが解除されました", - "account-content-deleted": "Account content deleted", + "unban-account": "BAN解除", + "mute-account": "アカウントをミュート", + "unmute-account": "ミュートを解除", + "delete-account": "アカウントを削除", + "delete-account-as-admin": "アカウントを削除", + "delete-content": "アカウントのコンテンツを削除", + "delete-all": "アカウントコンテンツを削除", + "delete-account-confirm": "投稿を匿名化してアカウントを削除してもよろしいですか?
    この操作は取り消せず、データを復元できません

    アカウントを削除するにはパスワードを入力してください。", + "delete-this-account-confirm": "コンテンツを残したままこのアカウントを削除してもよろしいですか?
    この操作は取り消せず、投稿は匿名化され、削除されたアカウントとの関連付けは復元できません

    ", + "delete-account-content-confirm": "このアカウントのコンテンツ(投稿/スレッド/アップロード)を削除してもよろしいですか?
    この操作は取り消せず、データを復元できません

    ", + "delete-all-confirm": "このアカウントとすべてのコンテンツ(投稿/スレッド/アップロード)を削除してもよろしいですか?
    この操作は取り消せず、データを復元できません

    ", + "account-deleted": "アカウントが削除されました", + "account-content-deleted": "アカウントのコンテンツを削除しました", "fullname": "フルネーム", "website": "ウェブサイト", "location": "ロケーション", @@ -39,147 +39,147 @@ "reputation": "評価", "bookmarks": "ブックマーク", "watched-categories": "ウォッチ中のカテゴリ", - "watched-tags": "Watched tags", - "change-all": "Change All", + "watched-tags": "ウォッチ中のタグ", + "change-all": "すべて変更", "watched": "ウォッチ済み", "ignored": "無視済み", - "read": "Read", + "read": "既読", "default-category-watch-state": "デフォルトのカテゴリウォッチ状態", "followers": "フォロワー", "following": "フォロー中", - "shares": "Shares", + "shares": "共有", "blocks": "ブロックの設定", - "blocked-users": "Blocked users", + "blocked-users": "ブロック中のユーザー", "block-toggle": "ブロックを切替", "block-user": "ユーザーをブロック", "unblock-user": "ブロックを解除", - "aboutme": "About me", + "aboutme": "私について", "signature": "署名", "birthday": "誕生日", "chat": "チャット", "chat-with": "%1とチャットを続ける", "new-chat-with": "%1とチャットを始める", - "view-remote": "View Original", + "view-remote": "元の投稿を表示", "flag-profile": "プロフィールを報告する", - "profile-flagged": "Already flagged", + "profile-flagged": "既に報告済み", "follow": "フォロー", "unfollow": "フォロー解除", - "cancel-follow": "Cancel follow request", + "cancel-follow": "フォローリクエストをキャンセル", "more": "つづき", "profile-update-success": "プロフィールを更新しました!", "change-picture": "画像を変更", "change-username": "ユーザー名の変更", - "change-email": "メール変更", - "email-updated": "Email Updated", + "change-email": "メールを変更", + "email-updated": "メールを更新しました", "email-same-as-password": "現在のパスワードを入力して続行してください – 新しいメールアドレスをもう一度入力しました", "edit": "編集", "edit-profile": "プロフィールを編集", - "default-picture": "元のアイコン", + "default-picture": "デフォルトアイコン", "uploaded-picture": "アップロード済みの画像", "upload-new-picture": "新しい画像をアップロード", - "upload-new-picture-from-url": "URLにより新しい写真をアップします", + "upload-new-picture-from-url": "URLから新しい画像をアップロード", "current-password": "現在のパスワード", - "new-password": "New Password", + "new-password": "新しいパスワード", "change-password": "パスワードを変更", - "change-password-error": "無効のパスワード!", + "change-password-error": "無効なパスワードです!", "change-password-error-wrong-current": "現在のパスワードは正しくありません!", - "change-password-error-same-password": "Your new password matches your current password, please use a new password.", + "change-password-error-same-password": "新しいパスワードが現在のパスワードと同じです。別のパスワードを使用してください。", "change-password-error-match": "パスワードは一致しません!", "change-password-error-privileges": "パスワードを更新する権限はありません。", "change-password-success": "パスワードを更新しました!", "confirm-password": "パスワードを再入力", "password": "パスワード", - "username-taken-workaround": "このユーザー名はすでに使用されています。いまのユーザー名は %1 です。", - "password-same-as-username": "パスワードがユーザー名と同じですから、他のパスワードを使って下さい。", - "password-same-as-email": "パスワードがメールアドレスと同じです。他のパスワードを使って下さい。", + "username-taken-workaround": "ご希望のユーザー名は既に使用されていたため、少し変更しました。現在のユーザー名は %1 です。", + "password-same-as-username": "パスワードがユーザー名と同じです。別のパスワードを選択してください。", + "password-same-as-email": "パスワードがメールアドレスと同じです。別のパスワードを選択してください。", "weak-password": "弱いパスワード", "upload-picture": "画像をアップロード", "upload-a-picture": "画像をアップロード", - "remove-uploaded-picture": "アップした写真を取り消します", + "remove-uploaded-picture": "アップロードした画像を削除", "upload-cover-picture": "カバー写真をアップロード", "remove-cover-picture-confirm": "カバー写真を削除してもよろしいですか?", "crop-picture": "画像を切り抜く", "upload-cropped-picture": "切り抜いてアップロード", - "avatar-background-colour": "Avatar background colour", + "avatar-background-colour": "アバターの背景色", "settings": "設定", "show-email": "メールアドレスを表示", "show-fullname": "フルネームで表示", "restrict-chats": "フォローしたユーザーからのチャットメッセージだけを許可する", - "disable-incoming-chats": "Disable incoming chat messages ", - "chat-allow-list": "Allow chat messages from the following users", - "chat-deny-list": "Deny chat messages from the following users", - "chat-list-add-user": "Add user", + "disable-incoming-chats": "受信チャットメッセージを無効にする ", + "chat-allow-list": "以下のユーザーからのチャットメッセージを許可", + "chat-deny-list": "以下のユーザーからのチャットメッセージを拒否", + "chat-list-add-user": "ユーザーを追加", "digest-label": "お知らせを購読する", - "digest-description": "この掲示板のアップデートを受信する", + "digest-description": "設定したスケジュールに従って、このフォーラムのメール更新(新しい通知とトピック)を購読する", "digest-off": "オフ", "digest-daily": "デイリー", "digest-weekly": "ウィークリー", - "digest-biweekly": "Bi-Weekly", + "digest-biweekly": "隔週", "digest-monthly": "マンスリー", "has-no-follower": "フォロワーはまだいません :(", "follows-no-one": "フォロー中のユーザーはまだいません :(", "has-no-posts": "このユーザーはまだ一つも投稿していません", - "has-no-best-posts": "This user does not have any upvoted posts yet.", + "has-no-best-posts": "このユーザーはまだ高評価された投稿がありません", "has-no-topics": "このユーザーはまだ一つもスレッドを作っていません", "has-no-watched-topics": "このユーザーはまだ一つもスレッドをウォッチしていません", - "has-no-ignored-topics": "この利用者はまだトピックを無視していません。", - "has-no-read-topics": "This user hasn't read any topics yet.", + "has-no-ignored-topics": "このユーザーはまだスレッドを無視していません", + "has-no-read-topics": "このユーザーはまだスレッドを読んでいません", "has-no-upvoted-posts": "このユーザーはまだ一つも投稿に高評価を付けていません。", "has-no-downvoted-posts": "このユーザーはまだ一つも投稿に低評価を付けていません。", - "has-no-controversial-posts": "This user does not have any downvoted posts yet.", + "has-no-controversial-posts": "このユーザーはまだ低評価された投稿がありません", "has-no-blocks": "ブロック中のユーザーはいません。", - "has-no-shares": "This user has not shared any topics.", - "email-hidden": "メールアドレスを非表示", + "has-no-shares": "このユーザーはまだスレッドを共有していません", + "email-hidden": "メールアドレスは非表示", "hidden": "非表示", "paginate-description": "無限スクロールの代わりに、投稿やスレッドをページ別で切り替える。", - "topics-per-page": "Topics per page", - "posts-per-page": "Posts per page", - "category-topic-sort": "Category topic sort", - "topic-post-sort": "Topic post sort", + "topics-per-page": "ページごとのスレッド数", + "posts-per-page": "ページごとの投稿数", + "category-topic-sort": "カテゴリのスレッド並び替え", + "topic-post-sort": "スレッドの投稿並び替え", "max-items-per-page": "最大 %1", - "acp-language": "ページ言語の管理", - "notifications": "Notifications", - "upvote-notif-freq": "投票の通知頻度", + "acp-language": "管理画面の言語", + "notifications": "通知", + "upvote-notif-freq": "高評価通知の頻度", "upvote-notif-freq.all": "すべての高評価", "upvote-notif-freq.first": "はじめの投稿", - "upvote-notif-freq.everyTen": "10の投票数", - "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", - "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.everyTen": "10件ごと", + "upvote-notif-freq.threshold": "1、5、10、25、50、100、150、200…のとき", + "upvote-notif-freq.logarithmic": "10、100、1000…のとき", "upvote-notif-freq.disabled": "無効", "notification-type-web": "Web", - "notification-type-email": "Email", + "notification-type-email": "メール", "browsing": "ブラウジングの設定", - "unread.cutoff": "Unread cutoff (Maximum %1 days)", - "unread.cutoff-help": "Topics will be marked read if they have not been updated within this number of days.", + "unread.cutoff": "既読の基準(最大 %1 日)", + "unread.cutoff-help": "この日数以内に更新されていないスレッドは既読としてマークされます。", "open-links-in-new-tab": "外部リンクを新しいタブで開く", "enable-topic-searching": "スレッド内検索を有効にする", - "topic-search-help": "有効にしたら、インースレッドの検索はブラウザの既定機能を無視して、スクリーンに示したよりスレッド内からの全部を検索します", - "update-url-with-post-index": "Update url with post index while browsing topics", + "topic-search-help": "有効にすると、スレッド内検索がブラウザのデフォルトのページ検索動作を上書きし、画面に表示されている内容だけでなく、スレッド全体を検索できるようになります。", + "update-url-with-post-index": "スレッド閲覧中にURLを投稿インデックスで更新", "scroll-to-my-post": "返信を投稿した後、新しい投稿を表示する", "follow-topics-you-reply-to": "あなたが返信したスレッドをウォッチする", "follow-topics-you-create": "あなたが作成したスレッドをウォッチする", "grouptitle": "グループ題名", - "group-order-help": "Select a group and use the arrows to order titles", - "show-group-title": "Show group title", - "hide-group-title": "Hide group title", - "order-group-up": "Order group up", - "order-group-down": "Order group down", + "group-order-help": "グループを選択し、矢印でタイトルの順序を変更", + "show-group-title": "グループタイトルを表示", + "hide-group-title": "グループタイトルを非表示", + "order-group-up": "グループを上に移動", + "order-group-down": "グループを下に移動", "no-group-title": "グループ名がありません", - "select-skin": "スキンを選んで下さい", - "default": "Default (%1)", - "no-skin": "No Skin", - "select-homepage": "ホームページの設定", + "select-skin": "スキンを選択", + "default": "デフォルト(%1)", + "no-skin": "スキンなし", + "select-homepage": "ホームページを選択", "homepage": "ホームページ", - "homepage-description": "フォーラムのホームに指定するページを選んで下さい。デフォルトのホームページを使用する場合は’None’を選んで下さい。", + "homepage-description": "フォーラムのホームページとして使用するページを選択するか、「なし」でデフォルトのホームページを使用します。", "custom-route": "カスタムホームページルート", - "custom-route-help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "custom-route-help": "先頭のスラッシュなしでルート名を入力してください(例: \\\"recent\\\" または \\\"category/2/general-discussion\\\")", "sso.title": "シングルサインオンサービス", "sso.associated": "関連付けられています", - "sso.not-associated": "ここを押して、関連付けられています", - "sso.dissociate": "離脱する", - "sso.dissociate-confirm-title": "離脱の際に確認する", - "sso.dissociate-confirm": "アカウントと %1 の関連付けを解除しますか?", - "info.invited-by": "Invited by", + "sso.not-associated": "ここをクリックして関連付け", + "sso.dissociate": "関連付けを解除", + "sso.dissociate-confirm-title": "関連付け解除の確認", + "sso.dissociate-confirm": "%1 からアカウントの関連付けを解除してもよろしいですか?", + "info.invited-by": "招待者", "info.latest-flags": "最近のフラグ", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/ja/users.json b/public/language/ja/users.json index d0408692ee..4a20e13aef 100644 --- a/public/language/ja/users.json +++ b/public/language/ja/users.json @@ -1,20 +1,20 @@ { - "all-users": "All Users", - "followed-users": "Followed Users", + "all-users": "すべてのユーザー", + "followed-users": "フォロー中のユーザー", "latest-users": "新しいユーザー", "top-posters": "最も投稿したユーザー", "most-reputation": "最も評価されたユーザー", "most-flags": "最も多いフラグ", "search": "検索", "enter-username": "ユーザー名を入力", - "search-user-for-chat": "Search for a user to start chat", + "search-user-for-chat": "チャットを開始するユーザーを検索", "load-more": "もっと見る", "users-found-search-took": "%1人のユーザーを見つけました!(検索まで%2秒掛かりました。)", "filter-by": "フィルタ", "online-only": "オンラインのみ", "invite": "招待", - "prompt-email": "Emails:", - "groups-to-join": "Groups to be joined when invite is accepted:", + "prompt-email": "メールアドレス:", + "groups-to-join": "招待承認時に参加するグループ:", "invitation-email-sent": "招待メールが%1に送られました。", "user-list": "ユーザー一覧", "recent-topics": "最新スレッド", From 361134f9a21ff180da9fd6a5e83a1343056da382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 20 Mar 2026 10:38:03 -0400 Subject: [PATCH 4677/4744] fix: share url for ap posts, fallback to window.location.href if pid doesnt exist closes #14109 --- public/src/modules/share.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/public/src/modules/share.js b/public/src/modules/share.js index e9e6c27489..94478d30cf 100644 --- a/public/src/modules/share.js +++ b/public/src/modules/share.js @@ -17,7 +17,7 @@ define('share', ['hooks'], function (hooks) { $('#content').off('shown.bs.dropdown', '.share-dropdown').on('shown.bs.dropdown', '.share-dropdown', function () { const postLink = $(this).find('.post-link'); - postLink.val(baseUrl + getPostUrl($(this))); + postLink.val(getPostUrl($(this))); // without the setTimeout can't select the text in the input setTimeout(function () { @@ -77,9 +77,10 @@ define('share', ['hooks'], function (hooks) { } function getPostUrl(clickedElement) { - const pid = parseInt(clickedElement.parents('[data-pid]').attr('data-pid'), 10); - const path = '/post' + (pid ? '/' + (pid) : ''); - return baseUrl + config.relative_path + path; + const pid = clickedElement.parents('[data-pid]').attr('data-pid'); + return pid ? + `${baseUrl + config.relative_path}/post/${pid}` : + window.location.href; } return share; From f5e2a0f49ac64fefdc8df0d94ea64b31d6e121a2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 20 Mar 2026 11:15:24 -0400 Subject: [PATCH 4678/4744] fix: #14112, federation/rules and relays ACP pages not refreshing table properly on changes, basic form validation --- .../language/en-GB/admin/settings/activitypub.json | 1 + public/src/admin/federation/relays.js | 9 ++++++--- public/src/admin/federation/rules.js | 12 +++++++++--- src/controllers/write/admin.js | 5 ++++- src/views/admin/partials/activitypub/relays.tpl | 2 +- src/views/admin/partials/activitypub/rules.tpl | 2 +- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/public/language/en-GB/admin/settings/activitypub.json b/public/language/en-GB/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/en-GB/admin/settings/activitypub.json +++ b/public/language/en-GB/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/src/admin/federation/relays.js b/public/src/admin/federation/relays.js index 190720318d..77fade6c1b 100644 --- a/public/src/admin/federation/relays.js +++ b/public/src/admin/federation/relays.js @@ -24,7 +24,7 @@ function setupRelays() { case 'relays.remove': { const url = subselector.closest('tr').getAttribute('data-url'); del(`/admin/activitypub/relays/${encodeURIComponent(url)}`, {}).then(async (data) => { - const html = await app.parseAndTranslate('admin/settings/activitypub', 'relays', { relays: data }); + const html = await app.parseAndTranslate('admin/federation/relays', 'relays', { relays: data }); const tbodyEl = document.querySelector('#relays tbody'); if (tbodyEl) { $(tbodyEl).html(html); @@ -41,10 +41,13 @@ function throwModal() { render('admin/partials/activitypub/relays', {}).then(function (html) { const submit = function () { const formEl = modal.find('form').get(0); - const payload = Object.fromEntries(new FormData(formEl)); + if (!formEl.reportValidity()) { + return false; + } + const payload = Object.fromEntries(new FormData(formEl)); post('/admin/activitypub/relays', payload).then(async (data) => { - const html = await app.parseAndTranslate('admin/settings/activitypub', 'relays', { relays: data }); + const html = await app.parseAndTranslate('admin/federation/relays', 'relays', { relays: data }); const tbodyEl = document.querySelector('#relays tbody'); if (tbodyEl) { $(tbodyEl).html(html); diff --git a/public/src/admin/federation/rules.js b/public/src/admin/federation/rules.js index fca956e567..78b058886d 100644 --- a/public/src/admin/federation/rules.js +++ b/public/src/admin/federation/rules.js @@ -29,7 +29,7 @@ function setupRules() { case 'rules.delete': { const rid = subselector.closest('tr').getAttribute('data-rid'); del(`/admin/activitypub/rules/${rid}`, {}).then(async (data) => { - const html = await render('admin/settings/activitypub', { rules: data }, 'rules'); + const html = await render('admin/federation/rules', { rules: data }, 'rules'); const tbodyEl = document.querySelector('#rules tbody'); if (tbodyEl) { tbodyEl.innerHTML = html; @@ -68,15 +68,21 @@ function throwModal() { render('admin/partials/activitypub/rules', {}).then(function (html) { const submit = function () { const formEl = modal.find('form').get(0); - const payload = Object.fromEntries(new FormData(formEl)); + if (!formEl.reportValidity()) { + return false; + } + const payload = Object.fromEntries(new FormData(formEl)); post('/admin/activitypub/rules', payload).then(async (data) => { - const html = await render('admin/settings/activitypub', { rules: data }, 'rules'); + const html = await render('admin/federation/rules', { rules: data }, 'rules'); const tbodyEl = document.querySelector('#rules tbody'); if (tbodyEl) { tbodyEl.innerHTML = html; } + modal.modal('hide'); }).catch(error); + + return false; }; const modal = bootbox.dialog({ title: '[[admin/settings/activitypub:rules.add]]', diff --git a/src/controllers/write/admin.js b/src/controllers/write/admin.js index 53662f6b34..65621bc853 100644 --- a/src/controllers/write/admin.js +++ b/src/controllers/write/admin.js @@ -110,8 +110,11 @@ Admin.activitypub.reorderRules = async (req, res) => { helpers.formatApiResponse(200, res, await activitypub.rules.list()); }; -Admin.activitypub.addRelay = async (req, res) => { +Admin.activitypub.addRelay = async (req, res, next) => { const { url } = req.body; + if (!url) { + return next(); + } await activitypub.relays.add(url); helpers.formatApiResponse(200, res, await activitypub.relays.list()); diff --git a/src/views/admin/partials/activitypub/relays.tpl b/src/views/admin/partials/activitypub/relays.tpl index 8f53b167f0..be8fd4e510 100644 --- a/src/views/admin/partials/activitypub/relays.tpl +++ b/src/views/admin/partials/activitypub/relays.tpl @@ -6,6 +6,6 @@
    - +
    \ No newline at end of file diff --git a/src/views/admin/partials/activitypub/rules.tpl b/src/views/admin/partials/activitypub/rules.tpl index 33273942b5..eaf463f6dc 100644 --- a/src/views/admin/partials/activitypub/rules.tpl +++ b/src/views/admin/partials/activitypub/rules.tpl @@ -14,7 +14,7 @@
    - +

    From 26e892347ea6dfbe5a35bb16c3cc0ef2ec1c36d8 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 20 Mar 2026 15:17:28 +0000 Subject: [PATCH 4679/4744] chore(i18n): fallback strings for new resources: nodebb.admin-settings-activitypub --- public/language/ar/admin/settings/activitypub.json | 1 + public/language/az/admin/settings/activitypub.json | 1 + public/language/bg/admin/settings/activitypub.json | 1 + public/language/bn/admin/settings/activitypub.json | 1 + public/language/cs/admin/settings/activitypub.json | 1 + public/language/da/admin/settings/activitypub.json | 1 + public/language/de/admin/settings/activitypub.json | 1 + public/language/el/admin/settings/activitypub.json | 1 + public/language/en-US/admin/settings/activitypub.json | 1 + public/language/en-x-pirate/admin/settings/activitypub.json | 1 + public/language/es/admin/settings/activitypub.json | 1 + public/language/et/admin/settings/activitypub.json | 1 + public/language/fa-IR/admin/settings/activitypub.json | 1 + public/language/fi/admin/settings/activitypub.json | 1 + public/language/fr/admin/settings/activitypub.json | 1 + public/language/gl/admin/settings/activitypub.json | 1 + public/language/he/admin/settings/activitypub.json | 1 + public/language/hr/admin/settings/activitypub.json | 1 + public/language/hu/admin/settings/activitypub.json | 1 + public/language/hy/admin/settings/activitypub.json | 1 + public/language/id/admin/settings/activitypub.json | 1 + public/language/it/admin/settings/activitypub.json | 1 + public/language/ja/admin/settings/activitypub.json | 1 + public/language/ko/admin/settings/activitypub.json | 1 + public/language/lt/admin/settings/activitypub.json | 1 + public/language/lv/admin/settings/activitypub.json | 1 + public/language/ms/admin/settings/activitypub.json | 1 + public/language/nb/admin/settings/activitypub.json | 1 + public/language/nl/admin/settings/activitypub.json | 1 + public/language/nn-NO/admin/settings/activitypub.json | 1 + public/language/pl/admin/settings/activitypub.json | 1 + public/language/pt-BR/admin/settings/activitypub.json | 1 + public/language/pt-PT/admin/settings/activitypub.json | 1 + public/language/ro/admin/settings/activitypub.json | 1 + public/language/ru/admin/settings/activitypub.json | 1 + public/language/rw/admin/settings/activitypub.json | 1 + public/language/sc/admin/settings/activitypub.json | 1 + public/language/sk/admin/settings/activitypub.json | 1 + public/language/sl/admin/settings/activitypub.json | 1 + public/language/sq-AL/admin/settings/activitypub.json | 1 + public/language/sr/admin/settings/activitypub.json | 1 + public/language/sv/admin/settings/activitypub.json | 1 + public/language/th/admin/settings/activitypub.json | 1 + public/language/tr/admin/settings/activitypub.json | 1 + public/language/uk/admin/settings/activitypub.json | 1 + public/language/ur/admin/settings/activitypub.json | 1 + public/language/vi/admin/settings/activitypub.json | 1 + public/language/zh-CN/admin/settings/activitypub.json | 1 + public/language/zh-TW/admin/settings/activitypub.json | 1 + 49 files changed, 49 insertions(+) diff --git a/public/language/ar/admin/settings/activitypub.json b/public/language/ar/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/ar/admin/settings/activitypub.json +++ b/public/language/ar/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/az/admin/settings/activitypub.json b/public/language/az/admin/settings/activitypub.json index d68d9dbdaf..743357311d 100644 --- a/public/language/az/admin/settings/activitypub.json +++ b/public/language/az/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrlə", "count": "Bu NodeBB hazırda %1 server(lər)dən xəbərdardır", diff --git a/public/language/bg/admin/settings/activitypub.json b/public/language/bg/admin/settings/activitypub.json index 089529287a..3cc4a2c35c 100644 --- a/public/language/bg/admin/settings/activitypub.json +++ b/public/language/bg/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "В изчакване", "relays.state-1": "Само приемане", "relays.state-2": "Активен", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Филтриране", "count": "Този NodeBB в момента знае за наличието на %1 сървър(а)", diff --git a/public/language/bn/admin/settings/activitypub.json b/public/language/bn/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/bn/admin/settings/activitypub.json +++ b/public/language/bn/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/cs/admin/settings/activitypub.json b/public/language/cs/admin/settings/activitypub.json index 7cbf3281c2..6cb1acc7ad 100644 --- a/public/language/cs/admin/settings/activitypub.json +++ b/public/language/cs/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Čeká na schválení", "relays.state-1": "Pouze příjem", "relays.state-2": "Aktivní", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrování", "count": "Tento NodeBB momentálně vi o %1 serveru/serverech.", diff --git a/public/language/da/admin/settings/activitypub.json b/public/language/da/admin/settings/activitypub.json index 49819c963e..649b7a2fd5 100644 --- a/public/language/da/admin/settings/activitypub.json +++ b/public/language/da/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrering", "count": "Denne NodeBB instans er lige nu bevidst om %1 server(e)", diff --git a/public/language/de/admin/settings/activitypub.json b/public/language/de/admin/settings/activitypub.json index 98ea96fe66..3dc4cc4a5e 100644 --- a/public/language/de/admin/settings/activitypub.json +++ b/public/language/de/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Ausstehend", "relays.state-1": "Nur Empfang", "relays.state-2": "Aktiv", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filterung", "count": "Dieses NodeBB kennt derzeit %1 Server", diff --git a/public/language/el/admin/settings/activitypub.json b/public/language/el/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/el/admin/settings/activitypub.json +++ b/public/language/el/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/en-US/admin/settings/activitypub.json b/public/language/en-US/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/en-US/admin/settings/activitypub.json +++ b/public/language/en-US/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/en-x-pirate/admin/settings/activitypub.json b/public/language/en-x-pirate/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/en-x-pirate/admin/settings/activitypub.json +++ b/public/language/en-x-pirate/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/es/admin/settings/activitypub.json b/public/language/es/admin/settings/activitypub.json index 033001a12f..bff8ef9958 100644 --- a/public/language/es/admin/settings/activitypub.json +++ b/public/language/es/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/et/admin/settings/activitypub.json b/public/language/et/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/et/admin/settings/activitypub.json +++ b/public/language/et/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/fa-IR/admin/settings/activitypub.json b/public/language/fa-IR/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/fa-IR/admin/settings/activitypub.json +++ b/public/language/fa-IR/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/fi/admin/settings/activitypub.json b/public/language/fi/admin/settings/activitypub.json index 678232c912..83b3a399eb 100644 --- a/public/language/fi/admin/settings/activitypub.json +++ b/public/language/fi/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/fr/admin/settings/activitypub.json b/public/language/fr/admin/settings/activitypub.json index 2fde051d4e..f6fb57eb86 100644 --- a/public/language/fr/admin/settings/activitypub.json +++ b/public/language/fr/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "En attente", "relays.state-1": "Réception uniquement", "relays.state-2": "Actif", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrage", "count": "Ce NodeBB connaît actuellement %1 serveur(s)", diff --git a/public/language/gl/admin/settings/activitypub.json b/public/language/gl/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/gl/admin/settings/activitypub.json +++ b/public/language/gl/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/he/admin/settings/activitypub.json b/public/language/he/admin/settings/activitypub.json index cf39104619..6be5a095f0 100644 --- a/public/language/he/admin/settings/activitypub.json +++ b/public/language/he/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "ממתין", "relays.state-1": "קבלה בלבד", "relays.state-2": "פעיל", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "סינון", "count": "NodeBB זה מודע כרגע ל-%1 שרתים", diff --git a/public/language/hr/admin/settings/activitypub.json b/public/language/hr/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/hr/admin/settings/activitypub.json +++ b/public/language/hr/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/hu/admin/settings/activitypub.json b/public/language/hu/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/hu/admin/settings/activitypub.json +++ b/public/language/hu/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/hy/admin/settings/activitypub.json b/public/language/hy/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/hy/admin/settings/activitypub.json +++ b/public/language/hy/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/id/admin/settings/activitypub.json b/public/language/id/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/id/admin/settings/activitypub.json +++ b/public/language/id/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/it/admin/settings/activitypub.json b/public/language/it/admin/settings/activitypub.json index 2a37d00c8a..a898066330 100644 --- a/public/language/it/admin/settings/activitypub.json +++ b/public/language/it/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "In sospeso", "relays.state-1": "Solo ricezione", "relays.state-2": "Attivo", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtraggio", "count": "Questo NodeBB è attualmente a conoscenza di %1 server", diff --git a/public/language/ja/admin/settings/activitypub.json b/public/language/ja/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/ja/admin/settings/activitypub.json +++ b/public/language/ja/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/ko/admin/settings/activitypub.json b/public/language/ko/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/ko/admin/settings/activitypub.json +++ b/public/language/ko/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/lt/admin/settings/activitypub.json b/public/language/lt/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/lt/admin/settings/activitypub.json +++ b/public/language/lt/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/lv/admin/settings/activitypub.json b/public/language/lv/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/lv/admin/settings/activitypub.json +++ b/public/language/lv/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/ms/admin/settings/activitypub.json b/public/language/ms/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/ms/admin/settings/activitypub.json +++ b/public/language/ms/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/nb/admin/settings/activitypub.json b/public/language/nb/admin/settings/activitypub.json index 63597bafc7..d012ab0428 100644 --- a/public/language/nb/admin/settings/activitypub.json +++ b/public/language/nb/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/nl/admin/settings/activitypub.json b/public/language/nl/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/nl/admin/settings/activitypub.json +++ b/public/language/nl/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/nn-NO/admin/settings/activitypub.json b/public/language/nn-NO/admin/settings/activitypub.json index c573484437..a74d1f5bf9 100644 --- a/public/language/nn-NO/admin/settings/activitypub.json +++ b/public/language/nn-NO/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrer etter", "count": "Denne NodeBB-en er for tida klar over %1 server(ar)", diff --git a/public/language/pl/admin/settings/activitypub.json b/public/language/pl/admin/settings/activitypub.json index 66f6baa799..1b7c990981 100644 --- a/public/language/pl/admin/settings/activitypub.json +++ b/public/language/pl/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Oczekujący", "relays.state-1": "Tylko odbiór", "relays.state-2": "Aktywny", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrowanie", "count": "NodeBB obecnie wykrywa 1% serwerów", diff --git a/public/language/pt-BR/admin/settings/activitypub.json b/public/language/pt-BR/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/pt-BR/admin/settings/activitypub.json +++ b/public/language/pt-BR/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/pt-PT/admin/settings/activitypub.json b/public/language/pt-PT/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/pt-PT/admin/settings/activitypub.json +++ b/public/language/pt-PT/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/ro/admin/settings/activitypub.json b/public/language/ro/admin/settings/activitypub.json index f4d9a3d629..f40ffb147d 100644 --- a/public/language/ro/admin/settings/activitypub.json +++ b/public/language/ro/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "În Așteptare", "relays.state-1": "Doar Primește", "relays.state-2": "Activ", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtrează", "count": "NodeBB cunoaște acum %1 server(e)", diff --git a/public/language/ru/admin/settings/activitypub.json b/public/language/ru/admin/settings/activitypub.json index 11d3ce79b5..ab61a8f523 100644 --- a/public/language/ru/admin/settings/activitypub.json +++ b/public/language/ru/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "На рассмотрении", "relays.state-1": "Receiving only", "relays.state-2": "Активный", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Фильтрация", "count": "В настоящее время NodeBB знает о %1 сервере(ах)", diff --git a/public/language/rw/admin/settings/activitypub.json b/public/language/rw/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/rw/admin/settings/activitypub.json +++ b/public/language/rw/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/sc/admin/settings/activitypub.json b/public/language/sc/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/sc/admin/settings/activitypub.json +++ b/public/language/sc/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/sk/admin/settings/activitypub.json b/public/language/sk/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/sk/admin/settings/activitypub.json +++ b/public/language/sk/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/sl/admin/settings/activitypub.json b/public/language/sl/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/sl/admin/settings/activitypub.json +++ b/public/language/sl/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/sq-AL/admin/settings/activitypub.json b/public/language/sq-AL/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/sq-AL/admin/settings/activitypub.json +++ b/public/language/sq-AL/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/sr/admin/settings/activitypub.json b/public/language/sr/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/sr/admin/settings/activitypub.json +++ b/public/language/sr/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/sv/admin/settings/activitypub.json b/public/language/sv/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/sv/admin/settings/activitypub.json +++ b/public/language/sv/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/th/admin/settings/activitypub.json b/public/language/th/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/th/admin/settings/activitypub.json +++ b/public/language/th/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/tr/admin/settings/activitypub.json b/public/language/tr/admin/settings/activitypub.json index 707c485bf2..01faba1fd4 100644 --- a/public/language/tr/admin/settings/activitypub.json +++ b/public/language/tr/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Beklemede", "relays.state-1": "Sadece al", "relays.state-2": "Etkin", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtreleme", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/uk/admin/settings/activitypub.json b/public/language/uk/admin/settings/activitypub.json index 64508849aa..677af75cd7 100644 --- a/public/language/uk/admin/settings/activitypub.json +++ b/public/language/uk/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Filtering", "count": "This NodeBB is currently aware of %1 server(s)", diff --git a/public/language/ur/admin/settings/activitypub.json b/public/language/ur/admin/settings/activitypub.json index fea9adb3a6..bdfe0ef6a0 100644 --- a/public/language/ur/admin/settings/activitypub.json +++ b/public/language/ur/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "فلٹرنگ", "count": "یہ نوڈ بی بی فی الحال %1 سرور(ز) کے بارے میں جانتا ہے", diff --git a/public/language/vi/admin/settings/activitypub.json b/public/language/vi/admin/settings/activitypub.json index e1f3cd8804..a117555fe5 100644 --- a/public/language/vi/admin/settings/activitypub.json +++ b/public/language/vi/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Đang đợi", "relays.state-1": "Chỉ nhận", "relays.state-2": "Kích hoạt", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "Lọc", "count": "NodeBB này hiện đã biết về %1 máy chủ", diff --git a/public/language/zh-CN/admin/settings/activitypub.json b/public/language/zh-CN/admin/settings/activitypub.json index 75308dab20..b9c826fd79 100644 --- a/public/language/zh-CN/admin/settings/activitypub.json +++ b/public/language/zh-CN/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "待处理", "relays.state-1": "仅接收", "relays.state-2": "已启用", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "过滤", "count": "该 NodeBB 目前可检测到 %1 台服务器", diff --git a/public/language/zh-TW/admin/settings/activitypub.json b/public/language/zh-TW/admin/settings/activitypub.json index 3a6e0f137a..c22ed8f4c6 100644 --- a/public/language/zh-TW/admin/settings/activitypub.json +++ b/public/language/zh-TW/admin/settings/activitypub.json @@ -39,6 +39,7 @@ "relays.state-0": "Pending", "relays.state-1": "Receiving only", "relays.state-2": "Active", + "relays.errors.invalid-url": "Please enter a valid URL", "server-filtering": "過濾...", "count": "本 NodeBB 已發現 %1 台伺服器。", From 0f8f50af06a17c90d4991ba6086ab15f9b0b8692 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 21 Mar 2026 09:07:35 +0000 Subject: [PATCH 4680/4744] Latest translations and fallbacks --- .../bg/admin/settings/activitypub.json | 2 +- .../de/admin/settings/activitypub.json | 2 +- .../it/admin/settings/activitypub.json | 2 +- public/language/ja/admin/advanced/cache.json | 2 +- public/language/ja/admin/advanced/errors.json | 4 +- public/language/ja/admin/advanced/jobs.json | 2 +- public/language/ja/admin/extend/rewards.json | 4 +- public/language/ja/admin/extend/widgets.json | 46 ++++++++-------- .../ja/admin/manage/user-custom-fields.json | 52 +++++++++---------- .../ja/admin/settings/activitypub.json | 6 +-- public/language/ja/login.json | 4 +- public/language/ja/rewards.json | 14 ++--- public/language/ja/search.json | 36 ++++++------- public/language/ja/unread.json | 10 ++-- public/language/ja/uploads.json | 6 +-- 15 files changed, 96 insertions(+), 96 deletions(-) diff --git a/public/language/bg/admin/settings/activitypub.json b/public/language/bg/admin/settings/activitypub.json index 3cc4a2c35c..2bb55d5a3c 100644 --- a/public/language/bg/admin/settings/activitypub.json +++ b/public/language/bg/admin/settings/activitypub.json @@ -39,7 +39,7 @@ "relays.state-0": "В изчакване", "relays.state-1": "Само приемане", "relays.state-2": "Активен", - "relays.errors.invalid-url": "Please enter a valid URL", + "relays.errors.invalid-url": "Моля, въведете правилен адрес", "server-filtering": "Филтриране", "count": "Този NodeBB в момента знае за наличието на %1 сървър(а)", diff --git a/public/language/de/admin/settings/activitypub.json b/public/language/de/admin/settings/activitypub.json index 3dc4cc4a5e..0457b88314 100644 --- a/public/language/de/admin/settings/activitypub.json +++ b/public/language/de/admin/settings/activitypub.json @@ -39,7 +39,7 @@ "relays.state-0": "Ausstehend", "relays.state-1": "Nur Empfang", "relays.state-2": "Aktiv", - "relays.errors.invalid-url": "Please enter a valid URL", + "relays.errors.invalid-url": "Bitte gib eine gültige URL ein", "server-filtering": "Filterung", "count": "Dieses NodeBB kennt derzeit %1 Server", diff --git a/public/language/it/admin/settings/activitypub.json b/public/language/it/admin/settings/activitypub.json index a898066330..01b835de45 100644 --- a/public/language/it/admin/settings/activitypub.json +++ b/public/language/it/admin/settings/activitypub.json @@ -39,7 +39,7 @@ "relays.state-0": "In sospeso", "relays.state-1": "Solo ricezione", "relays.state-2": "Attivo", - "relays.errors.invalid-url": "Please enter a valid URL", + "relays.errors.invalid-url": "Inserisci un URL valido", "server-filtering": "Filtraggio", "count": "Questo NodeBB è attualmente a conoscenza di %1 server", diff --git a/public/language/ja/admin/advanced/cache.json b/public/language/ja/admin/advanced/cache.json index 05094e56a7..b592e15cc3 100644 --- a/public/language/ja/admin/advanced/cache.json +++ b/public/language/ja/admin/advanced/cache.json @@ -1,6 +1,6 @@ { "cache": "キャッシュ", - "percent-full": "%1% がフル", + "percent-full": "%1% 満杯", "post-cache-size": "投稿キャッシュのサイズ", "items-in-cache": "キャッシュ内のアイテム" } \ No newline at end of file diff --git a/public/language/ja/admin/advanced/errors.json b/public/language/ja/admin/advanced/errors.json index 105c9384aa..3b426b969d 100644 --- a/public/language/ja/admin/advanced/errors.json +++ b/public/language/ja/admin/advanced/errors.json @@ -1,6 +1,6 @@ { - "errors": "Errors", - "figure-x": "%1を見つける", + "errors": "エラー", + "figure-x": "図%1", "error-events-per-day": "%1 日あたりのイベント", "error.404": "404 Not Found", "error.503": "503 サービスは利用できません", diff --git a/public/language/ja/admin/advanced/jobs.json b/public/language/ja/admin/advanced/jobs.json index 2cf132d0d6..2e77999203 100644 --- a/public/language/ja/admin/advanced/jobs.json +++ b/public/language/ja/admin/advanced/jobs.json @@ -5,5 +5,5 @@ "next-run": "次を実行", "last-duration": "前回の所要時間", "running": "実行中", - "active": "Active" + "active": "アクティブ" } \ No newline at end of file diff --git a/public/language/ja/admin/extend/rewards.json b/public/language/ja/admin/extend/rewards.json index 3c7e0a4d7a..d977999d9f 100644 --- a/public/language/ja/admin/extend/rewards.json +++ b/public/language/ja/admin/extend/rewards.json @@ -1,12 +1,12 @@ { "rewards": "報酬", - "add-reward": "Add reward", + "add-reward": "報酬を追加", "condition-if-users": "ユーザーの", "condition-is": ":", "condition-then": "それから:", "max-claims": "報酬が請求可能な金額", "zero-infinite": "無限に0を入力します。", - "select-reward": "Select reward", + "select-reward": "報酬を選択", "delete": "削除", "enable": "有効", "disable": "無効", diff --git a/public/language/ja/admin/extend/widgets.json b/public/language/ja/admin/extend/widgets.json index e6a320a8de..3470807072 100644 --- a/public/language/ja/admin/extend/widgets.json +++ b/public/language/ja/admin/extend/widgets.json @@ -1,37 +1,37 @@ { - "widgets": "Widgets", + "widgets": "ウィジェット", "available": "利用可能なウィジェット", "explanation": "ドロップダウンメニューからウィジェットを選択し、左のテンプレートのウィジェットエリアにドラッグ&ドロップします。", - "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", - "clone-from": "Clone widgets from", + "none-installed": "ウィジェットが見つかりません!プラグインコントロールパネルでウィジェットエッセンシャルプラグインを有効にしてください。", + "clone-from": "ウィジェットを複製元", "containers.available": "利用可能なコンテナ", - "containers.explanation": "Drag and drop on top of any widget", + "containers.explanation": "任意のウィジェットの上にドラッグ&ドロップ", "containers.none": "なし", - "container.well": "十分", + "container.well": "ウェル", "container.jumbotron": "ジャンボトロン", - "container.card": "Card", - "container.card-header": "Card Header", - "container.card-body": "Card Body", - "container.title": "Title", - "container.body": "Body", + "container.card": "カード", + "container.card-header": "カードヘッダー", + "container.card-body": "カード本文", + "container.title": "タイトル", + "container.body": "本文", "container.alert": "警告", "alert.confirm-delete": "このウィジェットを削除してもよろしいですか?", "alert.updated": "ウィジェットが更新されました。", "alert.update-success": "ウィジェットを保存しました", - "alert.clone-success": "Successfully cloned widgets", + "alert.clone-success": "ウィジェットを正常に複製しました", - "error.select-clone": "Please select a page to clone from", + "error.select-clone": "複製元のページを選択してください", - "title": "Title", - "title.placeholder": "Title (only shown on some containers)", - "container": "Container", - "container.placeholder": "Drag and drop a container or enter HTML here.", - "show-to-groups": "Show to groups", - "hide-from-groups": "Hide from groups", - "start-date": "Start date", - "end-date": "End date", - "hide-on-mobile": "Hide on mobile", - "hide-drafts": "Hide drafts", - "show-drafts": "Show drafts" + "title": "タイトル", + "title.placeholder": "タイトル(一部のコンテナでのみ表示)", + "container": "コンテナ", + "container.placeholder": "コンテナをドラッグ&ドロップするか、ここにHTMLを入力してください。", + "show-to-groups": "表示するグループ", + "hide-from-groups": "非表示にするグループ", + "start-date": "開始日", + "end-date": "終了日", + "hide-on-mobile": "モバイルで非表示", + "hide-drafts": "下書きを非表示", + "show-drafts": "下書きを表示" } \ No newline at end of file diff --git a/public/language/ja/admin/manage/user-custom-fields.json b/public/language/ja/admin/manage/user-custom-fields.json index dab10670d2..2ecd06b67a 100644 --- a/public/language/ja/admin/manage/user-custom-fields.json +++ b/public/language/ja/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": "カスタムユーザーフィールドの管理", + "create-field": "フィールドを作成", + "edit-field": "フィールドを編集", + "manage-custom-fields": "カスタムフィールドを管理", + "type-of-input": "入力タイプ", + "key": "キー", + "name": "名前", + "icon": "アイコン", + "type": "タイプ", + "min-rep": "最低評価", + "input-type-text": "入力(テキスト)", + "input-type-link": "入力(リンク)", + "input-type-number": "入力(数値)", + "input-type-date": "入力(日付)", + "input-type-select": "選択", + "input-type-select-multi": "複数選択", + "select-options": "オプション", + "select-options-help": "選択要素のオプションを1行に1つ追加", + "minimum-reputation": "最低評価", + "minimum-reputation-help": "ユーザーがこの値未満の場合、このフィールドを使用できません", + "delete-field-confirm-x": "カスタムフィールド「%1」を本当に削除しますか?", + "custom-fields-saved": "カスタムフィールドを保存しました", + "visibility": "表示", + "visibility-all": "誰でもフィールドを見られます", + "visibility-loggedin": "ログインユーザーのみフィールドを見られます", + "visibility-privileged": "管理者やモデレーターなどの特権ユーザーのみフィールドを見られます" } \ No newline at end of file diff --git a/public/language/ja/admin/settings/activitypub.json b/public/language/ja/admin/settings/activitypub.json index 677af75cd7..d21cc86d60 100644 --- a/public/language/ja/admin/settings/activitypub.json +++ b/public/language/ja/admin/settings/activitypub.json @@ -1,7 +1,7 @@ { - "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.)", - "general": "General", + "intro-lead": "フェデレーションとは?", + "intro-body": "NodeBBは、それをサポートする他のNodeBBインスタンスと通信できます。これはActivityPubと呼ばれるプロトコルで実現されます。有効にすると、NodeBBはActivityPubを使用する他のアプリやウェブサイト(Mastodon、Peertubeなど)とも通信できるようになります。", + "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)", diff --git a/public/language/ja/login.json b/public/language/ja/login.json index b94bf657ed..f9a72f4a75 100644 --- a/public/language/ja/login.json +++ b/public/language/ja/login.json @@ -4,9 +4,9 @@ "remember-me": "ログイン情報を記憶", "forgot-password": "パスワードを忘れましたか?", "alternative-logins": "ほかのログイン方法", - "failed-login-attempt": "ログインに成功", + "failed-login-attempt": "ログインに失敗", "login-successful": "ログインしました!", "dont-have-account": "アカウントをもっていませんか?", "logged-out-due-to-inactivity": "しばらく操作されていなかったため、管理パネルよりログアウトされました。", - "caps-lock-enabled": "Caps Lock is enabled" + "caps-lock-enabled": "Caps Lockが有効です" } \ No newline at end of file diff --git a/public/language/ja/rewards.json b/public/language/ja/rewards.json index f923cf1500..de9deaf315 100644 --- a/public/language/ja/rewards.json +++ b/public/language/ja/rewards.json @@ -1,10 +1,10 @@ { - "awarded-x-reputation": "You have been awarded %1 reputation", - "awarded-group-membership": "You have been added to the group %1", + "awarded-x-reputation": "%1の評価を獲得しました", + "awarded-group-membership": "%1グループに追加されました", - "essentials/user.reputation-conditional-value": "(Reputation %1 %2)", - "essentials/user.postcount-conditional-value": "(Post Count %1 %2)", - "essentials/user.lastonline-conditional-value": "(Last Online %1 %2)", - "essentials/user.joindate-conditional-value": "(Join Date %1 %2)", - "essentials/user.daysregistered-conditional-value": "(Days Registered %1 %2)" + "essentials/user.reputation-conditional-value": "(評価 %1 %2)", + "essentials/user.postcount-conditional-value": "(投稿数 %1 %2)", + "essentials/user.lastonline-conditional-value": "(最終オンライン %1 %2)", + "essentials/user.joindate-conditional-value": "(参加日 %1 %2)", + "essentials/user.daysregistered-conditional-value": "(登録日数 %1 %2)" } \ No newline at end of file diff --git a/public/language/ja/search.json b/public/language/ja/search.json index be8e0e5cd1..21d713b344 100644 --- a/public/language/ja/search.json +++ b/public/language/ja/search.json @@ -1,24 +1,24 @@ { - "type-to-search": "Type to search", - "results-matching": "%1 件の結果(s) キーワード \"%2\", (検索時間 %3 秒)", - "no-matches": "見つかりませんでした", + "type-to-search": "検索するには入力", + "results-matching": "%1 件の結果(s) キーワード \\\"%2\\\", (検索時間 %3 秒)", + "no-matches": "一致なし", "advanced-search": "高度な検索", "in": "検索範囲", - "in-titles": "In titles", - "in-titles-posts": "In titles and posts", - "in-posts": "In posts", - "in-bookmarks": "In bookmarks", - "in-categories": "In categories", - "in-users": "In users", - "in-tags": "In tags", - "categories": "Categories", - "all-categories": "All categories", - "categories-x": "Categories: %1", - "categories-watched-categories": "Categories: Watched categories", - "type-a-category": "Type a category", - "tags": "Tags", - "tags-x": "Tags: %1", - "type-a-tag": "Type a tag", + "in-titles": "タイトル内", + "in-titles-posts": "タイトルと投稿内", + "in-posts": "投稿内", + "in-bookmarks": "ブックマーク内", + "in-categories": "カテゴリ内", + "in-users": "ユーザー内", + "in-tags": "タグ内", + "categories": "カテゴリ", + "all-categories": "すべてのカテゴリ", + "categories-x": "カテゴリ: %1", + "categories-watched-categories": "カテゴリ: ウォッチ中のカテゴリ", + "type-a-category": "カテゴリを入力", + "tags": "タグ", + "tags-x": "タグ: %1", + "type-a-tag": "タグを入力", "match-words": "Match words", "match-all-words": "Match all words", "match-any-word": "Match any word", diff --git a/public/language/ja/unread.json b/public/language/ja/unread.json index 0e980376e6..2b3e108968 100644 --- a/public/language/ja/unread.json +++ b/public/language/ja/unread.json @@ -3,14 +3,14 @@ "no-unread-topics": "未読のスレッドはありません。", "load-more": "もっと見る", "mark-as-read": "既読にする", - "mark-as-unread": "Mark as Unread", + "mark-as-unread": "未読にする", "selected": "選択済み", - "all": "全て", - "all-categories": "全てのカテゴリ", + "all": "すべて", + "all-categories": "すべてのカテゴリ", "topics-marked-as-read.success": "すべてのスレッドを既読にしました。", "all-topics": "すべてのスレッド", "new-topics": "新しいスレッド", "watched-topics": "ウォッチ済みのスレッド", - "unreplied-topics": "Unreplied Topics", - "multiple-categories-selected": "Multiple Selected" + "unreplied-topics": "返信のないスレッド", + "multiple-categories-selected": "複数選択中" } \ No newline at end of file diff --git a/public/language/ja/uploads.json b/public/language/ja/uploads.json index 6540439e65..efe2537e16 100644 --- a/public/language/ja/uploads.json +++ b/public/language/ja/uploads.json @@ -3,7 +3,7 @@ "select-file-to-upload": "アップロードするファイルを選択してください!", "upload-success": "ファイルのアップロードに成功しました!", "maximum-file-size": "最大 %1 kb", - "no-uploads-found": "No uploads found", - "public-uploads-info": "Uploads are public, all visitors can see them.", - "private-uploads-info": "Uploads are private, only logged in users can see them." + "no-uploads-found": "アップロードが見つかりません", + "public-uploads-info": "アップロードは公開されています。すべての訪問者が閲覧できます。", + "private-uploads-info": "アップロードは非公開です。ログインしたユーザーのみが閲覧できます。" } \ No newline at end of file From de72970ebc3936e936872902d6d0cefb24db7b32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:28:23 -0400 Subject: [PATCH 4681/4744] chore(deps): update dependency jsdom to v29.0.1 (#14110) 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 c31cbf4889..7c8bc4468f 100644 --- a/install/package.json +++ b/install/package.json @@ -172,7 +172,7 @@ "grunt": "1.6.1", "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", - "jsdom": "29.0.0", + "jsdom": "29.0.1", "lint-staged": "16.4.0", "mocha": "11.7.5", "mocha-lcov-reporter": "1.3.0", From eb6532b18a92e2afc219e48c60d1ddd8800f9d72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:28:36 -0400 Subject: [PATCH 4682/4744] fix(deps): update dependency satori to v0.26.0 (#14111) 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 7c8bc4468f..c0fa936533 100644 --- a/install/package.json +++ b/install/package.json @@ -132,7 +132,7 @@ "rtlcss": "4.3.0", "sanitize-html": "2.17.1", "sass": "1.98.0", - "satori": "0.25.0", + "satori": "0.26.0", "sbd": "^1.0.19", "semver": "7.7.4", "serve-favicon": "2.5.1", From 6f577d6d9b0a37aba073ed057479d0e8f874fb01 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:28:49 -0400 Subject: [PATCH 4683/4744] fix(deps): update dependency sanitize-html to v2.17.2 (#14106) 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 c0fa936533..c5a9691f26 100644 --- a/install/package.json +++ b/install/package.json @@ -130,7 +130,7 @@ "rimraf": "6.1.3", "rss": "1.2.2", "rtlcss": "4.3.0", - "sanitize-html": "2.17.1", + "sanitize-html": "2.17.2", "sass": "1.98.0", "satori": "0.26.0", "sbd": "^1.0.19", From d00ca02a52feffebfdb76ddeb9f4f51d1963327e Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 22 Mar 2026 09:07:37 +0000 Subject: [PATCH 4684/4744] Latest translations and fallbacks --- public/language/ja/admin/dashboard.json | 30 ++--- .../language/ja/admin/development/info.json | 16 +-- .../language/ja/admin/manage/categories.json | 122 +++++++++--------- .../language/ja/admin/settings/advanced.json | 50 +++---- .../ja/admin/settings/web-crawler.json | 2 +- public/language/ja/world.json | 48 +++---- .../zh-CN/admin/settings/activitypub.json | 4 +- public/language/zh-CN/world.json | 12 +- 8 files changed, 142 insertions(+), 142 deletions(-) diff --git a/public/language/ja/admin/dashboard.json b/public/language/ja/admin/dashboard.json index 9b28fc4394..7ded741332 100644 --- a/public/language/ja/admin/dashboard.json +++ b/public/language/ja/admin/dashboard.json @@ -2,34 +2,34 @@ "forum-traffic": "フォーラムのトラフィック", "page-views": "ページビュー", "unique-visitors": "ユニークな訪問者", - "logins": "Logins", - "new-users": "New Users", + "logins": "ログイン", + "new-users": "新規ユーザー", "posts": "投稿", "topics": "スレッド", - "remote-posts": "Remote Posts", - "remote-topics": "Remote Topics", - "messages": "Messages", + "remote-posts": "リモート投稿", + "remote-topics": "リモートスレッド", + "messages": "メッセージ", "page-views-seven": "過去7日間", "page-views-thirty": "過去30日間", "page-views-last-day": "過去24時間", - "page-views-custom": "Custom Range", + "page-views-custom": "カスタム期間", "page-views-custom-start": "期間開始", "page-views-custom-end": "期間終了", "page-views-custom-help": "表示したいページビューの日付範囲を入力します。日付選択ツールが使用できない場合、受け入れ可能な形式は次のとおりです。YYYY-MM-DD", - "page-views-custom-error": "有効な期間をフォーマットで入力してくださいYYYY-MM-DD", + "page-views-custom-error": "有効な日付範囲をYYYY-MM-DD形式で入力してください", - "stats.yesterday": "Yesterday", - "stats.today": "Today", - "stats.last-week": "Last Week", - "stats.this-week": "This Week", - "stats.last-month": "Last Month", - "stats.this-month": "This Month", - "stats.all": "全て", + "stats.yesterday": "昨日", + "stats.today": "今日", + "stats.last-week": "先週", + "stats.this-week": "今週", + "stats.last-month": "先月", + "stats.this-month": "今月", + "stats.all": "全期間", "updates": "更新", "running-version": "NodeBB v%1 を実行しています。", "keep-updated": "常に最新のセキュリティパッチとバグ修正のためにNodeBBが最新であることを確認してください。", - "up-to-date": "You are up-to-date ", + "up-to-date": "最新です ", "upgrade-available": "A new version (v%1) has been released. Consider upgrading your NodeBB.", "prerelease-upgrade-available": "This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.", "prerelease-warning": "This is a pre-release version of NodeBB. Unintended bugs may occur. ", diff --git a/public/language/ja/admin/development/info.json b/public/language/ja/admin/development/info.json index 619f886e7b..6ad81084ce 100644 --- a/public/language/ja/admin/development/info.json +++ b/public/language/ja/admin/development/info.json @@ -1,5 +1,5 @@ { - "you-are-on": "You are on %1:%2", + "you-are-on": "あなたは%1:%2にいます", "ip": "IP %1", "nodes-responded": "%1ノードは%2ms以内に応答しました!", "host": "ホスト", @@ -9,17 +9,17 @@ "online": "オンライン", "git": "git", "process-memory": "rss/heap used", - "system-memory": "system memory", - "used-memory-process": "Used memory by process", - "used-memory-os": "Used system memory", - "total-memory-os": "Total system memory", - "load": "system load", - "cpu-usage": "cpu usage", + "system-memory": "システムメモリ", + "used-memory-process": "プロセスが使用するメモリ", + "used-memory-os": "使用中のシステムメモリ", + "total-memory-os": "合計システムメモリ", + "load": "システム負荷", + "cpu-usage": "CPU使用率", "uptime": "稼働時間", "registered": "登録数", "sockets": "ソケット数", - "connection-count": "Connection Count", + "connection-count": "接続数", "guests": "ゲスト数", "info": "情報" diff --git a/public/language/ja/admin/manage/categories.json b/public/language/ja/admin/manage/categories.json index 75e406f93d..c1ce65d67c 100644 --- a/public/language/ja/admin/manage/categories.json +++ b/public/language/ja/admin/manage/categories.json @@ -1,61 +1,61 @@ { - "manage-categories": "Manage Categories", - "add-category": "Add category", - "add-local-category": "Add Local category", - "add-remote-category": "Add Remote category", - "remove": "Remove", - "rename": "Rename", - "jump-to": "Jump to...", + "manage-categories": "カテゴリを管理", + "add-category": "カテゴリを追加", + "add-local-category": "ローカルカテゴリを追加", + "add-remote-category": "リモートカテゴリを追加", + "remove": "削除", + "rename": "名前を変更", + "jump-to": "移動...", "settings": "カテゴリ設定", - "edit-category": "Edit Category", - "privileges": "特権", - "back-to-categories": "Back to categories", - "id": "Category ID", + "edit-category": "カテゴリを編集", + "privileges": "権限", + "back-to-categories": "カテゴリに戻る", + "id": "カテゴリID", "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": "カテゴリハンドル", + "handle.help": "カテゴリハンドルは、ユーザー名と同様に、他のネットワークでこのカテゴリを表すために使用されます。カテゴリハンドルは既存のユーザー名またはユーザーグループと一致してはいけません。", "description": "カテゴリの説明", - "topic-template": "Topic Template", - "topic-template.help": "Define a template for new topics created in this category.", + "topic-template": "トピックテンプレート", + "topic-template.help": "このカテゴリで作成される新しいトピックのテンプレートを定義します。", "bg-color": "背景色", - "text-color": "テキストカラー", + "text-color": "文字色", "bg-image-size": "背景画像サイズ", - "custom-class": "カスタムClass", + "custom-class": "カスタムクラス", "num-recent-replies": "# 最近の返信数", "ext-link": "外部リンク", - "subcategories-per-page": "Subcategories per page", + "subcategories-per-page": "1ページあたりのサブカテゴリ数", "is-section": "このカテゴリをセクションとして扱う", - "post-queue": "Post queue", - "tag-whitelist": "Tag Whitelist", + "post-queue": "投稿キュー", + "tag-whitelist": "タグホワイトリスト", "upload-image": "画像をアップロード", - "upload": "Upload", + "upload": "アップロード", "delete-image": "削除", "category-image": "カテゴリ画像", - "image-and-icon": "Image & Icon", + "image-and-icon": "画像とアイコン", "parent-category": "親カテゴリ", "optional-parent-category": "(任意)親カテゴリ", - "top-level": "Top Level", + "top-level": "トップレベル", "parent-category-none": "(なし)", "copy-parent": "親をコピー", - "copy-settings": "設定をコピー", - "optional-clone-settings": "カテゴリからのクローン設定(任意)", - "clone-children": "子カテゴリを複製して設定", - "purge": "カテゴリを切り離す", + "copy-settings": "設定のコピー元", + "optional-clone-settings": "(任意) 設定を複製するカテゴリ", + "clone-children": "子カテゴリと設定を複製", + "purge": "カテゴリを完全削除", "enable": "有効", "disable": "無効", "edit": "編集", - "analytics": "Analytics", - "federation": "Federation", + "analytics": "分析", + "federation": "フェデレーション", - "view-category": "View category", - "set-order": "Set order", - "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + "view-category": "カテゴリを表示", + "set-order": "順序を設定", + "set-order-help": "カテゴリの順序を設定すると、このカテゴリが指定した順序に移動し、必要に応じて他のカテゴリの順序も更新されます。最小値は1で、カテゴリが最上部に配置されます。", "select-category": "カテゴリを選択", - "set-parent-category": "親カテゴリとして設定", + "set-parent-category": "親カテゴリを設定", - "privileges.description": "You can configure the access control privileges for portions of the site in this section. Privileges can be granted on a per-user or a per-group basis.", + "privileges.description": "このセクションでは、サイトの各領域に対するアクセス権限を設定できます。権限はユーザー単位またはグループ単位で付与できます。", "privileges.category-selector": "権限を設定", "privileges.warning": ":特権の設定はすぐに有効になります。これらの設定を調整した後は、カテゴリを保存する必要はありません。", "privileges.section-viewing": "特権の表示", @@ -86,32 +86,32 @@ "analytics.topics-daily": "図3 &ndash;このカテゴリで作成された日別のスレッド", "analytics.posts-daily": "図4 &ndash;このカテゴリで作成された日ごとの投稿", - "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.syncing-actorUri": "Actor", - "federation.syncing-follow": "Follow", - "federation.syncing-unfollow": "Unfollow", - "federation.followers": "Remote users following this category", - "federation.followers-handle": "Handle", + "federation.title": "カテゴリ「%1」のフェデレーション設定", + "federation.disabled": "サイト全体でフェデレーションが無効のため、カテゴリのフェデレーション設定は現在利用できません。", + "federation.disabled-cta": "フェデレーション設定 →", + "federation.syncing-header": "同期", + "federation.syncing-intro": "カテゴリは ActivityPub プロトコルを通じて「グループアクター」をフォローできます。以下にあるアクターからコンテンツを受信すると、このカテゴリに自動追加されます。", + "federation.syncing-caveat": "注: ここでの同期設定は一方向です。NodeBB はアクターの購読/フォローを試みますが、逆方向は保証されません。", + "federation.syncing-none": "このカテゴリは現在誰もフォローしていません。", + "federation.syncing-add": "同期先を追加...", + "federation.syncing-actorUri": "アクター", + "federation.syncing-follow": "フォロー", + "federation.syncing-unfollow": "フォロー解除", + "federation.followers": "このカテゴリをフォローしているリモートユーザー", + "federation.followers-handle": "ハンドル", "federation.followers-id": "ID", - "federation.followers-none": "No followers.", - "federation.followers-autofill": "Autofill", + "federation.followers-none": "フォロワーはいません。", + "federation.followers-autofill": " 自動入力", "alert.created": "作成されました", - "alert.create-success": "カテゴリが正常に作成されました!", - "alert.none-active": "アクティブなカテゴリがありません。", + "alert.create-success": "カテゴリを作成しました!", + "alert.none-active": "有効なカテゴリがありません。", "alert.create": "カテゴリを作成", - "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": "カテゴリを追加", + "alert.add-help": "ハンドルを指定するとリモートカテゴリをカテゴリ一覧に追加できます。

    - 少なくとも1人のローカルユーザーが追跡/ウォッチしていない場合、公開済みトピックのすべてが反映されない可能性があります。", + "alert.rename": "リモートカテゴリ名を変更", + "alert.rename-help": "このカテゴリの新しい名前を入力してください。空欄にすると元の名前に戻ります。", + "alert.confirm-remove": "このカテゴリを削除してもよろしいですか? 後で再追加できます。", "alert.confirm-purge": "

    本当にこのカテゴリ \"%1\"を切り離しますか?

    警告!このカテゴリのすべてのスレッドと投稿が削除されます。

    カテゴリをパージすると、すべてのスレッドと投稿が削除され、データベースからカテゴリが削除されます。一時的にカテゴリを削除する場合は、代わりにカテゴリを無効にすることをおすすめします。

    ", "alert.purge-success": "カテゴリが切り離されました!", "alert.copy-success": "設定をコピーしました。", @@ -122,10 +122,10 @@ "alert.find-user": "ユーザーの検索", "alert.user-search": "ここでユーザーを検索...", "alert.find-group": "グループを探す", - "alert.group-search": "ここでグループを検索する...", - "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "alert.group-search": "ここでグループを検索...", + "alert.not-enough-whitelisted-tags": "ホワイトリスト済みタグ数が最小タグ数を下回っています。ホワイトリストタグを追加してください!", "collapse-all": "すべて折りたたむ", - "expand-all": "すべて展開する", - "disable-on-create": "作成時に無効にする", - "no-matches": "No matches" + "expand-all": "すべて展開", + "disable-on-create": "作成時に無効化", + "no-matches": "一致なし" } \ No newline at end of file diff --git a/public/language/ja/admin/settings/advanced.json b/public/language/ja/admin/settings/advanced.json index 7aafbb94f0..1490db3c2a 100644 --- a/public/language/ja/admin/settings/advanced.json +++ b/public/language/ja/admin/settings/advanced.json @@ -1,47 +1,47 @@ { "maintenance-mode": "メンテナンスモード", "maintenance-mode.help": "フォーラムがメンテナンスモードの場合、すべてのリクエストは静的な一時ページにリダイレクトされます。管理者はこのリダイレクトから免除され、通常のサイトにアクセスできます。", - "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.status": "メンテナンスモードのステータスコード", "maintenance-mode.message": "メンテナンスメッセージ", - "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "maintenance-mode.groups-exempt-from-maintenance-mode": "メンテナンスモードを免除するグループを選択", "headers": "ヘッダー", "headers.allow-from": "NodeBBをインラインフレーム内に配置するようALLOW-FROMを設定する", - "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", - "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.csp-frame-ancestors": "Content-Security-Policy frame-ancestorsヘッダーを設定してNodeBBをiFrame内に配置", + "headers.csp-frame-ancestors-help": "'none'、'self'(デフォルト)、または許可するURIのリスト。", "headers.powered-by": "NodeBBから送信された「Powered By」ヘッダーをカスタマイズする", - "headers.acao": "アクセス-制御-有効-原点", - "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin正規表現", "headers.acao-help": "すべてのサイトへのアクセスを拒否する場合、空のままにしておいてください。", - "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acao-regex-help": "動的なオリジンに一致する正規表現をここに入力してください。すべてのサイトへのアクセスを拒否するには、空白のままにしてください", "headers.acac": "Access-Control-Allow-Credentials", - "headers.acam": "アクセス-制御-有効-メソッド", - "headers.acah": "アクセス-制御-有効-ヘッダー", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", "headers.coep": "Cross-Origin-Embedder-Policy", - "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coep-help": "有効にすると(デフォルト)、ヘッダーがrequire-corpに設定されます", "headers.coop": "Cross-Origin-Opener-Policy", "headers.corp": "Cross-Origin-Resource-Policy", "headers.permissions-policy": "Permissions-Policy", - "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", - "hsts": "Strict Transport Security", - "hsts.enabled": "Enabled HSTS (recommended)", - "hsts.maxAge": "HSTS Max Age", - "hsts.subdomains": "Include subdomains in HSTS header", - "hsts.preload": "Allow preloading of HSTS header", - "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "headers.permissions-policy-help": "Permissions-Policyヘッダーの設定を許可します。例: \\\"geolocation=*, camera=()\\\"、詳細をご覧ください。", + "hsts": "厳格なトランスポートセキュリティ", + "hsts.enabled": "HSTSを有効にする(推奨)", + "hsts.maxAge": "HSTS最大経過時間", + "hsts.subdomains": "HSTSヘッダーにサブドメインを含める", + "hsts.preload": "HSTSヘッダーのプリロードを許可", + "hsts.help": "有効にすると、このサイト用のHSTSヘッダーが設定されます。ヘッダーにサブドメインとプリロードフラグを含めることを選択できます。不明な場合は、これらをチェックしないままにしておいてください。詳細 ", "traffic-management": "トラフィック管理", - "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.help": "NodeBBは高トラフィック時にリクエストを自動的に拒否するモジュールを使用しています。デフォルトは良い出発点ですが、ここでこれらの設定を調整できます。", "traffic.enable": "トラフィック管理を有効にする", "traffic.event-lag": "イベントループの場所のしきい値(ミリ秒単位)", "traffic.event-lag-help": "この値を下げるとページの読み込み時間が短縮されますが、さらに多くのユーザーには「過剰な読み込み」メッセージが表示されます。(再起動が必要)", "traffic.lag-check-interval": "チェック間隔(ミリ秒単位)", "traffic.lag-check-interval-help": "この値を小さくすると、NodeBBは負荷のスパイクに対してより敏感になりますが、チェックが過敏になる可能性もあります。(再起動が必要)", - "sockets.settings": "WebSocket Settings", - "sockets.max-attempts": "Max Reconnection Attempts", - "sockets.default-placeholder": "Default: %1", - "sockets.delay": "Reconnection Delay", + "sockets.settings": "WebSocket設定", + "sockets.max-attempts": "最大再接続試行数", + "sockets.default-placeholder": "デフォルト: %1", + "sockets.delay": "再接続遅延", - "compression.settings": "Compression Settings", - "compression.enable": "Enable Compression", - "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." + "compression.settings": "圧縮設定", + "compression.enable": "圧縮を有効にする", + "compression.help": "この設定でgzip圧縮が有効になります。本番環境の高トラフィックウェブサイトでは、リバースプロキシレベルで圧縮を実装するのが最善の方法です。テスト目的でここで有効にできます。" } \ No newline at end of file diff --git a/public/language/ja/admin/settings/web-crawler.json b/public/language/ja/admin/settings/web-crawler.json index b87a3d3af4..b686b9f901 100644 --- a/public/language/ja/admin/settings/web-crawler.json +++ b/public/language/ja/admin/settings/web-crawler.json @@ -5,7 +5,7 @@ "disable-rss-feeds": "RSSフィードを無効にする", "disable-sitemap-xml": "Sitemap.xmlを無効にする", "sitemap-topics": "サイトマップに表示するスレッドの数", - "sitemap-cache-duration-hours": "Sitemap Cache Duration (hours)", + "sitemap-cache-duration-hours": "サイトマップのキャッシュ期間(時間)", "clear-sitemap-cache": "サイトマップのキャッシュをクリア", "view-sitemap": "サイトマップを表示" } \ No newline at end of file diff --git a/public/language/ja/world.json b/public/language/ja/world.json index 2057e13d00..6b64a29603 100644 --- a/public/language/ja/world.json +++ b/public/language/ja/world.json @@ -1,29 +1,29 @@ { - "name": "World", - "latest": "Latest", - "latest-local": "Latest (Local)", - "latest-all": "Latest (All)", - "popular-day": "Popular (Day)", - "popular-week": "Popular (Week)", - "popular-month": "Popular (Month)", - "popular-year": "Popular (Year)", - "popular-alltime": "Popular (All Time)", - "recent": "All", - "help": "Help", + "name": "世界", + "latest": "最新", + "latest-local": "最新(ローカル)", + "latest-all": "最新(すべて)", + "popular-day": "人気(日)", + "popular-week": "人気(週)", + "popular-month": "人気(月)", + "popular-year": "人気(年)", + "popular-alltime": "人気(全期間)", + "recent": "すべて", + "help": "ヘルプ", - "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": "このページとは?", + "help.intro": "フェディバースのあなたのコーナーへようこそ。", + "help.fediverse": "「フェディバース」は、相互に接続されたアプリケーションとウェブサイトのネットワークで、互いに通信し、ユーザーがお互いを見ることができます。このフォーラムはフェデレーションされており、そのソーシャルウェブ(「フェディバース」)とやり取りできます。このページはフェディバースのあなたのコーナーです。あなたがフォローしているユーザーが作成または共有したスレッドのみで構成されています。", + "help.build": "最初は多くのスレッドがないかもしれません。それは正常です。他のユーザーをフォローし始めると、時間とともにより多くのコンテンツが表示されるようになります。", + "help.federating": "同様に、このフォーラム外のユーザーがあなたをフォローし始めると、あなたの投稿もそれらのアプリやウェブサイトに表示され始めます。", + "help.next-generation": "これが次世代のソーシャルメディアです。今日から貢献を始めましょう!", - "onboard.title": "A world of content at your fingertips…", - "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", - "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", - "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", + "onboard.title": "指先でコンテンツの世界へ…", + "onboard.what": "これはグローバルなディスカバリーフィードです。ウェブ上や他のコミュニティからの興味深い議論を一か所にまとめて表示します。", + "onboard.why": "今トレンドになっているものを閲覧することもできますが、このフィードを最大限に活用するには、自分好みにカスタマイズするのがおすすめです。アカウントを作成すると、特定のクリエイターやトピックをフォローして不要な情報を除外し、自分に関係あるものだけを表示できます。", + "onboard.how": "さっそく始めてみませんか?アカウントを作成して、他のユーザーをフォローしたり、返信の通知を受け取ったり、お気に入りのコンテンツを保存したりしましょう。", - "category-search": "Find a category...", - "see-more": "See more", - "see-less": "See less" + "category-search": "カテゴリを検索...", + "see-more": "もっと見る", + "see-less": "折りたたむ" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/activitypub.json b/public/language/zh-CN/admin/settings/activitypub.json index b9c826fd79..3f654d41e6 100644 --- a/public/language/zh-CN/admin/settings/activitypub.json +++ b/public/language/zh-CN/admin/settings/activitypub.json @@ -39,7 +39,7 @@ "relays.state-0": "待处理", "relays.state-1": "仅接收", "relays.state-2": "已启用", - "relays.errors.invalid-url": "Please enter a valid URL", + "relays.errors.invalid-url": "请输入有效的 URL", "server-filtering": "过滤", "count": "该 NodeBB 目前可检测到 %1 台服务器", @@ -52,5 +52,5 @@ "content.summary-limit-help": "当联合输出内容超过此字符限制时,将生成 摘要 ,包含该限制前的所有完整句子。(默认值:500)", "content.break-string": "注释/文章分隔符", "content.break-string-help": "此分隔符可由高级用户在撰写新主题时手动插入。它指示 NodeBB 将该分隔符之前的内容作为摘要的一部分。若未使用此字符串,则采用字符计数备用方案。(默认值:[...])", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.world-default-cid": "“世界”页面编辑器的默认版块 ID" } \ No newline at end of file diff --git a/public/language/zh-CN/world.json b/public/language/zh-CN/world.json index 4e2f056e85..e125c7216f 100644 --- a/public/language/zh-CN/world.json +++ b/public/language/zh-CN/world.json @@ -1,7 +1,7 @@ { "name": "世界", - "latest": "Latest", - "latest-local": "Latest (Local)", + "latest": "最新", + "latest-local": "最新(本地)", "latest-all": "最新(全部)", "popular-day": "热门(按天)", "popular-week": "热门(按周)", @@ -18,10 +18,10 @@ "help.federating": "同样,如果本论坛以外的用户开始关注 ,那么您的帖子也会开始出现在这些应用程序和网站上。", "help.next-generation": "这是新一代的社交媒体,从今天开始,贡献力量吧!", - "onboard.title": "A world of content at your fingertips…", - "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", - "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", - "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", + "onboard.title": "海量内容尽在指尖 …", + "onboard.what": "不妨将此视为您专属的全球发现信息流。它汇集了来自互联网各处及其他社区的有趣讨论,一应俱全。", + "onboard.why": "虽然您可以浏览当前的热门内容,但使用该信息流的最佳方式是将其个性化。通过注册账号,您可以关注特定的创作者和主题,从而过滤掉无关信息,只查看对您真正重要的内容。", + "onboard.how": "准备好开始了吗?注册一个账号,即可关注他人、在收到回复时获得通知,并收藏您喜欢的内容。", "category-search": "查找版块...", "see-more": "显示更多", From 43e7f0abb93e8c7d0b543091db6c144bc2877077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 22 Mar 2026 21:25:43 -0400 Subject: [PATCH 4685/4744] feat: add email share --- public/language/en-GB/topic.json | 2 ++ public/src/modules/share.js | 12 +++++++++++- src/social.js | 5 +++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index ac69995c89..52808ea5bd 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -104,6 +104,8 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share-this-post": "Share this Post", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/src/modules/share.js b/public/src/modules/share.js index 94478d30cf..db6e4e19ac 100644 --- a/public/src/modules/share.js +++ b/public/src/modules/share.js @@ -1,7 +1,7 @@ 'use strict'; -define('share', ['hooks'], function (hooks) { +define('share', ['hooks', 'translator'], function (hooks, translator) { const share = {}; const baseUrl = window.location.protocol + '//' + window.location.host; @@ -69,6 +69,16 @@ define('share', ['hooks'], function (hooks) { return openShare(mastodon_url, postUrl, 626, 760); }); + addHandler('[component="share/email"]', async function () { + const postUrl = getPostUrl($(this)); + const [subject, body] = await translator.translateKeys([ + translator.compile('topic:share-mail-subject', config.siteTitle), + translator.compile('topic:share-mail-body', postUrl), + ]); + const mailtoUrl = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + window.location.href = mailtoUrl; + }); + hooks.fire('action:share.addHandlers', { openShare: openShare }); }; diff --git a/src/social.js b/src/social.js index 9d52114583..c540a391c3 100644 --- a/src/social.js +++ b/src/social.js @@ -45,6 +45,11 @@ social.getPostSharing = async function () { name: 'Mastodon', class: 'fa-brands fa-mastodon', }, + { + id: 'email', + name: 'Email', + class: 'fa-regular fa-envelope', + }, ]; networks = await plugins.hooks.fire('filter:social.posts', networks); networks.forEach((network) => { From 59c6d8591f1b9d7ce983c3a5d7fa419d9650117a Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 23 Mar 2026 09:07:37 +0000 Subject: [PATCH 4686/4744] Latest translations and fallbacks --- public/language/ja/admin/dashboard.json | 66 +++++++------- .../language/ja/admin/development/info.json | 8 +- .../language/ja/admin/development/logger.json | 8 +- .../ja/admin/manage/custom-reasons.json | 28 +++--- public/language/ja/admin/manage/users.json | 56 ++++++------ public/language/ja/admin/menu.json | 84 ++++++++--------- .../ja/admin/settings/activitypub.json | 90 +++++++++---------- public/language/ja/admin/settings/email.json | 32 +++---- .../ja/admin/settings/notifications.json | 8 +- public/language/vi/admin/advanced/jobs.json | 8 +- .../language/vi/admin/development/info.json | 2 +- .../vi/admin/settings/activitypub.json | 4 +- .../language/vi/admin/settings/general.json | 2 +- .../vi/admin/settings/web-crawler.json | 2 +- public/language/vi/modules.json | 2 +- public/language/vi/topic.json | 2 +- public/language/vi/world.json | 12 +-- 17 files changed, 207 insertions(+), 207 deletions(-) diff --git a/public/language/ja/admin/dashboard.json b/public/language/ja/admin/dashboard.json index 7ded741332..9f047cad1a 100644 --- a/public/language/ja/admin/dashboard.json +++ b/public/language/ja/admin/dashboard.json @@ -30,38 +30,38 @@ "running-version": "NodeBB v%1 を実行しています。", "keep-updated": "常に最新のセキュリティパッチとバグ修正のためにNodeBBが最新であることを確認してください。", "up-to-date": "最新です ", - "upgrade-available": "A new version (v%1) has been released. Consider upgrading your NodeBB.", - "prerelease-upgrade-available": "This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.", - "prerelease-warning": "This is a pre-release version of NodeBB. Unintended bugs may occur. ", - "fallback-emailer-not-found": "Fallback emailer not found!", - "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator", - "latest-lookup-failed": "Failed to look up latest available version of NodeBB", + "upgrade-available": "新しいバージョン(v%1)がリリースされました。NodeBBのアップグレードをご検討ください。", + "prerelease-upgrade-available": "これは古いNodeBBのプレリリース版です。新しいバージョン(v%1)がリリースされました。NodeBBのアップグレードをご検討ください。", + "prerelease-warning": "これはNodeBBのプレリリース版です。予期しないバグが発生する可能性があります。", + "fallback-emailer-not-found": "フォールバックメーラーが見つかりません!", + "running-in-development": "フォーラムは開発モードで実行中です。潜在的な脆弱性にさらされている可能性があります。システム管理者にお問い合わせください。", + "latest-lookup-failed": "NodeBBの最新バージョンの検索に失敗しました", "notices": "通知", "restart-not-required": "再起動は必要ありません", "restart-required": "再起動が必要です", - "search-plugin-installed": "検索プラグインのインストール", + "search-plugin-installed": "検索プラグインはインストール済み", "search-plugin-not-installed": "検索プラグインがインストールされていません", "search-plugin-tooltip": "検索機能を有効にするには、プラグインページから検索プラグインをインストールしてください", "control-panel": "システムコントロール", - "rebuild-and-restart": "再構築 & 再起動", + "rebuild-and-restart": "再構築して再起動", "restart": "再起動", "restart-warning": "NodeBBを再構築または再起動すると、数秒間既存の接続がすべて切断されます。", "restart-disabled": "適切なデーモンを介してNodeBBを実行しているようには見えないため、NodeBBの再構築および再起動は無効になっています。", "maintenance-mode": "メンテナンスモード", "maintenance-mode-title": "NodeBBのメンテナンスモードを設定するには、ここをクリックしてください", - "dark-mode": "Dark Mode", + "dark-mode": "ダークモード", "realtime-chart-updates": "リアルタイムチャートの更新", "active-users": "アクティブユーザー", "active-users.users": "ユーザー", "active-users.guests": "ゲスト", - "active-users.total": "総合", + "active-users.total": "合計", "active-users.connections": "接続", - "guest-registered-users": "Guest vs Registered Users", - "guest": "Guest", + "guest-registered-users": "ゲスト vs 登録ユーザー", + "guest": "ゲスト", "registered": "登録数", "user-presence": "ユーザープレゼンス", @@ -72,34 +72,34 @@ "unread": "未読", "high-presence-topics": "ハイプレゼンススレッド", - "popular-searches": "Popular Searches", + "popular-searches": "人気の検索", "graphs.page-views": "ページビュー", "graphs.page-views-registered": "ページビュー登録済み", "graphs.page-views-guest": "ページビューゲスト", "graphs.page-views-bot": "ページビューBot", - "graphs.page-views-ap": "ActivityPub Page Views", + "graphs.page-views-ap": "ActivityPubページビュー", "graphs.unique-visitors": "ユニークな訪問者", "graphs.registered-users": "登録したユーザー", - "graphs.guest-users": "Guest Users", - "last-restarted-by": "最後に再起動された順", + "graphs.guest-users": "ゲストユーザー", + "last-restarted-by": "最後に再起動したユーザー", "no-users-browsing": "閲覧中のユーザーなし", - "back-to-dashboard": "Back to Dashboard", - "details.no-users": "No users have joined within the selected timeframe", - "details.no-topics": "No topics have been posted within the selected timeframe", - "details.no-searches": "No searches have been made within the selected timeframe", - "details.no-logins": "No logins have been recorded within the selected timeframe", - "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", - "details.logins-login-time": "Login Time", - "start": "Start", - "end": "End", - "filter": "Filter", - "view-as-json": "View as JSON", - "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-count": "Count", - "view-all": "View all" + "back-to-dashboard": "ダッシュボードに戻る", + "details.no-users": "選択した期間に参加したユーザーはいません", + "details.no-topics": "選択した期間に投稿されたスレッドはありません", + "details.no-searches": "選択した期間に行われた検索はありません", + "details.no-logins": "選択した期間に記録されたログインはありません", + "details.logins-static": "NodeBBは%1日間のみセッションデータを保存するため、以下の表には直近のアクティブセッションのみ表示されます", + "details.logins-login-time": "ログイン日時", + "start": "開始", + "end": "終了", + "filter": "フィルター", + "view-as-json": "JSONとして表示", + "expand-analytics": "分析を展開", + "clear-search-history": "検索履歴をクリア", + "clear-search-history-confirm": "検索履歴をすべてクリアしてもよろしいですか?", + "search-term": "検索語", + "search-count": "件数", + "view-all": "すべて表示" } diff --git a/public/language/ja/admin/development/info.json b/public/language/ja/admin/development/info.json index 6ad81084ce..0395bbc304 100644 --- a/public/language/ja/admin/development/info.json +++ b/public/language/ja/admin/development/info.json @@ -3,12 +3,12 @@ "ip": "IP %1", "nodes-responded": "%1ノードは%2ms以内に応答しました!", "host": "ホスト", - "primary": "primary / jobs", + "primary": "プライマリ / ジョブ", "pid": "pid", - "nodejs": "nodejs", + "nodejs": "Node.js", "online": "オンライン", - "git": "git", - "process-memory": "rss/heap used", + "git": "Git", + "process-memory": "プロセスメモリ", "system-memory": "システムメモリ", "used-memory-process": "プロセスが使用するメモリ", "used-memory-os": "使用中のシステムメモリ", diff --git a/public/language/ja/admin/development/logger.json b/public/language/ja/admin/development/logger.json index 3e6943e340..da1d2b9adb 100644 --- a/public/language/ja/admin/development/logger.json +++ b/public/language/ja/admin/development/logger.json @@ -1,12 +1,12 @@ { - "logger": "Logger", + "logger": "ロガー", "logger-settings": "ロガー設定", - "description": "チェックボックスをオンにすると、ターミナルにログが送信されます。パスを指定した場合、ログはファイルに保存されます。HTTPロギングは誰が、いつ、どんなユーザがあなたのフォーラムにアクセスしたかに関する統計を収集するのに便利です。HTTPリクエストだけでなく、socket.ioイベントのロギングをすることもできます。redis-cliモニタと組み合わせたsocket.ioロギングは、NodeBBの内部を学習するのに非常に役立ちます。", - "explanation": "ロギング設定をオンまたはオフにするだけで、瞬時にロギングを有効または無効にすることができます。再起動する必要はありません。", + "description": "チェックボックスを有効にすると、ターミナルにログが表示されます。パスを指定すると、ログはファイルに保存されます。HTTPロギングは、フォーラムで誰が、いつ、何にアクセスしたかの統計を収集するのに便利です。HTTPリクエストのロギングに加えて、socket.ioイベントもログに記録できます。socket.ioロギングは、redis-cli monitorと組み合わせることで、NodeBBの内部動作を理解するのに非常に役立ちます。", + "explanation": "ロギング設定のチェックをオン/オフするだけで、ロギングを有効または無効にできます。再起動は不要です。", "enable-http": "HTTPロギングを有効にする", "enable-socket": "socket.ioイベントのロギングを有効にする", "file-path": "ログファイルのパス", - "file-path-placeholder": "/path/to/log/file.log ::: 空白の状態でターミナルにログを表示する", + "file-path-placeholder": "/path/to/log/file.log ::: 空白のままにするとターミナルにログを表示", "control-panel": "ロガーのコントロールパネル", "update-settings": "ロガー設定を更新する" diff --git a/public/language/ja/admin/manage/custom-reasons.json b/public/language/ja/admin/manage/custom-reasons.json index 90a2e620af..6af9626382 100644 --- a/public/language/ja/admin/manage/custom-reasons.json +++ b/public/language/ja/admin/manage/custom-reasons.json @@ -1,16 +1,16 @@ { - "title": "Manage Custom Reasons", - "create-reason": "Create Reason", - "edit-reason": "Edit Reason", - "reasons-help": "Reasons are predefined explanations used when banning or muting users, or when rejecting posts in the post queue.", - "reason-title": "Title", - "reason-type": "Type", - "reason-body": "Body", - "reason-all": "All", - "reason-ban": "Ban", - "reason-mute": "Mute", - "reason-post-queue": "Post Queue", - "reason-type-help": "The type of action this reason applies to. If 'All' is selected, this reason will be available for all action types.", - "custom-reasons-saved": "Custom reasons saved successfully", - "delete-reason-confirm-x": "Are you sure you want to delete the custom reason with the title %1?" + "title": "通知文の管理", + "create-reason": "通知文を作成", + "edit-reason": "通知文を編集", + "reasons-help": "通知文は、ユーザーのBANやミュート、または投稿キューでの投稿却下時に使用する事前設定の文面です。", + "reason-title": "タイトル", + "reason-type": "種類", + "reason-body": "本文", + "reason-all": "すべて", + "reason-ban": "BAN", + "reason-mute": "ミュート", + "reason-post-queue": "投稿キュー", + "reason-type-help": "この通知文を使用する対応の種類です。「すべて」を選択すると、すべての対応でこの通知文を使用できます。", + "custom-reasons-saved": "通知文を保存しました", + "delete-reason-confirm-x": "タイトルが %1 の通知文を削除してもよろしいですか?" } \ No newline at end of file diff --git a/public/language/ja/admin/manage/users.json b/public/language/ja/admin/manage/users.json index 3f2408fd56..05ca18a673 100644 --- a/public/language/ja/admin/manage/users.json +++ b/public/language/ja/admin/manage/users.json @@ -1,7 +1,7 @@ { - "manage-users": "Manage Users", + "manage-users": "ユーザーを管理", "users": "ユーザー", - "edit": "Actions", + "edit": "アクション", "make-admin": "管理者にする", "remove-admin": "管理者を削除", "change-email": "Change Email", @@ -22,8 +22,8 @@ "delete-content": "Delete User(s) Content", "purge": "Delete User(s) and Content", "download-csv": "CSVでダウンロード", - "custom-user-fields": "Custom User Fields", - "custom-reasons": "Custom Reasons", + "custom-user-fields": "カスタムユーザーフィールド", + "custom-reasons": "通知文", "manage-groups": "Manage Groups", "set-reputation": "Set Reputation", "add-group": "Add Group", @@ -128,31 +128,31 @@ "alerts.email-sent-to": "招待メールが%1に送られました。", "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", "alerts.select-a-single-user-to-change-email": "Select a single user to change email", - "export": "Export", - "export-users-fields-title": "Select CSV Fields", - "export-field-email": "Email", - "export-field-username": "Username", + "export": "エクスポート", + "export-users-fields-title": "CSVフィールドを選択", + "export-field-email": "メール", + "export-field-username": "ユーザー名", "export-field-uid": "UID", "export-field-ip": "IP", - "export-field-joindate": "Join date", - "export-field-lastonline": "Last Online", - "export-field-lastposttime": "Last Post Time", - "export-field-reputation": "Reputation", - "export-field-postcount": "Post Count", - "export-field-topiccount": "Topic Count", - "export-field-profileviews": "Profile Views", - "export-field-followercount": "Follower Count", - "export-field-followingcount": "Following Count", - "export-field-fullname": "Full Name", - "export-field-website": "Website", - "export-field-location": "Location", - "export-field-birthday": "Birthday", - "export-field-signature": "Signature", - "export-field-aboutme": "About Me", + "export-field-joindate": "参加日", + "export-field-lastonline": "最終オンライン", + "export-field-lastposttime": "最終投稿日時", + "export-field-reputation": "評価", + "export-field-postcount": "投稿数", + "export-field-topiccount": "トピック数", + "export-field-profileviews": "プロフィール閲覧数", + "export-field-followercount": "フォロワー数", + "export-field-followingcount": "フォロー数", + "export-field-fullname": "氏名", + "export-field-website": "ウェブサイト", + "export-field-location": "場所", + "export-field-birthday": "誕生日", + "export-field-signature": "署名", + "export-field-aboutme": "自己紹介", - "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", - "export-users-completed": "Users exported as csv, click here to download.", - "email": "Email", - "password": "Password", - "manage": "Manage" + "export-users-started": "ユーザーをCSVでエクスポート中です。しばらくかかる場合があります。完了すると通知が届きます。", + "export-users-completed": "ユーザーをCSVでエクスポートしました。ここをクリックしてダウンロードしてください。", + "email": "メール", + "password": "パスワード", + "manage": "管理" } \ No newline at end of file diff --git a/public/language/ja/admin/menu.json b/public/language/ja/admin/menu.json index 346c3f38d0..d920d7ecc8 100644 --- a/public/language/ja/admin/menu.json +++ b/public/language/ja/admin/menu.json @@ -1,46 +1,46 @@ { - "section-dashboard": "Dashboards", - "dashboard/overview": "Overview", - "dashboard/logins": "Logins", - "dashboard/users": "Users", - "dashboard/topics": "Topics", - "dashboard/searches": "Searches", + "section-dashboard": "ダッシュボード", + "dashboard/overview": "概要", + "dashboard/logins": "ログイン", + "dashboard/users": "ユーザー", + "dashboard/topics": "スレッド", + "dashboard/searches": "検索", "section-general": "一般", "section-manage": "管理", "manage/categories": "カテゴリ", - "manage/privileges": "Privileges", + "manage/privileges": "権限", "manage/tags": "タグ", "manage/users": "ユーザー", - "manage/admins-mods": "Admins & Mods", + "manage/admins-mods": "管理者とモデレーター", "manage/registration": "登録キュー", - "manage/flagged-content": "Flagged Content", + "manage/flagged-content": "フラグ付きコンテンツ", "manage/post-queue": "投稿キュー", "manage/groups": "グループ", "manage/ip-blacklist": "IPブラックリスト", - "manage/uploads": "Uploads", - "manage/digest": "Digests", + "manage/uploads": "アップロード", + "manage/digest": "ダイジェスト", "section-settings": "設定", "settings/general": "一般", - "settings/homepage": "Home Page", - "settings/navigation": "Navigation", - "settings/reputation": "Reputation & Flags", + "settings/homepage": "ホームページ", + "settings/navigation": "ナビゲーション", + "settings/reputation": "評価とフラグ", "settings/email": "メール", - "settings/user": "Users", - "settings/group": "Groups", + "settings/user": "メール", + "settings/group": "グループ", "settings/guest": "ゲスト", "settings/uploads": "アップロード", - "settings/languages": "Languages", - "settings/post": "Posts", - "settings/chat": "Chats", + "settings/languages": "言語", + "settings/post": "投稿", + "settings/chat": "チャット", "settings/pagination": "ページ", "settings/tags": "タグ", "settings/notifications": "通知", - "settings/api": "API Access", - "settings/activitypub": "Federation (ActivityPub)", - "settings/sounds": "Sounds", - "settings/social": "Social", + "settings/api": "APIアクセス", + "settings/activitypub": "フェデレーション(ActivityPub)", + "settings/sounds": "サウンド", + "settings/social": "ソーシャル", "settings/cookies": "クッキー", "settings/web-crawler": "Webクローラー", "settings/sockets": "接続数", @@ -48,18 +48,18 @@ "settings.page-title": "%1の設定", - "section-federation": "Federation", - "federation/general": "General", - "federation/content": "Content", - "federation/rules": "Categorization", - "federation/relays": "Relays", - "federation/pruning": "Storage", - "federation/safety": "Trust & Safety", + "section-federation": "フェデレーション", + "federation/general": "一般", + "federation/content": "コンテンツ", + "federation/rules": "カテゴリ分け", + "federation/relays": "リレー", + "federation/pruning": "ストレージ", + "federation/safety": "信頼と安全", "section-appearance": "外観", "appearance/themes": "テーマ", "appearance/skins": "スキン", - "appearance/customise": "Custom Content (HTML/JS/CSS)", + "appearance/customise": "カスタムコンテンツ(HTML/JS/CSS)", "section-extend": "拡張", "extend/plugins": "プラグイン", @@ -74,29 +74,29 @@ "section-advanced": "高度", "advanced/database": "データベース", "advanced/events": "イベント", - "advanced/hooks": "Hooks", + "advanced/hooks": "フック", "advanced/logs": "ログ", "advanced/errors": "エラー", "advanced/cache": "キャッシュ", - "advanced/jobs": "Jobs", + "advanced/jobs": "ジョブ", "development/logger": "ロガー", "development/info": "情報", - "rebuild-and-restart-forum": "Rebuild & Restart Forum", - "rebuild-and-restart": "Rebuild & Restart", - "restart-forum": "フォーラムを再開", - "restart": "Restart", + "rebuild-and-restart-forum": "フォーラムを再構築して再起動", + "rebuild-and-restart": "再構築して再起動", + "restart-forum": "フォーラムを再起動", + "restart": "再起動", "logout": "ログアウト", "view-forum": "フォーラムを表示", - "search.placeholder": "Search settings", + "search.placeholder": "設定を検索", "search.no-results": "結果がありません...", "search.search-forum": "フォーラムでを検索", "search.keep-typing": "結果を見るにはもっと入力してください...", - "search.start-typing": "結果を見るために入力を開始...", + "search.start-typing": "入力を開始して結果を表示...", - "connection-lost": "%1への接続が切れたので、再接続しています...", + "connection-lost": "%1 への接続が切断されました。再接続を試みています...", - "alerts.version": "Running NodeBB v%1", - "alerts.upgrade": "Upgrade to v%1" + "alerts.version": "NodeBB v%1を実行中", + "alerts.upgrade": "v%1にアップグレード" } \ No newline at end of file diff --git a/public/language/ja/admin/settings/activitypub.json b/public/language/ja/admin/settings/activitypub.json index d21cc86d60..d936f2b363 100644 --- a/public/language/ja/admin/settings/activitypub.json +++ b/public/language/ja/admin/settings/activitypub.json @@ -2,55 +2,55 @@ "intro-lead": "フェデレーションとは?", "intro-body": "NodeBBは、それをサポートする他のNodeBBインスタンスと通信できます。これはActivityPubと呼ばれるプロトコルで実現されます。有効にすると、NodeBBはActivityPubを使用する他のアプリやウェブサイト(Mastodon、Peertubeなど)とも通信できるようになります。", "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": "保持期間", + "content-pruning": "リモートコンテンツを保持する日数", + "content-pruning-help": "エンゲージメント(返信や高評価/低評価)を受けたリモートコンテンツは保持されます。(0で無効)", + "user-pruning": "リモートユーザーアカウントをキャッシュする日数", + "user-pruning-help": "リモートユーザーアカウントは、投稿がない場合にのみ削除されます。投稿がある場合は再取得されます。(0で無効)", + "enabled": "フェデレーションを有効にする", + "enabled-help": "有効にすると、このNodeBBは広いフェディバース上のすべてのActivityPub対応クライアントと通信できるようになります。", + "allowLoopback": "ループバック処理を許可", + "allowLoopback-help": "デバッグ目的でのみ有用です。無効のままにしておくことをお勧めします。", - "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": "アプリで開く", + "probe-enabled": "ActivityPub対応リソースをNodeBBで開く", + "probe-enabled-help": "有効にすると、NodeBBはすべての外部リンクでActivityPub相当をチェックし、代わりにNodeBBで読み込みます。", + "probe-timeout": "ルックアップタイムアウト(ミリ秒)", + "probe-timeout-help": "(デフォルト: 2000)ルックアップクエリが設定時間内に応答を受け取らない場合、ユーザーをリンクに直接送信します。サイトの応答が遅く、余分な時間を与えたい場合は、この数値を高く調整してください。", - "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": "カテゴリ分け", + "rules-intro": "ActivityPubで発見されたコンテンツは、特定のルール(ハッシュタグなど)に基づいて自動的にカテゴリ分けできます", + "rules.modal.title": "仕組み", + "rules.modal.instructions": "受信するすべてのコンテンツはこれらのカテゴリ分けルールと照合され、一致するコンテンツは選択したカテゴリに自動的に移動されます。

    注意 既にカテゴリ分けされているコンテンツ(リモートカテゴリ内)はこれらのルールを通過しません。", + "rules.add": "新しいルールを追加", + "rules.help-hashtag": "この大文字小文字を区別しないハッシュタグを含むスレッドが一致します。#記号は入力しないでください", + "rules.help-user": "入力したユーザーが作成したスレッドが一致します。ハンドルまたは完全なIDを入力(例: bob@example.orgまたはhttps://example.org/users/bob)。", + "rules.type": "タイプ", + "rules.value": "値", + "rules.cid": "カテゴリ", - "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": "リレー", + "relays.intro": "リレーは、NodeBBとのコンテンツの発見を改善します。リレーを購読すると、リレーが受信したコンテンツがここに転送され、ここで投稿されたコンテンツがリレーによって外部に配信されます。", + "relays.warning": "注意: リレーは大量のトラフィックを送信する可能性があり、ストレージと処理コストが増加する可能性があります。", + "relays.litepub": "NodeBBはLitePubスタイルのリレー標準に従います。ここに入力するURLは/actorで終わる必要があります。", + "relays.add": "新しいリレーを追加", + "relays.relay": "リレー", + "relays.state": "状態", + "relays.state-0": "保留中", + "relays.state-1": "受信のみ", + "relays.state-2": "アクティブ", "relays.errors.invalid-url": "Please enter a valid URL", - "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": "フィルタリング", + "count": "このNodeBBは現在%1台のサーバーを認識しています", + "server.filter-help": "NodeBBとのフェデレーションから除外したいサーバーを指定してください。または、特定のサーバーとのフェデレーションを選択的に許可することもできます。両方のオプションがサポートされていますが、相互に排他的です。", + "server.filter-help-hostname": "以下にインスタンスのホスト名のみを入力してください(例: example.org)。改行で区切ります。", + "server.filter-allow-list": "これを許可リストとして使用", - "content.outgoing": "Outgoing", - "content.summary-limit": "Character count after which a summary is generated", - "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", - "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.outgoing": "送信", + "content.summary-limit": "要約を生成する文字数", + "content.summary-limit-help": "この文字数を超えるコンテンツがフェデレーション送信される場合、この制限までの完全な文で構成されるsummaryが生成されます。(デフォルト: 500)", + "content.break-string": "注記/記事の区切り文字", + "content.break-string-help": "この区切り文字は、パワーユーザーが新しいスレッドを作成する際に手動で挿入できます。これにより、NodeBBはその時点までのコンテンツをsummaryの一部として使用します。この文字列が使用されない場合は、文字数によるフォールバックが適用されます。(デフォルト: [...])", + "content.world-default-cid": ""ワールド"ページコンポーザーのデフォルトカテゴリID" } \ No newline at end of file diff --git a/public/language/ja/admin/settings/email.json b/public/language/ja/admin/settings/email.json index 5e654cf334..f12beca5bc 100644 --- a/public/language/ja/admin/settings/email.json +++ b/public/language/ja/admin/settings/email.json @@ -26,24 +26,24 @@ "smtp-transport.username": "Username", "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", "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.test-success": "SMTP Test email sent successfully.", + "smtp-transport.pool": "プール接続を有効にする", + "smtp-transport.pool-help": "プール接続により、NodeBBはメールごとに新しい接続を作成しなくなります。このオプションはSMTPトランスポートが有効な場合にのみ適用されます。", + "smtp-transport.allow-self-signed": "自己署名証明書を許可", + "smtp-transport.allow-self-signed-help": "この設定を有効にすると、自己署名または無効なTLS証明書の使用が許可されます。", + "smtp-transport.test-success": "SMTPテストメールの送信に成功しました。", - "template": "電子メールテンプレートの編集", - "template.select": "電子メールテンプレートを選択", + "template": "メールテンプレートの編集", + "template.select": "メールテンプレートを選択", "template.revert": "オリジナルに戻す", - "test-smtp-settings": "Test SMTP Settings", - "testing": "Eメールテスト", - "testing.success": "Test Email Sent.", - "testing.select": "電子メールテンプレートを選択", - "testing.send": "テスト電子メールを送信する", - "testing.send-help-plugin": "\"%1\" will be used to send test emails.", - "testing.send-help-smtp": "SMTP transport is enabled and will be used to send emails.", - "testing.send-help-no-plugin": "No emailer plugin is installed to send emails, nodemailer will be used by default.", - "testing.send-help": "The test email will be sent to the currently logged in user's email address using the saved settings on this page. ", + "test-smtp-settings": "SMTP設定をテスト", + "testing": "メールテスト", + "testing.success": "テストメールを送信しました。", + "testing.select": "メールテンプレートを選択", + "testing.send": "テストメールを送信", + "testing.send-help-plugin": "「%1」 がテストメールの送信に使用されます。", + "testing.send-help-smtp": "SMTPトランスポートが有効で、メール送信に使用されます。", + "testing.send-help-no-plugin": "メール送信用のメーラープラグインがインストールされていないため、デフォルトでnodemailerが使用されます。", + "testing.send-help": "テストメールは、このページの保存された設定を使用して、現在ログインしているユーザーのメールアドレスに送信されます。", "subscriptions": "Email Digests", "subscriptions.disable": "Disable email digests", "subscriptions.hour": "ダイジェストアワー", diff --git a/public/language/ja/admin/settings/notifications.json b/public/language/ja/admin/settings/notifications.json index 5f195def57..4c40043fd0 100644 --- a/public/language/ja/admin/settings/notifications.json +++ b/public/language/ja/admin/settings/notifications.json @@ -2,8 +2,8 @@ "notifications": "通知", "welcome-notification": "ウェルカム通知", "welcome-notification-link": "ウェルカム通知のリンク", - "welcome-notification-uid": "Welcome Notification User (UID)", - "post-queue-notification-uid": "Post Queue User (UID)", - "notification-delay": "Delay for sending notification emails (seconds)", - "notification-delay-help": "If the user has read the notification within this time, the email will not be sent.
    Default: 60 seconds." + "welcome-notification-uid": "ウェルカム通知ユーザー(UID)", + "post-queue-notification-uid": "投稿キュー通知ユーザー(UID)", + "notification-delay": "通知メール送信の遅延(秒)", + "notification-delay-help": "この時間内にユーザーが通知を読んだ場合、メールは送信されません。
    デフォルト: 60秒。" } \ No newline at end of file diff --git a/public/language/vi/admin/advanced/jobs.json b/public/language/vi/admin/advanced/jobs.json index 7cd625a139..cf2ec3f5b0 100644 --- a/public/language/vi/admin/advanced/jobs.json +++ b/public/language/vi/admin/advanced/jobs.json @@ -2,8 +2,8 @@ "jobs": "Công việc", "job-name": "Tên Công Việc", "schedule": "Lên lịch", - "next-run": "Next Run", - "last-duration": "Last Duration", - "running": "Running", - "active": "Active" + "next-run": "Chạy Tiếp Theo", + "last-duration": "Thời Gian Cuối Cùng", + "running": "Đang chạy", + "active": "Kích hoạt" } \ No newline at end of file diff --git a/public/language/vi/admin/development/info.json b/public/language/vi/admin/development/info.json index 4a49ec9ee9..f7d98e698f 100644 --- a/public/language/vi/admin/development/info.json +++ b/public/language/vi/admin/development/info.json @@ -8,7 +8,7 @@ "nodejs": "nodejs", "online": "trực tuyến", "git": "git", - "process-memory": "rss/heap used", + "process-memory": "rss/heap được sử dụng", "system-memory": "bộ nhớ hệ thống", "used-memory-process": "Đã sử dụng bộ nhớ theo quy trình", "used-memory-os": "Bộ nhớ hệ thống đã sử dụng", diff --git a/public/language/vi/admin/settings/activitypub.json b/public/language/vi/admin/settings/activitypub.json index a117555fe5..de3ec7976e 100644 --- a/public/language/vi/admin/settings/activitypub.json +++ b/public/language/vi/admin/settings/activitypub.json @@ -39,7 +39,7 @@ "relays.state-0": "Đang đợi", "relays.state-1": "Chỉ nhận", "relays.state-2": "Kích hoạt", - "relays.errors.invalid-url": "Please enter a valid URL", + "relays.errors.invalid-url": "Vui lòng nhập URL hợp lệ", "server-filtering": "Lọc", "count": "NodeBB này hiện đã biết về %1 máy chủ", @@ -52,5 +52,5 @@ "content.summary-limit-help": "Khi nội dung được phân phối vượt quá số lượng ký tự này, tóm tắt được tạo ra, bao gồm tất cả các câu hoàn chỉnh trước giới hạn này. (Mặc định: 500)", "content.break-string": "Dấu phân cách Ghi chú/Bài đăng", "content.break-string-help": "Dấu phân cách này có thể được người dùng thành thạo chèn thủ công khi soạn thảo chủ đề mới. Nó hướng dẫn NodeBB sử dụng nội dung cho đến thời điểm đó như một phần của tóm tắt. Nếu chuỗi này không được sử dụng, thì phương án dự phòng dựa trên số lượng ký tự sẽ được áp dụng. (Mặc định: [...])", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.world-default-cid": "ID danh mục mặc định cho trình soạn thảo trang "Thế giới"" } \ No newline at end of file diff --git a/public/language/vi/admin/settings/general.json b/public/language/vi/admin/settings/general.json index bfbdbde36a..7bd3db182e 100644 --- a/public/language/vi/admin/settings/general.json +++ b/public/language/vi/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Mô Tả Trang", "keywords": "Từ Khóa Trang", "keywords-placeholder": "Các từ khóa mô tả cộng đồng của bạn, được phân tách bằng dấu phẩy", - "logo-and-icons": "Media & Branding", + "logo-and-icons": "Truyền Thông & Thương Hiệu", "logo.image": "Ảnh", "logo.image-placeholder": "Đường dẫn đến biểu trưng để hiển thị phần đầu diễn đàn", "logo.upload": "Tải lên", diff --git a/public/language/vi/admin/settings/web-crawler.json b/public/language/vi/admin/settings/web-crawler.json index 7b9a5e6fa8..70f59acb43 100644 --- a/public/language/vi/admin/settings/web-crawler.json +++ b/public/language/vi/admin/settings/web-crawler.json @@ -5,7 +5,7 @@ "disable-rss-feeds": "Tắt Nguồn Cấp RSS", "disable-sitemap-xml": "Tắt Sitemap.xml", "sitemap-topics": "Số lượng Chủ đề để hiển thị trong Sơ đồ trang web", - "sitemap-cache-duration-hours": "Sitemap Cache Duration (hours)", + "sitemap-cache-duration-hours": "Khoảng Thời Gian Bộ Nhớ Đệm Sơ Đồ Trang Web (giờ)", "clear-sitemap-cache": "Xóa Bộ Đệm Sơ Đồ Trang Web", "view-sitemap": "Xem Sơ Đồ Trang" } \ No newline at end of file diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 912ef6c505..469b095cf4 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -82,7 +82,7 @@ "composer.hide-preview": "Ẩn Xem Thử", "composer.help": "Trợ giúp", "composer.user-said-in": "%1 nói trong %2:", - "composer.user-said": "%1 [said](%2):", + "composer.user-said": "%1 [nói](%2):", "composer.discard": "Bạn có chắc muốn loại bỏ bài đăng này?", "composer.submit-and-lock": "Gửi và Khoá", "composer.toggle-dropdown": "Chuyển Đổi Thả Xuống", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 5e687d910d..968a438392 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -205,7 +205,7 @@ "stale.title": "Tạo chủ đề mới thay thế?", "stale.warning": "Chủ đề bạn đang trả lời đã khá cũ. Thay vào đó, bạn có muốn tạo một chủ đề mới và tham khảo phần này trong câu trả lời của bạn không?", "stale.create": "Tạo một chủ đề mới", - "stale.reply-anyway": "Trả lời chủ đề này bằng mọi cách", + "stale.reply-anyway": "Vẫn trả lời chủ đề này", "link-back": "Trả lời: [%1](%2)", "diffs.title": "Lịch Sử Sửa Bài", "diffs.description": "Bài này có %1 sửa đổi. Bấm vào một trong các bản sửa đổi bên dưới để xem nội dung bài tại thời điểm đó.", diff --git a/public/language/vi/world.json b/public/language/vi/world.json index 77a23d73a5..2984e469e8 100644 --- a/public/language/vi/world.json +++ b/public/language/vi/world.json @@ -1,7 +1,7 @@ { "name": "Thế giới", - "latest": "Latest", - "latest-local": "Latest (Local)", + "latest": "Mới nhất", + "latest-local": "Mới nhất (Cục bộ)", "latest-all": "Mới nhất (Tất cả)", "popular-day": "Phổ biến (Ngày)", "popular-week": "Phổ biến (Tuần)", @@ -18,10 +18,10 @@ "help.federating": "Tương tự như vậy, nếu người dùng bên ngoài diễn đàn này bắt đầu theo dõi bạn, thì bài đăng của bạn cũng sẽ bắt đầu xuất hiện trên các ứng dụng và trang web đó.", "help.next-generation": "Đây là thế hệ mạng xã hội kế tiếp, hãy bắt đầu đóng góp ngay hôm nay!", - "onboard.title": "A world of content at your fingertips…", - "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", - "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", - "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", + "onboard.title": "Cả thế giới nội dung nằm trong tầm tay bạn…", + "onboard.what": "Hãy coi đây như một nguồn cấp dữ liệu khám phá toàn cầu. Nó tập hợp các cuộc thảo luận thú vị từ khắp nơi trên web và các cộng đồng khác, tất cả ở cùng một nơi.", + "onboard.why": "Mặc dù bạn có thể xem những gì đang thịnh hành hiện nay, nhưng cách tốt nhất để sử dụng nguồn cấp dữ liệu này là tùy chỉnh nó theo ý mình. Bằng cách tạo tài khoản, bạn có thể theo dõi những người sáng tạo nội dung và chủ đề cụ thể để lọc bỏ những thông tin nhiễu và chỉ xem những gì quan trọng đối với bạn.", + "onboard.how": "Sẵn sàng khám phá chưa? Hãy tạo tài khoản để bắt đầu theo dõi người khác, nhận thông báo khi ai đó trả lời bạn và lưu lại những nội dung yêu thích.", "category-search": "Tìm danh mục...", "see-more": "Xem nhiều hơn", From 9bcef6b5ea4e28c17b7897a4ebce2d1aa73d6a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 23 Mar 2026 09:43:15 -0400 Subject: [PATCH 4687/4744] fix: #14116, don't return ban reason if login credentials are incorrect --- src/controllers/authentication.js | 19 +++++++++---------- test/user.js | 11 +++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 63c551c5af..fab6b8d8cc 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -409,21 +409,20 @@ authenticationController.localLogin = async function (req, username, password, n userData.isAdminOrGlobalMod = isAdminOrGlobalMod; - if (!canLoginIfBanned) { - return next(await getBanError(uid)); - } - - // Doing this after the ban check, because user's privileges might change after a ban expires - const hasLoginPrivilege = await privileges.global.can('local:login', uid); - if (parseInt(uid, 10) && !hasLoginPrivilege) { - return next(new Error('[[error:local-login-disabled]]')); - } - try { const passwordMatch = await user.isPasswordCorrect(uid, password, req.ip); if (!passwordMatch) { return next(new Error('[[error:invalid-login-credentials]]')); } + if (!canLoginIfBanned) { + return next(await getBanError(uid)); + } + + // Doing this after the ban check, because user's privileges might change after a ban expires + const hasLoginPrivilege = await privileges.global.can('local:login', uid); + if (parseInt(uid, 10) && !hasLoginPrivilege) { + return next(new Error('[[error:local-login-disabled]]')); + } } catch (e) { if (req.loggedIn) { await logoutAsync(req); diff --git a/test/user.js b/test/user.js index 3eb85d83ed..f6619bd651 100644 --- a/test/user.js +++ b/test/user.js @@ -1394,6 +1394,17 @@ describe('User', () => { assert.strictEqual(await db.isSortedSetMember('users:banned', testUid), false); }); + it('should not return ban reason if login is incorrect', async () => { + const testUid = await User.create({ username: 'bannedUser4', password: '654321' }); + await User.bans.ban(testUid, 0, 'testing bans'); + let { response, body } = await helpers.loginUser('bannedUser4', '5555555'); + assert.strictEqual(response.status, 403); + assert.strictEqual(body, '[[error:invalid-login-credentials]]'); + + ({ response, body } = await helpers.loginUser('bannedUser4', '654321')); + assert.strictEqual(response.status, 403); + assert.strictEqual(body.reason, 'testing bans'); + }); }); describe('Digest.getSubscribers', () => { From ad1433e14b73ff8937d0803e0e7ef6ad42e06472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 23 Mar 2026 09:51:56 -0400 Subject: [PATCH 4688/4744] fix: #14108, reset filter on notif dropdown open --- public/src/client/header/notifications.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/src/client/header/notifications.js b/public/src/client/header/notifications.js index 94a23c9ecd..7c0a73cfa9 100644 --- a/public/src/client/header/notifications.js +++ b/public/src/client/header/notifications.js @@ -9,6 +9,9 @@ define('forum/header/notifications', function () { notifTrigger.on('show.bs.dropdown', async (ev) => { const notifications = await app.require('notifications'); const triggerEl = $(ev.target); + const dropdownEl = triggerEl.parent().find('.dropdown-menu'); + dropdownEl.find('[data-filter="all"]').addClass('active'); + dropdownEl.find('[data-filter="unread"]').removeClass('active'); notifications.loadNotifications(triggerEl, triggerEl.parent().find('[component="notifications/list"]')); }); From ce0943242b4547cfe5b810b88cb048ff54ad6f9d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:57:05 -0400 Subject: [PATCH 4689/4744] chore(deps): update dependency smtp-server to v3.18.2 (#14117) 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 c5a9691f26..b163cc4c3a 100644 --- a/install/package.json +++ b/install/package.json @@ -178,7 +178,7 @@ "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "18.0.0", - "smtp-server": "3.18.1" + "smtp-server": "3.18.2" }, "optionalDependencies": { "sass-embedded": "1.98.0" From 246b2f1b1950b4afebe5091035c688d818412a9f Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 23 Mar 2026 14:03:54 +0000 Subject: [PATCH 4690/4744] chore(i18n): fallback strings for new resources: nodebb.topic --- public/language/ar/topic.json | 2 ++ public/language/az/topic.json | 2 ++ public/language/bg/topic.json | 2 ++ public/language/bn/topic.json | 2 ++ public/language/cs/topic.json | 2 ++ public/language/da/topic.json | 2 ++ public/language/de/topic.json | 2 ++ public/language/el/topic.json | 2 ++ public/language/en-US/topic.json | 2 ++ public/language/en-x-pirate/topic.json | 2 ++ public/language/es/topic.json | 2 ++ public/language/et/topic.json | 2 ++ public/language/fa-IR/topic.json | 2 ++ public/language/fi/topic.json | 2 ++ public/language/fr/topic.json | 2 ++ public/language/gl/topic.json | 2 ++ public/language/he/topic.json | 2 ++ public/language/hr/topic.json | 2 ++ public/language/hu/topic.json | 2 ++ public/language/hy/topic.json | 2 ++ public/language/id/topic.json | 2 ++ public/language/it/topic.json | 2 ++ public/language/ja/topic.json | 2 ++ public/language/ko/topic.json | 2 ++ public/language/lt/topic.json | 2 ++ public/language/lv/topic.json | 2 ++ public/language/ms/topic.json | 2 ++ public/language/nb/topic.json | 2 ++ public/language/nl/topic.json | 2 ++ public/language/nn-NO/topic.json | 2 ++ public/language/pl/topic.json | 2 ++ public/language/pt-BR/topic.json | 2 ++ public/language/pt-PT/topic.json | 2 ++ public/language/ro/topic.json | 2 ++ public/language/ru/topic.json | 2 ++ public/language/rw/topic.json | 2 ++ public/language/sc/topic.json | 2 ++ public/language/sk/topic.json | 2 ++ public/language/sl/topic.json | 2 ++ public/language/sq-AL/topic.json | 2 ++ public/language/sr/topic.json | 2 ++ public/language/sv/topic.json | 2 ++ public/language/th/topic.json | 2 ++ public/language/tr/topic.json | 2 ++ public/language/uk/topic.json | 2 ++ public/language/ur/topic.json | 2 ++ public/language/vi/topic.json | 2 ++ public/language/zh-CN/topic.json | 2 ++ public/language/zh-TW/topic.json | 2 ++ 49 files changed, 98 insertions(+) diff --git a/public/language/ar/topic.json b/public/language/ar/topic.json index b69b8b4aec..cdbc174ac7 100644 --- a/public/language/ar/topic.json +++ b/public/language/ar/topic.json @@ -92,6 +92,8 @@ "watch.title": "استلم تنبيها بالردود الجديدة في هذا الموضوع", "unwatch.title": "ألغ مراقبة هذا الموضوع", "share-this-post": "انشر هذا الموضوع", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "مراقبة", "not-watching": "غير مراقب", "ignoring": "تجاهل", diff --git a/public/language/az/topic.json b/public/language/az/topic.json index f39df08e21..17cc0b9eb8 100644 --- a/public/language/az/topic.json +++ b/public/language/az/topic.json @@ -92,6 +92,8 @@ "watch.title": "Bu mövzuda yeni cavablardan xəbərdar olun", "unwatch.title": "Bu mövzuya izləməni dayandır", "share-this-post": "Bu yazını paylaş", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "İzlənilən", "not-watching": "İzlənilməyən", "ignoring": "İqnor edilir", diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index 00bc76158f..efcf9d0d94 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -92,6 +92,8 @@ "watch.title": "Получавайте известия за новите отговори в тази тема", "unwatch.title": "Спрете да наблюдавате тази тема", "share-this-post": "Споделете тази публикация", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Наблюдавате", "not-watching": "Не наблюдавате", "ignoring": "Пренебрегвате", diff --git a/public/language/bn/topic.json b/public/language/bn/topic.json index 0ec97352bf..b083eec02b 100644 --- a/public/language/bn/topic.json +++ b/public/language/bn/topic.json @@ -92,6 +92,8 @@ "watch.title": "এই টপিকে নতুন উত্তর এলে বিজ্ঞাপণের মাধ্যমে জানুন।", "unwatch.title": "এই টপিক দেখা বন্ধ করুন", "share-this-post": "এই পোষ্টটি শেয়ার করুন", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index df4f0babd8..5a03a5ec82 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -92,6 +92,8 @@ "watch.title": "Být upozorněn u nových odpovědí v tomto tématu", "unwatch.title": "Přestat sledovat toto téma", "share-this-post": "Sdílet toto téma", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Sledováno", "not-watching": "Nesledováno", "ignoring": "Ignorování", diff --git a/public/language/da/topic.json b/public/language/da/topic.json index 6484acf8f0..ed66ae212c 100644 --- a/public/language/da/topic.json +++ b/public/language/da/topic.json @@ -92,6 +92,8 @@ "watch.title": "Bliv notificeret ved nye indlæg i dette emne", "unwatch.title": "Fjern overvågning af dette emne", "share-this-post": "Del dette indlæg", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/de/topic.json b/public/language/de/topic.json index ef4bb801a6..5aee5c12ff 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -92,6 +92,8 @@ "watch.title": "Bei neuen Antworten benachrichtigen", "unwatch.title": "Dieses Thema nicht mehr beobachten", "share-this-post": "Diesen Beitrag teilen", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Beobachtet", "not-watching": "Nicht beobachtet", "ignoring": "Ignoriert", diff --git a/public/language/el/topic.json b/public/language/el/topic.json index 0b4ec8b7b9..a0644e5fbd 100644 --- a/public/language/el/topic.json +++ b/public/language/el/topic.json @@ -92,6 +92,8 @@ "watch.title": "Να ειδοποιούμαι για νέες απαντήσεις σε αυτό το θέμα", "unwatch.title": "Να μην παρακολουθώ αυτό το θέμα", "share-this-post": "Μοιράσου αυτή την Δημοσίευση", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/en-US/topic.json b/public/language/en-US/topic.json index 648200eb74..891a07dbfb 100644 --- a/public/language/en-US/topic.json +++ b/public/language/en-US/topic.json @@ -92,6 +92,8 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share-this-post": "Share this Post", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/en-x-pirate/topic.json b/public/language/en-x-pirate/topic.json index 648200eb74..891a07dbfb 100644 --- a/public/language/en-x-pirate/topic.json +++ b/public/language/en-x-pirate/topic.json @@ -92,6 +92,8 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share-this-post": "Share this Post", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/es/topic.json b/public/language/es/topic.json index addda06590..d9748a4868 100644 --- a/public/language/es/topic.json +++ b/public/language/es/topic.json @@ -92,6 +92,8 @@ "watch.title": "Serás notificado cuando haya nuevas respuestas en este tema", "unwatch.title": "Dejar de seguir este tema", "share-this-post": "Compartir este mensaje", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Siguiendo", "not-watching": "No siguiendo", "ignoring": "Ignorando", diff --git a/public/language/et/topic.json b/public/language/et/topic.json index cb3b8377d4..ebc6550487 100644 --- a/public/language/et/topic.json +++ b/public/language/et/topic.json @@ -92,6 +92,8 @@ "watch.title": "Saa teateid uutest postitustest siin teemas", "unwatch.title": "Ära järgi enam seda teemat", "share-this-post": "Jaga seda postitust", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Vaatan", "not-watching": "Ei vaata", "ignoring": "Ignoreerin", diff --git a/public/language/fa-IR/topic.json b/public/language/fa-IR/topic.json index cba5ba57b3..fc3757465f 100644 --- a/public/language/fa-IR/topic.json +++ b/public/language/fa-IR/topic.json @@ -92,6 +92,8 @@ "watch.title": "از پاسخ‌های تازه به این موضوع آگاه شوید.", "unwatch.title": "توقف پیگیری این موضوع", "share-this-post": "به اشتراک‌گذاری این موضوع", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "درحال پیگیری", "not-watching": "درحال پیگیری نیستید", "ignoring": "نادیده گرفتن", diff --git a/public/language/fi/topic.json b/public/language/fi/topic.json index 01854c1383..e542adf4eb 100644 --- a/public/language/fi/topic.json +++ b/public/language/fi/topic.json @@ -92,6 +92,8 @@ "watch.title": "Ilmoita, kun tähän keskusteluun tulee uusia viestejä", "unwatch.title": "Lopeta aiheen seuraaminen", "share-this-post": "Jaa viesti", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Seurataan", "not-watching": "Ei seurata", "ignoring": "Sivuutettu", diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index c1d8676507..9da26d2fd4 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -92,6 +92,8 @@ "watch.title": "Être notifié des nouvelles réponses dans ce sujet", "unwatch.title": "Cesser de suivre ce sujet", "share-this-post": "Partager ce message", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Suivi", "not-watching": "Suivre", "ignoring": "Ignoré", diff --git a/public/language/gl/topic.json b/public/language/gl/topic.json index fb506285fb..9a676d2ba4 100644 --- a/public/language/gl/topic.json +++ b/public/language/gl/topic.json @@ -92,6 +92,8 @@ "watch.title": "Serás notificado canto haxa novas respostas neste tema", "unwatch.title": "Deixar de seguir este tema", "share-this-post": "Compartir esta publicación", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Seguindo", "not-watching": "Non seguindo", "ignoring": "Ignorar", diff --git a/public/language/he/topic.json b/public/language/he/topic.json index 393046d12e..e6c69f47c3 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -92,6 +92,8 @@ "watch.title": "קבלת התראה כאשר יש תגובות חדשות בנושא זה", "unwatch.title": "הפסקת מעקב אחר נושא זה", "share-this-post": "שיתוף פוסט זה", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "במעקב", "not-watching": "לא במעקב", "ignoring": "התעלמות", diff --git a/public/language/hr/topic.json b/public/language/hr/topic.json index 3dccfbf9cb..10948c645e 100644 --- a/public/language/hr/topic.json +++ b/public/language/hr/topic.json @@ -92,6 +92,8 @@ "watch.title": "Budi obaviješten o novim objavama u ovoj temi", "unwatch.title": "Prestani pratiti ovu temu", "share-this-post": "Podijeli ovu objavu", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Prati", "not-watching": "Ne pratiš", "ignoring": "Ignoriraš", diff --git a/public/language/hu/topic.json b/public/language/hu/topic.json index fbb3829cad..94a7f41570 100644 --- a/public/language/hu/topic.json +++ b/public/language/hu/topic.json @@ -92,6 +92,8 @@ "watch.title": "Értesítsen a témakör új válaszairól", "unwatch.title": "Témakör figyelésének leállítása.", "share-this-post": "Hozzászólás megosztása", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Figyelés", "not-watching": "Nincs figyelés", "ignoring": "Mellőzés", diff --git a/public/language/hy/topic.json b/public/language/hy/topic.json index e5b6186230..9aa3711b77 100644 --- a/public/language/hy/topic.json +++ b/public/language/hy/topic.json @@ -92,6 +92,8 @@ "watch.title": "Տեղեկացեք այս թեմայի նոր պատասխանների մասին", "unwatch.title": "Դադարեք դիտել այս թեման", "share-this-post": "Տարածեք այս գրառումը", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Դիտում", "not-watching": "Չեն դիտում", "ignoring": "Անտեսել", diff --git a/public/language/id/topic.json b/public/language/id/topic.json index 1c4020103a..30e421eeda 100644 --- a/public/language/id/topic.json +++ b/public/language/id/topic.json @@ -92,6 +92,8 @@ "watch.title": "Beritahukan balasan baru untuk topik ini", "unwatch.title": "Berhenti memantau topik ini", "share-this-post": "Bagikan Posting ini", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 9a0472f63d..25ca87ff06 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -92,6 +92,8 @@ "watch.title": "Ricevi notifiche di nuove risposte in questa discussione", "unwatch.title": "Smetti di osservare questa discussione", "share-this-post": "Condividi questo Post", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Seguito", "not-watching": "Non Seguito", "ignoring": "Ignorato", diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json index 1174660b11..2542ecff75 100644 --- a/public/language/ja/topic.json +++ b/public/language/ja/topic.json @@ -92,6 +92,8 @@ "watch.title": "新しい投稿の通知を受ける", "unwatch.title": "このスレッドの通知を停止します", "share-this-post": "投稿を共有", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "ウォッチ中", "not-watching": "未ウォッチ", "ignoring": "無視中", diff --git a/public/language/ko/topic.json b/public/language/ko/topic.json index d39fa603d1..753607aae2 100644 --- a/public/language/ko/topic.json +++ b/public/language/ko/topic.json @@ -92,6 +92,8 @@ "watch.title": "이 토픽에 대한 새로운 답글 알림", "unwatch.title": "이 토픽의 알림 받기 중단", "share-this-post": "이 게시물 공유", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "구독 중", "not-watching": "구독 안 함", "ignoring": "무시 중", diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json index 147a49a87f..7a790c0a47 100644 --- a/public/language/lt/topic.json +++ b/public/language/lt/topic.json @@ -92,6 +92,8 @@ "watch.title": "Gauti pranešimą apie naujus įrašus šioje temoje", "unwatch.title": "Baigti šios temos stebėjimą", "share-this-post": "Dalintis šiuo įrašu", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Stebima", "not-watching": "Not Watching", "ignoring": "Ignoruojama", diff --git a/public/language/lv/topic.json b/public/language/lv/topic.json index bed7f11019..a5b96bc2ee 100644 --- a/public/language/lv/topic.json +++ b/public/language/lv/topic.json @@ -92,6 +92,8 @@ "watch.title": "Tiec informēts par jaunām atbildēm šajā tematā", "unwatch.title": "Pārtraukt temata novērošanu", "share-this-post": "Kopīgot rakstu", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Novērots", "not-watching": "Nav novērots", "ignoring": "Ignorēts", diff --git a/public/language/ms/topic.json b/public/language/ms/topic.json index 90a0497bd0..d18b1e31b3 100644 --- a/public/language/ms/topic.json +++ b/public/language/ms/topic.json @@ -92,6 +92,8 @@ "watch.title": "Akan dimaklumkan sekiranya ada balasan dalam topik ini", "unwatch.title": "Berhenti melihat topik ini", "share-this-post": "Kongsi kiriman ini", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index 8365a10001..e13350ac81 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -92,6 +92,8 @@ "watch.title": "Varlse meg om nye svar på dette innlegget", "unwatch.title": "Slutt å følge denne tråden", "share-this-post": "Del ditt innlegg", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Følger", "not-watching": "Følger ikke", "ignoring": "Ignorerer", diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index 75e7ba9326..9d4c4bcd61 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -92,6 +92,8 @@ "watch.title": "Krijg meldingen van nieuwe reacties op dit onderwerp", "unwatch.title": "Dit onderwerp niet langer volgen", "share-this-post": "Deel dit bericht", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Gevolgd", "not-watching": "Niet gevolgd", "ignoring": "Genegeerd", diff --git a/public/language/nn-NO/topic.json b/public/language/nn-NO/topic.json index b9da7ac5bd..af2aecf4db 100644 --- a/public/language/nn-NO/topic.json +++ b/public/language/nn-NO/topic.json @@ -92,6 +92,8 @@ "watch.title": "Varsle meg om nye svar på dette innlegget", "unwatch.title": "Slutt å følge dette innlegget", "share-this-post": "Del dette innlegget", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Følger", "not-watching": "Følger ikkje", "ignoring": "Ignorerer", diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 4eb3081f62..5e33b9acfd 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -92,6 +92,8 @@ "watch.title": "Otrzymuj powiadomienia o nowych odpowiedziach w tym temacie", "unwatch.title": "Przestań obserwować ten temat", "share-this-post": "Udostępnij", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Obserwuj", "not-watching": "Nie obserwuj", "ignoring": "Ignoruj", diff --git a/public/language/pt-BR/topic.json b/public/language/pt-BR/topic.json index 5ea0bb085a..3877ee3ce8 100644 --- a/public/language/pt-BR/topic.json +++ b/public/language/pt-BR/topic.json @@ -92,6 +92,8 @@ "watch.title": "Seja notificado sobre novas respostas neste tópico", "unwatch.title": "Parar de acompanhar este tópico", "share-this-post": "Compartilhar este Post", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Acompanhar", "not-watching": "Não Acompanhar", "ignoring": "Ignorando", diff --git a/public/language/pt-PT/topic.json b/public/language/pt-PT/topic.json index feaba1bd16..e7c37705a2 100644 --- a/public/language/pt-PT/topic.json +++ b/public/language/pt-PT/topic.json @@ -92,6 +92,8 @@ "watch.title": "Ser notificado de novas respostas neste tópicos", "unwatch.title": "Deixar de seguir este tópico", "share-this-post": "Partilhar esta publicação", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "A seguir", "not-watching": "A não seguir", "ignoring": "A ignorar", diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index 8a194de409..d88a055e34 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -92,6 +92,8 @@ "watch.title": "Abonează-te la notificări legate de acest subiect", "unwatch.title": "Oprește urmărirea acestui subiect", "share-this-post": "Distribuie acest mesaj", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index cdbd32efc5..aed8b066d9 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -92,6 +92,8 @@ "watch.title": "Получать уведомления о новых сообщениях в этой теме", "unwatch.title": "Перестать отслеживать эту тему", "share-this-post": "Поделиться сообщением", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Отслеживается", "not-watching": "Не отслеживается", "ignoring": "Игнорируется", diff --git a/public/language/rw/topic.json b/public/language/rw/topic.json index e1a0e0b5c4..6ffbe95a80 100644 --- a/public/language/rw/topic.json +++ b/public/language/rw/topic.json @@ -92,6 +92,8 @@ "watch.title": "Ujye umenyeshwa ibyongerwaho bishya kuri iki kiganiro", "unwatch.title": "Rekera aho gucunga iki kiganiro", "share-this-post": "Sangiza Ibi", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/sc/topic.json b/public/language/sc/topic.json index 78834112ac..03bb305098 100644 --- a/public/language/sc/topic.json +++ b/public/language/sc/topic.json @@ -92,6 +92,8 @@ "watch.title": "Be notified of new replies in this topic", "unwatch.title": "Stop watching this topic", "share-this-post": "Cumpartzi custu Arresonu", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Watching", "not-watching": "Not Watching", "ignoring": "Ignoring", diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json index 3426d3e0b2..079be9c0ce 100644 --- a/public/language/sk/topic.json +++ b/public/language/sk/topic.json @@ -92,6 +92,8 @@ "watch.title": "Buďte informovaní o nových odpovediach k tejto téme", "unwatch.title": "Prestať sledovať túto tému", "share-this-post": "Zdielať tento príspevok", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Sledované", "not-watching": "Nesledované", "ignoring": "Ignorované", diff --git a/public/language/sl/topic.json b/public/language/sl/topic.json index 37ad9995d7..1798f1f45b 100644 --- a/public/language/sl/topic.json +++ b/public/language/sl/topic.json @@ -92,6 +92,8 @@ "watch.title": "Bodi obveščen o novih odgovorih v tej temi", "unwatch.title": "Prenehaj spremljati to temo", "share-this-post": "Deli to objavo", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Spremljano", "not-watching": "Ni spremljano", "ignoring": "Prezri", diff --git a/public/language/sq-AL/topic.json b/public/language/sq-AL/topic.json index ef140e7b83..aae345939a 100644 --- a/public/language/sq-AL/topic.json +++ b/public/language/sq-AL/topic.json @@ -92,6 +92,8 @@ "watch.title": "Njoftohuni për njoftimet e reja në këtë temë", "unwatch.title": "Ndaloni së ndjekuri këtë temë", "share-this-post": "Shpërnda këtë postim", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Duke e ndjekur", "not-watching": "Nuk jam duke ndjekur", "ignoring": "Duke injoruar", diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json index 6141956457..542a7e53eb 100644 --- a/public/language/sr/topic.json +++ b/public/language/sr/topic.json @@ -92,6 +92,8 @@ "watch.title": "Будите обавештени о новим одговорима у овој теми", "unwatch.title": "Заустави надгледање ове теме", "share-this-post": "Дели ову поруку", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Надгледај", "not-watching": "Не надгледај", "ignoring": "Игнориши", diff --git a/public/language/sv/topic.json b/public/language/sv/topic.json index 2a349f13e2..9434054c0b 100644 --- a/public/language/sv/topic.json +++ b/public/language/sv/topic.json @@ -92,6 +92,8 @@ "watch.title": "Få notis om nya svar till det här ämnet", "unwatch.title": "Sluta bevaka detta ämne", "share-this-post": "Dela detta inlägg", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Bevakar", "not-watching": "Bevakar inte", "ignoring": "Ignorerar", diff --git a/public/language/th/topic.json b/public/language/th/topic.json index dc0941dc58..14e527c0a1 100644 --- a/public/language/th/topic.json +++ b/public/language/th/topic.json @@ -92,6 +92,8 @@ "watch.title": "ให้แจ้งเตือนเมื่อมีการตอบกลับกระทู้นี้", "unwatch.title": "ยกเลิกการเฝ้าดูกระทู้นี้", "share-this-post": "แชร์โพสต์นี้", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "กำลังดู", "not-watching": "ไม่ดูแล้ว", "ignoring": "ความเมินเฉย", diff --git a/public/language/tr/topic.json b/public/language/tr/topic.json index 265df0f67a..81a6c7373f 100644 --- a/public/language/tr/topic.json +++ b/public/language/tr/topic.json @@ -92,6 +92,8 @@ "watch.title": "Bu konuya gelen yeni iletilerden haberdar ol", "unwatch.title": "Bu başlığı izleme", "share-this-post": "Bu iletiyi paylaş", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Takip ediliyor", "not-watching": "Takip edilmiyor", "ignoring": "Susturulmuş", diff --git a/public/language/uk/topic.json b/public/language/uk/topic.json index f578f6585b..6b43e01d81 100644 --- a/public/language/uk/topic.json +++ b/public/language/uk/topic.json @@ -92,6 +92,8 @@ "watch.title": "Отримуйте сповіщення про відповіді в цій темі", "unwatch.title": "Перестати стежити за цією темою", "share-this-post": "Поширити цей пост", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Відстежується", "not-watching": "Не відстежується", "ignoring": "Ігнорується", diff --git a/public/language/ur/topic.json b/public/language/ur/topic.json index 1e207d9b24..a189dee35f 100644 --- a/public/language/ur/topic.json +++ b/public/language/ur/topic.json @@ -92,6 +92,8 @@ "watch.title": "اس موضوع میں نئے جوابات کے لیے نوٹیفکیشنز حاصل کریں", "unwatch.title": "اس موضوع کا مشاہدہ بند کریں", "share-this-post": "اس پوسٹ کو شیئر کریں", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "مشاہدہ کر رہے ہیں", "not-watching": "مشاہدہ نہیں کر رہے", "ignoring": "نظر انداز کر رہے ہیں", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 968a438392..cc0d2f89a6 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -92,6 +92,8 @@ "watch.title": "Thông báo trả lời mới trong chủ đề này", "unwatch.title": "Ngừng xem chủ đề này", "share-this-post": "Chia sẻ bài viết này", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "Đang xem", "not-watching": "Chưa Xem", "ignoring": "Bỏ qua", diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json index df47079cb6..7febb739f5 100644 --- a/public/language/zh-CN/topic.json +++ b/public/language/zh-CN/topic.json @@ -92,6 +92,8 @@ "watch.title": "当此主题有新回复时,通知我", "unwatch.title": "取消关注此主题", "share-this-post": "分享此帖子", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "关注中", "not-watching": "未关注", "ignoring": "忽略中", diff --git a/public/language/zh-TW/topic.json b/public/language/zh-TW/topic.json index d3993b079d..46a6356efd 100644 --- a/public/language/zh-TW/topic.json +++ b/public/language/zh-TW/topic.json @@ -92,6 +92,8 @@ "watch.title": "當此主題有新回覆時,通知我", "unwatch.title": "取消關注此主題", "share-this-post": "分享此貼文", + "share-mail-subject": "Check out this post on \"%1\"", + "share-mail-body": "I thought you might be interested in this post: %1", "watching": "關注中", "not-watching": "未關注", "ignoring": "忽略中", From 334478fe4c635a85978b904746b2fc0d231731b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:05:51 -0400 Subject: [PATCH 4691/4744] fix(deps): update dependency diff to v8.0.4 (#14118) 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 4f39b800b0..83fb640024 100644 --- a/install/package.json +++ b/install/package.json @@ -66,7 +66,7 @@ "cropperjs": "1.6.2", "csrf-sync": "4.2.1", "daemon": "1.1.0", - "diff": "8.0.3", + "diff": "8.0.4", "esbuild": "0.27.4", "express": "4.22.1", "express-session": "1.19.0", From 52e42685e8c87ae8be88488b6f1e15c3a7881099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 23 Mar 2026 11:24:05 -0400 Subject: [PATCH 4692/4744] fix: key name --- src/user/delete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user/delete.js b/src/user/delete.js index 3de2127ed0..5c15c5c660 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -134,7 +134,7 @@ module.exports = function (User) { `uid:${uid}:flag:pids`, `uid:${uid}:sessions`, `uid:${uid}:shares`, - `uid:${uid}:profile:images`, + `uid:${uid}:profile:pictures`, `invitation:uid:${uid}`, ]; From 38901c0f02487637893accaeddb36ee1c57bcad2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 23 Mar 2026 11:25:27 -0400 Subject: [PATCH 4693/4744] fix: move AP pageviews middleware down the chain, after s2s assertion and http sig verification, so as to truly count AP requests --- src/routes/activitypub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index 6c13fea365..4e87aa1aa6 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -20,9 +20,9 @@ module.exports = function (app, middleware, controllers) { const middlewares = [ middleware.activitypub.enabled, - middleware.activitypub.pageview, middleware.activitypub.assertS2S, middleware.activitypub.verify, + middleware.activitypub.pageview, middleware.activitypub.configureResponse, ]; From 4666765a28a1487138374877ba3f9f95e09dc19f Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 24 Mar 2026 09:07:36 +0000 Subject: [PATCH 4694/4744] Latest translations and fallbacks --- public/language/bg/topic.json | 4 +- public/language/de/topic.json | 4 +- public/language/it/topic.json | 4 +- .../ja/admin/settings/activitypub.json | 2 +- public/language/ja/admin/settings/post.json | 60 +++++++++---------- public/language/ja/groups.json | 50 ++++++++-------- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index efcf9d0d94..6472e1b748 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -92,8 +92,8 @@ "watch.title": "Получавайте известия за новите отговори в тази тема", "unwatch.title": "Спрете да наблюдавате тази тема", "share-this-post": "Споделете тази публикация", - "share-mail-subject": "Check out this post on \"%1\"", - "share-mail-body": "I thought you might be interested in this post: %1", + "share-mail-subject": "Виж тази публикация в „%1“", + "share-mail-body": "Реших, че тази публикация може да ти е интересна: %1", "watching": "Наблюдавате", "not-watching": "Не наблюдавате", "ignoring": "Пренебрегвате", diff --git a/public/language/de/topic.json b/public/language/de/topic.json index 5aee5c12ff..c7e3c17516 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -92,8 +92,8 @@ "watch.title": "Bei neuen Antworten benachrichtigen", "unwatch.title": "Dieses Thema nicht mehr beobachten", "share-this-post": "Diesen Beitrag teilen", - "share-mail-subject": "Check out this post on \"%1\"", - "share-mail-body": "I thought you might be interested in this post: %1", + "share-mail-subject": "Schau dir diesen Beitrag zu „%1“ an", + "share-mail-body": "Ich dachte, dieser Beitrag könnte dich interessieren: %1", "watching": "Beobachtet", "not-watching": "Nicht beobachtet", "ignoring": "Ignoriert", diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 25ca87ff06..742d6667b3 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -92,8 +92,8 @@ "watch.title": "Ricevi notifiche di nuove risposte in questa discussione", "unwatch.title": "Smetti di osservare questa discussione", "share-this-post": "Condividi questo Post", - "share-mail-subject": "Check out this post on \"%1\"", - "share-mail-body": "I thought you might be interested in this post: %1", + "share-mail-subject": "Dai un'occhiata a questo post in \"%1\"", + "share-mail-body": "Ho pensato che questo post potesse interessarti: %1", "watching": "Seguito", "not-watching": "Non Seguito", "ignoring": "Ignorato", diff --git a/public/language/ja/admin/settings/activitypub.json b/public/language/ja/admin/settings/activitypub.json index d936f2b363..1f7b9347a6 100644 --- a/public/language/ja/admin/settings/activitypub.json +++ b/public/language/ja/admin/settings/activitypub.json @@ -39,7 +39,7 @@ "relays.state-0": "保留中", "relays.state-1": "受信のみ", "relays.state-2": "アクティブ", - "relays.errors.invalid-url": "Please enter a valid URL", + "relays.errors.invalid-url": "有効なURLを入力してください", "server-filtering": "フィルタリング", "count": "このNodeBBは現在%1台のサーバーを認識しています", diff --git a/public/language/ja/admin/settings/post.json b/public/language/ja/admin/settings/post.json index 782dd694ab..07b23e76ba 100644 --- a/public/language/ja/admin/settings/post.json +++ b/public/language/ja/admin/settings/post.json @@ -1,64 +1,64 @@ { - "general": "General", + "general": "一般", "sorting": "投稿の並び順", "sorting.post-default": "標準のポスト並び順", - "sorting.oldest-to-newest": "新しい順に", + "sorting.oldest-to-newest": "古い順に", "sorting.newest-to-oldest": "新しいものから古いものへ", - "sorting.recently-replied": "Recently Replied", - "sorting.recently-created": "Recently Created", + "sorting.recently-replied": "最近返信された順", + "sorting.recently-created": "最近作成された順", "sorting.most-votes": "最も多い評価", "sorting.most-posts": "最大投稿", - "sorting.most-views": "Most Views", + "sorting.most-views": "閲覧数順", "sorting.topic-default": "デフォルトのスレッドの並び順", "length": "投稿の長さ", - "post-queue": "Post Queue", + "post-queue": "投稿キュー", "restrictions": "転記の制限", "restrictions.post-queue": "投稿キューを有効にする", - "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", - "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions.post-queue-rep-threshold": "投稿キューをバイパスするために必要な評価", + "restrictions.groups-exempt-from-post-queue": "投稿キューを免除するグループを選択", "restrictions-new.post-queue": "新しいユーザー制限を有効にする", - "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", - "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", - "restrictions.seconds-between": "Number of seconds between posts", - "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", - "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", - "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", - "restrictions.title-length": "Title Length", - "restrictions.post-length": "Post Length", - "restrictions.days-until-stale": "Days until topic is considered stale", - "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic. (set to 0 to disable)", + "restrictions.post-queue-help": "投稿キューを有効にすると、新規ユーザーの投稿が承認待ちのキューに入ります", + "restrictions-new.post-queue-help": "新規ユーザー制限を有効にすると、新規ユーザーが作成する投稿に制限が設定されます", + "restrictions.seconds-between": "投稿間の秒数", + "restrictions.seconds-edit-after": "投稿が編集可能な秒数(0で無効)", + "restrictions.seconds-delete-after": "投稿が削除可能な秒数(0で無効)", + "restrictions.replies-no-delete": "ユーザーが自分のスレッドを削除できなくなる返信数(0で無効)", + "restrictions.title-length": "タイトルの長さ", + "restrictions.post-length": "投稿の長さ", + "restrictions.days-until-stale": "スレッドが古いと見なされるまでの日数", + "restrictions.stale-help": "スレッドが「古い」と見なされると、そのスレッドに返信しようとするユーザーに警告が表示されます。(0で無効)", "timestamp": "タイムスタンプ", "timestamp.cut-off": "日付のカットオフ(日数)", "timestamp.cut-off-help": "日付&時間は相対的な方法で表示されます(例:「3時間前」/「5日前」)。そしてさまざまな地域にローカライズされています。\n\\t\\t\\t\\t\\t言語。特定のポイントの後、このテキストは、ローカライズされた日付自体を表示するように切り替えることができます。\n\\t\\t\\t\\t\\t(例:2016年11月5日15:30)
    (デフォルト:30 、または1か月)。日付を常に表示するには0に設定し、常に相対時間を表示するには空白のままにします。", - "timestamp.necro-threshold": "Necro Threshold (in days)", - "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.
    ", - "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", - "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "timestamp.necro-threshold": "ネクロ閾値(日数)", + "timestamp.necro-threshold-help": "投稿間の時間がネクロ閾値より長い場合、投稿間にメッセージが表示されます。(デフォルト: 7、1週間)。0で無効。", + "timestamp.topic-views-interval": "スレッド閲覧数の増分間隔(分)", + "timestamp.topic-views-interval-help": "スレッド閲覧数は、この設定で定義されたX分ごとに1回のみ増加します。", "teaser": "ティーザーの投稿", "teaser.last-post": "最後&ndash;返信がない場合は、元の投稿を含む最新の投稿を表示", "teaser.last-reply": "最後&ndash;最新の返信を表示するか、返信がない場合は「返信なし」のプレースホルダを表示する", "teaser.first": "最初", - "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", - "unread-and-recent": "Unread & Recent Settings", + "showPostPreviewsOnHover": "マウスオーバー時に投稿のプレビューを表示", + "unread-and-recent": "未読と最近の設定", "unread.cutoff": "未読のカットオフ日", "unread.min-track-last": "最後に読み込みを行う前に追跡するスレッドの最小投稿数", - "recent.max-topics": "Maximum topics on /recent", + "recent.max-topics": "/recentの最大スレッド数", "recent.categoryFilter.disable": "最近のページで無視されたカテゴリのトピックのフィルタリングを無効にする", "signature": "署名の設定", "signature.disable": "署名を無効にする", "signature.no-links": "署名内のリンクを無効にする", "signature.no-images": "署名内の画像を無効にする", - "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.hide-duplicates": "スレッド内の重複署名を非表示にする", "signature.max-length": "署名の最大文字数", "composer": "Composerの設定", "composer-help": "次の設定は、投稿者の機能や外観を制御します。\n\\t\\t\\t\\tユーザーに新しいスレッドを作成したり、既存のトピックに返信したりできます。", "composer.show-help": "「ヘルプ」タグを表示", "composer.enable-plugin-help": "プラグインがヘルプタブにコンテンツを追加できるようにする", "composer.custom-help": "カスタムヘルプテキスト", - "backlinks": "Backlinks", - "backlinks.enabled": "Enable topic backlinks", - "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "backlinks": "バックリンク", + "backlinks.enabled": "スレッドバックリンクを有効にする", + "backlinks.help": "投稿が別のスレッドを参照している場合、その時点で参照されたスレッドに投稿へのリンクが挿入されます。", "ip-tracking": "IPトラッキング", "ip-tracking.each-post": "各投稿のトラックIPアドレス", - "enable-post-history": "Enable Post History" + "enable-post-history": "投稿履歴を有効にする" } \ No newline at end of file diff --git a/public/language/ja/groups.json b/public/language/ja/groups.json index abb2a03f43..de23469b11 100644 --- a/public/language/ja/groups.json +++ b/public/language/ja/groups.json @@ -1,24 +1,24 @@ { - "group": "Group", - "all-groups": "All groups", + "group": "グループ", + "all-groups": "すべてのグループ", "groups": "グループ", - "members": "Members", - "x-members": "%1 member(s)", - "view-group": "グループを閲覧", - "owner": "グループ管理人", + "members": "メンバー", + "x-members": "%1人", + "view-group": "グループを表示", + "owner": "グループオーナー", "new-group": "新しいグループを作成", "no-groups-found": "グループはありません", "pending.accept": "承認", "pending.reject": "拒否", "pending.accept-all": "すべて承認", "pending.reject-all": "すべて拒否", - "pending.none": "保留中のメンバーは現在居ません", - "invited.none": "招待しているメンバーは現在居ません。", + "pending.none": "保留中のメンバーは現在いません", + "invited.none": "招待中のメンバーは現在いません", "invited.uninvite": "招待を取り消す", "invited.search": "このグループに招待しているユーザーを検索", - "invited.notification-title": "%1に招待されました", - "request.notification-title": "%1から、グループメンバーへのリクエストです。", - "request.notification-text": "%1は、%2のメンバーになることをリクエストしています。", + "invited.notification-title": "グループ%1に招待されました", + "request.notification-title": "%1からのグループ参加リクエスト", + "request.notification-text": "%1%2への参加をリクエストしています。", "cover-save": "保存", "cover-saving": "保存しています", "details.title": "グループ詳細", @@ -29,40 +29,40 @@ "details.latest-posts": "最近の投稿", "details.private": "プライベート", "details.disableJoinRequests": "参加申請を無効にする", - "details.disableLeave": "Disallow users from leaving the group", - "details.grant": "寄贈/取り消す管理権限", + "details.disableLeave": "グループからの離脱を禁止する", + "details.grant": "オーナー権限の付与/取り消し", "details.kick": "キック", - "details.kick-confirm": "このメンバーをグループから削除", - "details.add-member": "Add Member", + "details.kick-confirm": "このメンバーをグループから削除してもよろしいですか?", + "details.add-member": "メンバーを追加", "details.owner-options": "グループの管理", "details.group-name": "グループ名", "details.member-count": "メンバー数", "details.creation-date": "作成日", "details.description": "説明", - "details.member-post-cids": "Category IDs to display posts from", + "details.member-post-cids": "投稿を表示するカテゴリID", "details.badge-preview": "バッジプレビュー", "details.change-icon": "アイコンを変更", - "details.change-label-colour": "Change Label Colour", - "details.change-text-colour": "Change Text Colour", + "details.change-label-colour": "ラベル色を変更", + "details.change-text-colour": "テキスト色を変更", "details.badge-text": "バッジテキスト", "details.userTitleEnabled": "バッジを表示", "details.private-help": "有効の場合、グループへの参加はグループ管理人からの承認が必要です。", "details.hidden": "非表示", - "details.hidden-help": "有効の場合、このグループはグループ一覧で発見することは出来ず、ユーザーが手動で招待する必要があります。", + "details.hidden-help": "有効にすると、このグループはグループ一覧に表示されず、ユーザーは手動で招待される必要があります。", "details.delete-group": "グループを削除", - "details.private-system-help": "プライベートグループは、システムレベルで無効です。このオプションは何もしません。", + "details.private-system-help": "プライベートグループはシステムレベルで無効化されています。このオプションは機能しません。", "event.updated": "グループ詳細が更新されました。", "event.deleted": "グループ\"%1\"は削除されました。", "membership.accept-invitation": "招待を受ける", - "membership.accept.notification-title": "You are now a member of %1", - "membership.invitation-pending": "招待を保留", + "membership.accept.notification-title": "%1のメンバーになりました", + "membership.invitation-pending": "招待保留中", "membership.join-group": "グループへ参加", "membership.leave-group": "グループから離脱", - "membership.leave.notification-title": "%1 has left group %2", + "membership.leave.notification-title": "%1がグループ%2を退会しました", "membership.reject": "拒否", "new-group.group-name": "グループ名:", "upload-group-cover": "グループのカバーをアップロード", - "bulk-invite-instructions": "ユーザー名をカンマ区切りして入力することで、このグループへ招待します。", - "bulk-invite": "バルク招待", + "bulk-invite-instructions": "このグループに招待するユーザー名をカンマ区切りで入力してください", + "bulk-invite": "一括招待", "remove-group-cover-confirm": "カバー写真を削除してもよろしいですか?" } \ No newline at end of file From e265704e8b26aa76820bdef4ddad664094f9d1c5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:08:30 -0400 Subject: [PATCH 4695/4744] fix(deps): update dependency mongodb to v7.1.1 (#14119) 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 83fb640024..c5ee9fa0b1 100644 --- a/install/package.json +++ b/install/package.json @@ -92,7 +92,7 @@ "lru-cache": "11.2.7", "mime": "3.0.0", "mkdirp": "3.0.1", - "mongodb": "7.1.0", + "mongodb": "7.1.1", "morgan": "1.10.1", "mousetrap": "1.6.5", "multer": "2.1.1", From bd0157c3ebca8e6fa4573f89e2dc4747c510d28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 24 Mar 2026 10:05:52 -0400 Subject: [PATCH 4696/4744] allow different clam-fade-xx values like clamp-fade-4 vs clamp-fade-sm-4 --- public/src/client/category.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/public/src/client/category.js b/public/src/client/category.js index 1e950e8983..3e86df0ae0 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -115,14 +115,9 @@ define('forum/category', [ } function handleDescription() { - const fadeEl = document.querySelector(`.description.clamp-fade-sm-4`); - if (!fadeEl) { - return; - } - - fadeEl.addEventListener('click', () => { - const state = fadeEl.classList.contains('line-clamp-4'); - fadeEl.classList.toggle('line-clamp-4', !state); + const fadeEl = $(`.description[class*="clamp-fade-"]`); + fadeEl.on('click', function () { + fadeEl.toggleClass('line-clamp-4'); }); } From 9b885162508249e726dec31b730adf70932abf41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 24 Mar 2026 10:17:19 -0400 Subject: [PATCH 4697/4744] refactor: work with different line-clamp values --- public/src/client/category.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/public/src/client/category.js b/public/src/client/category.js index 3e86df0ae0..abcaed35a6 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -117,7 +117,18 @@ define('forum/category', [ function handleDescription() { const fadeEl = $(`.description[class*="clamp-fade-"]`); fadeEl.on('click', function () { - fadeEl.toggleClass('line-clamp-4'); + const $this = $(this); + let clampClass = $this.data('clampClass'); + if (!clampClass) { + const match = $this.attr('class').match(/line-clamp-(\S+)/); + if (match && match[1]) { + clampClass = `line-clamp-${match[1]}`; + fadeEl.data('clampClass', clampClass); + } + } + if (clampClass) { + fadeEl.toggleClass(clampClass); + } }); } From a10471fce766c3c070a60b3d2290ada3dcf66281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 24 Mar 2026 12:43:57 -0400 Subject: [PATCH 4698/4744] fix: #14121, use normalizedPath when uploading add test for normalize --- src/user/picture.js | 2 +- test/user.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/user/picture.js b/src/user/picture.js index db135b463f..fe8a99c9a8 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -143,7 +143,7 @@ module.exports = function (User) { const filename = generateProfileImageFilename(updateUid, extension); const uploadedImage = await image.uploadImage(filename, `profile/uid-${updateUid}`, { uid: updateUid, - path: picture.path, + path: normalizedPath, name: 'profileAvatar', }); diff --git a/test/user.js b/test/user.js index f6619bd651..cd0ba94e57 100644 --- a/test/user.js +++ b/test/user.js @@ -1144,6 +1144,30 @@ describe('User', () => { }); }); + it('should normalize uploaded image to png', async () => { + const oldValue = meta.config['profile:convertProfileImageToPNG']; + meta.config['profile:convertProfileImageToPNG'] = 1; + + const uid = await User.create({ username: 'pngnormalize', password: '123456' }); + const { jar, csrf_token } = await helpers.loginUser('pngnormalize', '123456'); + const pathToJpeg = path.join(__dirname, '../test/files/normalise.jpg'); + + const { response } = await helpers.uploadFile( + `${nconf.get('url')}/api/user/pngnormalize/uploadpicture`, + pathToJpeg, { }, jar, csrf_token + ); + assert.strictEqual(response.statusCode, 200); + const picture = await db.getObjectField(`user:${uid}`, 'picture'); + const uploadedPath = path.join( + nconf.get('upload_path'), `${picture.replace(nconf.get('upload_url'), '')}` + ); + const sharp = require('sharp'); + const metadata = await sharp(uploadedPath).metadata(); + assert.strictEqual(metadata.format, 'png'); + + meta.config['profile:convertProfileImageToPNG'] = oldValue; + }); + it('should not allow image data with bad MIME type to be passed in', (done) => { User.uploadCroppedPicture({ callerUid: uid, From a5e23e9b13c8111c004cf1b5ce86c7bc951cd31c Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 25 Mar 2026 09:07:37 +0000 Subject: [PATCH 4699/4744] Latest translations and fallbacks --- public/language/ja/global.json | 156 ++++++++++++++++----------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/public/language/ja/global.json b/public/language/ja/global.json index 67a7bdf5e8..d01c08aafd 100644 --- a/public/language/ja/global.json +++ b/public/language/ja/global.json @@ -4,153 +4,153 @@ "buttons.close": "閉じる", "403.title": "アクセス拒否", "403.message": "アクセス権限が無いページを閲覧しようとしています。", - "403.login": "Perhaps you should try logging in?", + "403.login": "ログインをお試しください。", "404.title": "見つかりません", - "404.message": "You seem to have stumbled upon a page that does not exist.
    Return to the home page.
    ", + "404.message": "お探しのページは存在しないようです。
    ", "500.title": "内部エラー", "500.message": "何か問題が発生しているようです。", "400.title": "無効なリクエスト", - "400.message": "It looks like this link is malformed, please double-check and try again.
    Return to the home page.
    ", + "400.message": "このリンクが正しくないようです。確認して再度お試しください。
    ", "register": "登録", "login": "ログイン", - "please-log-in": "ログインください", + "please-log-in": "ログインしてください", "logout": "ログアウト", - "posting-restriction-info": "登録ユーザーのみが投稿可能となります.こちらからログインください。", + "posting-restriction-info": "現在、投稿は登録ユーザーのみ可能です。こちらからログインしてください。", "welcome-back": "おかえりなさい", "you-have-successfully-logged-in": "ログインできました", - "save-changes": "保存する", + "save-changes": "変更を保存", "save": "保存", - "create": "Create", - "cancel": "Cancel", + "create": "作成", + "cancel": "キャンセル", "close": "閉じる", "pagination": "ページ", - "pagination.previouspage": "Previous Page", - "pagination.nextpage": "Next Page", - "pagination.firstpage": "First Page", - "pagination.lastpage": "Last Page", + "pagination.previouspage": "前のページ", + "pagination.nextpage": "次のページ", + "pagination.firstpage": "最初のページ", + "pagination.lastpage": "最後のページ", "pagination.out-of": "%2件中%1件目", - "pagination.enter-index": "Go to post index", - "pagination.go-to-page": "Go to page", - "pagination.page-x": "Page %1", - "header.brand-logo": "Brand Logo", + "pagination.enter-index": "投稿インデックスへ移動", + "pagination.go-to-page": "ページへ移動", + "pagination.page-x": "%1ページ目", + "header.brand-logo": "ブランドロゴ", "header.admin": "管理", "header.categories": "カテゴリ", "header.recent": "最近", "header.unread": "未読", "header.tags": "タグ", "header.popular": "人気", - "header.top": "Top", + "header.top": "トップ", "header.users": "ユーザー", "header.groups": "グループ", "header.chats": "チャット", "header.notifications": "通知", "header.search": "検索", "header.profile": "プロフィール", - "header.account": "Account", + "header.account": "アカウント", "header.navigation": "ナビゲーション", - "header.manage": "Manage", - "header.drafts": "Drafts", - "header.world": "World", + "header.manage": "管理", + "header.drafts": "下書き", + "header.world": "世界", "notifications.loading": "通知をロード中", "chats.loading": "チャットをロード中", - "drafts.loading": "Loading Drafts", + "drafts.loading": "下書きを読み込み中", "motd.welcome": "次世代の掲示板システムNodeBBへようこそ!", "alert.success": "成功", "alert.error": "エラー", - "alert.warning": "Warning", - "alert.info": "Info", - "alert.banned": "停止した", - "alert.banned.message": "You have just been banned, your access is now restricted.", - "alert.unbanned": "Unbanned", - "alert.unbanned.message": "Your ban has been lifted.", + "alert.warning": "警告", + "alert.info": "情報", + "alert.banned": "BANされました", + "alert.banned.message": "BANされました。アクセスが制限されています。", + "alert.unbanned": "BAN解除", + "alert.unbanned.message": "BANが解除されました。", "alert.unfollow": "%1へのフォローを停止しました!", "alert.follow": "%1をフォローしています!", "users": "ユーザー", "topics": "スレッド", "posts": "投稿", - "crossposts": "Cross-posts", - "x-posts": "%1 posts", - "x-topics": "%1 topics", - "x-reputation": "%1 reputation", + "crossposts": "クロスポスト", + "x-posts": "%1 件の投稿", + "x-topics": "%1 件のスレッド", + "x-reputation": "%1 評価", "best": "ベスト", - "controversial": "Controversial", - "votes": "Votes", - "x-votes": "%1 votes", - "voters": "Voters", + "controversial": "賛否両論", + "votes": "投票", + "x-votes": "%1 票", + "voters": "投票者", "upvoters": "高評価したユーザー", "upvoted": "高評価", "downvoters": "低評価したユーザー", "downvoted": "低評価", "views": "閲覧数", - "posters": "Posters", - "watching": "Watching", + "posters": "投稿者", + "watching": "ウォッチ中", "reputation": "評価", - "lastpost": "Last post", - "firstpost": "First post", - "about": "About", + "lastpost": "最後の投稿", + "firstpost": "最初の投稿", + "about": "概要", "read-more": "続きを読む", "more": "詳しく", - "none": "None", - "posted-ago-by-guest": "%1にゲストが投稿", - "posted-ago-by": "%1に%2が投稿", - "posted-ago": "%1に投稿された", - "posted-in": "%1に投稿されました", - "posted-in-by": "%1に%2に投稿されました", - "posted-in-ago": "%1に投稿されました %2", - "posted-in-ago-by": "%1 %2に %3 が投稿", - "user-posted-ago": "%1 が%2に投稿", - "guest-posted-ago": "ゲストが%1に投稿", - "last-edited-by": "最後に編集した時間%1", - "edited-timestamp": "Edited %1", + "none": "なし", + "posted-ago-by-guest": "%1前にゲストが投稿", + "posted-ago-by": "%1前に%2が投稿", + "posted-ago": "%1前に投稿", + "posted-in": "%1に投稿", + "posted-in-by": "%1に%2が投稿", + "posted-in-ago": "%1に%2前に投稿", + "posted-in-ago-by": "%1に%2前、%3が投稿", + "user-posted-ago": "%1が%2前に投稿", + "guest-posted-ago": "ゲストが%1前に投稿", + "last-edited-by": "最後に編集したのは%1です", + "edited-timestamp": "%1に編集", "norecentposts": "最近の投稿はありません", "norecenttopics": "最近のスレッドはありません", "recentposts": "最近の投稿", "recentips": "最近ログインしたIPアドレス", "moderator-tools": "モデレーターツール", - "status": "Status", + "status": "ステータス", "online": "オンライン", "away": "退席中", - "dnd": "取り込み中", + "dnd": "通知しない", "invisible": "オフライン表示", "offline": "オフライン", - "remote-user": "This user is from outside of this forum", + "remote-user": "このユーザーはこのフォーラム外からの参加者です", "email": "メール", "language": "言語", "guest": "ゲスト", "guests": "ゲスト", - "former-user": "A Former User", - "system-user": "System", - "unknown-user": "Unknown user", - "updated.title": "Forum Updated", - "updated.message": "This forum has just been updated to the latest version. Click here to refresh the page.", + "former-user": "退会ユーザー", + "system-user": "システム", + "unknown-user": "不明なユーザー", + "updated.title": "フォーラムが更新されました", + "updated.message": "このフォーラムは最新版に更新されました。ここをクリックしてページを更新してください。", "privacy": "プライバシー設定", - "follow": "Follow", - "unfollow": "Unfollow", - "delete-all": "Delete All", - "map": "Map", - "sessions": "Login Sessions", - "ip-address": "IP Address", + "follow": "フォロー", + "unfollow": "フォロー解除", + "delete-all": "すべて削除", + "map": "マップ", + "sessions": "ログインセッション", + "ip-address": "IPアドレス", "enter-page-number": "ページ番号を入力", "upload-file": "ファイルをアップロード", "upload": "アップロード", - "uploads": "Uploads", + "uploads": "アップロード", "allowed-file-types": "有効なファイル形式は %1 です。", "unsaved-changes": "変更はまだ保存されていません。本当にこのページから離れますか?", "reconnecting-message": "%1への接続が失われたと思われます。再接続されるまでしばらくお待ちください。", - "reconnected-message": "Reconnected to %1 successfully.", + "reconnected-message": "%1への再接続に成功しました。", "play": "再生", - "cookies.message": "このWEBサイトは、心地良くご使用頂くためにクッキーを使用しています。", + "cookies.message": "このウェブサイトでは、快適にご利用いただくために Cookie を使用しています。", "cookies.accept": "了解!", "cookies.learn-more": "もっと詳しく", "edited": "編集されました", "disabled": "無効", "select": "選択", - "selected": "Selected", - "copied": "Copied", - "user-search-prompt": "Type something here to find users...", - "hidden": "Hidden", - "sort": "Sort", - "actions": "Actions", - "rss-feed": "RSS Feed", - "skip-to-content": "Skip to content" + "selected": "選択済み", + "copied": "コピーしました", + "user-search-prompt": "ユーザーを検索するにはここに入力...", + "hidden": "非表示", + "sort": "並び替え", + "actions": "操作", + "rss-feed": "RSSフィード", + "skip-to-content": "コンテンツへスキップ" } \ No newline at end of file From 72f48fd9c47eaddfacd85d5ad963236190270278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 25 Mar 2026 10:21:24 -0400 Subject: [PATCH 4700/4744] fix: #14123, aria-hidden fixes add aria-labels --- install/package.json | 2 +- src/views/admin/extend/plugins.tpl | 2 +- src/views/admin/footer.tpl | 2 +- src/views/admin/manage/tags.tpl | 4 ++-- src/views/admin/partials/create_group_modal.tpl | 4 ++-- src/views/chat.tpl | 4 ++-- src/views/modals/crop_picture.tpl | 6 +++--- src/views/modals/flag.tpl | 4 ++-- src/views/modals/upload-file.tpl | 6 +++--- src/views/modals/upload-picture-from-url.tpl | 6 +++--- src/views/partials/reconnect-alert.tpl | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/install/package.json b/install/package.json index 9e90dc0dac..47294dcd3e 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.61", + "nodebb-theme-harmony": "2.2.62", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", "nodebb-theme-persona": "14.2.33", diff --git a/src/views/admin/extend/plugins.tpl b/src/views/admin/extend/plugins.tpl index c43e286411..34f2de1bfe 100644 --- a/src/views/admin/extend/plugins.tpl +++ b/src/views/admin/extend/plugins.tpl @@ -125,7 +125,7 @@
    -
    - {{{each pictures}}} - - {{{end}}} +
    + {{{ each pictures }}} +
    + + +
    + {{{ end }}}
    -
    +
    +
    [[user:avatar-background-colour]]
    +
    + + {{{ each iconBackgrounds }}} + + {{{ end }}} +
    +
    {{{ if allowProfileImageUploads }}} - {{{ end }}} - - {{{ if uploaded }}} - - {{{ end }}}
    -
    - -
    - -

    [[user:avatar-background-colour]]

    -
    - - {{{ each iconBackgrounds }}} - -{{{ end }}}
    \ No newline at end of file diff --git a/test/notifications.js b/test/notifications.js index 33d03fadcf..9596fd8112 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -75,7 +75,8 @@ describe('Notifications', () => { const notifData = await notifications.get(nid); assert.strictEqual(notifData.icon, undefined); assert.strictEqual(notifData.user['icon:text'], 'I'); - assert.strictEqual(notifData.user['icon:bgColor'], '#3f51b5'); + assert(notifData.user['icon:bgColor'].length === 7 && + notifData.user['icon:bgColor'].startsWith('#')); }); it('should return null if pid is same and importance is lower', (done) => { diff --git a/test/user.js b/test/user.js index 6ecb92814f..3eb85d83ed 100644 --- a/test/user.js +++ b/test/user.js @@ -1028,7 +1028,8 @@ describe('User', () => { it('should set user picture to uploaded', async () => { await User.setUserField(uid, 'uploadedpicture', '/test'); - await apiUser.changePicture({ uid: uid }, { type: 'uploaded', uid: uid }); + await db.sortedSetAdd(`uid:${uid}:profile:pictures`, Date.now(), '/test'); + await apiUser.changePicture({ uid: uid }, { type: 'uploaded', picture: '/test', uid: uid }); const picture = await User.getUserField(uid, 'picture'); assert.equal(picture, `${nconf.get('relative_path')}/test`); }); From 6c3daebbce0879abfc3f2ba4dfcf4f3899789502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 13 Mar 2026 19:53:35 -0400 Subject: [PATCH 4629/4744] update fix_username_zsets upgrade script to show progress while deleting --- src/upgrades/3.2.0/fix_username_zsets.js | 25 +++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/upgrades/3.2.0/fix_username_zsets.js b/src/upgrades/3.2.0/fix_username_zsets.js index 22ab35c152..01c2663a04 100644 --- a/src/upgrades/3.2.0/fix_username_zsets.js +++ b/src/upgrades/3.2.0/fix_username_zsets.js @@ -11,9 +11,28 @@ module.exports = { method: async function () { const { progress } = this; - await db.deleteAll(['username:uid', 'username:sorted']); + const [userNameUid, usernameSorted, usersJoindate] = await db.sortedSetsCard([ + 'username:uid', 'username:sorted', 'users:joindate', + ]); + progress.total = userNameUid + usernameSorted + usersJoindate; + + await batch.processSortedSet('username:uid', async (usernames) => { + await db.sortedSetRemove('username:uid', usernames); + progress.incr(usernames.length); + }, { + batch: 500, + alwaysStartAt: 0, + }); + + await batch.processSortedSet('username:sorted', async (usernames) => { + await db.sortedSetRemove('username:sorted', usernames); + progress.incr(usernames.length); + }, { + batch: 500, + alwaysStartAt: 0, + }); + await batch.processSortedSet('users:joindate', async (uids) => { - progress.incr(uids.length); const usersData = await db.getObjects(uids.map(uid => `user:${uid}`)); const bulkAdd = []; usersData.forEach((userData) => { @@ -23,9 +42,9 @@ module.exports = { } }); await db.sortedSetAddBulk(bulkAdd); + progress.incr(uids.length); }, { batch: 500, - progress: progress, }); }, }; From 698758d994b165bc44ff79c602b0eae3a0d5f357 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 14 Mar 2026 09:07:38 +0000 Subject: [PATCH 4630/4744] Latest translations and fallbacks --- public/language/de/admin/settings/general.json | 4 ++-- public/language/de/world.json | 4 ++-- public/language/vi/admin/manage/uploads.json | 2 +- .../language/vi/admin/settings/activitypub.json | 2 +- public/language/vi/admin/settings/general.json | 4 ++-- public/language/vi/error.json | 2 +- public/language/vi/global.json | 2 +- public/language/vi/modules.json | 4 ++-- public/language/vi/reset_password.json | 2 +- public/language/vi/search.json | 4 ++-- public/language/vi/topic.json | 2 +- public/language/vi/world.json | 4 ++-- .../language/zh-CN/admin/settings/general.json | 16 ++++++++-------- public/language/zh-CN/world.json | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/public/language/de/admin/settings/general.json b/public/language/de/admin/settings/general.json index 278b7fbf52..4f6a5e7ef6 100644 --- a/public/language/de/admin/settings/general.json +++ b/public/language/de/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "Seitenbeschreibung", "keywords": "Forum Schlüsselworte", "keywords-placeholder": "Schlüsselworte, die ihre Community beschreiben, mit Komma getrennt", - "logo-and-icons": "Media & Branding", + "logo-and-icons": "Medien & Branding", "logo.image": "Bild", "logo.image-placeholder": "Pfad zu einem Logo, welches im Header des Forums angezeigt werden soll", "logo.upload": "Hochladen", @@ -36,7 +36,7 @@ "maskable-icon": "Maskierbares (Start-Bildschirm) Symbol", "maskable-icon.help": "Empfohlene Größe und Format: 512x512, nur PNG-Format. Wenn kein maskierbares Icon angegeben wird, greift NodeBB auf das Touch-Symbol zurück.", "screenshot": "Screenshot", - "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", + "screenshot.help": "Empfohlene Größe und Format: zwischen 320px und 3480px, nur JPG- und PNG-Format. Wenn kein Screenshot angegeben wird, greift NodeBB auf einen Standard-Screenshot zurück", "outgoing-links": "Ausgehende Links", "outgoing-links.warning-page": "Warnseite für ausgehende links verwenden", "search": "Suche", diff --git a/public/language/de/world.json b/public/language/de/world.json index 95e16612b9..55a1fbfb24 100644 --- a/public/language/de/world.json +++ b/public/language/de/world.json @@ -1,7 +1,7 @@ { "name": "Welt", - "latest": "Latest (Following)", - "latest-all": "Latest (All)", + "latest": "Neueste (Gefolgt)", + "latest-all": "Neueste (Alle)", "popular-day": "Beliebt (Tag)", "popular-week": "Beliebt (Woche)", "popular-month": "Beliebt (Monat)", diff --git a/public/language/vi/admin/manage/uploads.json b/public/language/vi/admin/manage/uploads.json index 32f4d447e3..39e2308e7c 100644 --- a/public/language/vi/admin/manage/uploads.json +++ b/public/language/vi/admin/manage/uploads.json @@ -2,7 +2,7 @@ "manage-uploads": "Quản Lý Tải Lên", "upload-file": "Tải Lên Tệp", "filename": "Tên Tệp", - "usage": "Dùng Bài Đăng", + "usage": "Bài Đăng Sử Dụng", "orphaned": "Đơn độc", "size/filecount": "Kích cỡ/ Số tệp", "confirm-delete": "Bạn có chắc muốn xóa tệp này không?", diff --git a/public/language/vi/admin/settings/activitypub.json b/public/language/vi/admin/settings/activitypub.json index ed7b3b8643..4343357b7d 100644 --- a/public/language/vi/admin/settings/activitypub.json +++ b/public/language/vi/admin/settings/activitypub.json @@ -42,7 +42,7 @@ "server-filtering": "Lọc", "count": "NodeBB này hiện đã biết về %1 máy chủ", - "server.filter-help": "Chỉ định các máy chủ mà bạn muốn cấm liên kết với NodeBB của mình. Ngoài ra, bạn có thể chọn tham gia có chọn lọc cho phép liên kết có chọn lọc với các máy chủ cụ thể. Cả hai tùy chọn đều được hỗ trợ, mặc dù chúng loại trừ lẫn nhau.", + "server.filter-help": "Chỉ ra các máy chủ mà bạn muốn cấm liên kết với NodeBB của mình. Ngoài ra, bạn có thể chọn tham gia có chọn lọc cho phép liên kết có chọn lọc với các máy chủ cụ thể. Cả hai tùy chọn đều được hỗ trợ, mặc dù chúng loại trừ lẫn nhau.", "server.filter-help-hostname": "Chỉ nhập tên máy chủ bên dưới (vd: example.org), tách nhau bằng ngắt dòng.", "server.filter-allow-list": "Dùng nó làm Danh Sách Cho Phép Thay Thế", diff --git a/public/language/vi/admin/settings/general.json b/public/language/vi/admin/settings/general.json index 5907a11dc6..bfbdbde36a 100644 --- a/public/language/vi/admin/settings/general.json +++ b/public/language/vi/admin/settings/general.json @@ -35,8 +35,8 @@ "touch-icon.help": "Kích cỡ và định dạng được đề xuất: 512x512, chỉ định dạng PNG. Nếu không có biểu tượng cảm ứng nào, NodeBB sẽ quay trở lại sử dụng favicon.", "maskable-icon": "Biểu tượng có thể che được (Màn Hình Trang Chủ)", "maskable-icon.help": "Kích thước và định dạng nên là: 512x512, chỉ định dạng PNG. Nếu không có biểu tượng có thể che được nào được chỉ định, NodeBB sẽ trở lại Biểu Tượng Chạm.", - "screenshot": "Screenshot", - "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", + "screenshot": "Ảnh chụp màn hình", + "screenshot.help": "Kích thước và định dạng được đề xuất: từ 320px đến 3480px, chỉ định dạng JPG và PNG. Nếu không chỉ định ảnh chụp màn hình, NodeBB sẽ sử dụng ảnh chụp màn hình mặc định.", "outgoing-links": "Liên Kết Đi", "outgoing-links.warning-page": "Sử Dụng Trang Cảnh Báo Liên Kết Đi", "search": "Tìm kiếm", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index 1d4e90a668..76ab019087 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -228,7 +228,7 @@ "invalid-session-text": "Có vẻ như phiên đăng nhập của bạn không còn hoạt động. Vui lòng làm mới trang này.", "session-mismatch": "‎Phiên Không Khớp‎", "session-mismatch-text": "Có vẻ như phiên đăng nhập của bạn không còn khớp với máy chủ. Vui lòng làm mới trang này.", - "no-topics-selected": "Không chọn chủ đề!", + "no-topics-selected": "Chưa chọn chủ đề!", "cant-move-to-same-topic": "Bạn không thể đưa bài đăng vào cùng chủ đề!", "cant-move-topic-to-same-category": "Không thể di chuyển chủ đề đến cùng danh mục!", "cant-move-topic-to-from-remote-categories": "Bạn không thể di chuyển chủ đề vào hoặc ra khỏi các danh mục từ xa; hãy cân nhắc đăng chéo bài viết thay thế.", diff --git a/public/language/vi/global.json b/public/language/vi/global.json index f26ee302c1..f9cc9bcada 100644 --- a/public/language/vi/global.json +++ b/public/language/vi/global.json @@ -49,7 +49,7 @@ "header.account": "Tài khoản", "header.navigation": "Điều hướng", "header.manage": "Quản lý", - "header.drafts": "Bản thảo", + "header.drafts": "Bản nháp", "header.world": "Thế giới", "notifications.loading": "Đang Tải Thông Báo", "chats.loading": "Đang Tải Trò Chuyện", diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 6834051f81..912ef6c505 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -113,8 +113,8 @@ "composer.cancel-scheduling": "Hủy Lịch Trình", "composer.change-schedule-date": "Đổi Ngày", "composer.set-schedule-date": "Đặt Ngày", - "composer.discard-all-drafts": "Hủy tất cả bản nháp", - "composer.no-drafts": "Bạn không có bản nháp nào", + "composer.discard-all-drafts": "Loại bỏ tất cả bản nháp", + "composer.no-drafts": "Bạn không có bản nháp", "composer.discard-draft-confirm": "Bạn có muốn hủy bản nháp này không?", "composer.remote-pid-editing": "Sửa bài đăng từ xa", "composer.remote-pid-content-immutable": "Nội dung của bài viết từ xa không thể được chỉnh sửa. Tuy nhiên, bạn có thể thay đổi tiêu đề và gắn thẻ chủ đề.", diff --git a/public/language/vi/reset_password.json b/public/language/vi/reset_password.json index a8cfb41fc3..261cec9f0d 100644 --- a/public/language/vi/reset_password.json +++ b/public/language/vi/reset_password.json @@ -12,7 +12,7 @@ "enter-email-address": "Nhập địa chỉ Email", "password-reset-sent": "Nếu có địa chỉ cụ thể ứng với tài khoản người dùng hiện có, một email đặt lại mật khẩu đã được gửi. Xin lưu ý chỉ có một email được gửi mỗi phút.", "invalid-email": "Email không đúng/không tồn tại!", - "password-too-short": "Mật khẩu bạn nhập quá ngắn, vui lòng chọn một mật khẩu khác.", + "password-too-short": "Mật khẩu đã nhập quá ngắn, vui lòng chọn một mật khẩu khác.", "passwords-do-not-match": "Hai mật khẩu bạn nhập không khớp với nhau.", "password-expired": "Mật khẩu của bạn đã hết hạn, vui lòng chọn một mật khẩu mới." } \ No newline at end of file diff --git a/public/language/vi/search.json b/public/language/vi/search.json index 43b468535f..a71c3441bb 100644 --- a/public/language/vi/search.json +++ b/public/language/vi/search.json @@ -103,8 +103,8 @@ "search-preferences-saved": "Đã lưu tùy chọn tìm kiếm", "search-preferences-cleared": "Đã xóa tùy chọn tìm kiếm", "show-results-as": "Hiện thị kết quả theo", - "show-results-as-topics": "Hiển thị kết quả dưới dạng chủ đề", - "show-results-as-posts": "Hiển thị kết quả dưới dạng bài viết", + "show-results-as-topics": "Kết quả dạng chủ đề", + "show-results-as-posts": "Kết quả dạng bài đăng", "see-more-results": "Xem thêm kết quả (%1)", "search-in-category": "Tìm kiếm trong \"%1\"" } \ No newline at end of file diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 76b1583950..5e687d910d 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -168,7 +168,7 @@ "move-topic-instruction": "Chọn danh mục nhắm đến và sau đó nhấp vào di chuyển", "change-owner-instruction": "Bấm vào bài viết bạn muốn chỉ định cho người dùng khác", "manage-editors-instruction": "Quản lý những người dùng có thể chỉnh sửa bài đăng này bên dưới.", - "crossposts.instructions": "Chọn một hoặc nhiều danh mục để đăng chéo bài viết. Bài viết sẽ được truy cập từ danh mục gốc và tất cả các danh mục được đăng chéo.", + "crossposts.instructions": "Chọn một hoặc nhiều danh mục để đăng chéo bài viết. Chủ đề sẽ được truy cập từ danh mục gốc và tất cả các danh mục được đăng chéo.", "crossposts.listing": "Chủ đề này đã được đăng tải chéo lên các chuyên mục nội bộ sau:", "crossposts.none": "Chủ đề này chưa được đăng tải chéo lên bất kỳ chuyên mục nào khác.", "composer.title-placeholder": "Nhập tiêu đề chủ đề của bạn tại đây...", diff --git a/public/language/vi/world.json b/public/language/vi/world.json index 7e8d1181c8..4d86569784 100644 --- a/public/language/vi/world.json +++ b/public/language/vi/world.json @@ -1,7 +1,7 @@ { "name": "Thế giới", - "latest": "Latest (Following)", - "latest-all": "Latest (All)", + "latest": "Mới nhất (Tiếp theo)", + "latest-all": "Mới nhất (Tất cả)", "popular-day": "Phổ biến (Ngày)", "popular-week": "Phổ biến (Tuần)", "popular-month": "Phổ biến (Tháng)", diff --git a/public/language/zh-CN/admin/settings/general.json b/public/language/zh-CN/admin/settings/general.json index 489ebf012b..a6b4916b8e 100644 --- a/public/language/zh-CN/admin/settings/general.json +++ b/public/language/zh-CN/admin/settings/general.json @@ -7,7 +7,7 @@ "title.short-placeholder": "如果没有指定短标题,将会使用站点标题", "title.url": "标题链接地址", "title.url-placeholder": "网站标题链接", - "title.url-help": "当标题被点击时,将向用户发送该地址。如果留空,用户将跳转到论坛索引页面。注意:这不是在电子邮件中使用的外部URL,这由config.json中的url属性设置。", + "title.url-help": "点击标题时,将用户重定向至此地址。若留空,用户将被重定向至论坛首页。注意:此处并非用于电子邮件等场景的外部网址。该网址由 config.json 文件中的 url 属性设定。", "title.name": "您的社区名称", "title.show-in-header": "在顶部显示站点标题", "browser-title": "浏览器标题", @@ -18,7 +18,7 @@ "description": "站点描述", "keywords": "站点关键字", "keywords-placeholder": "描述您的社区的关键字(以逗号分隔)", - "logo-and-icons": "Media & Branding", + "logo-and-icons": "媒体与品牌建设", "logo.image": "图像", "logo.image-placeholder": "要在论坛标题上显示的 Logo 的路径", "logo.upload": "上传", @@ -35,8 +35,8 @@ "touch-icon.help": "推荐的尺寸和格式:512x512,仅限PNG格式。如果没有指定触摸图标,NodeBB将回退到站点图标。", "maskable-icon": "可遮蔽(主屏)图标", "maskable-icon.help": "推荐的尺寸和格式:512x512,仅限PNG格式。如果没有指定可遮蔽图标,NodeBB将回退到触摸图标。", - "screenshot": "Screenshot", - "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", + "screenshot": "屏幕截图", + "screenshot.help": "推荐尺寸和格式:320px 至 3480px 之间,仅限 JPG 和 PNG 格式。如果未指定截图,NodeBB 将使用通用截图作为替代", "outgoing-links": "站外链接", "outgoing-links.warning-page": "使用站外链接警告页", "search": "搜索", @@ -49,17 +49,17 @@ "background-color": "背景色", "background-color-help": "当网站安装为 PWA 时用于启动屏幕背景的颜色", "undo-timeout": "撤销超时", - "undo-timeout-help": "部分操作,例如移动主题,将允许版主在一定时间内撤销其操作。设置为 0 可完全禁用撤消。", + "undo-timeout-help": "某些操作(例如移动主题)允许版主在一定时间内撤销该操作。将该值设为 0 可完全禁用撤销功能。", "topic-tools": "主题工具", "home-page": "主页", "home-page-route": "主页路由", - "home-page-description": "选择用户导航到论坛根 URL 时显示的页面。", + "home-page-description": "选择用户访问论坛根 URL 时显示的页面。", "custom-route": "自定义路由", "allow-user-home-pages": "允许用户主页", "home-page-title": "首页标题(默认“Home”)", "default-language": "默认语言", "auto-detect": "自动检测游客的语言设置", - "default-language-help": "默认语言会决定所有用户的语言设定。
    单一用户可以各自在帐户设置中覆盖此项设定。", + "default-language-help": "默认语言决定了所有访问您论坛的用户的语言设置。
    用户可以在其账号设置页面中覆盖默认语言。", "post-sharing": "帖子分享", - "info-plugins-additional": "插件可以增加可选的用于分享帖子的网络。" + "info-plugins-additional": "插件可以添加额外的社交网络,用于分享帖子。" } \ No newline at end of file diff --git a/public/language/zh-CN/world.json b/public/language/zh-CN/world.json index db61c5c92d..cae52d19af 100644 --- a/public/language/zh-CN/world.json +++ b/public/language/zh-CN/world.json @@ -1,7 +1,7 @@ { "name": "世界", - "latest": "Latest (Following)", - "latest-all": "Latest (All)", + "latest": "最新(关注)", + "latest-all": "最新(全部)", "popular-day": "热门(按天)", "popular-week": "热门(按周)", "popular-month": "热门(按月)", From 09de6fb9ae95762ebadc9f22f88091c3c4dc7882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 14 Mar 2026 17:39:43 -0400 Subject: [PATCH 4631/4744] perf: switch to set, remove parseFloat in redis add test to cover float --- src/database/redis/sorted.js | 8 ++++---- test/database/sorted.js | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index 5b9652e6e0..8433133f15 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -301,7 +301,7 @@ module.exports = function (module) { const returnData = []; let done; - const seen = Object.create(null); + const seen = new Set(); do { /* eslint-disable no-await-in-loop */ const res = await module.client.zScan(params.key, cursor, { MATCH: params.match, COUNT: 5000 }); @@ -310,11 +310,11 @@ module.exports = function (module) { for (let i = 0; i < res.members.length; i ++) { const item = res.members[i]; - if (!seen[item.value]) { - seen[item.value] = 1; + if (!seen.has(item.value)) { + seen.add(item.value); if (params.withScores) { - returnData.push({ value: item.value, score: parseFloat(item.score) }); + returnData.push({ value: item.value, score: item.score }); } else { returnData.push(item.value); } diff --git a/test/database/sorted.js b/test/database/sorted.js index a375b2ec48..fde2bafb0b 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -93,6 +93,22 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea }); assert.strictEqual(data.length, 0); }); + + it('should handle floating point scores', async () => { + await db.sortedSetAdd('scanzset6', [1.5, 2.5, 3.5, 4.5, 5.5, 6.5], ['aaab{', 'bbbb', 'bbcb', 'ddb', 'dddd', 'adb']); + const data = await db.getSortedSetScan({ + key: 'scanzset6', + match: '*b', + withScores: true, + }); + data.sort((a, b) => b.score - a.score); + assert.deepStrictEqual(data, [ + { value: 'adb', score: 6.5 }, + { value: 'ddb', score: 4.5 }, + { value: 'bbcb', score: 3.5 }, + { value: 'bbbb', score: 2.5 }, + ]); + }); }); describe('sortedSetAdd()', () => { From 5f2b6b8ec63eefde018390e30540ee4728b88d44 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 15 Mar 2026 09:07:40 +0000 Subject: [PATCH 4632/4744] Latest translations and fallbacks --- public/language/bg/world.json | 4 ++-- public/language/zh-CN/admin/settings/user.json | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/language/bg/world.json b/public/language/bg/world.json index 0f78ca89f7..4eb44f7d03 100644 --- a/public/language/bg/world.json +++ b/public/language/bg/world.json @@ -1,7 +1,7 @@ { "name": "Свят", - "latest": "Latest (Following)", - "latest-all": "Latest (All)", + "latest": "Последни (следвани)", + "latest-all": "Последни (всички)", "popular-day": "Популярни (за деня)", "popular-week": "Популярни (за седмицата)", "popular-month": "Популярни (за месеца)", diff --git a/public/language/zh-CN/admin/settings/user.json b/public/language/zh-CN/admin/settings/user.json index ff2acbaa24..7858667fff 100644 --- a/public/language/zh-CN/admin/settings/user.json +++ b/public/language/zh-CN/admin/settings/user.json @@ -6,20 +6,20 @@ "allow-login-with.username-email": "用户名或者邮箱", "allow-login-with.username": "仅限用户名", "account-settings": "用户设置", - "gdpr-enabled": "启用通用数据保护条例(GDPR)许可的个人信息收集", - "gdpr-enabled-help": "当启用时,所有的新注册用户需要明确同意允许数据采集和在通用数据保护协议(GDPR)保护下的使用。注意:开启GDPR不一定要之前已经存在的用户同意。在这之前,您需要去安装GDPR插件。", + "gdpr-enabled": "启用《通用数据保护条例》(GDPR)的同意收集功能", + "gdpr-enabled-help": "启用此功能后,所有新注册用户都必须根据《通用数据保护条例》(GDPR)明确同意数据收集和使用。注意:启用GDPR不会强制现有用户提供同意。若要实现此功能,您需要安装GDPR插件。", "disable-username-changes": "禁用修改用户名", "disable-email-changes": "禁用修改邮箱", "disable-password-changes": "禁用修改密码", - "allow-account-deletion": "允许消除帐号", + "allow-account-deletion": "允许删除账号", "hide-fullname": "隐藏用户的全名", "hide-email": "隐藏用户的电子邮箱", - "show-fullname-as-displayname": "如果可以,把用户的全名作为他们的显示名称。", + "show-fullname-as-displayname": "如果可用,则显示用户的全名作为其显示名称", "themes": "主题", - "disable-user-skins": "阻止用户选择自定义皮肤", + "disable-user-skins": "禁止用户选择自定义皮肤", "account-protection": "帐号保护", "admin-relogin-duration": "管理员无操作自动退出时长 (分钟)", - "admin-relogin-duration-help": "访问管理面板一段时间后需要重新登录以保证管理面板的安全,设置为0以禁用。", + "admin-relogin-duration-help": "在管理员区域停留一定时间后,系统将要求重新登录;将该值设为 0 可禁用此功能", "login-attempts": "每小时尝试登录次数", "login-attempts-help": "如果用户的尝试登录次数超过此界限,该帐号将会被被锁定预设的时间。", "lockout-duration": "帐户锁定时间(分钟)", From ec4e87ff84531b4f461cc3e92b8ec61ed488c4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Mar 2026 14:23:01 -0400 Subject: [PATCH 4633/4744] chore: up harmony change group description to a textarea --- install/package.json | 2 +- src/views/admin/manage/group.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 507f5c5a8d..9eaf413ac3 100644 --- a/install/package.json +++ b/install/package.json @@ -108,7 +108,7 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.53", + "nodebb-theme-harmony": "2.2.54", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", "nodebb-theme-persona": "14.2.28", diff --git a/src/views/admin/manage/group.tpl b/src/views/admin/manage/group.tpl index f08142547c..775bd5f77f 100644 --- a/src/views/admin/manage/group.tpl +++ b/src/views/admin/manage/group.tpl @@ -44,7 +44,7 @@
    - +
    From 47f59dcf8ca6ac34d446d934a67e79d1c1ee4b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Mar 2026 15:18:17 -0400 Subject: [PATCH 4634/4744] fix profile image update in header after changes --- public/src/modules/accounts/picture.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/src/modules/accounts/picture.js b/public/src/modules/accounts/picture.js index 651ce7deaa..7ddd4cebb7 100644 --- a/public/src/modules/accounts/picture.js +++ b/public/src/modules/accounts/picture.js @@ -108,7 +108,9 @@ define('accounts/picture', [ const headerIconEl = $(`[component="header/avatar"] [component="avatar/icon"]`); if (picture) { - if (!headerPictureEl.length && headerIconEl.length) { + if (headerPictureEl.length) { + headerPictureEl.attr('src', picture); + } else if (headerIconEl.length) { const img = $(''); $(headerIconEl[0].attributes).each(function () { img.attr(this.nodeName, this.nodeValue); From 0124b50a28cf684ebcb80bb15c6e1a90b18e4aa9 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 16 Mar 2026 09:07:39 +0000 Subject: [PATCH 4635/4744] Latest translations and fallbacks --- public/language/ru/topic.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index 8020fa99fd..cdbd32efc5 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -15,7 +15,7 @@ "replies-to-this-post": "%1 ответов", "one-reply-to-this-post": "1 ответ", "last-reply-time": "Последний ответ", - "reply-options": "Reply options", + "reply-options": "Варианты ответа", "reply-as-topic": "Ответить, создав новую тему", "guest-login-reply": "Авторизуйтесь, чтобы ответить", "login-to-view": "Авторизуйтесь, чтобы просмотреть", @@ -36,7 +36,7 @@ "pinned": "Прикреплена", "pinned-with-expiry": "Закреплен до %1", "scheduled": "Запланировано", - "deleted": "Deleted", + "deleted": "Удалён", "moved": "Перенесена", "moved-from": "Перенесено с %1", "copy-code": "Copy Code", @@ -145,7 +145,7 @@ "loading-more-posts": "Загружаем больше сообщений", "move-topic": "Перенести тему", "move-topics": "Перенести темы", - "crosspost-topic": "Cross-post Topic", + "crosspost-topic": "Дублировать тему", "move-post": "Перенести сообщение", "post-moved": "Сообщение перенесено!", "fork-topic": "Создать дополнительную ветвь дискуссии", @@ -168,7 +168,7 @@ "move-topic-instruction": "Select the target category and then click move", "change-owner-instruction": "Нажмите на сообщения, которые вы хотите присвоить другому пользователю", "manage-editors-instruction": "Manage the users who can edit this post below.", - "crossposts.instructions": "Select one or more categories to cross-post to. Topic(s) will be accessible from the original category and all cross-posted categories.", + "crossposts.instructions": "Укажите дополнительные категории для этой темы. Она будет видна во всех выбранных разделах, исходная категория сохранится.", "crossposts.listing": "This topic has been cross-posted to the following local categories:", "crossposts.none": "This topic has not been cross-posted to any additional categories", "composer.title-placeholder": "Введите название темы...", @@ -228,14 +228,14 @@ "post-quick-create": "Quick post", "navigator.index": "Сообщений %1 от %2", "navigator.unread": "%1 непрочитано", - "upvote-post": "Upvote post", - "downvote-post": "Downvote post", + "upvote-post": "Полезно", + "downvote-post": "Не полезно", "post-tools": "Post tools", "unread-posts-link": "Unread posts link", "thumb-image": "Topic thumbnail image", - "announcers": "Shares", - "announcers-x": "Shares (%1)", - "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", - "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", - "guest-cta.closing": "With your input, this post could be even better 💗" + "announcers": "Поделиться", + "announcers-x": "Поделиться (%1)", + "guest-cta.title": "Здравствуйте! Похоже, вам интересна эта беседа, но у вас пока нет учетной записи.", + "guest-cta.message": "Вы устали просматривать одни и те же посты каждый раз, когда заходите на сайт? После регистрации, вам не придётся искать обсуждения в которых вы принимали участие, настройте уведомления о новых сообщениях так как вам это удобно (по электронной почте или уведомлением). У вас появится возможность сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.", + "guest-cta.closing": "С вашими комментариями этот пост может стать ещё лучше 💗" } \ No newline at end of file From 2eb0964d3a58eaf00e1d9b7d3cfd1ecd77f74f0a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 16 Mar 2026 11:08:53 -0400 Subject: [PATCH 4636/4744] fix: restrict contextmenu preventDefault to the checkbox only --- public/src/modules/topicSelect.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js index 54cffc4465..70a53f25a0 100644 --- a/public/src/modules/topicSelect.js +++ b/public/src/modules/topicSelect.js @@ -43,9 +43,6 @@ define('topicSelect', ['components'], function (components) { // Long press let longPressTimeout; const start = function (ev) { - if (ev.type === 'touchstart') { - ev.preventDefault(); - } isLongPress = false; longPressTimeout = setTimeout(() => { isLongPress = true; @@ -68,7 +65,7 @@ define('topicSelect', ['components'], function (components) { topicsContainer.on('mouseleave', '[component="topic/select"]', cancel); topicsContainer.on('touchend', '[component="topic/select"]', cancel); topicsContainer.on('touchcancel', '[component="topic/select"]', cancel); - topicsContainer.on('contextmenu', (e) => { + topicsContainer.on('contextmenu', '[component="topic/select"]', (e) => { e.preventDefault(); }); }; From 6c01a5d84f59c3ed80a7da9a9094e9b7cc943003 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 16 Mar 2026 12:33:47 -0400 Subject: [PATCH 4637/4744] feat: #14094, notification drawer UX improvements --- install/package.json | 4 ++-- public/src/client/header/notifications.js | 18 +++++++++++++++++- public/src/modules/notifications.js | 6 ++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/install/package.json b/install/package.json index 9eaf413ac3..6631108574 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.54", + "nodebb-theme-harmony": "2.2.55", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", - "nodebb-theme-persona": "14.2.28", + "nodebb-theme-persona": "14.2.29", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.2", "nprogress": "0.2.0", diff --git a/public/src/client/header/notifications.js b/public/src/client/header/notifications.js index fc402b8d96..6d833ccbd6 100644 --- a/public/src/client/header/notifications.js +++ b/public/src/client/header/notifications.js @@ -5,6 +5,7 @@ define('forum/header/notifications', function () { notifications.prepareDOM = function () { const notifTrigger = $('[component="notifications"] [data-bs-toggle="dropdown"]'); + const listEl = document.querySelector('[component="notifications/list"]'); notifTrigger.on('show.bs.dropdown', async (ev) => { const notifications = await app.require('notifications'); @@ -15,11 +16,26 @@ define('forum/header/notifications', function () { notifTrigger.each((index, el) => { const triggerEl = $(el); const dropdownEl = triggerEl.parent().find('.dropdown-menu'); + const listEl = dropdownEl.find('[component="notifications/list"]'); if (dropdownEl.hasClass('show')) { app.require('notifications').then((notifications) => { - notifications.loadNotifications(triggerEl, dropdownEl.find('[component="notifications/list"]')); + notifications.loadNotifications(triggerEl, listEl); }); } + + dropdownEl.on('click', '[data-filter]', (e) => { + const filter = e.target.getAttribute('data-filter'); + + if (filter === 'unread') { + listEl.get(0).querySelectorAll('[data-nid]:not(.unread)').forEach((e) => { + e.classList.toggle('hidden', true); + }); + } else { + listEl.get(0).querySelectorAll('[data-nid]').forEach((e) => { + e.classList.toggle('hidden', false); + }); + } + }); }); socket.removeListener('event:new_notification', onNewNotification); diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index f4806eafdf..70dc54e3bb 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -42,7 +42,7 @@ define('notifications', [ hooks.fire('filter:notifications.load', { notifications: notifs }).then(({ notifications }) => { app.parseAndTranslate('partials/notifications_list', { notifications }, function (html) { notifList.html(html); - notifList.off('click').on('click', '[data-nid]', function (ev) { + notifList.off('click').on('click', '[component="notifications/item/link"]', function (ev) { const notifEl = $(this); if (scrollToPostIndexIfOnPage(notifEl)) { ev.stopPropagation(); @@ -62,6 +62,9 @@ define('notifications', [ components.get('notifications').on('click', '.mark-all-read', () => { Notifications.markAllRead(); }); + components.get('notifications').on('click', `[href="${config.relative_path}/notifications"]`, () => { + triggerEl.dropdown('toggle'); + }); Notifications.handleUnreadButton(notifList); @@ -86,7 +89,6 @@ define('notifications', [ $this.find('.unread').toggleClass('hidden', unread); $this.find('.read').toggleClass('hidden', !unread); }); - return false; }); }; From 44e65b8d73f058bf3d11b8dc3bef5036292e321f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 16 Mar 2026 13:34:04 -0400 Subject: [PATCH 4638/4744] feat: ability to show only local posts in /world --- install/package.json | 4 +- public/language/en-GB/world.json | 3 +- public/src/client/world.js | 8 +++- src/controllers/activitypub/topics.js | 60 +++++++++++++++------------ 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/install/package.json b/install/package.json index 6631108574..17da5d468c 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.55", + "nodebb-theme-harmony": "2.2.56", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", - "nodebb-theme-persona": "14.2.29", + "nodebb-theme-persona": "14.2.30", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.2", "nprogress": "0.2.0", diff --git a/public/language/en-GB/world.json b/public/language/en-GB/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/en-GB/world.json +++ b/public/language/en-GB/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/src/client/world.js b/public/src/client/world.js index 6e63b85a23..61492a5d9f 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -45,7 +45,13 @@ define('forum/world', [ } default: { - translator.translate(`[[world:latest${params.get('all') === '1' ? '-all' : ''}]]`, function (translated) { + let suffix = ''; + if (params.get('all') === '1') { + suffix = '-all'; + } else if (params.get('local') === '1') { + suffix = '-local'; + } + translator.translate(`[[world:latest${suffix}]]`, function (translated) { sortLabelEl.innerText = translated; }); break; diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 8785fecc14..d45b187761 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -16,8 +16,8 @@ const helpers = require('../helpers'); const controller = module.exports; controller.list = async function (req, res) { - if (!req.uid && !req.query.sort && !req.query.all) { - return helpers.redirect(res, '/world?all=1', false); + if (!req.uid && !req.query.sort && !req.query.local) { + return helpers.redirect(res, '/world?local=1', false); } const { topicsPerPage } = await user.getSettings(req.uid); @@ -50,12 +50,14 @@ controller.list = async function (req, res) { let tids; let topicCount; + let { local } = req.query; + local = parseInt(local, 10) === 1; if (req.query.sort === 'popular') { cidQuery = { ...cidQuery, sort: 'posts', term: req.query.term || 'day', - includeRemote: true, + includeRemote: !local, followingOnly: !req.query.all || !parseInt(req.query.all, 10), }; delete cidQuery.cid; @@ -65,7 +67,7 @@ controller.list = async function (req, res) { cidQuery = { ...cidQuery, term: req.query.term, - includeRemote: true, + includeRemote: !local, followingOnly: !req.query.all || !parseInt(req.query.all, 10), }; delete cidQuery.cid; @@ -110,29 +112,33 @@ controller.list = async function (req, res) { data.showSelect = true; // Tracked/watched categories - let cids = await user.getCategoriesByStates(req.uid, [ - categories.watchStates.tracking, categories.watchStates.watching, - ]); - cids = cids.filter(cid => !utils.isNumber(cid)); - const [categoryData, watchState] = await Promise.all([ - categories.getCategories(cids), - categories.getWatchState(cids, req.uid), - ]); - data.categories = categories.getTree(categoryData, 0); - await Promise.all([ - categories.getRecentTopicReplies(categoryData, req.uid, req.query), - categories.setUnread(data.categories, cids, req.uid), - ]); - data.categories.forEach((category, idx) => { - if (category) { - helpers.trimChildren(category); - helpers.setCategoryTeaser(category); - category.isWatched = watchState[idx] === categories.watchStates.watching; - category.isTracked = watchState[idx] === categories.watchStates.tracking; - category.isNotWatched = watchState[idx] === categories.watchStates.notwatching; - category.isIgnored = watchState[idx] === categories.watchStates.ignoring; - } - }); + if (req.uid) { + let cids = await user.getCategoriesByStates(req.uid, [ + categories.watchStates.tracking, categories.watchStates.watching, + ]); + cids = cids.filter(cid => !utils.isNumber(cid)); + const [categoryData, watchState] = await Promise.all([ + categories.getCategories(cids), + categories.getWatchState(cids, req.uid), + ]); + data.categories = categories.getTree(categoryData, 0); + await Promise.all([ + categories.getRecentTopicReplies(categoryData, req.uid, req.query), + categories.setUnread(data.categories, cids, req.uid), + ]); + data.categories.forEach((category, idx) => { + if (category) { + helpers.trimChildren(category); + helpers.setCategoryTeaser(category); + category.isWatched = watchState[idx] === categories.watchStates.watching; + category.isTracked = watchState[idx] === categories.watchStates.tracking; + category.isNotWatched = watchState[idx] === categories.watchStates.notwatching; + category.isIgnored = watchState[idx] === categories.watchStates.ignoring; + } + }); + } else { + data.categories = []; + } data.title = translator.escape(data.name); data.breadcrumbs = helpers.buildBreadcrumbs([]); From 61414fa4fbe901909a259f6b3e3fc90ef608f0d3 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 16 Mar 2026 17:34:32 +0000 Subject: [PATCH 4639/4744] chore(i18n): fallback strings for new resources: nodebb.world --- public/language/ar/world.json | 3 ++- public/language/az/world.json | 3 ++- public/language/bg/world.json | 3 ++- public/language/bn/world.json | 3 ++- public/language/cs/world.json | 3 ++- public/language/da/world.json | 3 ++- public/language/de/world.json | 3 ++- public/language/el/world.json | 3 ++- public/language/en-US/world.json | 3 ++- public/language/en-x-pirate/world.json | 3 ++- public/language/es/world.json | 3 ++- public/language/et/world.json | 3 ++- public/language/fa-IR/world.json | 3 ++- public/language/fi/world.json | 3 ++- public/language/fr/world.json | 3 ++- public/language/gl/world.json | 3 ++- public/language/he/world.json | 3 ++- public/language/hr/world.json | 3 ++- public/language/hu/world.json | 3 ++- public/language/hy/world.json | 3 ++- public/language/id/world.json | 3 ++- public/language/it/world.json | 3 ++- public/language/ja/world.json | 3 ++- public/language/ko/world.json | 3 ++- public/language/lt/world.json | 3 ++- public/language/lv/world.json | 3 ++- public/language/ms/world.json | 3 ++- public/language/nb/world.json | 3 ++- public/language/nl/world.json | 3 ++- public/language/nn-NO/world.json | 3 ++- public/language/pl/world.json | 3 ++- public/language/pt-BR/world.json | 3 ++- public/language/pt-PT/world.json | 3 ++- public/language/ro/world.json | 3 ++- public/language/ru/world.json | 3 ++- public/language/rw/world.json | 3 ++- public/language/sc/world.json | 3 ++- public/language/sk/world.json | 3 ++- public/language/sl/world.json | 3 ++- public/language/sq-AL/world.json | 3 ++- public/language/sr/world.json | 3 ++- public/language/sv/world.json | 3 ++- public/language/th/world.json | 3 ++- public/language/tr/world.json | 3 ++- public/language/uk/world.json | 3 ++- public/language/ur/world.json | 3 ++- public/language/vi/world.json | 3 ++- public/language/zh-CN/world.json | 3 ++- public/language/zh-TW/world.json | 3 ++- 49 files changed, 98 insertions(+), 49 deletions(-) diff --git a/public/language/ar/world.json b/public/language/ar/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/ar/world.json +++ b/public/language/ar/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/az/world.json b/public/language/az/world.json index f4101247d6..2197d73e9f 100644 --- a/public/language/az/world.json +++ b/public/language/az/world.json @@ -1,6 +1,7 @@ { "name": "Dünya", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/bg/world.json b/public/language/bg/world.json index 4eb44f7d03..fc44b81ec8 100644 --- a/public/language/bg/world.json +++ b/public/language/bg/world.json @@ -1,6 +1,7 @@ { "name": "Свят", - "latest": "Последни (следвани)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Последни (всички)", "popular-day": "Популярни (за деня)", "popular-week": "Популярни (за седмицата)", diff --git a/public/language/bn/world.json b/public/language/bn/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/bn/world.json +++ b/public/language/bn/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/cs/world.json b/public/language/cs/world.json index 5d64fc0acd..c5c4a0e9e6 100644 --- a/public/language/cs/world.json +++ b/public/language/cs/world.json @@ -1,6 +1,7 @@ { "name": "Svět", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/da/world.json b/public/language/da/world.json index ed22d19f06..699c95f0c0 100644 --- a/public/language/da/world.json +++ b/public/language/da/world.json @@ -1,6 +1,7 @@ { "name": "Verden", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/de/world.json b/public/language/de/world.json index 55a1fbfb24..f450566099 100644 --- a/public/language/de/world.json +++ b/public/language/de/world.json @@ -1,6 +1,7 @@ { "name": "Welt", - "latest": "Neueste (Gefolgt)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Neueste (Alle)", "popular-day": "Beliebt (Tag)", "popular-week": "Beliebt (Woche)", diff --git a/public/language/el/world.json b/public/language/el/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/el/world.json +++ b/public/language/el/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/en-US/world.json b/public/language/en-US/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/en-US/world.json +++ b/public/language/en-US/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/en-x-pirate/world.json b/public/language/en-x-pirate/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/en-x-pirate/world.json +++ b/public/language/en-x-pirate/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/es/world.json b/public/language/es/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/es/world.json +++ b/public/language/es/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/et/world.json b/public/language/et/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/et/world.json +++ b/public/language/et/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/fa-IR/world.json b/public/language/fa-IR/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/fa-IR/world.json +++ b/public/language/fa-IR/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/fi/world.json b/public/language/fi/world.json index 87d193efa9..cbf5526761 100644 --- a/public/language/fi/world.json +++ b/public/language/fi/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/fr/world.json b/public/language/fr/world.json index fb839a8ce8..e4975a78c3 100644 --- a/public/language/fr/world.json +++ b/public/language/fr/world.json @@ -1,6 +1,7 @@ { "name": "Monde", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Populaires (Jour)", "popular-week": "Populaires (Semaine)", diff --git a/public/language/gl/world.json b/public/language/gl/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/gl/world.json +++ b/public/language/gl/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/he/world.json b/public/language/he/world.json index 9235d136c3..1c7ba7a867 100644 --- a/public/language/he/world.json +++ b/public/language/he/world.json @@ -1,6 +1,7 @@ { "name": "עולם", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "פופולרי (יומי)", "popular-week": "פופולרי (שבועי)", diff --git a/public/language/hr/world.json b/public/language/hr/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/hr/world.json +++ b/public/language/hr/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/hu/world.json b/public/language/hu/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/hu/world.json +++ b/public/language/hu/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/hy/world.json b/public/language/hy/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/hy/world.json +++ b/public/language/hy/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/id/world.json b/public/language/id/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/id/world.json +++ b/public/language/id/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/it/world.json b/public/language/it/world.json index 859fdbedcd..103ab1daad 100644 --- a/public/language/it/world.json +++ b/public/language/it/world.json @@ -1,6 +1,7 @@ { "name": "Mondo", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popolare (Giorno)", "popular-week": "Popolare (Settimana)", diff --git a/public/language/ja/world.json b/public/language/ja/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/ja/world.json +++ b/public/language/ja/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/ko/world.json b/public/language/ko/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/ko/world.json +++ b/public/language/ko/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/lt/world.json b/public/language/lt/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/lt/world.json +++ b/public/language/lt/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/lv/world.json b/public/language/lv/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/lv/world.json +++ b/public/language/lv/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/ms/world.json b/public/language/ms/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/ms/world.json +++ b/public/language/ms/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/nb/world.json b/public/language/nb/world.json index bd39706af9..d0c3e5e232 100644 --- a/public/language/nb/world.json +++ b/public/language/nb/world.json @@ -1,6 +1,7 @@ { "name": "Verden", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/nl/world.json b/public/language/nl/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/nl/world.json +++ b/public/language/nl/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/nn-NO/world.json b/public/language/nn-NO/world.json index 45ae2a8b3e..3a43c31a8b 100644 --- a/public/language/nn-NO/world.json +++ b/public/language/nn-NO/world.json @@ -1,6 +1,7 @@ { "name": "Verda", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/pl/world.json b/public/language/pl/world.json index 62f0daf537..1b37dc05eb 100644 --- a/public/language/pl/world.json +++ b/public/language/pl/world.json @@ -1,6 +1,7 @@ { "name": "Świat", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popularne (Dziś)", "popular-week": "Popularne (W Tygodniu)", diff --git a/public/language/pt-BR/world.json b/public/language/pt-BR/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/pt-BR/world.json +++ b/public/language/pt-BR/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/pt-PT/world.json b/public/language/pt-PT/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/pt-PT/world.json +++ b/public/language/pt-PT/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/ro/world.json b/public/language/ro/world.json index dc182ccab2..7938c5ebeb 100644 --- a/public/language/ro/world.json +++ b/public/language/ro/world.json @@ -1,6 +1,7 @@ { "name": "Lumea", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/ru/world.json b/public/language/ru/world.json index e83f4ee497..e3b375a865 100644 --- a/public/language/ru/world.json +++ b/public/language/ru/world.json @@ -1,6 +1,7 @@ { "name": "Мир", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/rw/world.json b/public/language/rw/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/rw/world.json +++ b/public/language/rw/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/sc/world.json b/public/language/sc/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/sc/world.json +++ b/public/language/sc/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/sk/world.json b/public/language/sk/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/sk/world.json +++ b/public/language/sk/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/sl/world.json b/public/language/sl/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/sl/world.json +++ b/public/language/sl/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/sq-AL/world.json b/public/language/sq-AL/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/sq-AL/world.json +++ b/public/language/sq-AL/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/sr/world.json b/public/language/sr/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/sr/world.json +++ b/public/language/sr/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/sv/world.json b/public/language/sv/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/sv/world.json +++ b/public/language/sv/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/th/world.json b/public/language/th/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/th/world.json +++ b/public/language/th/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/tr/world.json b/public/language/tr/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/tr/world.json +++ b/public/language/tr/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/uk/world.json b/public/language/uk/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/uk/world.json +++ b/public/language/uk/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/ur/world.json b/public/language/ur/world.json index df3cc4f334..80727acc33 100644 --- a/public/language/ur/world.json +++ b/public/language/ur/world.json @@ -1,6 +1,7 @@ { "name": "جهان", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", diff --git a/public/language/vi/world.json b/public/language/vi/world.json index 4d86569784..a883e532dd 100644 --- a/public/language/vi/world.json +++ b/public/language/vi/world.json @@ -1,6 +1,7 @@ { "name": "Thế giới", - "latest": "Mới nhất (Tiếp theo)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Mới nhất (Tất cả)", "popular-day": "Phổ biến (Ngày)", "popular-week": "Phổ biến (Tuần)", diff --git a/public/language/zh-CN/world.json b/public/language/zh-CN/world.json index cae52d19af..e43b0fb8e7 100644 --- a/public/language/zh-CN/world.json +++ b/public/language/zh-CN/world.json @@ -1,6 +1,7 @@ { "name": "世界", - "latest": "最新(关注)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "最新(全部)", "popular-day": "热门(按天)", "popular-week": "热门(按周)", diff --git a/public/language/zh-TW/world.json b/public/language/zh-TW/world.json index 58e6526fbb..af21f98948 100644 --- a/public/language/zh-TW/world.json +++ b/public/language/zh-TW/world.json @@ -1,6 +1,7 @@ { "name": "World", - "latest": "Latest (Following)", + "latest": "Latest", + "latest-local": "Latest (Local)", "latest-all": "Latest (All)", "popular-day": "Popular (Day)", "popular-week": "Popular (Week)", From 67a93da507b62d3a2347ba51393a559a524d628c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 16 Mar 2026 13:50:39 -0400 Subject: [PATCH 4640/4744] fix: add back 'after' query param handling in /world that was removed accidentally --- src/controllers/activitypub/topics.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index d45b187761..85659fc466 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -23,8 +23,8 @@ controller.list = async function (req, res) { const { topicsPerPage } = await user.getSettings(req.uid); let { page, after } = req.query; page = parseInt(page, 10) || 1; - const start = Math.max(0, (page - 1) * topicsPerPage); - const stop = start + topicsPerPage - 1; + let start = Math.max(0, (page - 1) * topicsPerPage); + let stop = start + topicsPerPage - 1; const [userSettings, userPrivileges] = await Promise.all([ user.getSettings(req.uid), @@ -72,6 +72,16 @@ controller.list = async function (req, res) { }; delete cidQuery.cid; ({ tids, topicCount } = await topics.getSortedTopics(cidQuery)); + + if (after) { + // Update start/stop with values inferred from `after` + const index = tids.indexOf(utils.isNumber(after) ? parseInt(after, 10) : after); + if (index && start - index < 1) { + const count = stop - start; + start = index + 1; + stop = start + count; + } + } tids = tids.slice(start, stop !== -1 ? stop + 1 : undefined); } data.topicCount = topicCount; From 53286625107c1c4cfbbc66e4608ef01b9fd51994 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 16 Mar 2026 14:55:31 -0400 Subject: [PATCH 4641/4744] fix: removing topic tools/checkbox from /world for guests, reword guest CTA in /world --- public/language/en-GB/world.json | 8 ++++---- src/controllers/activitypub/topics.js | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/public/language/en-GB/world.json b/public/language/en-GB/world.json index af21f98948..2057e13d00 100644 --- a/public/language/en-GB/world.json +++ b/public/language/en-GB/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 85659fc466..775d738305 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -26,9 +26,10 @@ controller.list = async function (req, res) { let start = Math.max(0, (page - 1) * topicsPerPage); let stop = start + topicsPerPage - 1; - const [userSettings, userPrivileges] = await Promise.all([ + const [userSettings, userPrivileges, isAdminOrGlobalMod] = await Promise.all([ user.getSettings(req.uid), privileges.categories.get('-1', req.uid), + user.isAdminOrGlobalMod(req.uid), ]); const targetUid = await user.getUidByUserslug(req.query.author); let cidQuery = { @@ -118,8 +119,8 @@ controller.list = async function (req, res) { }); data.showThumbs = req.loggedIn || meta.config.privateUploads !== 1; data.posts = postData; - data.showTopicTools = true; - data.showSelect = true; + data.showTopicTools = isAdminOrGlobalMod; + data.showSelect = isAdminOrGlobalMod; // Tracked/watched categories if (req.uid) { From 3aa8d5baddcba6e6e26046fb17a02608a9a15b3b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 16 Mar 2026 14:59:37 -0400 Subject: [PATCH 4642/4744] fix: bump themes --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 17da5d468c..55d3247aba 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.56", + "nodebb-theme-harmony": "2.2.57", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", - "nodebb-theme-persona": "14.2.30", + "nodebb-theme-persona": "14.2.31", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.2", "nprogress": "0.2.0", From e9063a63e7aa73c1e7c8c3f1fca087a4bbaeffdf Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 16 Mar 2026 19:00:04 +0000 Subject: [PATCH 4643/4744] chore(i18n): fallback strings for new resources: nodebb.world --- public/language/ar/world.json | 8 ++++---- public/language/az/world.json | 8 ++++---- public/language/bg/world.json | 8 ++++---- public/language/bn/world.json | 8 ++++---- public/language/cs/world.json | 8 ++++---- public/language/da/world.json | 8 ++++---- public/language/de/world.json | 8 ++++---- public/language/el/world.json | 8 ++++---- public/language/en-US/world.json | 8 ++++---- public/language/en-x-pirate/world.json | 8 ++++---- public/language/es/world.json | 8 ++++---- public/language/et/world.json | 8 ++++---- public/language/fa-IR/world.json | 8 ++++---- public/language/fi/world.json | 8 ++++---- public/language/fr/world.json | 8 ++++---- public/language/gl/world.json | 8 ++++---- public/language/he/world.json | 8 ++++---- public/language/hr/world.json | 8 ++++---- public/language/hu/world.json | 8 ++++---- public/language/hy/world.json | 8 ++++---- public/language/id/world.json | 8 ++++---- public/language/it/world.json | 8 ++++---- public/language/ja/world.json | 8 ++++---- public/language/ko/world.json | 8 ++++---- public/language/lt/world.json | 8 ++++---- public/language/lv/world.json | 8 ++++---- public/language/ms/world.json | 8 ++++---- public/language/nb/world.json | 8 ++++---- public/language/nl/world.json | 8 ++++---- public/language/nn-NO/world.json | 8 ++++---- public/language/pl/world.json | 8 ++++---- public/language/pt-BR/world.json | 8 ++++---- public/language/pt-PT/world.json | 8 ++++---- public/language/ro/world.json | 8 ++++---- public/language/ru/world.json | 8 ++++---- public/language/rw/world.json | 8 ++++---- public/language/sc/world.json | 8 ++++---- public/language/sk/world.json | 8 ++++---- public/language/sl/world.json | 8 ++++---- public/language/sq-AL/world.json | 8 ++++---- public/language/sr/world.json | 8 ++++---- public/language/sv/world.json | 8 ++++---- public/language/th/world.json | 8 ++++---- public/language/tr/world.json | 8 ++++---- public/language/uk/world.json | 8 ++++---- public/language/ur/world.json | 8 ++++---- public/language/vi/world.json | 8 ++++---- public/language/zh-CN/world.json | 8 ++++---- public/language/zh-TW/world.json | 8 ++++---- 49 files changed, 196 insertions(+), 196 deletions(-) diff --git a/public/language/ar/world.json b/public/language/ar/world.json index af21f98948..2057e13d00 100644 --- a/public/language/ar/world.json +++ b/public/language/ar/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/az/world.json b/public/language/az/world.json index 2197d73e9f..65e4bface8 100644 --- a/public/language/az/world.json +++ b/public/language/az/world.json @@ -18,10 +18,10 @@ "help.federating": "Eyni şəkildə, bu forumdan kənar istifadəçilər sizi izləməyə başlayarsa, o zaman yazılarınız həmin proqramlarda və vebsaytlarda da görünməyə başlayacaq.", "help.next-generation": "Bu, sosial medianın yeni nəslidir, bu gün töhfə verməyə başlayın!", - "onboard.title": "Sizin fediverse pəncərəniz...", - "onboard.what": "Bu, yalnız bu forumdan kənarda tapılan məzmundan ibarət sizin fərdiləşdirilmiş kateqoriyanızdır. Bu səhifədə nəyinsə görünüb-göstərilməməsi onları izlədiyinizdən və ya həmin postun izlədiyiniz biri tərəfindən paylaşılıb-paylaşılmamasından asılıdır.", - "onboard.why": "Bu forumdan kənarda gedən çox şey var və bunların heç də hamısı maraqlarınıza uyğun deyil. Buna görə də insanları izləmək, kimdənsə daha çox görmək istədiyinizi bildirməyin ən yaxşı yoludur.", - "onboard.how": "Bu arada, bu forumun daha nələr haqqında bildiyini görmək üçün yuxarıdakı qısayol düymələrinə klikləyə və bəzi yeni məzmunlar kəşf etməyə başlaya bilərsiniz!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/bg/world.json b/public/language/bg/world.json index fc44b81ec8..6e332f3fe8 100644 --- a/public/language/bg/world.json +++ b/public/language/bg/world.json @@ -18,10 +18,10 @@ "help.federating": "По същия начин, ако потребители извън този форум започнат да следват Вас, тогава Вашите публикации ще започнат да се появяват в техните приложения и уеб сайтове.", "help.next-generation": "Това е новото поколение социална мрежа. Започнете да допринасяте още днес!", - "onboard.title": "Вашият прозорец към федивселената…", - "onboard.what": "Това е Вашата персонализирана категория съставена само от съдържание извън този форум. Тук се появяват неща от хора, които следвате, както и такива споделени от тях.", - "onboard.why": "Много неща се случват извън този форум, и не всичко отговаря на Вашите интереси. Затова следването на конкретни хора е най-добрият начин да покажете, че искате да виждате повече от тях.", - "onboard.how": "Междувременно можете да използвате бутоните в горната част, за да видите до какво има достъп този форум. Така може да започнете да откривате ново съдържание!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Търсене на категория…", "see-more": "Вижте повече", diff --git a/public/language/bn/world.json b/public/language/bn/world.json index af21f98948..2057e13d00 100644 --- a/public/language/bn/world.json +++ b/public/language/bn/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/cs/world.json b/public/language/cs/world.json index c5c4a0e9e6..c2ccc649f9 100644 --- a/public/language/cs/world.json +++ b/public/language/cs/world.json @@ -18,10 +18,10 @@ "help.federating": "Stejně tak, pokud vás začnou sledovat uživatelé mimo toto fórum, vaše příspěvky se začnou zobrazovat i v těchto aplikacích a na webových stránkách.", "help.next-generation": "Toto je nová generace sociálních sítí – začněte přispívat ještě dnes!", - "onboard.title": "Vaše okno do fediverse…", - "onboard.what": "Toto je vaše personalizovaná kategorie složená pouze z obsahu nalezeného mimo toto fórum. To, zda se něco zobrazí na této stránce, závisí na tom, zda tyto zdroje sledujete, nebo zda byl příspěvek sdílen někým, koho sledujete.", - "onboard.why": "Mimo toto fórum se děje spousta věcí, a ne všechno je relevantní pro vaše zájmy. Proto je sledování lidí nejlepší způsob, jak naznačit, že chcete vidět více obsahu od konkrétní osoby.", - "onboard.how": "Mezitím můžete kliknout na tlačítka zkratek nahoře, abyste viděli o čem dalším toto fórum ví, a začít objevovat nový obsah!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/da/world.json b/public/language/da/world.json index 699c95f0c0..3637ccc7b0 100644 --- a/public/language/da/world.json +++ b/public/language/da/world.json @@ -18,10 +18,10 @@ "help.federating": "Ligeså, hvis brugere udefra dette fourm begynder at følge dig, så vil dine opslag begynde at dukke op på deres programmer og hjemmesider også.", "help.next-generation": "Dette er den næste generation af sociale medier, bliv en del af det i dag!", - "onboard.title": "Dit vindue til fødiverset...", - "onboard.what": "Dette er din personaliserede kategori som består kun af indhold fra udefra dette forum. Om noget dukker op her eller ej afhænger af om følger personen der lavede indlægget, eller om du følger nogen der har fremhævet indlægget.", - "onboard.why": "Der foregår en masse udenfor dette forum, og ikke det hele er relevant for dine interesser. At følge folk er derfor den bedste måde at signalere at du gerne vil se mere fra dem.", - "onboard.how": "I mellemtiden kan du klikke på genvejs-knapperne i toppen for at se, hvad forummet kender til og begynd at opdage nyt indhold!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/de/world.json b/public/language/de/world.json index f450566099..f03c9cd773 100644 --- a/public/language/de/world.json +++ b/public/language/de/world.json @@ -18,10 +18,10 @@ "help.federating": "Wenn Leute außerhalb dieses Forums anfangen, dirzu folgen, werden deine Beiträge auch in diesen Apps und auf diesen Websites angezeigt.", "help.next-generation": "Das ist die nächste Generation der sozialen Medien, leg noch heute los!", - "onboard.title": "Dein Fenster zum Fediversum...", - "onboard.what": "Das ist deine persönliche Kategorie, die nur aus Inhalten außerhalb dieses Forums besteht. Ob etwas auf dieser Seite angezeigt wird, hängt davon ab, ob du diesen Inhalten folgst oder ob der Beitrag von jemandem geteilt wurde, dem du folgst.", - "onboard.why": "Außerhalb dieses Forums passiert echt viel, und nicht alles ist für dich interessant. Deshalb ist das Folgen von Leuten die beste Möglichkeit, um zu zeigen, dass du mehr von jemandem sehen willst.", - "onboard.how": "In der Zwischenzeit kannst du auf die Shortcut-Buttons oben klicken, um zu sehen, was es sonst noch in diesem Forum gibt, und neue Inhalte entdecken!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Such eine Kategorie...", "see-more": "Mehr sehen", diff --git a/public/language/el/world.json b/public/language/el/world.json index af21f98948..2057e13d00 100644 --- a/public/language/el/world.json +++ b/public/language/el/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/en-US/world.json b/public/language/en-US/world.json index af21f98948..2057e13d00 100644 --- a/public/language/en-US/world.json +++ b/public/language/en-US/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/en-x-pirate/world.json b/public/language/en-x-pirate/world.json index af21f98948..2057e13d00 100644 --- a/public/language/en-x-pirate/world.json +++ b/public/language/en-x-pirate/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/es/world.json b/public/language/es/world.json index af21f98948..2057e13d00 100644 --- a/public/language/es/world.json +++ b/public/language/es/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/et/world.json b/public/language/et/world.json index af21f98948..2057e13d00 100644 --- a/public/language/et/world.json +++ b/public/language/et/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/fa-IR/world.json b/public/language/fa-IR/world.json index af21f98948..2057e13d00 100644 --- a/public/language/fa-IR/world.json +++ b/public/language/fa-IR/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/fi/world.json b/public/language/fi/world.json index cbf5526761..d08b09de6f 100644 --- a/public/language/fi/world.json +++ b/public/language/fi/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/fr/world.json b/public/language/fr/world.json index e4975a78c3..d8467a263e 100644 --- a/public/language/fr/world.json +++ b/public/language/fr/world.json @@ -18,10 +18,10 @@ "help.federating": "De même, si des utilisateurs extérieurs à ce forum commencent à vous suivre, vos publications commenceront à apparaître sur ces applications et sites web également.", "help.next-generation": "C'est la prochaine génération des réseaux sociaux, commencez à contribuer dès aujourd'hui !", - "onboard.title": "Votre fenêtre sur le fediverse...", - "onboard.what": "Voici votre catégorie personnalisée, composée uniquement de contenu trouvé en dehors de ce forum. La présence de contenu sur cette page dépend de si vous suivez la personne, ou si ce post a été partagé par quelqu'un que vous suivez.", - "onboard.why": "Il se passe beaucoup de choses en dehors de ce forum, et tout n'est pas nécessairement pertinent pour vos intérêts. C'est pourquoi suivre des personnes est le meilleur moyen de signaler que vous souhaitez voir plus de contenu de leur part.", - "onboard.how": "En attendant, vous pouvez cliquer sur les boutons de raccourci en haut pour voir ce que ce forum connaît d'autre et commencer à découvrir de nouveaux contenus !", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Trouver une catégorie...", "see-more": "Voir plus", diff --git a/public/language/gl/world.json b/public/language/gl/world.json index af21f98948..2057e13d00 100644 --- a/public/language/gl/world.json +++ b/public/language/gl/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/he/world.json b/public/language/he/world.json index 1c7ba7a867..ace8b5cad4 100644 --- a/public/language/he/world.json +++ b/public/language/he/world.json @@ -18,10 +18,10 @@ "help.federating": "באופן דומה, אם משתמשים מחוץ לפורום זה מתחילים לעקוב אחריכם, אז הפוסטים שלכם יתחילו להופיע גם באפליקציות ובאתרים אלה.", "help.next-generation": "זהו הדור הבא של המדיה החברתית, התחלו לתרום עוד היום!", - "onboard.title": "החלון שלכם אל ה-fediverse...", - "onboard.what": "זוהי הקטגוריה המותאמת אישית שלכם המורכבת רק מתוכן שנמצא מחוץ לפורום זה. אם משהו מופיע בדף הזה תלוי אם אתם עוקבים אחריו, או אם הפוסט הזה שותף על ידי מישהו שאתם עוקבים אחריו.", - "onboard.why": "יש הרבה דברים שקורים מחוץ לפורום הזה, ולא כל זה רלוונטי לתחומי העניין שלכם. לכן מעקב אחר אנשים הוא הדרך הטובה ביותר לאותת שאתם רוצים לראות יותר ממישהו אחר.", - "onboard.how": "בינתיים, תוכלו ללחוץ על כפתורי הקיצור בחלק העליון כדי לראות על מה עוד הפורום הזה יודע, ולהתחיל לגלות תוכן חדש!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "מצא קטגוריה...", "see-more": "ראה עוד", diff --git a/public/language/hr/world.json b/public/language/hr/world.json index af21f98948..2057e13d00 100644 --- a/public/language/hr/world.json +++ b/public/language/hr/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/hu/world.json b/public/language/hu/world.json index af21f98948..2057e13d00 100644 --- a/public/language/hu/world.json +++ b/public/language/hu/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/hy/world.json b/public/language/hy/world.json index af21f98948..2057e13d00 100644 --- a/public/language/hy/world.json +++ b/public/language/hy/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/id/world.json b/public/language/id/world.json index af21f98948..2057e13d00 100644 --- a/public/language/id/world.json +++ b/public/language/id/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/it/world.json b/public/language/it/world.json index 103ab1daad..3af26b4563 100644 --- a/public/language/it/world.json +++ b/public/language/it/world.json @@ -18,10 +18,10 @@ "help.federating": "Allo stesso modo, se gli utenti esterni a questo forum iniziano a seguirti, i tuoi post inizieranno ad apparire anche su quelle app e quei siti web.", "help.next-generation": "Questa è la prossima generazione di social media, inizia a contribuire oggi!", - "onboard.title": "La tua finestra sul fediverso...", - "onboard.what": "Questa è la tua categoria personalizzata composta solo da contenuti trovati al di fuori di questo forum. La presenza di qualcosa in questa pagina dipende dal fatto che tu la segua o che il post sia stato condiviso da qualcuno che segui.", - "onboard.why": "Ci sono molte cose che accadono al di fuori di questo forum e non tutte sono rilevanti per i tuoi interessi. Ecco perché seguire le persone è il modo migliore per segnalare che vuoi vedere di più da qualcuno.", - "onboard.how": "Nel frattempo, puoi cliccare sui pulsanti di scelta rapida in alto per vedere cos'altro conosce questo forum e iniziare a scoprire nuovi contenuti!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Trova una categoria...", "see-more": "Vedi di più", diff --git a/public/language/ja/world.json b/public/language/ja/world.json index af21f98948..2057e13d00 100644 --- a/public/language/ja/world.json +++ b/public/language/ja/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/ko/world.json b/public/language/ko/world.json index af21f98948..2057e13d00 100644 --- a/public/language/ko/world.json +++ b/public/language/ko/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/lt/world.json b/public/language/lt/world.json index af21f98948..2057e13d00 100644 --- a/public/language/lt/world.json +++ b/public/language/lt/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/lv/world.json b/public/language/lv/world.json index af21f98948..2057e13d00 100644 --- a/public/language/lv/world.json +++ b/public/language/lv/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/ms/world.json b/public/language/ms/world.json index af21f98948..2057e13d00 100644 --- a/public/language/ms/world.json +++ b/public/language/ms/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/nb/world.json b/public/language/nb/world.json index d0c3e5e232..62c2f9ddde 100644 --- a/public/language/nb/world.json +++ b/public/language/nb/world.json @@ -18,10 +18,10 @@ "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": "Dette er neste generasjon sosiale medier, begynn å bidra i dag!", - "onboard.title": "Ditt vindu til fødiverset...", - "onboard.what": "Dette er din personlige kategori, som kun består av innhold funnet utenfor dette forumet. Om noe vises på denne siden, avhenger av om du følger dem, eller om innlegget ble delt av noen du følger.", - "onboard.why": "Det skjer mye utenfor dette forumet, og ikke alt er relevant for dine interesser. Derfor er det å følge folk den beste måten å vise at du vil se mer fra noen.", - "onboard.how": "I mellomtiden kan du klikke på snarveisknappene øverst for å se hva annet dette forumet inneholder, og begynne å oppdage nytt innhold!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/nl/world.json b/public/language/nl/world.json index af21f98948..2057e13d00 100644 --- a/public/language/nl/world.json +++ b/public/language/nl/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/nn-NO/world.json b/public/language/nn-NO/world.json index 3a43c31a8b..f402724f70 100644 --- a/public/language/nn-NO/world.json +++ b/public/language/nn-NO/world.json @@ -18,10 +18,10 @@ "help.federating": "På same måte, dersom brukarar frå utsida av dette forumet begynner å følge deg, vil innlegga dine òg begynne å visast på desse appane og nettsidene.", "help.next-generation": "Dette er den neste generasjonen av sosiale medium, begynn å bidra i dag!", - "onboard.title": "Ditt vindauge til fødiverset...", - "onboard.what": "Dette er din personlege kategori som berre består av innhald funne utanfor dette forumet. Om noko blir vist på denne sida, avheng av om du følger dei, eller om innlegget blei delt av nokon du følger.", - "onboard.why": "Det skjer mykje utanfor dette forumet, og ikkje alt er relevant for interessene dine. Difor er det å følge folk den beste måten å signalisere at du ønskjer å sjå meir frå nokon.", - "onboard.how": "I mellomtida kan du klikke på snarvegsknappane øvst for å sjå kva anna dette forumet inneheld, og begynne å oppdage nytt innhald!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/pl/world.json b/public/language/pl/world.json index 1b37dc05eb..f2bfa77c65 100644 --- a/public/language/pl/world.json +++ b/public/language/pl/world.json @@ -18,10 +18,10 @@ "help.federating": "O ile użytkownicy spoza tego forum zaczną Cię śledzić, to w efekcie Twoje wpisy pojawią się na zewnętrznych stronach i w aplikacjach.", "help.next-generation": "To są media społecznościowe kolejnej generacji, dołącz już dzisiaj!", - "onboard.title": "Twoje okno na fediverse...", - "onboard.what": "Zawartość tej kategorii jest dostowana do Twoich poczynań i zawiera jedynie dane spoza tego forum. Aby się coś tu pojawiło trzeba zacząć śledzić zdalne źródła informacji.", - "onboard.why": "Mnóstwo rzeczy dzieje się poza tym forum ale niekoniecznie zgodnych z Twoimi zainteresowaniami. Dlatego śledzenie konkretnych użytkowników jest dobrą metodą aby uzyskać więcej zawartości przez nich dodawanych.", - "onboard.how": "W międzyczasie możesz użyć przycisków skrótów na górze aby przekonać się jak forum jest powiązane a okaże się, że zapewnia wiele dodatkowych treści!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Znajdź kategorię...", "see-more": "See more", diff --git a/public/language/pt-BR/world.json b/public/language/pt-BR/world.json index af21f98948..2057e13d00 100644 --- a/public/language/pt-BR/world.json +++ b/public/language/pt-BR/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/pt-PT/world.json b/public/language/pt-PT/world.json index af21f98948..2057e13d00 100644 --- a/public/language/pt-PT/world.json +++ b/public/language/pt-PT/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/ro/world.json b/public/language/ro/world.json index 7938c5ebeb..69f9b8e5d0 100644 --- a/public/language/ro/world.json +++ b/public/language/ro/world.json @@ -18,10 +18,10 @@ "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": "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!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/ru/world.json b/public/language/ru/world.json index e3b375a865..56688fcbda 100644 --- a/public/language/ru/world.json +++ b/public/language/ru/world.json @@ -18,10 +18,10 @@ "help.federating": "Аналогично, если пользователи за пределами этого форума начнут подписываться на вас, то ваши сообщения также начнут появляться в этих приложениях и на этих сайтах.", "help.next-generation": "Это новое поколение социальных сетей, начните вносить свой вклад уже сегодня!", - "onboard.title": "Ваше окно в мир fediverse...", - "onboard.what": "Это ваша персонализированная категория, состоящая только из контента, найденного за пределами этого форума. Появится ли что-то на этой странице, зависит от того, подписаны ли вы на них, или же этот пост был опубликован кем-то, на кого вы подписаны.", - "onboard.why": "За пределами этого форума происходит много всего, и не все из этого соответствует вашим интересам. Вот почему подписка на людей — лучший способ подать сигнал о том, что вы хотите видеть больше от кого-то.", - "onboard.how": "А пока вы можете нажать на кнопки быстрого доступа вверху, чтобы узнать, что еще известно на этом форуме, и начать открывать для себя новый контент!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/rw/world.json b/public/language/rw/world.json index af21f98948..2057e13d00 100644 --- a/public/language/rw/world.json +++ b/public/language/rw/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/sc/world.json b/public/language/sc/world.json index af21f98948..2057e13d00 100644 --- a/public/language/sc/world.json +++ b/public/language/sc/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/sk/world.json b/public/language/sk/world.json index af21f98948..2057e13d00 100644 --- a/public/language/sk/world.json +++ b/public/language/sk/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/sl/world.json b/public/language/sl/world.json index af21f98948..2057e13d00 100644 --- a/public/language/sl/world.json +++ b/public/language/sl/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/sq-AL/world.json b/public/language/sq-AL/world.json index af21f98948..2057e13d00 100644 --- a/public/language/sq-AL/world.json +++ b/public/language/sq-AL/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/sr/world.json b/public/language/sr/world.json index af21f98948..2057e13d00 100644 --- a/public/language/sr/world.json +++ b/public/language/sr/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/sv/world.json b/public/language/sv/world.json index af21f98948..2057e13d00 100644 --- a/public/language/sv/world.json +++ b/public/language/sv/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/th/world.json b/public/language/th/world.json index af21f98948..2057e13d00 100644 --- a/public/language/th/world.json +++ b/public/language/th/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/tr/world.json b/public/language/tr/world.json index af21f98948..2057e13d00 100644 --- a/public/language/tr/world.json +++ b/public/language/tr/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/uk/world.json b/public/language/uk/world.json index af21f98948..2057e13d00 100644 --- a/public/language/uk/world.json +++ b/public/language/uk/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/ur/world.json b/public/language/ur/world.json index 80727acc33..62b8f7d916 100644 --- a/public/language/ur/world.json +++ b/public/language/ur/world.json @@ -18,10 +18,10 @@ "help.federating": "اسی طرح، اگر اس فورم سے باہر کے صارفین آپ کو فالو کرنا شروع کر دیں، تو آپ کی پوسٹس ان کے ایپلیکیشنز اور ویب سائٹس پر ظاہر ہونا شروع ہو جائیں گی۔", "help.next-generation": "یہ سوشل نیٹ ورک کی نئی نسل ہے۔ آج ہی سے حصہ ڈالنا شروع کریں!", - "onboard.title": "فیڈی ورس کی طرف آپ کی کھڑکی…", - "onboard.what": "یہ آپ کی ذاتی نوعیت کی کیٹیگری ہے جو صرف اس فورم سے باہر کے مواد پر مشتمل ہے۔ یہاں وہ چیزیں ظاہر ہوتی ہیں جو آپ کے فالو کیے ہوئے لوگوں نے بنائیں یا شیئر کیں۔", - "onboard.why": "اس فورم سے باہر بہت کچھ ہو رہا ہے، اور ہر چیز آپ کے مفادات سے مطابقت نہیں رکھتی۔ اس لیے مخصوص لوگوں کو فالو کرنا یہ ظاہر کرنے کا بہترین طریقہ ہے کہ آپ ان سے مزید دیکھنا چاہتے ہیں۔", - "onboard.how": "اس دوران، آپ اس فورم کے قابل رسائی مواد کو دیکھنے کے لیے اوپر کے بٹن استعمال کر سکتے ہیں۔ اس طرح آپ نئے مواد کی دریافت شروع کر سکتے ہیں!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", diff --git a/public/language/vi/world.json b/public/language/vi/world.json index a883e532dd..77a23d73a5 100644 --- a/public/language/vi/world.json +++ b/public/language/vi/world.json @@ -18,10 +18,10 @@ "help.federating": "Tương tự như vậy, nếu người dùng bên ngoài diễn đàn này bắt đầu theo dõi bạn, thì bài đăng của bạn cũng sẽ bắt đầu xuất hiện trên các ứng dụng và trang web đó.", "help.next-generation": "Đây là thế hệ mạng xã hội kế tiếp, hãy bắt đầu đóng góp ngay hôm nay!", - "onboard.title": "Cửa sổ của bạn đến với liên đoàn...", - "onboard.what": "Đây là danh mục được cá nhân hóa của bạn chỉ bao gồm nội dung được tìm thấy bên ngoài diễn đàn này. Việc nội dung nào đó có hiển thị trên trang này hay không tùy thuộc vào việc bạn có theo dõi họ hay không hoặc liệu bài đăng đó có được chia sẻ bởi người mà bạn theo dõi hay không.", - "onboard.why": "Có rất nhiều điều diễn ra bên ngoài diễn đàn này và không phải tất cả đều phù hợp với sở thích của bạn. Đó là lý do tại sao theo dõi mọi người là cách tốt nhất để báo hiệu rằng bạn muốn biết thêm thông tin từ ai đó.", - "onboard.how": "Trong thời gian chờ đợi, bạn có thể nhấp vào các nút tắt ở trên cùng để xem diễn đàn này biết thêm những gì và bắt đầu khám phá một số nội dung mới!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Tìm danh mục...", "see-more": "Xem nhiều hơn", diff --git a/public/language/zh-CN/world.json b/public/language/zh-CN/world.json index e43b0fb8e7..4e2f056e85 100644 --- a/public/language/zh-CN/world.json +++ b/public/language/zh-CN/world.json @@ -18,10 +18,10 @@ "help.federating": "同样,如果本论坛以外的用户开始关注 ,那么您的帖子也会开始出现在这些应用程序和网站上。", "help.next-generation": "这是新一代的社交媒体,从今天开始,贡献力量吧!", - "onboard.title": "您通往联邦宇宙的窗口...", - "onboard.what": "这是您的个性化版块,只包含本论坛以外的内容。内容是否显示在本页取决于您是否关注他们,或者该帖子是否由您关注的人分享。", - "onboard.why": "论坛之外的事情很多,而且并非所有事情都与您的兴趣相关。因此,关注他人是表明您想从某人那里了解更多信息的最佳方式。", - "onboard.how": "在此期间,您可以点击顶部的快捷按钮,了解本论坛的其他内容,并开始发现一些新内容!", + "onboard.title": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "查找版块...", "see-more": "显示更多", diff --git a/public/language/zh-TW/world.json b/public/language/zh-TW/world.json index af21f98948..2057e13d00 100644 --- a/public/language/zh-TW/world.json +++ b/public/language/zh-TW/world.json @@ -18,10 +18,10 @@ "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!", - "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": "A world of content at your fingertips…", + "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", + "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", + "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", "category-search": "Find a category...", "see-more": "See more", From 2f5021e54730a835da442850357f4e6eb17479ed Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 16 Mar 2026 15:34:08 -0400 Subject: [PATCH 4644/4744] feat: add category selector to /world quick composer --- install/data/defaults.json | 3 ++- install/package.json | 4 ++-- .../en-GB/admin/settings/activitypub.json | 3 ++- public/src/modules/quickreply.js | 15 ++++++++++++++- src/controllers/activitypub/topics.js | 4 +++- src/views/admin/federation/content.tpl | 4 ++++ 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/install/data/defaults.json b/install/data/defaults.json index 2636fa875c..a6b25a2732 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -208,5 +208,6 @@ "activitypubUserPruneDays": 7, "activitypubFilter": 0, "activitypubSummaryLimit": 500, - "activitypubBreakString": "[...]" + "activitypubBreakString": "[...]", + "activitypubWorldDefaultCid": -1 } diff --git a/install/package.json b/install/package.json index 55d3247aba..ee1a0c15d6 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.57", + "nodebb-theme-harmony": "2.2.58", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.57", - "nodebb-theme-persona": "14.2.31", + "nodebb-theme-persona": "14.2.32", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.2", "nprogress": "0.2.0", diff --git a/public/language/en-GB/admin/settings/activitypub.json b/public/language/en-GB/admin/settings/activitypub.json index 0486870db1..64508849aa 100644 --- a/public/language/en-GB/admin/settings/activitypub.json +++ b/public/language/en-GB/admin/settings/activitypub.json @@ -50,5 +50,6 @@ "content.summary-limit": "Character count after which a summary is generated", "content.summary-limit-help": "When content is federated out that exceeds this character count, a summary is generated, comprising of all complete sentences prior to this limit. (Default: 500)", "content.break-string": "Note/Article Delimiter", - "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])" + "content.break-string-help": "This delimiter can be manually inserted by power users when composing new topics. It instructs NodeBB to use content up until that point as part of the summary. If this string is not used, then the character count fallback applies. (Default: [...])", + "content.world-default-cid": "Default category ID for "World" page composer" } \ No newline at end of file diff --git a/public/src/modules/quickreply.js b/public/src/modules/quickreply.js index 9172ca3769..f081c59588 100644 --- a/public/src/modules/quickreply.js +++ b/public/src/modules/quickreply.js @@ -3,9 +3,11 @@ define('quickreply', [ 'components', 'autocomplete', 'api', 'alerts', 'uploadHelpers', 'mousetrap', 'storage', 'hooks', + 'categorySelector', ], function ( components, autocomplete, api, - alerts, uploadHelpers, mousetrap, storage, hooks + alerts, uploadHelpers, mousetrap, storage, hooks, + categorySelector, ) { const QuickReply = { _autocomplete: null, @@ -17,6 +19,17 @@ define('quickreply', [ return; } + if ($('[component="topic/quickreply/container"] [component="category-selector"]')) { + categorySelector.init($('[component="category-selector"]'), { + privilege: 'topics:create', + selectedCategory: ajaxify.data.selectedCategory, + onSelect: function (category) { + opts.body = opts.body || {}; + opts.body.cid = category.cid; + }, + }); + } + const qrDraftId = ajaxify.data.tid ? `qr:draft:tid:${ajaxify.data.tid}` : `qr:draft:cid:${opts?.body?.cid || -1}`; const data = { element: element, diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 775d738305..26008e416c 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -26,10 +26,11 @@ controller.list = async function (req, res) { let start = Math.max(0, (page - 1) * topicsPerPage); let stop = start + topicsPerPage - 1; - const [userSettings, userPrivileges, isAdminOrGlobalMod] = await Promise.all([ + const [userSettings, userPrivileges, isAdminOrGlobalMod, selectedCategory] = await Promise.all([ user.getSettings(req.uid), privileges.categories.get('-1', req.uid), user.isAdminOrGlobalMod(req.uid), + categories.getCategoryData(meta.config.activitypubWorldDefaultCid), ]); const targetUid = await user.getUidByUserslug(req.query.author); let cidQuery = { @@ -48,6 +49,7 @@ controller.list = async function (req, res) { delete data.children; data.sort = req.query.sort; data.privileges = userPrivileges; + data.selectedCategory = selectedCategory; let tids; let topicCount; diff --git a/src/views/admin/federation/content.tpl b/src/views/admin/federation/content.tpl index 3ee43fcf21..be584f6de2 100644 --- a/src/views/admin/federation/content.tpl +++ b/src/views/admin/federation/content.tpl @@ -20,6 +20,10 @@ [[admin/settings/activitypub:content.break-string-help]]
    name capacity count size hits misses hit ratio hits/sec ttl name capacity count size hits misses hit ratio hits/sec ttl
    From f885a51009eaca7652f42614486429f147facbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Feb 2026 10:52:47 -0500 Subject: [PATCH 4187/4744] nice numbers in tables --- src/controllers/admin/dashboard.js | 2 +- src/views/admin/dashboard.tpl | 4 ++-- src/views/admin/manage/users.tpl | 8 ++++---- src/views/admin/partials/dashboard/stats.tpl | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index 91d6e401c1..edcad4b066 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -211,7 +211,7 @@ function calculateDeltas(results) { function increasePercent(last, now) { const percent = last ? (now - last) / last * 100 : 0; - return percent.toFixed(1); + return percent.toFixed(0); } results.yesterday -= results.today; results.dayIncrease = increasePercent(results.yesterday, results.today); diff --git a/src/views/admin/dashboard.tpl b/src/views/admin/dashboard.tpl index 4f0f41c496..739ecaac02 100644 --- a/src/views/admin/dashboard.tpl +++ b/src/views/admin/dashboard.tpl @@ -114,7 +114,7 @@
    - + @@ -140,7 +140,7 @@
    [[admin/dashboard:popular-searches]] [[admin/dashboard:view-all]]
    -
    [[admin/dashboard:active-users.users]]
    +
    {{{ each popularSearches }}} diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index 21fa9d5e1c..1f196d361b 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -121,7 +121,7 @@ {{{ each users }}} - + - - - + + + diff --git a/src/views/admin/partials/dashboard/stats.tpl b/src/views/admin/partials/dashboard/stats.tpl index e0b9910ea7..43c1d661a3 100644 --- a/src/views/admin/partials/dashboard/stats.tpl +++ b/src/views/admin/partials/dashboard/stats.tpl @@ -17,7 +17,7 @@ {{{ end }}} - + {{{ each stats }}} - + - + - + {{{ if !hideAllTime}}} {{{ end }}} From 8423da047cb9eabc1d892de3cbfc0c34165545aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Feb 2026 10:58:27 -0500 Subject: [PATCH 4188/4744] chore: up composer --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index cf85ac1842..dc9d721efe 100644 --- a/install/package.json +++ b/install/package.json @@ -97,7 +97,7 @@ "multer": "2.0.2", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.6.1", - "nodebb-plugin-composer-default": "10.3.16", + "nodebb-plugin-composer-default": "10.3.17", "nodebb-plugin-dbsearch": "6.3.5", "nodebb-plugin-emoji": "6.0.5", "nodebb-plugin-emoji-android": "4.1.1", From 3de668794dbc74f4c26a46249fa0cafb4e268e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Feb 2026 11:25:02 -0500 Subject: [PATCH 4189/4744] dont show gaps in admin manage users due to AP users --- src/controllers/admin/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index a1f48f35b1..4b5e89d145 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -101,7 +101,7 @@ async function getUsers(req, res) { ]); await render(req, res, { - users: users.filter(user => user && parseInt(user.uid, 10)), + users: users.filter(user => user && user.userslug), page: page, pageCount: Math.max(1, Math.ceil(count / resultsPerPage)), resultsPerPage: resultsPerPage, From 0a25ed034ef1c6963c333806e6c22be293d09c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Feb 2026 12:05:15 -0500 Subject: [PATCH 4190/4744] swap count and term, fix th in users --- src/views/admin/dashboard/searches.tpl | 4 ++-- src/views/admin/manage/users.tpl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/admin/dashboard/searches.tpl b/src/views/admin/dashboard/searches.tpl index a5f34852b3..72fac3a0f2 100644 --- a/src/views/admin/dashboard/searches.tpl +++ b/src/views/admin/dashboard/searches.tpl @@ -19,8 +19,8 @@
    {users.uid}{users.uid} @@ -172,9 +172,9 @@ {{{ end }}} {formattedNumber(users.postcount)}{formattedNumber(users.reputation)}{{{ if users.flags }}}{users.flags}{{{ else }}}0{{{ end }}}{formattedNumber(users.postcount)}{formattedNumber(users.reputation)}{{{ if users.flags }}}{users.flags}{{{ else }}}0{{{ end }}}
    @@ -31,15 +31,15 @@ {formattedNumber(./yesterday)} {formattedNumber(./today)}{./dayIncrease}%{./dayIncrease}% {formattedNumber(./lastweek)} {formattedNumber(./thisweek)}{./weekIncrease}%{./weekIncrease}% {formattedNumber(./lastmonth)} {formattedNumber(./thismonth)}{./monthIncrease}%{./monthIncrease}%{formattedNumber(./alltime)}
    - + {{{ if !searches.length}}} @@ -30,8 +30,8 @@ {{{ end }}} {{{ each searches }}} + - {{{ end }}} diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index 1f196d361b..c3a411f854 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -120,7 +120,7 @@ {{{ each users }}} - +
    [[admin/dashboard:search-term]] [[admin/dashboard:search-count]][[admin/dashboard:search-term]]
    {formattedNumber(searches.score)} {searches.value}{formattedNumber(searches.score)}
    {users.uid} From b5977c20f4e7e73c50e767a82b5fc6d8a1fe861a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Feb 2026 12:32:22 -0500 Subject: [PATCH 4191/4744] add pagination to admin searches --- src/controllers/admin/dashboard.js | 41 ++++++++++++++++++-------- src/views/admin/dashboard/searches.tpl | 1 + src/views/admin/partials/paginator.tpl | 40 ++++++++++++------------- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index edcad4b066..df1f94a830 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -15,6 +15,7 @@ const user = require('../../user'); const topics = require('../../topics'); const utils = require('../../utils'); const emailer = require('../../emailer'); +const pagination = require('../../pagination'); const dashboardController = module.exports; @@ -341,24 +342,30 @@ dashboardController.getTopics = async (req, res) => { }; dashboardController.getSearches = async (req, res) => { - let start = 0; - let end = 0; + const page = parseInt(req.query.page, 10) || 1; + const perPage = 25; + const start = Math.max(0, (page - 1) * perPage); + const stop = start + perPage - 1; + + let startDate = 0; + let endDate = 0; if (req.query.start) { - start = new Date(req.query.start); - start.setHours(24, 0, 0, 0); - end = new Date(); - end.setHours(24, 0, 0, 0); + startDate = new Date(req.query.start); + startDate.setHours(24, 0, 0, 0); + endDate = new Date(); + endDate.setHours(24, 0, 0, 0); } if (req.query.end) { - end = new Date(req.query.end); - end.setHours(24, 0, 0, 0); + endDate = new Date(req.query.end); + endDate.setHours(24, 0, 0, 0); } let searches; - if (start && end && start <= end) { - const daysArr = [start]; - const nextDay = new Date(start.getTime()); - while (nextDay < end) { + let itemCount; + if (startDate && endDate && startDate <= endDate) { + const daysArr = [startDate]; + const nextDay = new Date(startDate.getTime()); + while (nextDay < endDate) { nextDay.setDate(nextDay.getDate() + 1); nextDay.setHours(0, 0, 0, 0); daysArr.push(new Date(nextDay.getTime())); @@ -382,13 +389,21 @@ dashboardController.getSearches = async (req, res) => { searches = Object.keys(map) .map(key => ({ value: key, score: map[key] })) .sort((a, b) => b.score - a.score); + itemCount = searches.length; + searches = searches.slice(start, stop + 1); } else { - searches = await db.getSortedSetRevRangeWithScores('searches:all', 0, 99); + [itemCount, searches] = await Promise.all([ + db.sortedSetCard('searches:all'), + db.getSortedSetRevRangeWithScores('searches:all', start, stop), + ]); } + const pageCount = Math.ceil(itemCount / perPage); + res.render('admin/dashboard/searches', { searches: searches.map(s => ({ value: validator.escape(String(s.value)), score: s.score })), startDate: req.query.start ? validator.escape(String(req.query.start)) : null, endDate: req.query.end ? validator.escape(String(req.query.end)) : null, + pagination: pagination.create(page, pageCount, req.query), }); }; diff --git a/src/views/admin/dashboard/searches.tpl b/src/views/admin/dashboard/searches.tpl index 72fac3a0f2..ef20b9223b 100644 --- a/src/views/admin/dashboard/searches.tpl +++ b/src/views/admin/dashboard/searches.tpl @@ -36,5 +36,6 @@ {{{ end }}}
    +
    \ No newline at end of file diff --git a/src/views/admin/partials/paginator.tpl b/src/views/admin/partials/paginator.tpl index ccbc4cd409..a73410161d 100644 --- a/src/views/admin/partials/paginator.tpl +++ b/src/views/admin/partials/paginator.tpl @@ -1,47 +1,47 @@ -
    +
    \ No newline at end of file + \ No newline at end of file From 086a580329b50f664f3c6a76fc00844431d21233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Feb 2026 12:36:15 -0500 Subject: [PATCH 4192/4744] fix spec --- public/openapi/read/admin/dashboard/searches.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/public/openapi/read/admin/dashboard/searches.yaml b/public/openapi/read/admin/dashboard/searches.yaml index 1fda6f8423..a6af771fad 100644 --- a/public/openapi/read/admin/dashboard/searches.yaml +++ b/public/openapi/read/admin/dashboard/searches.yaml @@ -30,4 +30,5 @@ get: type: string description: A UNIX timestamp of the end date nullable: true + - $ref: ../../../components/schemas/Pagination.yaml#/Pagination - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file From 472178035f27b1976535736bd24acb2c36f24975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 15 Feb 2026 13:51:59 -0500 Subject: [PATCH 4193/4744] chore: up themes --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index dc9d721efe..1ba0208fbe 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.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.11", + "nodebb-theme-harmony": "2.2.12", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", - "nodebb-theme-persona": "14.2.5", + "nodebb-theme-persona": "14.2.6", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", "nprogress": "0.2.0", From b607a80aeb9fbd747ef6125d14bc1af262fd479b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 16 Feb 2026 08:48:21 -0500 Subject: [PATCH 4194/4744] fix: #13993, encodeURICompoent pid since it can be AP url --- src/controllers/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 27366459a9..554967cab4 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -396,7 +396,7 @@ helpers.setCategoryTeaser = function (category) { if (Array.isArray(category.posts) && category.posts.length && category.posts[0]) { const post = category.posts[0]; category.teaser = { - url: `${nconf.get('relative_path')}/post/${post.pid}`, + url: `${nconf.get('relative_path')}/post/${encodeURIComponent(post.pid)}`, timestampISO: post.timestampISO, pid: post.pid, tid: post.tid, From ec8e547cfe91ee3f893e9b4ceeeb7c77766b292e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:50:05 -0500 Subject: [PATCH 4195/4744] chore(deps): update dependency jsdom to v28.1.0 (#13992) 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 1ba0208fbe..084e038727 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": "28.0.0", + "jsdom": "28.1.0", "lint-staged": "16.2.7", "mocha": "11.7.5", "mocha-lcov-reporter": "1.3.0", From a8a85bcb0b78e44143eceaf327f57ce18998690f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 16 Feb 2026 12:04:11 -0500 Subject: [PATCH 4196/4744] refactor: shorter hook --- src/topics/teaser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/topics/teaser.js b/src/topics/teaser.js index f729afb4ec..03ea62b5c0 100644 --- a/src/topics/teaser.js +++ b/src/topics/teaser.js @@ -80,7 +80,7 @@ module.exports = function (Topics) { return tidToPost[topic.tid]; }); - const result = await plugins.hooks.fire('filter:teasers.get', { teasers: teasers, uid: uid }); + const result = await plugins.hooks.fire('filter:teasers.get', { teasers, uid }); return result.teasers; }; From 370fa6b18aa080c3ff6fefc1483c05320bc152b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 16 Feb 2026 16:44:52 -0500 Subject: [PATCH 4197/4744] make dropdown buttons fill their container --- install/package.json | 4 ++-- .../category/filter-dropdown-content.tpl | 2 +- src/views/partials/category/sort.tpl | 2 +- .../partials/category/tools-dropdown-content.tpl | 2 +- src/views/partials/category/watch.tpl | 2 +- .../partials/tags/filter-dropdown-content.tpl | 2 +- src/views/partials/tags/watch.tpl | 2 +- src/views/partials/topic-filters.tpl | 16 ++++++++++++++++ src/views/partials/topic-terms.tpl | 16 ++++++++++++++++ .../partials/users/filter-dropdown-content.tpl | 2 +- 10 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 src/views/partials/topic-filters.tpl create mode 100644 src/views/partials/topic-terms.tpl diff --git a/install/package.json b/install/package.json index 084e038727..873a3d1b66 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.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.12", + "nodebb-theme-harmony": "2.2.13", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", - "nodebb-theme-persona": "14.2.6", + "nodebb-theme-persona": "14.2.7", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", "nprogress": "0.2.0", diff --git a/src/views/partials/category/filter-dropdown-content.tpl b/src/views/partials/category/filter-dropdown-content.tpl index 19422fe6e6..8e92c5d7cf 100644 --- a/src/views/partials/category/filter-dropdown-content.tpl +++ b/src/views/partials/category/filter-dropdown-content.tpl @@ -1,4 +1,4 @@ - diff --git a/src/views/partials/category/tools-dropdown-content.tpl b/src/views/partials/category/tools-dropdown-content.tpl index 695735e29c..3dfa6bdabc 100644 --- a/src/views/partials/category/tools-dropdown-content.tpl +++ b/src/views/partials/category/tools-dropdown-content.tpl @@ -1,4 +1,4 @@ - + + \ No newline at end of file diff --git a/src/views/partials/topic-terms.tpl b/src/views/partials/topic-terms.tpl new file mode 100644 index 0000000000..da7c2abd7b --- /dev/null +++ b/src/views/partials/topic-terms.tpl @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/src/views/partials/users/filter-dropdown-content.tpl b/src/views/partials/users/filter-dropdown-content.tpl index e3ab5116d4..e639059b2d 100644 --- a/src/views/partials/users/filter-dropdown-content.tpl +++ b/src/views/partials/users/filter-dropdown-content.tpl @@ -1,4 +1,4 @@ -
    [[admin/development/info:uptime]]
    {info.os.hostname}:{info.process.port}[[admin/settings/activitypub:rules.cid]]
    From 94df97384e549eb985a10d777f397dc06cc38df7 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Feb 2026 16:17:16 -0500 Subject: [PATCH 4212/4744] fix: update quickreply.init so that it can be passed an options parameter, generate proper draft id for world page --- public/src/client/topic.js | 4 +++- public/src/client/world.js | 7 ++++++- public/src/modules/quickreply.js | 18 ++++-------------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index b54185bf80..873f05df6e 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -425,7 +425,9 @@ define('forum/topic', [ function setupQuickReply() { if (config.enableQuickReply || (config.theme && config.theme.enableQuickReply)) { - quickreply.init(); + quickreply.init({ + route: `/topics/${ajaxify.data.tid}`, + }); } } diff --git a/public/src/client/world.js b/public/src/client/world.js index 8d7a11b927..17ef935860 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -16,7 +16,12 @@ define('forum/world', [ World.init = function () { app.enterRoom('world'); categoryTools.init($('#world-feed')); - quickreply.init(); + quickreply.init({ + route: '/topics', + body: { + cid: ajaxify.data.cid, + }, + }); sort.handleSort('categoryTopicSort', 'world'); diff --git a/public/src/modules/quickreply.js b/public/src/modules/quickreply.js index 602c1ace05..f33f73fa91 100644 --- a/public/src/modules/quickreply.js +++ b/public/src/modules/quickreply.js @@ -11,9 +11,9 @@ define('quickreply', [ _autocomplete: null, }; - QuickReply.init = function () { + QuickReply.init = function (opts) { const element = components.get('topic/quickreply/text'); - const qrDraftId = `qr:draft:tid:${ajaxify.data.tid}`; + const qrDraftId = ajaxify.data.tid ? `qr:draft:tid:${ajaxify.data.tid}` : `qr:draft:cid:${opts?.body?.cid || -1}`; const data = { element: element, strategies: [], @@ -68,18 +68,8 @@ define('quickreply', [ tid: ajaxify.data.tid, handle: undefined, content: replyMsg, + ...opts.body, }; - let replyRoute = '/topics'; - switch(ajaxify.data.template.name) { - case 'topic': - replyData.tid = ajaxify.data.tid; - replyRoute = `/topics/${ajaxify.data.tid}`; - break; - - case 'world': - replyData.cid = '-1'; - break; - } const replyLen = replyMsg.length; if (replyLen < parseInt(config.minimumPostLength, 10)) { @@ -90,7 +80,7 @@ define('quickreply', [ ready = false; element.val(''); - api.post(replyRoute, replyData, function (err, data) { + api.post(opts.route, replyData, function (err, data) { ready = true; if (err) { element.val(replyMsg); From 5da35bdadd588934f70e445f91344324703742f2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Feb 2026 16:18:47 -0500 Subject: [PATCH 4213/4744] fix: call syncUserInboxes asyncronously --- 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 f4a5ac8b04..ac360abe6d 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -268,8 +268,8 @@ module.exports = function (Topics) { Topics.addParentPosts([postData], uid), Topics.syncBacklinks(postData), Topics.markAsRead([tid], uid), - activitypub.notes.syncUserInboxes(tid, uid), ]); + activitypub.notes.syncUserInboxes(tid, uid); // Returned data is a superset of post summary data postData.user = userInfo; From eb0aa6d8bdf989e37853aece80da574879c1b2ec Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 11:27:56 -0500 Subject: [PATCH 4214/4744] fix: render new post in feed when posting via quick create --- public/src/client/world.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/public/src/client/world.js b/public/src/client/world.js index 17ef935860..014e41315a 100644 --- a/public/src/client/world.js +++ b/public/src/client/world.js @@ -9,13 +9,8 @@ define('forum/world', [ translator, quickreply) { const World = {}; - $(window).on('action:ajaxify.start', function () { - categoryTools.removeListeners(); - }); - World.init = function () { app.enterRoom('world'); - categoryTools.init($('#world-feed')); quickreply.init({ route: '/topics', body: { @@ -28,6 +23,14 @@ define('forum/world', [ handleButtons(); handleHelp(); + categoryTools.init($('#world-feed')); + socket.on('event:new_post', onNewPost); + $(window).one('action:ajaxify.start', function () { + categoryTools.removeListeners(); + socket.removeListener('event:new_post', onNewPost); + }); + + // Add label to sort const sortLabelEl = document.getElementById('sort-label'); const sortOptionsEl = document.getElementById('sort-options'); @@ -217,5 +220,15 @@ define('forum/world', [ }); } + async function onNewPost({ posts }) { + const feedEl = document.getElementById('world-feed'); + const html = await app.parseAndTranslate('world', 'posts', { posts }); + if (!feedEl || !html) { + return; + } + + feedEl.prepend(...html); + } + return World; }); From a9c2457fd40a3ea5300ed5f35a8d9c50612fbe11 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 11:50:24 -0500 Subject: [PATCH 4215/4744] fix: minor cleanup of quick-reply args; opts.body --- public/src/client/topic.js | 3 +++ public/src/modules/quickreply.js | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 873f05df6e..c5a9f5cab3 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -427,6 +427,9 @@ define('forum/topic', [ if (config.enableQuickReply || (config.theme && config.theme.enableQuickReply)) { quickreply.init({ route: `/topics/${ajaxify.data.tid}`, + body: { + tid: ajaxify.data.tid, + }, }); } } diff --git a/public/src/modules/quickreply.js b/public/src/modules/quickreply.js index f33f73fa91..bb06d822e2 100644 --- a/public/src/modules/quickreply.js +++ b/public/src/modules/quickreply.js @@ -124,9 +124,9 @@ define('quickreply', [ storage.removeItem(qrDraftId); const textEl = components.get('topic/quickreply/text'); hooks.fire('action:composer.post.new', { - tid: ajaxify.data.tid, - title: ajaxify.data.titleRaw, + title: ajaxify.data.tid ? ajaxify.data.titleRaw : '', body: textEl.val(), + ...opts.body, }); textEl.val(''); }); From 3e2070b2c3db3e3e2716bdb4d8ed631d10685725 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 12:06:28 -0500 Subject: [PATCH 4216/4744] fix: schema... not sure why I need this all of a sudden --- .../openapi/components/schemas/PostObject.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 80c9a06cd3..1d8f2f5b67 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -122,6 +122,23 @@ PostObject: type: array items: $ref: ../../components/schemas/TagObject.yaml#/TagObject + thumbs: + type: array + items: + type: object + properties: + id: + type: number + description: The topic id + name: + type: string + description: The topic thumbnail filename + path: + type: string + description: Path to topic thumbnail without upload_url prefix + url: + type: string + description: Relative path to the topic thumbnail required: - uid - tid From 1869b807f2f56e63f1f5f6faf7a68f86dcfa2913 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 12:20:21 -0500 Subject: [PATCH 4217/4744] refactor: teaser object schema to its own file --- .../components/schemas/TeaserObject.yaml | 55 +++++++++ .../components/schemas/TopicObject.yaml | 56 +-------- public/openapi/read/categories.yaml | 53 +-------- public/openapi/read/index.yaml | 53 +-------- public/openapi/read/tags/tag.yaml | 48 +------- public/openapi/read/unread.yaml | 56 +-------- .../read/user/userslug/chats/roomid.yaml | 108 +----------------- public/openapi/write/chats.yaml | 49 +------- 8 files changed, 63 insertions(+), 415 deletions(-) create mode 100644 public/openapi/components/schemas/TeaserObject.yaml diff --git a/public/openapi/components/schemas/TeaserObject.yaml b/public/openapi/components/schemas/TeaserObject.yaml new file mode 100644 index 0000000000..93eeafa86e --- /dev/null +++ b/public/openapi/components/schemas/TeaserObject.yaml @@ -0,0 +1,55 @@ +TeaserObject: + type: object + properties: + roomId: + type: number + fromuid: + type: number + content: + type: string + timestamp: + type: number + timestampISO: + type: string + description: An ISO 8601 formatted date string (complementing `timestamp`) + user: + type: object + properties: + uid: + type: number + description: A user identifier + isLocal: + type: boolean + description: Whether the user belongs to the local installation or not. + username: + type: string + description: A friendly name for a given user account + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces + removed, etc.) + picture: + nullable: true + type: string + status: + type: string + lastonline: + type: number + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users + without an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's + auto-generated icon + example: "#f44336" + lastonlineISO: + type: string + nullable: true diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml index 605c32de85..6a3f212541 100644 --- a/public/openapi/components/schemas/TopicObject.yaml +++ b/public/openapi/components/schemas/TopicObject.yaml @@ -94,61 +94,7 @@ TopicObject: - icon:bgColor - banned_until_readable teaser: - type: object - properties: - pid: - type: number - uid: - type: number - description: A user identifier - timestamp: - type: number - tid: - type: number - description: A topic identifier - content: - type: string - sourceContent: - type: string - nullable: true - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - user: - type: object - properties: - uid: - type: number - description: A user identifier - username: - type: string - description: A friendly name for a given user account - displayname: - type: string - isLocal: - type: boolean - userslug: - type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces - removed, etc.) - picture: - nullable: true - type: string - icon:text: - type: string - description: A single-letter representation of a username. This is used in the - auto-generated icon given to users - without an avatar - icon:bgColor: - type: string - description: A six-character hexadecimal colour code assigned to the user. This - value is used in conjunction with - `icon:text` for the user's - auto-generated icon - example: "#f44336" - index: - type: number - nullable: true + $ref: './TeaserObject.yaml#/TeaserObject' tags: type: array items: diff --git a/public/openapi/read/categories.yaml b/public/openapi/read/categories.yaml index 76d2375ebb..c78533e267 100644 --- a/public/openapi/read/categories.yaml +++ b/public/openapi/read/categories.yaml @@ -197,58 +197,7 @@ get: title: type: string teaser: - type: object - properties: - url: - type: string - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - pid: - type: number - tid: - type: number - index: - type: number - description: The index of the post - topic: - type: object - properties: - tid: - type: number - slug: - type: string - title: - type: string - user: - type: object - properties: - uid: - type: number - example: 1 - isLocal: - type: boolean - description: Whether the user belongs to the local installation or not. - username: - type: string - example: Dragon Fruit - userslug: - type: string - example: dragon-fruit - picture: - type: string - example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80' - nullable: true - displayname: - type: string - description: This is either username or fullname depending on forum and user settings - example: Dragon Fruit - 'icon:text': - type: string - example: D - 'icon:bgColor': - type: string - example: '#9c27b0' + $ref: ../components/schemas/TeaserObject.yaml#/TeaserObject imageClass: type: string - $ref: ../components/schemas/Pagination.yaml#/Pagination diff --git a/public/openapi/read/index.yaml b/public/openapi/read/index.yaml index 4c45da4360..56e4048340 100644 --- a/public/openapi/read/index.yaml +++ b/public/openapi/read/index.yaml @@ -199,58 +199,7 @@ get: title: type: string teaser: - type: object - properties: - url: - type: string - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - pid: - type: number - tid: - type: number - index: - type: number - description: The index of the post - topic: - type: object - properties: - tid: - type: number - slug: - type: string - title: - type: string - user: - type: object - properties: - uid: - type: number - example: 1 - username: - type: string - example: Dragon Fruit - userslug: - type: string - example: dragon-fruit - picture: - type: string - example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80' - nullable: true - displayname: - type: string - description: This is either username or fullname depending on forum and user settings - example: Dragon Fruit - 'icon:text': - type: string - example: D - 'icon:bgColor': - type: string - example: '#9c27b0' - isLocal: - type: boolean - description: Whether the user belongs to the local installation or not. + $ref: ../components/schemas/TeaserObject.yaml#/TeaserObject imageClass: type: string - $ref: ../components/schemas/Pagination.yaml#/Pagination diff --git a/public/openapi/read/tags/tag.yaml b/public/openapi/read/tags/tag.yaml index c9a49c5160..409ffb150b 100644 --- a/public/openapi/read/tags/tag.yaml +++ b/public/openapi/read/tags/tag.yaml @@ -150,53 +150,7 @@ get: fullname: type: string teaser: - type: object - properties: - pid: - type: number - uid: - type: number - description: A user identifier - timestamp: - type: number - tid: - type: number - description: A topic identifier - content: - type: string - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - user: - type: object - properties: - uid: - type: number - description: A user identifier - username: - type: string - description: A friendly name for a given user account - userslug: - type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces - removed, etc.) - picture: - nullable: true - type: string - icon:text: - type: string - description: A single-letter representation of a username. This is used in the - auto-generated icon given to users - without an avatar - icon:bgColor: - type: string - description: A six-character hexadecimal colour code assigned to the user. This - value is used in conjunction with - `icon:text` for the user's - auto-generated icon - example: "#f44336" - index: - type: number + $ref: ../../components/schemas/TeaserObject.yaml#/TeaserObject tags: type: array items: diff --git a/public/openapi/read/unread.yaml b/public/openapi/read/unread.yaml index 6ae0e8500e..516299c1fc 100644 --- a/public/openapi/read/unread.yaml +++ b/public/openapi/read/unread.yaml @@ -127,61 +127,7 @@ get: - icon:bgColor - banned_until_readable teaser: - type: object - nullable: true - properties: - pid: - type: number - uid: - type: number - description: A user identifier - timestamp: - type: number - tid: - type: number - description: A topic identifier - content: - type: string - sourceContent: - type: string - nullable: true - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - user: - type: object - properties: - uid: - type: number - description: A user identifier - username: - type: string - description: A friendly name for a given user account - displayname: - type: string - isLocal: - type: boolean - userslug: - type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces - removed, etc.) - picture: - nullable: true - type: string - icon:text: - type: string - description: A single-letter representation of a username. This is used in the - auto-generated icon given to users - without an avatar - icon:bgColor: - type: string - description: A six-character hexadecimal colour code assigned to the user. This - value is used in conjunction with - `icon:text` for the user's - auto-generated icon - example: "#f44336" - index: - type: number + $ref: ../components/schemas/TeaserObject.yaml#/TeaserObject tags: type: array items: diff --git a/public/openapi/read/user/userslug/chats/roomid.yaml b/public/openapi/read/user/userslug/chats/roomid.yaml index 448f350a42..4468d9c0a1 100644 --- a/public/openapi/read/user/userslug/chats/roomid.yaml +++ b/public/openapi/read/user/userslug/chats/roomid.yaml @@ -264,60 +264,7 @@ get: unread: type: boolean teaser: - type: object - properties: - roomId: - type: number - fromuid: - type: number - content: - type: string - timestamp: - type: number - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - user: - type: object - properties: - uid: - type: number - description: A user identifier - isLocal: - type: boolean - description: Whether the user belongs to the local installation or not. - username: - type: string - description: A friendly name for a given user account - displayname: - type: string - description: This is either username or fullname depending on forum and user settings - userslug: - type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces - removed, etc.) - picture: - nullable: true - type: string - status: - type: string - lastonline: - type: number - icon:text: - type: string - description: A single-letter representation of a username. This is used in the - auto-generated icon given to users - without an avatar - icon:bgColor: - type: string - description: A six-character hexadecimal colour code assigned to the user. This - value is used in conjunction with - `icon:text` for the user's - auto-generated icon - example: "#f44336" - lastonlineISO: - type: string - nullable: true + $ref: ../../../../components/schemas/TeaserObject.yaml#/TeaserObject lastUser: type: object properties: @@ -428,58 +375,7 @@ get: unread: type: boolean teaser: - type: object - properties: - fromuid: - type: number - content: - type: string - timestamp: - type: number - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - user: - type: object - properties: - uid: - type: number - description: A user identifier - isLocal: - type: boolean - description: Whether the user belongs to the local installation or not. - username: - type: string - description: A friendly name for a given user account - displayname: - type: string - description: This is either username or fullname depending on forum and user settings - userslug: - type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces - removed, etc.) - picture: - nullable: true - type: string - status: - type: string - lastonline: - type: number - icon:text: - type: string - description: A single-letter representation of a username. This is used in the - auto-generated icon given to users - without an avatar - icon:bgColor: - type: string - description: A six-character hexadecimal colour code assigned to the user. This - value is used in conjunction with - `icon:text` for the user's - auto-generated icon - example: "#f44336" - lastonlineISO: - type: string - nullable: true + $ref: ../../../../components/schemas/TeaserObject.yaml#/TeaserObject lastUser: type: object properties: diff --git a/public/openapi/write/chats.yaml b/public/openapi/write/chats.yaml index 37204a9b5c..5206482565 100644 --- a/public/openapi/write/chats.yaml +++ b/public/openapi/write/chats.yaml @@ -47,54 +47,7 @@ get: type: boolean description: Whether or not the chat has unread messages within teaser: - type: object - nullable: true - properties: - fromuid: - type: number - content: - type: string - timestamp: - type: number - timestampISO: - type: string - user: - type: object - properties: - uid: - type: number - description: A user identifier - username: - type: string - description: A friendly name for a given user account - displayname: - type: string - description: This is either username or fullname depending on forum and user settings - userslug: - type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces - removed, etc.) - picture: - nullable: true - type: string - status: - type: string - lastonline: - type: number - icon:text: - type: string - description: A single-letter representation of a username. This is used in the - auto-generated icon given to users - without an avatar - icon:bgColor: - type: string - description: A six-character hexadecimal colour code assigned to the user. This - value is used in conjunction with - `icon:text` for the user's - auto-generated icon - example: "#f44336" - lastonlineISO: - type: string + $ref: ../components/schemas/TeaserObject.yaml#/TeaserObject users: type: array items: From 0178e4fb7c630b712be35f0c0e30d723dadb8a0b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 12:21:22 -0500 Subject: [PATCH 4218/4744] docs: add teaser to postobject schema --- public/openapi/components/schemas/PostObject.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 1d8f2f5b67..54dcc248c2 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -139,6 +139,8 @@ PostObject: url: type: string description: Relative path to the topic thumbnail + teaser: + $ref: ../../components/schemas/TeaserObject.yaml#/TeaserObject required: - uid - tid From 91323dce5509d6473d0448a491256ee29118f1ce Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 12:29:05 -0500 Subject: [PATCH 4219/4744] docs: chat teasers are different --- .../components/schemas/TeaserObject.yaml | 38 +++--- .../read/user/userslug/chats/roomid.yaml | 108 +++++++++++++++++- public/openapi/write/chats.yaml | 49 +++++++- 3 files changed, 177 insertions(+), 18 deletions(-) diff --git a/public/openapi/components/schemas/TeaserObject.yaml b/public/openapi/components/schemas/TeaserObject.yaml index 93eeafa86e..2018685c1b 100644 --- a/public/openapi/components/schemas/TeaserObject.yaml +++ b/public/openapi/components/schemas/TeaserObject.yaml @@ -1,14 +1,24 @@ TeaserObject: type: object + nullable: true properties: - roomId: + pid: type: number - fromuid: - type: number - content: + url: type: string + uid: + type: number + description: A user identifier timestamp: type: number + tid: + type: number + description: A topic identifier + content: + type: string + sourceContent: + type: string + nullable: true timestampISO: type: string description: An ISO 8601 formatted date string (complementing `timestamp`) @@ -18,15 +28,13 @@ TeaserObject: uid: type: number description: A user identifier - isLocal: - type: boolean - description: Whether the user belongs to the local installation or not. username: type: string description: A friendly name for a given user account displayname: type: string - description: This is either username or fullname depending on forum and user settings + isLocal: + type: boolean userslug: type: string description: An URL-safe variant of the username (i.e. lower-cased, spaces @@ -34,10 +42,6 @@ TeaserObject: picture: nullable: true type: string - status: - type: string - lastonline: - type: number icon:text: type: string description: A single-letter representation of a username. This is used in the @@ -50,6 +54,10 @@ TeaserObject: `icon:text` for the user's auto-generated icon example: "#f44336" - lastonlineISO: - type: string - nullable: true + topic: + type: object + additionalProperties: {} + index: + type: number + required: + - pid \ No newline at end of file diff --git a/public/openapi/read/user/userslug/chats/roomid.yaml b/public/openapi/read/user/userslug/chats/roomid.yaml index 4468d9c0a1..448f350a42 100644 --- a/public/openapi/read/user/userslug/chats/roomid.yaml +++ b/public/openapi/read/user/userslug/chats/roomid.yaml @@ -264,7 +264,60 @@ get: unread: type: boolean teaser: - $ref: ../../../../components/schemas/TeaserObject.yaml#/TeaserObject + type: object + properties: + roomId: + type: number + fromuid: + type: number + content: + type: string + timestamp: + type: number + timestampISO: + type: string + description: An ISO 8601 formatted date string (complementing `timestamp`) + user: + type: object + properties: + uid: + type: number + description: A user identifier + isLocal: + type: boolean + description: Whether the user belongs to the local installation or not. + username: + type: string + description: A friendly name for a given user account + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces + removed, etc.) + picture: + nullable: true + type: string + status: + type: string + lastonline: + type: number + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users + without an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's + auto-generated icon + example: "#f44336" + lastonlineISO: + type: string + nullable: true lastUser: type: object properties: @@ -375,7 +428,58 @@ get: unread: type: boolean teaser: - $ref: ../../../../components/schemas/TeaserObject.yaml#/TeaserObject + type: object + properties: + fromuid: + type: number + content: + type: string + timestamp: + type: number + timestampISO: + type: string + description: An ISO 8601 formatted date string (complementing `timestamp`) + user: + type: object + properties: + uid: + type: number + description: A user identifier + isLocal: + type: boolean + description: Whether the user belongs to the local installation or not. + username: + type: string + description: A friendly name for a given user account + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces + removed, etc.) + picture: + nullable: true + type: string + status: + type: string + lastonline: + type: number + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users + without an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's + auto-generated icon + example: "#f44336" + lastonlineISO: + type: string + nullable: true lastUser: type: object properties: diff --git a/public/openapi/write/chats.yaml b/public/openapi/write/chats.yaml index 5206482565..37204a9b5c 100644 --- a/public/openapi/write/chats.yaml +++ b/public/openapi/write/chats.yaml @@ -47,7 +47,54 @@ get: type: boolean description: Whether or not the chat has unread messages within teaser: - $ref: ../components/schemas/TeaserObject.yaml#/TeaserObject + type: object + nullable: true + properties: + fromuid: + type: number + content: + type: string + timestamp: + type: number + timestampISO: + type: string + user: + type: object + properties: + uid: + type: number + description: A user identifier + username: + type: string + description: A friendly name for a given user account + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces + removed, etc.) + picture: + nullable: true + type: string + status: + type: string + lastonline: + type: number + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users + without an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's + auto-generated icon + example: "#f44336" + lastonlineISO: + type: string users: type: array items: From 9a15b571548996717fe205f94bca66aeb8bd499b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 12:41:53 -0500 Subject: [PATCH 4220/4744] docs: add bookmarks to postobject --- public/openapi/components/schemas/PostObject.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 54dcc248c2..306a13f665 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -176,6 +176,8 @@ PostObject: type: boolean replies: type: number + bookmarks: + type: number PostDataObject: description: The output as returned from `Posts.getPostsData` allOf: From 07f9eda9fb490320c42646802b38993a7bcf69a0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 12:41:53 -0500 Subject: [PATCH 4221/4744] docs: add bookmarks to postobject in /world --- public/openapi/components/schemas/PostObject.yaml | 2 -- public/openapi/read/world.yaml | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml index 306a13f665..54dcc248c2 100644 --- a/public/openapi/components/schemas/PostObject.yaml +++ b/public/openapi/components/schemas/PostObject.yaml @@ -176,8 +176,6 @@ PostObject: type: boolean replies: type: number - bookmarks: - type: number PostDataObject: description: The output as returned from `Posts.getPostsData` allOf: diff --git a/public/openapi/read/world.yaml b/public/openapi/read/world.yaml index d4bf00a658..745d04348a 100644 --- a/public/openapi/read/world.yaml +++ b/public/openapi/read/world.yaml @@ -25,7 +25,12 @@ get: items: type: string posts: - $ref: ../components/schemas/PostsObject.yaml#/PostsObject + allOf: + - $ref: ../components/schemas/PostsObject.yaml#/PostsObject + - type: object + properties: + bookmarks: + type: number showThumbs: type: boolean showTopicTools: From 4ef9d5fa55a345bb3dbd6dde6b0e60c454d82bdf Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 13:18:25 -0500 Subject: [PATCH 4222/4744] docs: add missing privileges prop to world schema --- public/openapi/read/world.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/public/openapi/read/world.yaml b/public/openapi/read/world.yaml index 745d04348a..450d49e362 100644 --- a/public/openapi/read/world.yaml +++ b/public/openapi/read/world.yaml @@ -48,6 +48,33 @@ get: hasFollowers: type: boolean nullable: true + privileges: + type: object + properties: + topics:create: + type: boolean + topics:read: + type: boolean + topics:schedule: + type: boolean + topics:tag: + type: boolean + read: + type: boolean + posts:view_deleted: + type: boolean + cid: + type: string + example: '-1' + uid: + type: number + description: A user identifier + editable: + type: boolean + view_deleted: + type: boolean + isAdminOrMod: + type: boolean title: type: string topicCount: From b1c097f84baf0f0e0b8f531f2088b0e1837ab87a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Feb 2026 15:20:34 -0500 Subject: [PATCH 4223/4744] test: update tests to allow title-less topics --- test/topics.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/test/topics.js b/test/topics.js index 3aad58afe6..f2f1b985f7 100644 --- a/test/topics.js +++ b/test/topics.js @@ -100,13 +100,6 @@ describe('Topic\'s', () => { }); }); - it('should fail to create new topic with empty title', (done) => { - topics.post({ uid: fooUid, title: '', content: topic.content, cid: topic.categoryId }, (err) => { - assert.ok(err); - done(); - }); - }); - it('should fail to create new topic with empty content', (done) => { topics.post({ uid: fooUid, title: topic.title, content: '', cid: topic.categoryId }, (err) => { assert.ok(err); @@ -236,6 +229,27 @@ describe('Topic\'s', () => { assert.strictEqual(replyResult.body.response.user.displayname, 'guest124'); meta.config.allowGuestHandles = oldValue; }); + + describe('without a title', () => { + before(function () { + this.payload = { + uid: topic.userId, + // title: '', + content: topic.content, + cid: topic.categoryId, + }; + }); + + it('should create a new topic', async function () { + this.result = await topics.post(this.payload); + assert(this.result); + }); + + it('should contain a generated title', function () { + assert.strictEqual(this.result.title, this.result.content); + assert(this.result.topicData.generatedTitle); + }); + }); }); describe('.reply', () => { From 45d2e628dddeaa60114a7c1c19410853bfe34606 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 17 Feb 2026 10:45:29 -0500 Subject: [PATCH 4224/4744] fix: only call syncUserInboxes on post create if local uid creates post in cid -1 --- src/topics/create.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/topics/create.js b/src/topics/create.js index ac360abe6d..f1f6fd570e 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -269,7 +269,9 @@ module.exports = function (Topics) { Topics.syncBacklinks(postData), Topics.markAsRead([tid], uid), ]); - activitypub.notes.syncUserInboxes(tid, uid); + if (utils.isNumber(postOwner) && postData.category.cid === -1) { + activitypub.notes.syncUserInboxes(tid, uid); + } // Returned data is a superset of post summary data postData.user = userInfo; From a4369b9321e39db046d9e78a5dae2c4064b7bad5 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 17 Feb 2026 16:26:40 +0000 Subject: [PATCH 4225/4744] chore(i18n): fallback strings for new resources: nodebb.topic --- public/language/ar/topic.json | 1 + public/language/az/topic.json | 1 + public/language/bg/topic.json | 7 ++++--- public/language/bn/topic.json | 1 + public/language/cs/topic.json | 1 + public/language/da/topic.json | 1 + public/language/de/topic.json | 1 + public/language/el/topic.json | 1 + public/language/en-US/topic.json | 1 + public/language/en-x-pirate/topic.json | 1 + public/language/es/topic.json | 1 + public/language/et/topic.json | 1 + public/language/fa-IR/topic.json | 1 + public/language/fi/topic.json | 1 + public/language/fr/topic.json | 1 + public/language/gl/topic.json | 1 + public/language/he/topic.json | 1 + public/language/hr/topic.json | 1 + public/language/hu/topic.json | 1 + public/language/hy/topic.json | 1 + public/language/id/topic.json | 1 + public/language/it/topic.json | 7 ++++--- public/language/ja/topic.json | 1 + public/language/ko/topic.json | 1 + public/language/lt/topic.json | 1 + public/language/lv/topic.json | 1 + public/language/ms/topic.json | 1 + public/language/nb/topic.json | 1 + public/language/nl/topic.json | 1 + public/language/nn-NO/topic.json | 1 + public/language/pl/topic.json | 1 + public/language/pt-BR/topic.json | 1 + public/language/pt-PT/topic.json | 1 + public/language/ro/topic.json | 1 + public/language/ru/topic.json | 1 + public/language/rw/topic.json | 1 + public/language/sc/topic.json | 1 + public/language/sk/topic.json | 1 + public/language/sl/topic.json | 1 + public/language/sq-AL/topic.json | 1 + public/language/sr/topic.json | 1 + public/language/sv/topic.json | 1 + public/language/th/topic.json | 1 + public/language/tr/topic.json | 1 + public/language/uk/topic.json | 1 + public/language/ur/topic.json | 1 + public/language/vi/topic.json | 1 + public/language/zh-CN/topic.json | 7 ++++--- public/language/zh-TW/topic.json | 1 + 49 files changed, 58 insertions(+), 9 deletions(-) diff --git a/public/language/ar/topic.json b/public/language/ar/topic.json index dff6581696..b69b8b4aec 100644 --- a/public/language/ar/topic.json +++ b/public/language/ar/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/az/topic.json b/public/language/az/topic.json index 67414c4c79..f39df08e21 100644 --- a/public/language/az/topic.json +++ b/public/language/az/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Bu mövzuda başqa yazılarınız yoxdur", "open-composer": "Geniş redaktoru aç", "post-quick-reply": "Sürətli cavab", + "post-quick-create": "Quick post", "navigator.index": "%1-dən %2 yazı", "navigator.unread": "% 1 oxunmamış", "upvote-post": "Yazıya müsbət səs ver", diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index f80093702c..23dfc2495d 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Нямате повече публикации в тази тема", "open-composer": "Отваряне на редактора", "post-quick-reply": "Бърз отговор", + "post-quick-create": "Quick post", "navigator.index": "Публикация %1 от %2", "navigator.unread": "%1 непрочетени", "upvote-post": "Положително гласуване за публикацията", @@ -234,7 +235,7 @@ "thumb-image": "Иконка на темата", "announcers": "Споделяния", "announcers-x": "Споделяния (%1)", - "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", - "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", - "guest-cta.closing": "With your input, this post could be even better 💗" + "guest-cta.title": "Здравейте! Изглежда, че този разговор Ви е интересен, но все още нямате регистрация.", + "guest-cta.message": "Омръзна ли Ви да трябва да превъртате едни и същи публикации при всяко разглеждане? Ако се регистрирате и си създадете акаунт, винаги ще се връщате точно там, където сте били последно, както и ще можете да изберете дали да получавате известия за новите отговори (по е-поща или на мобилно устройство). Също така ще можете да запазвате отметки и да гласувате за публикациите, изказвайки благодарност към другите членове на общността.", + "guest-cta.closing": "С Ваша помощ, тази публикация може да бъде дори още по-добра 💗" } \ No newline at end of file diff --git a/public/language/bn/topic.json b/public/language/bn/topic.json index 34f86c06f3..0ec97352bf 100644 --- a/public/language/bn/topic.json +++ b/public/language/bn/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index dd2e4da749..df4f0babd8 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "V tomto tématu nemáte další příspěvky", "open-composer": "Otevřít editor příspěvku", "post-quick-reply": "Rychlá odpověď", + "post-quick-create": "Quick post", "navigator.index": "Publikovat %1 of %2", "navigator.unread": "%1 nepřečteno", "upvote-post": "Dát příspěvku kladný hlas", diff --git a/public/language/da/topic.json b/public/language/da/topic.json index 577acf3ab6..6484acf8f0 100644 --- a/public/language/da/topic.json +++ b/public/language/da/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/de/topic.json b/public/language/de/topic.json index e82f135a39..c4cc890697 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Du hast keine weiteren Beiträge zu diesem Thema", "open-composer": "Composer öffnen", "post-quick-reply": "Schnell antworten", + "post-quick-create": "Quick post", "navigator.index": "Beitrag %1 von %2", "navigator.unread": "%1 ungelesen", "upvote-post": "Beitrag positiv bewerten", diff --git a/public/language/el/topic.json b/public/language/el/topic.json index 3b6d5abe15..0b4ec8b7b9 100644 --- a/public/language/el/topic.json +++ b/public/language/el/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/en-US/topic.json b/public/language/en-US/topic.json index 0e1bcdfa97..648200eb74 100644 --- a/public/language/en-US/topic.json +++ b/public/language/en-US/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/en-x-pirate/topic.json b/public/language/en-x-pirate/topic.json index 0e1bcdfa97..648200eb74 100644 --- a/public/language/en-x-pirate/topic.json +++ b/public/language/en-x-pirate/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/es/topic.json b/public/language/es/topic.json index 072e21e832..addda06590 100644 --- a/public/language/es/topic.json +++ b/public/language/es/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Abrir editor", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Votar positivo", diff --git a/public/language/et/topic.json b/public/language/et/topic.json index 3c5e455644..cb3b8377d4 100644 --- a/public/language/et/topic.json +++ b/public/language/et/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/fa-IR/topic.json b/public/language/fa-IR/topic.json index ce21e4587b..cba5ba57b3 100644 --- a/public/language/fa-IR/topic.json +++ b/public/language/fa-IR/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "شما پست بیشتری در این تاپیک ندارید", "open-composer": "Open composer", "post-quick-reply": "پاسخ سریع", + "post-quick-create": "Quick post", "navigator.index": "پست %1 از %2", "navigator.unread": "%1 خوانده نشده", "upvote-post": "رای مثبت دادن به پست", diff --git a/public/language/fi/topic.json b/public/language/fi/topic.json index 246f538d9f..01854c1383 100644 --- a/public/language/fi/topic.json +++ b/public/language/fi/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Pikavastaus", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 lukematonta", "upvote-post": "Upvote post", diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json index 067f0c50dd..3544e3b361 100644 --- a/public/language/fr/topic.json +++ b/public/language/fr/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Vous n'avez plus de messages dans ce sujet", "open-composer": "Ouvrir l'éditeur", "post-quick-reply": "Réponse rapide", + "post-quick-create": "Quick post", "navigator.index": "Message %1 sur %2", "navigator.unread": "%1 non lu", "upvote-post": "Vote positif", diff --git a/public/language/gl/topic.json b/public/language/gl/topic.json index 92cc657553..fb506285fb 100644 --- a/public/language/gl/topic.json +++ b/public/language/gl/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/he/topic.json b/public/language/he/topic.json index f285904fde..e2771916e1 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "אין לכם יותר פוסטים בנושא זה", "open-composer": "פתיחת העורך", "post-quick-reply": "תגובה מהירה", + "post-quick-create": "Quick post", "navigator.index": "פוסט %1 מתוך %2", "navigator.unread": "%1 לא נקראו", "upvote-post": "הצבעה לפוסט", diff --git a/public/language/hr/topic.json b/public/language/hr/topic.json index 5c0e05436e..3dccfbf9cb 100644 --- a/public/language/hr/topic.json +++ b/public/language/hr/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": " Brzi odgovor", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/hu/topic.json b/public/language/hu/topic.json index 0c77afc488..fbb3829cad 100644 --- a/public/language/hu/topic.json +++ b/public/language/hu/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Neked nincs több bejegyzésed ebben a témakörben", "open-composer": "Composer megnyitása", "post-quick-reply": "Gyors válasz", + "post-quick-create": "Quick post", "navigator.index": "Bejegyzés %1 / %2", "navigator.unread": "%1 olvasatlan", "upvote-post": "Bejegyzés kedvelése", diff --git a/public/language/hy/topic.json b/public/language/hy/topic.json index 1ef24aba46..e5b6186230 100644 --- a/public/language/hy/topic.json +++ b/public/language/hy/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Այս թեմայում այլ գրառումներ չունեք", "open-composer": "Open composer", "post-quick-reply": "Արագ պատասխան", + "post-quick-create": "Quick post", "navigator.index": "Գրառում %1 %2 - ից", "navigator.unread": "%1 չկարդացված", "upvote-post": "Upvote post", diff --git a/public/language/id/topic.json b/public/language/id/topic.json index 50c74a8bae..1c4020103a 100644 --- a/public/language/id/topic.json +++ b/public/language/id/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 0ea72b5f05..e77ffd1bfe 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Non hai più post in questa discussione", "open-composer": "Apri compositore", "post-quick-reply": "Risposta rapida", + "post-quick-create": "Quick post", "navigator.index": "Post %1 di %2", "navigator.unread": "%1 non letto", "upvote-post": "Vota positivamente il post", @@ -234,7 +235,7 @@ "thumb-image": "Immagine anteprima della discussione", "announcers": "Condivisioni", "announcers-x": "Condivisioni (%1)", - "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", - "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", - "guest-cta.closing": "With your input, this post could be even better 💗" + "guest-cta.title": "Ciao! Sembra che tu sia interessato a questa conversazione, ma non hai ancora un account.", + "guest-cta.message": "Stanco di dover scorrere gli stessi post a ogni visita? Quando registri un account, tornerai sempre esattamente dove eri rimasto e potrai scegliere di essere avvisato delle nuove risposte (tramite email o notifica push). Potrai anche salvare segnalibri e votare i post per mostrare il tuo apprezzamento agli altri membri della comunità.", + "guest-cta.closing": "Con il tuo contributo, questo post potrebbe essere ancora migliore 💗" } \ No newline at end of file diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json index fb8fdca9b1..1174660b11 100644 --- a/public/language/ja/topic.json +++ b/public/language/ja/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/ko/topic.json b/public/language/ko/topic.json index 223196a7ca..d39fa603d1 100644 --- a/public/language/ko/topic.json +++ b/public/language/ko/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "이 토픽에 더 이상 게시물이 없습니다", "open-composer": "Composer 열기", "post-quick-reply": "빠른 답글", + "post-quick-create": "Quick post", "navigator.index": "전체 %2개 중 %1번째 게시물", "navigator.unread": "%1개의 읽지 않은 게시물", "upvote-post": "찬성표", diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json index 948e320587..147a49a87f 100644 --- a/public/language/lt/topic.json +++ b/public/language/lt/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/lv/topic.json b/public/language/lv/topic.json index 72b136f2a5..bed7f11019 100644 --- a/public/language/lv/topic.json +++ b/public/language/lv/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/ms/topic.json b/public/language/ms/topic.json index af1c8e01c4..90a0497bd0 100644 --- a/public/language/ms/topic.json +++ b/public/language/ms/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json index c3177f2487..8365a10001 100644 --- a/public/language/nb/topic.json +++ b/public/language/nb/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Du har ikke flere innlegg i dette emnet", "open-composer": "Åpne editor", "post-quick-reply": "Svar", + "post-quick-create": "Quick post", "navigator.index": "Innlegg %1 av %2", "navigator.unread": "%1 ulest", "upvote-post": "Anbefal innlegg", diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index 7564fee664..75e7ba9326 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/nn-NO/topic.json b/public/language/nn-NO/topic.json index a35cbf5541..b9da7ac5bd 100644 --- a/public/language/nn-NO/topic.json +++ b/public/language/nn-NO/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Du har ikkje fleire innlegg i dette emnet", "open-composer": "Opne editor", "post-quick-reply": "Svar", + "post-quick-create": "Quick post", "navigator.index": "Innlegg %1 av %2", "navigator.unread": "%1 uleste", "upvote-post": "Anbefal innlegg", diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 7a6b762b73..4eb3081f62 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Nie masz więcej postów w tym temacie", "open-composer": "Otwórz okno pisania", "post-quick-reply": "Szybka odpowiedź", + "post-quick-create": "Quick post", "navigator.index": "Post %1 z %2", "navigator.unread": "%1 nieprzeczytanych", "upvote-post": "Podoba się", diff --git a/public/language/pt-BR/topic.json b/public/language/pt-BR/topic.json index 21244f2196..5ea0bb085a 100644 --- a/public/language/pt-BR/topic.json +++ b/public/language/pt-BR/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Você não tem mais posts nesse tópico", "open-composer": "Open composer", "post-quick-reply": "Resposta rápida", + "post-quick-create": "Quick post", "navigator.index": "Post %1 de %2", "navigator.unread": "%1 não lida", "upvote-post": "Upvote post", diff --git a/public/language/pt-PT/topic.json b/public/language/pt-PT/topic.json index dc8eb35a40..feaba1bd16 100644 --- a/public/language/pt-PT/topic.json +++ b/public/language/pt-PT/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json index badbc2b499..8a194de409 100644 --- a/public/language/ro/topic.json +++ b/public/language/ro/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Deschide composer-ul", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Votează pentru postare", diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index f42021f6c4..8020fa99fd 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Больше нет записей в этой теме", "open-composer": "Open composer", "post-quick-reply": "Быстрый ответ", + "post-quick-create": "Quick post", "navigator.index": "Сообщений %1 от %2", "navigator.unread": "%1 непрочитано", "upvote-post": "Upvote post", diff --git a/public/language/rw/topic.json b/public/language/rw/topic.json index b640897aca..e1a0e0b5c4 100644 --- a/public/language/rw/topic.json +++ b/public/language/rw/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/sc/topic.json b/public/language/sc/topic.json index 28223106ff..78834112ac 100644 --- a/public/language/sc/topic.json +++ b/public/language/sc/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json index da85522a3b..3426d3e0b2 100644 --- a/public/language/sk/topic.json +++ b/public/language/sk/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/sl/topic.json b/public/language/sl/topic.json index 4f6795b5b1..37ad9995d7 100644 --- a/public/language/sl/topic.json +++ b/public/language/sl/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/sq-AL/topic.json b/public/language/sq-AL/topic.json index 60cadd85f7..ef140e7b83 100644 --- a/public/language/sq-AL/topic.json +++ b/public/language/sq-AL/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Nuk keni postime të tjera në këtë temë", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json index 9d511aa929..6141956457 100644 --- a/public/language/sr/topic.json +++ b/public/language/sr/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Немате више порука у овој теми", "open-composer": "Open composer", "post-quick-reply": "Брзи одговор", + "post-quick-create": "Quick post", "navigator.index": "Објава %1 од %2", "navigator.unread": "%1 непрочитане", "upvote-post": "Upvote post", diff --git a/public/language/sv/topic.json b/public/language/sv/topic.json index d2361ead1e..2a349f13e2 100644 --- a/public/language/sv/topic.json +++ b/public/language/sv/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Inlägg %1 av %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/th/topic.json b/public/language/th/topic.json index 818384ecf6..dc0941dc58 100644 --- a/public/language/th/topic.json +++ b/public/language/th/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "คุณได้มีโพสต์อีกในกระทู้นี้", "open-composer": "เปิดคอมโพสเซอร์", "post-quick-reply": "ตอบกลับอย่างรวดเร็ว", + "post-quick-create": "Quick post", "navigator.index": "โพสต์ %1 จากทั้งหมด %2", "navigator.unread": "ยังไม่ได้อ่าน %1", "upvote-post": "โหวดขึ้นโพสต์นี้", diff --git a/public/language/tr/topic.json b/public/language/tr/topic.json index 9e4c6e8dd1..265df0f67a 100644 --- a/public/language/tr/topic.json +++ b/public/language/tr/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Bu başlıkta başka bir iletiniz bulunmamaktadır.", "open-composer": "Yazı alanını aç", "post-quick-reply": "Hızlı Yanıt Gönder", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 Okunmamış", "upvote-post": "İletiye artı oy ver", diff --git a/public/language/uk/topic.json b/public/language/uk/topic.json index 7c2ccc6659..f578f6585b 100644 --- a/public/language/uk/topic.json +++ b/public/language/uk/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "You don't have more posts in this topic", "open-composer": "Open composer", "post-quick-reply": "Quick reply", + "post-quick-create": "Quick post", "navigator.index": "Post %1 of %2", "navigator.unread": "%1 unread", "upvote-post": "Upvote post", diff --git a/public/language/ur/topic.json b/public/language/ur/topic.json index 4ef539fdc1..1e207d9b24 100644 --- a/public/language/ur/topic.json +++ b/public/language/ur/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "اس موضوع میں آپ کی مزید پوسٹس نہیں ہیں", "open-composer": "ایڈیٹر کھولیں", "post-quick-reply": "فوری جواب", + "post-quick-create": "Quick post", "navigator.index": "پوسٹ %1 از %2", "navigator.unread": "%1 غیر پڑھے ہوئے", "upvote-post": "پوسٹ کے لیے مثبت ووٹ", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 1e1a969616..6aba87f482 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "Bạn không có bài viết nào khác trong chủ đề này", "open-composer": "Mở composer", "post-quick-reply": "Trả lời nhanh", + "post-quick-create": "Quick post", "navigator.index": "Bài đăng %1 trên %2", "navigator.unread": "%1 chưa đọc", "upvote-post": "Ủng hộ bài đăng", diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json index d307f3f906..c753f1fba7 100644 --- a/public/language/zh-CN/topic.json +++ b/public/language/zh-CN/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "您在此主题中没有更多的帖子了", "open-composer": "打开编辑器", "post-quick-reply": "快速回复", + "post-quick-create": "Quick post", "navigator.index": "%1 / %2", "navigator.unread": "未读 %1", "upvote-post": "顶贴", @@ -234,7 +235,7 @@ "thumb-image": "主题缩略图", "announcers": "分享", "announcers-x": "分享 (%1)", - "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", - "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", - "guest-cta.closing": "With your input, this post could be even better 💗" + "guest-cta.title": "你好!看起来您对这段对话很感兴趣,但您还没有一个账号。", + "guest-cta.message": "厌倦了每次访问都刷到同样的帖子?您注册账号后,您每次返回时都能精准定位到您上次浏览的位置,并可选择接收新回复通知(通过邮件或推送通知)。您还能收藏书签、为帖子顶,向社区成员表达您的欣赏。", + "guest-cta.closing": "有了你的建议,这篇帖子会更精彩哦 💗" } \ No newline at end of file diff --git a/public/language/zh-TW/topic.json b/public/language/zh-TW/topic.json index aaa0ef9d53..d3993b079d 100644 --- a/public/language/zh-TW/topic.json +++ b/public/language/zh-TW/topic.json @@ -225,6 +225,7 @@ "no-more-next-post": "您在這話題下已沒更多貼文了", "open-composer": "開啟編輯器", "post-quick-reply": "快速回覆", + "post-quick-create": "Quick post", "navigator.index": "貼文 %1 / %2", "navigator.unread": "%1 未讀", "upvote-post": "點讚貼文", From e76f8a6024757be0c1bed292ef2a66675bd5729c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 17 Feb 2026 11:54:37 -0500 Subject: [PATCH 4226/4744] fix: bump harmony for world page changes --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index cbef4a9111..88ac51ca5b 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.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.14", + "nodebb-theme-harmony": "2.2.15", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", "nodebb-theme-persona": "14.2.8", From a68311def92723fe0b4c2b33a3c9b7784d7eb421 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 17 Feb 2026 12:01:52 -0500 Subject: [PATCH 4227/4744] fix: bump persona --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 88ac51ca5b..e4fcfcdb62 100644 --- a/install/package.json +++ b/install/package.json @@ -110,7 +110,7 @@ "nodebb-theme-harmony": "2.2.15", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", - "nodebb-theme-persona": "14.2.8", + "nodebb-theme-persona": "14.2.9", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", "nprogress": "0.2.0", From ce9bd0bb680590cbfefc770d6d638dc17d155a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 12:32:50 -0500 Subject: [PATCH 4228/4744] refactor: use opendir instead of loading all files --- src/upgrades/3.8.0/user-upload-folders.js | 31 ++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/upgrades/3.8.0/user-upload-folders.js b/src/upgrades/3.8.0/user-upload-folders.js index fbacbc64c4..dbca210322 100644 --- a/src/upgrades/3.8.0/user-upload-folders.js +++ b/src/upgrades/3.8.0/user-upload-folders.js @@ -20,13 +20,28 @@ module.exports = { await mkdirp(folder); const userPicRegex = /^\d+-profile/; - const files = (await fs.promises.readdir(folder, { withFileTypes: true })) - .filter(item => !item.isDirectory() && String(item.name).match(userPicRegex)) - .map(item => item.name); + const dir = await fs.promises.opendir(folder); - progress.total = files.length; - await batch.processArray(files, async (files) => { - progress.incr(files.length); + let batchBuffer = []; + const BATCH_SIZE = 500; + + for await (const entry of dir) { + if (!entry.isDirectory() && userPicRegex.test(entry.name)) { + batchBuffer.push(entry.name); + } + if (batchBuffer.length >= BATCH_SIZE) { + await processBatch(batchBuffer); + progress.incr(batchBuffer.length); + batchBuffer = []; + } + } + if (batchBuffer.length > 0) { + await processBatch(batchBuffer); + progress.incr(batchBuffer.length); + batchBuffer = []; + } + + async function processBatch(files) { await Promise.all(files.map(async (file) => { const uid = file.split('-')[0]; if (parseInt(uid, 10) > 0) { @@ -37,9 +52,7 @@ module.exports = { ); } })); - }, { - batch: 500, - }); + } await batch.processSortedSet('users:joindate', async (uids) => { progress.incr(uids.length); From 652629df694967e5ce6634ed5777ff5928aee156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 12:45:19 -0500 Subject: [PATCH 4229/4744] lint: remove useless assignment --- src/upgrades/3.8.0/user-upload-folders.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/upgrades/3.8.0/user-upload-folders.js b/src/upgrades/3.8.0/user-upload-folders.js index dbca210322..3f24cedd49 100644 --- a/src/upgrades/3.8.0/user-upload-folders.js +++ b/src/upgrades/3.8.0/user-upload-folders.js @@ -38,7 +38,6 @@ module.exports = { if (batchBuffer.length > 0) { await processBatch(batchBuffer); progress.incr(batchBuffer.length); - batchBuffer = []; } async function processBatch(files) { From ed8cbd6ec3db559645e7f0d0648ddf7904280a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 13:01:35 -0500 Subject: [PATCH 4230/4744] refactor: don't create giant array, process in batches of 500 --- src/upgrades/3.8.3/topic-event-ids.js | 29 ++++++++++++++++++--------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/upgrades/3.8.3/topic-event-ids.js b/src/upgrades/3.8.3/topic-event-ids.js index 88368d5de6..ea91c7eca9 100644 --- a/src/upgrades/3.8.3/topic-event-ids.js +++ b/src/upgrades/3.8.3/topic-event-ids.js @@ -2,7 +2,6 @@ 'use strict'; const db = require('../../database'); -const batch = require('../../batch'); module.exports = { name: 'Add id field to all topic events', @@ -12,11 +11,11 @@ module.exports = { let nextId = await db.getObjectField('global', 'nextTopicEventId'); nextId = parseInt(nextId, 10) || 0; + progress.total = Math.max(0, nextId - 1); const ids = []; - for (let i = 1; i < nextId; i++) { - ids.push(i); - } - await batch.processArray(ids, async (eids) => { + const BATCH_SIZE = 500; + + async function processBatch(eids) { const eventData = await db.getObjects(eids.map(eid => `topicEvent:${eid}`)); const bulkSet = []; eventData.forEach((event, idx) => { @@ -28,10 +27,20 @@ module.exports = { } }); await db.setObjectBulk(bulkSet); - progress.incr(eids.length); - }, { - batch: 500, - progress, - }); + } + + for (let i = 1; i < nextId; i++) { + ids.push(i); + if (ids.length >= BATCH_SIZE) { + await processBatch(ids); + progress.incr(ids.length); + ids.length = 0; + } + } + + if (ids.length > 0) { + await processBatch(ids); + progress.incr(ids.length); + } }, }; From f5c31d204c21382c4f0c4b4d599468894d1b79e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 18:02:12 -0500 Subject: [PATCH 4231/4744] not used anymore --- public/src/modules/helpers.common.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 6a09a32869..3b3110746a 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -20,7 +20,6 @@ module.exports = function (utils, Benchpress, relative_path) { membershipBtn, spawnPrivilegeStates, localeToHTML, - renderTopicImage, renderDigestAvatar, userAgentIcons, buildAvatar, @@ -222,13 +221,6 @@ module.exports = function (utils, Benchpress, relative_path) { return locale.replace('_', '-'); } - function renderTopicImage(topicObj) { - if (topicObj.thumb) { - return ''; - } - return ''; - } - function renderDigestAvatar(block) { if (block.teaser) { if (block.teaser.user.picture) { From 59f35e6fdb6af4368a034d17f3c82bc272d9343e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 18:29:59 -0500 Subject: [PATCH 4232/4744] test: remove old test --- test/template-helpers.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/template-helpers.js b/test/template-helpers.js index ea4d142354..bab00021bd 100644 --- a/test/template-helpers.js +++ b/test/template-helpers.js @@ -229,20 +229,6 @@ describe('helpers', () => { done(); }); - it('should render thumb as topic image', (done) => { - const topicObj = { thumb: '/uploads/1.png', user: { username: 'baris', displayname: 'Baris Soner Usakli' } }; - const html = helpers.renderTopicImage(topicObj); - assert.equal(html, ``); - done(); - }); - - it('should render user picture as topic image', (done) => { - const topicObj = { thumb: '', user: { uid: 1, username: 'baris', displayname: 'Baris Soner Usakli', picture: '/uploads/2.png' } }; - const html = helpers.renderTopicImage(topicObj); - assert.equal(html, ``); - done(); - }); - it('should render digest avatar', (done) => { const block = { teaser: { user: { username: 'baris', picture: '/uploads/1.png' } } }; const html = helpers.renderDigestAvatar(block); From 2015777fd1f872deaac8030dc8fec2f7af398b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 19:07:28 -0500 Subject: [PATCH 4233/4744] fix: when registering through an invite, prepopulate the email field on /register/complete with the email --- src/controllers/authentication.js | 10 +++++++--- src/controllers/index.js | 1 + src/user/interstitials.js | 2 ++ src/user/invite.js | 11 +++++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index f948e053b5..3c5b337801 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -32,12 +32,12 @@ async function registerAndLoginUser(req, res, userData) { if (deferRegistration) { userData.register = true; req.session.registration = userData; - + const next = `${nconf.get('relative_path')}/register/complete`; if (req.body?.noscript === 'true') { - res.redirect(`${nconf.get('relative_path')}/register/complete`); + res.redirect(next); return; } - res.json({ next: `${nconf.get('relative_path')}/register/complete` }); + res.json({ next }); return; } @@ -71,6 +71,7 @@ async function registerAndLoginUser(req, res, userData) { return complete; } +// POST /register authenticationController.register = async function (req, res) { const registrationType = meta.config.registrationType || 'normal'; @@ -109,6 +110,7 @@ authenticationController.register = async function (req, res) { } }; +// POST /register/complete authenticationController.registerComplete = async function (req, res) { try { // For the interstitials that respond, execute the callback with the form body @@ -185,6 +187,7 @@ authenticationController.registerComplete = async function (req, res) { } }; +// POST /register/abort authenticationController.registerAbort = async (req, res) => { if (req.uid && req.session.registration) { // Email is the only cancelable interstitial @@ -204,6 +207,7 @@ authenticationController.registerAbort = async (req, res) => { }); }; +// POST /login authenticationController.login = async (req, res, next) => { let { strategy } = await plugins.hooks.fire('filter:login.override', { req, strategy: 'local' }); if (!passport._strategy(strategy)) { diff --git a/src/controllers/index.js b/src/controllers/index.js index 879774c17f..0336db8d1d 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -192,6 +192,7 @@ Controllers.register = async function (req, res, next) { } }; +// GET /register/complete Controllers.registerInterstitial = async function (req, res, next) { if (!req.session.hasOwnProperty('registration')) { return res.redirect(`${nconf.get('relative_path')}/register`); diff --git a/src/user/interstitials.js b/src/user/interstitials.js index 672af70d80..1558899497 100644 --- a/src/user/interstitials.js +++ b/src/user/interstitials.js @@ -36,6 +36,8 @@ Interstitials.email = async (data) => { let email; if (data.userData.uid) { email = await user.getUserField(data.userData.uid, 'email'); + } else if (data.userData.token) { + email = await user.getEmailFromToken(data.userData.token); } data.interstitials.push({ diff --git a/src/user/invite.js b/src/user/invite.js index 344e09fd6b..fd308af56c 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -78,6 +78,11 @@ module.exports = function (User) { return email && email === enteredEmail; }; + User.getEmailFromToken = async function (token) { + if (!token) return null; + return await db.getObjectField(`invitation:token:${token}`, 'email'); + }; + User.confirmIfInviteEmailIsUsed = async function (token, enteredEmail, uid) { if (!enteredEmail) { return; @@ -100,11 +105,9 @@ module.exports = function (User) { return; } - if (!groupsToJoin || groupsToJoin.length < 1) { - return; + if (Array.isArray(groupsToJoin) && groupsToJoin.length) { + await groups.join(groupsToJoin, uid); } - - await groups.join(groupsToJoin, uid); }; User.deleteInvitation = async function (invitedBy, email) { From 1dae3d222f342d791a4de89ef1a541bcf58a3972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 19:58:10 -0500 Subject: [PATCH 4234/4744] feat: add invitedBy to user info page, closes #13972, closes #13997 --- install/package.json | 4 ++-- public/language/en-GB/user.json | 1 + public/openapi/read/user/userslug/info.yaml | 17 +++++++++++++++ public/src/client/account/info.js | 24 +++++++++++---------- src/controllers/accounts/info.js | 13 ++++++++++- src/controllers/authentication.js | 1 + src/user/invite.js | 10 +++++++++ 7 files changed, 56 insertions(+), 14 deletions(-) diff --git a/install/package.json b/install/package.json index e4fcfcdb62..0280368610 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.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.15", + "nodebb-theme-harmony": "2.2.16", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", - "nodebb-theme-persona": "14.2.9", + "nodebb-theme-persona": "14.2.10", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", "nprogress": "0.2.0", diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 9186c80b74..c3e54eef2a 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -194,6 +194,7 @@ "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/openapi/read/user/userslug/info.yaml b/public/openapi/read/user/userslug/info.yaml index 66c3ba0730..2e1f2fbd67 100644 --- a/public/openapi/read/user/userslug/info.yaml +++ b/public/openapi/read/user/userslug/info.yaml @@ -20,6 +20,23 @@ get: - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull - type: object properties: + invitedBy: + type: object + nullable: true + properties: + username: + type: string + userslug: + type: string + picture: + type: string + uid: + type: number + icon:text: + type: string + icon:bgColor: + type: string + history: type: object properties: diff --git a/public/src/client/account/info.js b/public/src/client/account/info.js index f044860cd6..5dfc698769 100644 --- a/public/src/client/account/info.js +++ b/public/src/client/account/info.js @@ -11,6 +11,14 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s }; function handleModerationNote() { + const noteList = $('[component="account/moderation-note/list"]'); + + function adjustTextareaHeight(textarea) { + textarea.css({ + height: textarea.prop('scrollHeight') + 'px', + }); + } + $('[component="account/save-moderation-note"]').on('click', function () { const noteEl = $('[component="account/moderation-note"]'); const note = noteEl.val(); @@ -24,23 +32,24 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s noteEl.val(''); app.parseAndTranslate('account/info', 'moderationNotes', { moderationNotes: notes }, function (html) { - $('[component="account/moderation-note/list"]').prepend(html); + noteList.prepend(html); html.find('.timeago').timeago(); }); }); }); - $('[component="account/moderation-note/edit"]').on('click', function () { + noteList.on('click', '[component="account/moderation-note/edit"]', function () { const parent = $(this).parents('[data-id]'); const contentArea = parent.find('[component="account/moderation-note/content-area"]'); const editArea = parent.find('[component="account/moderation-note/edit-area"]'); contentArea.addClass('hidden'); editArea.removeClass('hidden'); + adjustTextareaHeight(editArea.find('textarea')); editArea.find('textarea').trigger('focus').putCursorAtEnd(); }); - $('[component="account/moderation-note/save-edit"]').on('click', function () { + noteList.on('click', '[component="account/moderation-note/save-edit"]', function () { const parent = $(this).parents('[data-id]'); const contentArea = parent.find('[component="account/moderation-note/content-area"]'); const editArea = parent.find('[component="account/moderation-note/edit-area"]'); @@ -63,20 +72,13 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s }); }); - $('[component="account/moderation-note/cancel-edit"]').on('click', function () { + noteList.on('click', '[component="account/moderation-note/cancel-edit"]', function () { const parent = $(this).parents('[data-id]'); const contentArea = parent.find('[component="account/moderation-note/content-area"]'); const editArea = parent.find('[component="account/moderation-note/edit-area"]'); contentArea.removeClass('hidden'); editArea.addClass('hidden'); }); - - $('[component="account/moderation-note/edit-area"] textarea').each((i, el) => { - const $el = $(el); - $el.css({ - height: $el.prop('scrollHeight') + 'px', - }).parent().addClass('hidden'); - }); } return Info; diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js index 7081acc7df..002010bfd1 100644 --- a/src/controllers/accounts/info.js +++ b/src/controllers/accounts/info.js @@ -15,12 +15,13 @@ infoController.get = async function (req, res) { const payload = res.locals.userData; const { username, userslug } = payload; - const [isPrivileged, history, sessions, usernames, emails] = await Promise.all([ + const [isPrivileged, history, sessions, usernames, emails, invitedBy] = await Promise.all([ user.isPrivileged(req.uid), user.getModerationHistory(res.locals.uid), user.auth.getSessions(res.locals.uid, req.sessionID), user.getHistory(`user:${res.locals.uid}:usernames`), user.getHistory(`user:${res.locals.uid}:emails`), + getInvitedBy(res.locals.uid), ]); const notes = await getNotes({ uid: res.locals.uid, isPrivileged }, start, stop); @@ -29,6 +30,7 @@ infoController.get = async function (req, res) { payload.sessions = sessions; payload.usernames = usernames; payload.emails = emails; + payload.invitedBy = invitedBy; if (isPrivileged) { payload.moderationNotes = notes.notes; @@ -51,3 +53,12 @@ async function getNotes({ uid, isPrivileged }, start, stop) { ]); return { notes: notes, count: count }; } + +async function getInvitedBy(uid) { + const invitedBy = await user.getUserField(uid, 'invitedBy'); + if (!invitedBy) { + return null; + } + const inviterData = await user.getUserFields(invitedBy, ['uid', 'username', 'userslug', 'picture']); + return inviterData.userslug ? inviterData : null; +}; diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 3c5b337801..63c551c5af 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -59,6 +59,7 @@ async function registerAndLoginUser(req, res, userData) { await Promise.all([ user.confirmIfInviteEmailIsUsed(userData.token, userData.email, uid), user.joinGroupsFromInvitation(uid, userData.token), + user.setInviterUid(uid, userData.token), ]); } await user.deleteInvitationKey(userData.email, userData.token); diff --git a/src/user/invite.js b/src/user/invite.js index fd308af56c..9699a2a6fe 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -83,6 +83,16 @@ module.exports = function (User) { return await db.getObjectField(`invitation:token:${token}`, 'email'); }; + User.setInviterUid = async function (uid, token) { + if (!token) { + return; + } + const inviterUid = await db.getObjectField(`invitation:token:${token}`, 'inviter'); + if (inviterUid) { + await User.setUserField(uid, 'invitedBy', inviterUid); + } + }; + User.confirmIfInviteEmailIsUsed = async function (token, enteredEmail, uid) { if (!enteredEmail) { return; From 3dfd9a43a4d730ccaa80bd18bf6d3d8467526fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 19:58:22 -0500 Subject: [PATCH 4235/4744] chore: white space --- public/openapi/read/user/userslug/info.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/public/openapi/read/user/userslug/info.yaml b/public/openapi/read/user/userslug/info.yaml index 2e1f2fbd67..d2b510b0d7 100644 --- a/public/openapi/read/user/userslug/info.yaml +++ b/public/openapi/read/user/userslug/info.yaml @@ -36,7 +36,6 @@ get: type: string icon:bgColor: type: string - history: type: object properties: From 42362ccf658093205294c359c8387b58c94700ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 22:38:57 -0500 Subject: [PATCH 4236/4744] fix: closes #13999, delay cache creation --- src/notifications.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index 12ed99f7d5..45696988ff 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -21,16 +21,21 @@ const ttlCache = require('./cache/ttl'); const Notifications = module.exports; -// ttlcache for email-only chat notifications -const notificationCache = ttlCache({ - name: 'notification-email-cache', - max: 1000, - ttl: (meta.config.notificationSendDelay || 60) * 1000, - noDisposeOnSet: true, - dispose: sendEmail, -}); - -Notifications.delayCache = notificationCache; +// used to delay email notifications, +// and cancel them if the notification is already read +let notificationCache = null; +function getOrCreateCache() { + if (!notificationCache) { + notificationCache = ttlCache({ + name: 'notification-email-cache', + max: 1000, + ttl: (meta.config.notificationSendDelay || 60) * 1000, + noDisposeOnSet: true, + dispose: sendEmail, + }); + } + return notificationCache; +} Notifications.baseTypes = [ 'notificationType_upvote', @@ -276,19 +281,20 @@ async function pushToUids(uids, notification) { if (results.uidsToEmail.length) { const delayNotificationTypes = ['new-chat', 'new-group-chat', 'new-public-chat']; + const delayCache = getOrCreateCache(); if (delayNotificationTypes.includes(notification.type)) { const cacheKey = `${notification.mergeId}|${results.uidsToEmail.join(',')}`; - const payload = notificationCache.get(cacheKey); + const payload = delayCache.get(cacheKey); let { bodyLong } = notification; if (payload !== undefined) { bodyLong = [payload.notification.bodyLong, bodyLong].join('\n'); } - notificationCache.set(cacheKey, { uids: results.uidsToEmail, notification: { ...notification, bodyLong } }); + delayCache.set(cacheKey, { uids: results.uidsToEmail, notification: { ...notification, bodyLong } }); if (notification.bodyLong.length >= 1000) { - notificationCache.delete(cacheKey); + delayCache.delete(cacheKey); } } else { - notificationCache.set(`delayed:nid:${notification.nid}`, { + delayCache.set(`delayed:nid:${notification.nid}`, { uids: results.uidsToEmail, notification, }); From d0cc1c95cc2918ab437a53fb90b4d270297901ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 22:57:49 -0500 Subject: [PATCH 4237/4744] chore: up composer --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 0280368610..a1b6b9a89e 100644 --- a/install/package.json +++ b/install/package.json @@ -97,7 +97,7 @@ "multer": "2.0.2", "nconf": "0.13.0", "nodebb-plugin-2factor": "7.6.1", - "nodebb-plugin-composer-default": "10.3.18", + "nodebb-plugin-composer-default": "10.3.19", "nodebb-plugin-dbsearch": "6.3.5", "nodebb-plugin-emoji": "6.0.5", "nodebb-plugin-emoji-android": "4.1.1", From 9fbdc792021d42dac0e66c1cb5f3bc226b12afb8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Feb 2026 10:26:43 -0500 Subject: [PATCH 4238/4744] fix: don't publish name on generated titles --- src/activitypub/mocks.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 5d423e5625..7189e59287 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -598,19 +598,19 @@ Mocks.notes.public = async (post) => { let tag = null; let followersUrl; - let name; - ({ titleRaw: name } = await topics.getTopicFields(post.tid, ['title'])); + let { titleRaw: name, generatedTitle } = await topics.getTopicFields(post.tid, ['title', 'generatedTitle']); + if (generatedTitle) { + name = null; + } if (post.toPid) { // direct reply inReplyTo = utils.isNumber(post.toPid) ? `${nconf.get('url')}/post/${post.toPid}` : post.toPid; - name = `Re: ${name}`; const parentId = await posts.getPostField(post.toPid, 'uid'); followersUrl = await user.getUserField(parentId, 'followersUrl'); to.add(utils.isNumber(parentId) ? `${nconf.get('url')}/uid/${parentId}` : parentId); } else if (!post.isMainPost) { // reply to OP inReplyTo = utils.isNumber(post.topic.mainPid) ? `${nconf.get('url')}/post/${post.topic.mainPid}` : post.topic.mainPid; - name = `Re: ${name}`; to.add(utils.isNumber(post.topic.uid) ? `${nconf.get('url')}/uid/${post.topic.uid}` : post.topic.uid); followersUrl = await user.getUserField(post.topic.uid, 'followersUrl'); @@ -798,15 +798,15 @@ Mocks.notes.public = async (post) => { type: isArticle ? 'Article' : 'Note', to: Array.from(to), cc: Array.from(cc), - inReplyTo, + ...(inReplyTo && { inReplyTo }), + ...(name && { name }), published, - updated, + ...(updated && { updated }), url: id, attributedTo: `${nconf.get('url')}/uid/${post.user.uid}`, context, audience, - summary, - name, + ...(summary && { summary }), preview, content: post.content, source, From 80f6102224621a5dfed662292fa4b5e58fe6f315 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Feb 2026 11:14:16 -0500 Subject: [PATCH 4239/4744] fix: #14001, regression from adjusted acceptable types list --- src/activitypub/helpers.js | 11 +++++++++-- src/middleware/activitypub.js | 7 ++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 9a6577af41..558a4ee8d7 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -66,8 +66,15 @@ Helpers.isUri = (value) => { }; Helpers.assertAccept = (accept) => { - if (!accept) return false; - const normalized = accept.split(',').map(s => s.trim().replace(/\s*;\s*/g, ';')).join(','); + if (!accept) { + return false; + } + + const normalized = accept + .split(',') + .map(s => s.trim().replace(/\s*;\s*/g, ';')) // spec allows spaces around semi-colon + .join(','); + return activitypub._constants.acceptableTypes.some(type => normalized.includes(type)); }; diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index 2805801561..9ea81c0035 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -19,11 +19,16 @@ middleware.pageview = async (req, res, next) => { middleware.assertS2S = async function (req, res, next) { // For whatever reason, express accepts does not recognize "profile" as a valid differentiator // Therefore, manual header parsing is used here. - const { accept, 'content-type': contentType } = req.headers; + let { accept, 'content-type': contentType } = req.headers; if (!(accept || contentType)) { return next('route'); } + // Normalize content-type + if (contentType) { + contentType = contentType.trim().replace(/\s*;\s*/g, ';'); // spec allows spaces around semi-colon + } + const pass = activitypub.helpers.assertAccept(accept) || (contentType && activitypub._constants.acceptableTypes.includes(contentType)); From 2f88f7766cafb8f6a584395c3d92e171c73e75b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 11:49:18 -0500 Subject: [PATCH 4240/4744] fix: keep chat input in view after adding new messages --- public/src/client/chats/messages.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index 062e19ef2f..40c46960c3 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -100,7 +100,6 @@ define('forum/chats/messages', [ newMessage.appendTo(chatContentEl); messages.onMessagesAddedToDom(newMessage); if (isAtBottom || msgData.self) { - messages.scrollToBottomAfterImageLoad(chatContentEl); // remove some message elements if there are too many const chatMsgEls = chatContentEl.find('[data-mid]'); if (chatMsgEls.length > 150) { @@ -108,6 +107,12 @@ define('forum/chats/messages', [ chatMsgEls.slice(0, removeCount).remove(); chatContentEl.find('[data-mid].new').removeClass('new'); } + + messages.scrollToBottomAfterImageLoad(chatContentEl); + const $composer = chatContentEl.siblings('[component="chat/composer"]'); + if ($composer.length) { + $composer[0].scrollIntoView(true); + } } hooks.fire('action:chat.received', { From 5e7f8635af34a1cbb3ffa99c374741608146f218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 11:54:16 -0500 Subject: [PATCH 4241/4744] add interactive-widget=resizes-content --- src/meta/tags.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/meta/tags.js b/src/meta/tags.js index 5afac6236d..cc3745d249 100644 --- a/src/meta/tags.js +++ b/src/meta/tags.js @@ -19,7 +19,8 @@ Tags.parse = async (req, data, meta, link) => { // Meta tags const defaultTags = isAPI ? [] : [{ name: 'viewport', - content: 'width=device-width, initial-scale=1.0', + // https://stackoverflow.com/a/77815388 for resizes-content + content: 'width=device-width, initial-scale=1.0, interactive-widget=resizes-content', }, { name: 'content-type', content: 'text/html; charset=UTF-8', From b061d078047c9492ead3da7a27a4fa7eb8bb9dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 11:54:52 -0500 Subject: [PATCH 4242/4744] chore: update harmony --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index a1b6b9a89e..dc2cfb6e54 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.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.16", + "nodebb-theme-harmony": "2.2.17", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", "nodebb-theme-persona": "14.2.10", From bcf0b39446f81ee578721a41c0d3d39f4d376008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 12:40:29 -0500 Subject: [PATCH 4243/4744] dont create tooltips on mobile --- public/src/client/chats.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index a4a4ed7288..0f2ef05082 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -628,7 +628,12 @@ define('forum/chats', [ ajaxify.data = { ...ajaxify.data, ...payload, roomId: roomId }; ajaxify.updateTitle(ajaxify.data.title); $('body').toggleClass('chat-loaded', !!roomId); - mainWrapper.find('[data-bs-toggle="tooltip"]').tooltip({ trigger: 'hover', container: '#content' }); + if (!utils.isMobile()) { + mainWrapper.find('[data-bs-toggle="tooltip"]').tooltip({ + trigger: 'hover', + container: '#content', + }); + } Chats.setActive(roomId); Chats.addEventListeners(); hooks.fire('action:chat.loaded', $('.chats-full')); From 053ce073db423d49d1bc7b2d86c273a0504793f8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Feb 2026 13:12:06 -0500 Subject: [PATCH 4244/4744] fix: fallbacks and latest translations for nodebb.user --- public/language/ar/user.json | 1 + public/language/az/user.json | 1 + public/language/bg/user.json | 11 ++++++----- public/language/bn/user.json | 1 + public/language/cs/user.json | 1 + public/language/da/user.json | 1 + public/language/de/user.json | 1 + public/language/el/user.json | 1 + public/language/en-US/user.json | 1 + public/language/en-x-pirate/user.json | 1 + public/language/es/user.json | 1 + public/language/et/user.json | 1 + public/language/fa-IR/user.json | 1 + public/language/fi/user.json | 1 + public/language/fr/user.json | 1 + public/language/gl/user.json | 1 + public/language/he/user.json | 1 + public/language/hr/user.json | 1 + public/language/hu/user.json | 1 + public/language/hy/user.json | 1 + public/language/id/user.json | 1 + public/language/it/user.json | 1 + public/language/ja/user.json | 1 + public/language/ko/user.json | 1 + public/language/lt/user.json | 1 + public/language/lv/user.json | 1 + public/language/ms/user.json | 1 + public/language/nb/user.json | 1 + public/language/nl/user.json | 1 + public/language/nn-NO/user.json | 1 + public/language/pl/user.json | 1 + public/language/pt-BR/user.json | 1 + public/language/pt-PT/user.json | 1 + public/language/ro/user.json | 1 + public/language/ru/user.json | 1 + public/language/rw/user.json | 1 + public/language/sc/user.json | 1 + public/language/sk/user.json | 1 + public/language/sl/user.json | 1 + public/language/sq-AL/user.json | 1 + public/language/sr/user.json | 1 + public/language/sv/user.json | 1 + public/language/th/user.json | 1 + public/language/tr/user.json | 1 + public/language/uk/user.json | 1 + public/language/ur/user.json | 1 + public/language/vi/user.json | 1 + public/language/zh-CN/user.json | 1 + public/language/zh-TW/user.json | 1 + 49 files changed, 54 insertions(+), 5 deletions(-) diff --git a/public/language/ar/user.json b/public/language/ar/user.json index c89d5befcb..606d90a50d 100644 --- a/public/language/ar/user.json +++ b/public/language/ar/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "فصل", "sso.dissociate-confirm-title": "تأكيد الفصل", "sso.dissociate-confirm": "هل تريد بالتأكيد فصل حسابك عن %1؟", + "info.invited-by": "Invited by", "info.latest-flags": "أحدث العلامات", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/az/user.json b/public/language/az/user.json index 904ee5cb75..568155a4eb 100644 --- a/public/language/az/user.json +++ b/public/language/az/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Ayırmaq", "sso.dissociate-confirm-title": "Ayrılmanı təsdiq et", "sso.dissociate-confirm": "Hesabınızı %1 hesabından ayırmaq istədiyinizə əminsinizmi?", + "info.invited-by": "Invited by", "info.latest-flags": "Ən son bayraqlar", "info.profile": "Profil", "info.post": "Yazı", diff --git a/public/language/bg/user.json b/public/language/bg/user.json index f173be33b8..8121a81534 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -105,7 +105,7 @@ "show-email": "Да се показва е-пощата ми", "show-fullname": "Да се показва цялото ми име", "restrict-chats": "Разрешаване на съобщенията само от потребители, които следвам", - "disable-incoming-chats": "Disable incoming chat messages ", + "disable-incoming-chats": "Забраняване на входящите съобщения ", "chat-allow-list": "Разрешаване на съобщенията от следните потребители", "chat-deny-list": "Забраняване на съобщенията от следните потребители", "chat-list-add-user": "Добавяне на потребител", @@ -132,8 +132,8 @@ "email-hidden": "Е-пощата е скрита", "hidden": "скрито", "paginate-description": "Разделяне на темите и публикациите на страници, вместо да се превърта безкрайно", - "topics-per-page": "Topics per page", - "posts-per-page": "Posts per page", + "topics-per-page": "Теми на страница", + "posts-per-page": "Публикации на страница", "category-topic-sort": "Подреждане на темите в категория", "topic-post-sort": "Подреждане на публикациите в тема", "max-items-per-page": "Най-много %1", @@ -147,8 +147,8 @@ "upvote-notif-freq.logarithmic": "На 10, 100, 1000…", "upvote-notif-freq.disabled": "Изключено", "browsing": "Настройки за страниците", - "unread.cutoff": "Unread cutoff (Maximum %1 days)", - "unread.cutoff-help": "Topics will be marked read if they have not been updated within this number of days.", + "unread.cutoff": "Възраст на публикациите, след която те не се показват в непрочетените (най-много %1 дни)", + "unread.cutoff-help": "Темите ще се отбелязват като прочетени, ако в тях няма нови неща в рамките на посочения брой дни.", "open-links-in-new-tab": "Отваряне на външните връзки в нов подпрозорец", "enable-topic-searching": "Включване на търсенето в темите", "topic-search-help": "Ако е включено, търсенето в темата ще замени стандартното поведение на браузъра при търсене в страницата и ще Ви позволи да претърсвате цялата тема, а не само това, което се вижда на екрана", @@ -177,6 +177,7 @@ "sso.dissociate": "Прекъсване на връзката", "sso.dissociate-confirm-title": "Потвърждаване на прекъсването", "sso.dissociate-confirm": "Наистина ли искате да прекъснете връзката на акаунта си от „%1“?", + "info.invited-by": "Поканен(а) от", "info.latest-flags": "Последни доклади", "info.profile": "Профил", "info.post": "Публикация", diff --git a/public/language/bn/user.json b/public/language/bn/user.json index c7589535e9..59157efef6 100644 --- a/public/language/bn/user.json +++ b/public/language/bn/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/cs/user.json b/public/language/cs/user.json index 3c4f2dcd9d..c4db610b98 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Odloučit", "sso.dissociate-confirm-title": "Potvrdit odloučení", "sso.dissociate-confirm": "Jste si jist/a, že chcete odloučit váš účet z %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Poslední označené", "info.profile": "Profil", "info.post": "Příspěvek", diff --git a/public/language/da/user.json b/public/language/da/user.json index 4e762aa0cf..9a69b3c2a9 100644 --- a/public/language/da/user.json +++ b/public/language/da/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/de/user.json b/public/language/de/user.json index 5e44e5c635..b058d13dbe 100644 --- a/public/language/de/user.json +++ b/public/language/de/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Trennen", "sso.dissociate-confirm-title": "Trennung bestätigen", "sso.dissociate-confirm": "Bist du sicher, dass du dein Konto von %1 trennen willst?", + "info.invited-by": "Invited by", "info.latest-flags": "Neuste Meldungen", "info.profile": "Profil", "info.post": "Beitrag", diff --git a/public/language/el/user.json b/public/language/el/user.json index 5f2561702a..914169747e 100644 --- a/public/language/el/user.json +++ b/public/language/el/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/en-US/user.json b/public/language/en-US/user.json index d46ca54341..2e6b3241c1 100644 --- a/public/language/en-US/user.json +++ b/public/language/en-US/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/en-x-pirate/user.json b/public/language/en-x-pirate/user.json index 600a9b9b88..ee9464bb5d 100644 --- a/public/language/en-x-pirate/user.json +++ b/public/language/en-x-pirate/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/es/user.json b/public/language/es/user.json index f49a2538b6..341d7ef1bd 100644 --- a/public/language/es/user.json +++ b/public/language/es/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Disociado", "sso.dissociate-confirm-title": "Confirmar Disociación", "sso.dissociate-confirm": "Está seguro de que desea disociar su cuenta de %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Ultimos reportes", "info.profile": "Perfil", "info.post": "Publicación", diff --git a/public/language/et/user.json b/public/language/et/user.json index 99dc1e78b6..e4f6619d52 100644 --- a/public/language/et/user.json +++ b/public/language/et/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Viimased raporteerimised", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/fa-IR/user.json b/public/language/fa-IR/user.json index 958023a06a..a81bf2a7b7 100644 --- a/public/language/fa-IR/user.json +++ b/public/language/fa-IR/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "لغو اتصال", "sso.dissociate-confirm-title": "جدا‌سازی را تایید کنید", "sso.dissociate-confirm": "آیا مطمئنی می خواهی اتصال %1 به حسابت را لغو کنی؟", + "info.invited-by": "Invited by", "info.latest-flags": "آخرین نشانه گذاری‌ها", "info.profile": "نمایه", "info.post": "فرسته", diff --git a/public/language/fi/user.json b/public/language/fi/user.json index 3959c29a9b..4d285f2f2d 100644 --- a/public/language/fi/user.json +++ b/public/language/fi/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/fr/user.json b/public/language/fr/user.json index f538709ba1..08bd50a2a0 100644 --- a/public/language/fr/user.json +++ b/public/language/fr/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissocier", "sso.dissociate-confirm-title": "Confirmer la dissociation", "sso.dissociate-confirm": "Êtes-vous sûr de vouloir dissocier votre compte de %1 ?", + "info.invited-by": "Invited by", "info.latest-flags": "Derniers signalements", "info.profile": "Profile", "info.post": "Message", diff --git a/public/language/gl/user.json b/public/language/gl/user.json index fef6f3903b..404d6fc7f7 100644 --- a/public/language/gl/user.json +++ b/public/language/gl/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Últimos reportes", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/he/user.json b/public/language/he/user.json index fef98fabea..a94d6e3530 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "ביטול שיוך", "sso.dissociate-confirm-title": "אישור ביטול שיוך", "sso.dissociate-confirm": "האם לבטל שיוך חשבונכם מ%1?", + "info.invited-by": "Invited by", "info.latest-flags": "דיווחים אחרונים", "info.profile": "פרופיל", "info.post": "פוסט", diff --git a/public/language/hr/user.json b/public/language/hr/user.json index b371d86f5d..bc06accd65 100644 --- a/public/language/hr/user.json +++ b/public/language/hr/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Zadnja zastava", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/hu/user.json b/public/language/hu/user.json index 704b02a295..878881d8db 100644 --- a/public/language/hu/user.json +++ b/public/language/hu/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Leválasztás", "sso.dissociate-confirm-title": "Leválasztás megerősítése", "sso.dissociate-confirm": "Biztos le akarod választani a fiókod (%1) ?", + "info.invited-by": "Invited by", "info.latest-flags": "Legutóbbi megjelölések", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/hy/user.json b/public/language/hy/user.json index 95c9794ae3..1cb5f6005f 100644 --- a/public/language/hy/user.json +++ b/public/language/hy/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Անջատվել", "sso.dissociate-confirm-title": "Հաստատեք տարանջատումը", "sso.dissociate-confirm": "Վստա՞հ եք, որ ցանկանում եք անջատել ձեր հաշիվը %1-ից:", + "info.invited-by": "Invited by", "info.latest-flags": "Վերջին դրոշները", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/id/user.json b/public/language/id/user.json index e65f86553d..4bc63c13cc 100644 --- a/public/language/id/user.json +++ b/public/language/id/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/it/user.json b/public/language/it/user.json index 9bd306a001..d111563e3f 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissocia", "sso.dissociate-confirm-title": "Conferma dissociazione", "sso.dissociate-confirm": "Sei sicuro di voler dissociare il tuo account da %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Ultime segnalazioni", "info.profile": "Profilo", "info.post": "Post", diff --git a/public/language/ja/user.json b/public/language/ja/user.json index 4a9e89e1d6..c2d929f168 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "離脱する", "sso.dissociate-confirm-title": "離脱の際に確認する", "sso.dissociate-confirm": "アカウントと %1 の関連付けを解除しますか?", + "info.invited-by": "Invited by", "info.latest-flags": "最近のフラグ", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/ko/user.json b/public/language/ko/user.json index 1fb7c1ed72..801c8febb6 100644 --- a/public/language/ko/user.json +++ b/public/language/ko/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "분리", "sso.dissociate-confirm-title": "분리 확인", "sso.dissociate-confirm": "계정을 %1에서 분리하시겠습니까?", + "info.invited-by": "Invited by", "info.latest-flags": "최신 신고", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/lt/user.json b/public/language/lt/user.json index 1828197438..4c50abaf2a 100644 --- a/public/language/lt/user.json +++ b/public/language/lt/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/lv/user.json b/public/language/lv/user.json index db9de2ab25..cc2010df44 100644 --- a/public/language/lv/user.json +++ b/public/language/lv/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Atsaistīt", "sso.dissociate-confirm-title": "Apstiprināt atsaistīšanu", "sso.dissociate-confirm": "Vai tiešām vēlies atsaistīt Tavu kontu no %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Jaunākās atzīmes", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/ms/user.json b/public/language/ms/user.json index c0559916e8..c68829513a 100644 --- a/public/language/ms/user.json +++ b/public/language/ms/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/nb/user.json b/public/language/nb/user.json index 260a4f5bf4..fcf1cd418b 100644 --- a/public/language/nb/user.json +++ b/public/language/nb/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Separer", "sso.dissociate-confirm-title": "Bekreft seperasjon", "sso.dissociate-confirm": "Er du sikker på at du vil separere kontoen din fra %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Siste rapporteringer", "info.profile": "Profil", "info.post": "Post", diff --git a/public/language/nl/user.json b/public/language/nl/user.json index bf343ee502..ff347bd108 100644 --- a/public/language/nl/user.json +++ b/public/language/nl/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Ontkoppelen", "sso.dissociate-confirm-title": "Bevestig ontkoppeling", "sso.dissociate-confirm": "Weet u zeker dat u uw account wilt ontkoppelen van %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Laatste markeringen", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/nn-NO/user.json b/public/language/nn-NO/user.json index 4a5b4b5a6f..b6b395c053 100644 --- a/public/language/nn-NO/user.json +++ b/public/language/nn-NO/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Fjern tilknyting", "sso.dissociate-confirm-title": "Stadfest fjerning av tilknyting", "sso.dissociate-confirm": "Er du sikker på at du vil fjerne tilknytinga til kontoen din frå %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Siste rapportar", "info.profile": "Profil", "info.post": "Innlegg", diff --git a/public/language/pl/user.json b/public/language/pl/user.json index 75601d231f..badd8e6ce0 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Odwiąż", "sso.dissociate-confirm-title": "Potwierdź odwiązanie", "sso.dissociate-confirm": "Czy na pewno odwiązać Twoje konto od %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Ostatnie flagi", "info.profile": "Profil", "info.post": "Wpis", diff --git a/public/language/pt-BR/user.json b/public/language/pt-BR/user.json index a9ef8e3036..4245b394d6 100644 --- a/public/language/pt-BR/user.json +++ b/public/language/pt-BR/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Desassociar", "sso.dissociate-confirm-title": "Confirmar Desassociação", "sso.dissociate-confirm": "Tem certeza de que deseja desassociar a sua conta de %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Últimas Sinalizações", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/pt-PT/user.json b/public/language/pt-PT/user.json index 9d55a4f74e..90fdd1caec 100644 --- a/public/language/pt-PT/user.json +++ b/public/language/pt-PT/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociar", "sso.dissociate-confirm-title": "Confirmar Dissociação", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Denúncias Recentes", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/ro/user.json b/public/language/ro/user.json index 67b4260c42..5a97927e25 100644 --- a/public/language/ro/user.json +++ b/public/language/ro/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/ru/user.json b/public/language/ru/user.json index 50406a9c27..120c72211b 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Открепить", "sso.dissociate-confirm-title": "Подтверждение открепления", "sso.dissociate-confirm": "Вы уверены, что хотите открепить свою учётную запись от %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Последние жалобы", "info.profile": "Профиль", "info.post": "Пост", diff --git a/public/language/rw/user.json b/public/language/rw/user.json index b0608e1cb6..3d6bb8d6bb 100644 --- a/public/language/rw/user.json +++ b/public/language/rw/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/sc/user.json b/public/language/sc/user.json index a723070c25..ffab12e58b 100644 --- a/public/language/sc/user.json +++ b/public/language/sc/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/sk/user.json b/public/language/sk/user.json index e693775f5a..0ea0c5e49d 100644 --- a/public/language/sk/user.json +++ b/public/language/sk/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Odlúčiť", "sso.dissociate-confirm-title": "Potvrdiť odlúčenie", "sso.dissociate-confirm": "Ste si istý, že chcete odlúčiť Váš účet z %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Najnovšie príznaky", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/sl/user.json b/public/language/sl/user.json index 738ebe9a37..db59f02485 100644 --- a/public/language/sl/user.json +++ b/public/language/sl/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Dissociate", "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/sq-AL/user.json b/public/language/sq-AL/user.json index 9ab1357f07..3a152be9e4 100644 --- a/public/language/sq-AL/user.json +++ b/public/language/sq-AL/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Shkëputeni", "sso.dissociate-confirm-title": "Konfirmo shkëputjen", "sso.dissociate-confirm": "Jeni i sigurt që dëshironi të shkëputni llogarinë tuaj nga %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Raportimet më të fundit", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/sr/user.json b/public/language/sr/user.json index 1626cca0a7..ce95d129a5 100644 --- a/public/language/sr/user.json +++ b/public/language/sr/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Одвоји", "sso.dissociate-confirm-title": "Потврди одвајање", "sso.dissociate-confirm": "Да ли сте сигурни да желите да одвојите овај налог од %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Најновији означени заставицом", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/sv/user.json b/public/language/sv/user.json index 4541a89cf8..f9c06035d0 100644 --- a/public/language/sv/user.json +++ b/public/language/sv/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Frånkoppla", "sso.dissociate-confirm-title": "Bekräfta frånkoppling", "sso.dissociate-confirm": "Är du säker att du vill koppla bort ditt konto från %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Senaste flaggade", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/th/user.json b/public/language/th/user.json index dfaa80c8ae..6545638bab 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "แยกตัวออก", "sso.dissociate-confirm-title": "ยืนยันการแยกตัวออก", "sso.dissociate-confirm": "คุณแน่ใจหรือไม่ว่าต้องการแยกบัญชีออกจาก %1?", + "info.invited-by": "Invited by", "info.latest-flags": "รายงานล่าสุด", "info.profile": "โปรไฟล์", "info.post": "โพสต์", diff --git a/public/language/tr/user.json b/public/language/tr/user.json index cb3f33b133..0d81d43c48 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Ayrış", "sso.dissociate-confirm-title": "Ayrışmayı Onayla", "sso.dissociate-confirm": "%1 'den ayrışmak istediğinizden emin misiniz?", + "info.invited-by": "Invited by", "info.latest-flags": "Son Şikayetler", "info.profile": "Profil", "info.post": "İleti", diff --git a/public/language/uk/user.json b/public/language/uk/user.json index 342f9d7941..6f2518a62d 100644 --- a/public/language/uk/user.json +++ b/public/language/uk/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Від'єднати", "sso.dissociate-confirm-title": "Підтвердьте від'єднання", "sso.dissociate-confirm": "Ви впевнені, що хочете від'єднати свій акаунт від %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Останні скарги", "info.profile": "Profile", "info.post": "Post", diff --git a/public/language/ur/user.json b/public/language/ur/user.json index 431f72d6fd..319397fb5b 100644 --- a/public/language/ur/user.json +++ b/public/language/ur/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "رابطہ منقطع کریں", "sso.dissociate-confirm-title": "منقطع کرنے کی تصدیق", "sso.dissociate-confirm": "کیا آپ واقعی اپنے اکاؤنٹ کو „%1“ سے منقطع کرنا چاہتے ہیں؟", + "info.invited-by": "Invited by", "info.latest-flags": "تازہ ترین رپورٹس", "info.profile": "پروفائل", "info.post": "پوسٹ", diff --git a/public/language/vi/user.json b/public/language/vi/user.json index dcf7d907d9..95e17d052a 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "Tách khỏi", "sso.dissociate-confirm-title": "Xác nhận việc tách khỏi", "sso.dissociate-confirm": "Bạn có chắc chắn muốn tách tài khoản của mình khỏi %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Gắn cờ mới nhất", "info.profile": "Hồ sơ", "info.post": "Bài viết", diff --git a/public/language/zh-CN/user.json b/public/language/zh-CN/user.json index 0d4443c776..5d46385974 100644 --- a/public/language/zh-CN/user.json +++ b/public/language/zh-CN/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "解除关联", "sso.dissociate-confirm-title": "确认解除关联", "sso.dissociate-confirm": "您确定您要将您的账号与 %1 解除关联吗?", + "info.invited-by": "Invited by", "info.latest-flags": "最新举报", "info.profile": "资料", "info.post": "帖子", diff --git a/public/language/zh-TW/user.json b/public/language/zh-TW/user.json index fcb867753a..8cc7d1072b 100644 --- a/public/language/zh-TW/user.json +++ b/public/language/zh-TW/user.json @@ -177,6 +177,7 @@ "sso.dissociate": "解除關聯", "sso.dissociate-confirm-title": "確認解除關聯", "sso.dissociate-confirm": "您確定要將您的帳戶與 %1 解除關聯嗎?", + "info.invited-by": "Invited by", "info.latest-flags": "最新舉報", "info.profile": "個人檔案", "info.post": "貼文", From 2dc83bbd96d2e3638be0f38c6c5b6c40080f01c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 13:36:04 -0500 Subject: [PATCH 4245/4744] dont setup all toggles just account tooltips --- public/src/client/account/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js index 9a519f71c8..f7c41a4006 100644 --- a/public/src/client/account/settings.js +++ b/public/src/client/account/settings.js @@ -20,7 +20,7 @@ define('forum/account/settings', [ savedSkin = $('#bootswatchSkin').length && $('#bootswatchSkin').val(); header.init(); - $('[data-bs-toggle]').tooltip(); + $('.account [data-bs-toggle="tooltip"]').tooltip(); $('#submitBtn').on('click', function () { const settings = loadSettings(); From effdbc4d956ae029238b327757f74ec8ad5f4618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 13:36:54 -0500 Subject: [PATCH 4246/4744] use visualViewport if it exists to detecth mobile keyboard open and scroll to bottom of chat list if we are near, this keeps the message list at the bottom when you open the keyboard on mobile --- public/src/client/chats.js | 34 ++++++++++++++++++++++++++-------- public/src/modules/chat.js | 2 +- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 0f2ef05082..9c0c7c0ddf 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -32,6 +32,8 @@ define('forum/chats', [ let chatNavWrapper = null; + let isAtBottom = true; + $(window).on('action:ajaxify.start', function () { Chats.destroyAutoComplete(ajaxify.data.roomId); if (ajaxify.data.template.chats) { @@ -90,7 +92,10 @@ define('forum/chats', [ const mainWrapper = $('[component="chat/main-wrapper"]'); const chatMessageContent = $('[component="chat/message/content"]'); const chatControls = components.get('chat/controls'); - Chats.addSendHandlers(roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]')); + const chatInput = $('[component="chat/input"]'); + + Chats.addSendHandlers(roomId, chatInput, $('.expanded-chat button[data-action="send"]')); + Chats.addMobileResizeHandler(chatMessageContent); Chats.addPopoutHandler(); Chats.addActionHandlers(components.get('chat/message/window'), roomId); Chats.addManageHandler(roomId, chatControls.find('[data-action="manage"]')); @@ -105,18 +110,16 @@ define('forum/chats', [ Chats.addTypingHandler(mainWrapper, roomId); Chats.addIPHandler(mainWrapper); Chats.addCopyTextLinkHandler(mainWrapper); - Chats.createAutoComplete(roomId, $('[component="chat/input"]')); + Chats.createAutoComplete(roomId, chatInput); Chats.addUploadHandler({ dragDropAreaEl: $('.chats-full'), - pasteEl: $('[component="chat/input"]'), + pasteEl: chatInput, uploadFormEl: $('[component="chat/upload"]'), uploadBtnEl: $('[component="chat/upload/button"]'), - inputEl: $('[component="chat/input"]'), + inputEl: chatInput, }); - $('[data-action="close"]').on('click', function () { - Chats.switchChat(); - }); + $('[data-action="close"]').on('click', () => Chats.switchChat()); userList.init(roomId, mainWrapper); Chats.addNotificationSettingHandler(roomId, mainWrapper); messageSearch.init(roomId, mainWrapper); @@ -295,7 +298,12 @@ define('forum/chats', [ let loading = false; let previousScrollTop = el.scrollTop(); let currentScrollTop = previousScrollTop; - el.off('scroll').on('scroll', utils.debounce(async function () { + + el.off('scroll'); + el.on('scroll', function () { + isAtBottom = messages.isAtBottom(el); + }); + el.on('scroll', utils.debounce(async function () { if (parseInt(el.attr('data-ignore-next-scroll'), 10) === 1) { el.removeAttr('data-ignore-next-scroll'); previousScrollTop = el.scrollTop(); @@ -546,6 +554,16 @@ define('forum/chats', [ }); }; + Chats.addMobileResizeHandler = function (chatMessageContent) { + if (utils.isMobile() && window.visualViewport) { + window.visualViewport.addEventListener('resize', function () { + if (isAtBottom) { + messages.scrollToBottom(chatMessageContent); + } + }); + } + }; + Chats.createAutoComplete = function (roomId, element, options = {}) { if (!element.length) { return; diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 81b9ce9d9c..be70ba0937 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -20,7 +20,7 @@ define('chat', [ roomId: roomId, uid: uid, }).then((hookData) => { - if (!hookData.modal) { + if (!hookData.modal || utils.isMobile()) { return ajaxify.go(`/chats/${roomId}`); } if (Chat.modalExists(roomId)) { From 8c35c5e8b439f0d53749d2bbf9f37b6a00847589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 13:45:30 -0500 Subject: [PATCH 4247/4744] add whitespace before room name --- public/language/en-GB/notifications.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/language/en-GB/notifications.json b/public/language/en-GB/notifications.json index 43eaa603b3..d4aa2cc16b 100644 --- a/public/language/en-GB/notifications.json +++ b/public/language/en-GB/notifications.json @@ -32,10 +32,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", From 9524359d8b8ee07e619670254b6ec4764b49e5a9 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 18 Feb 2026 18:45:56 +0000 Subject: [PATCH 4248/4744] chore(i18n): fallback strings for new resources: nodebb.notifications --- public/language/ar/notifications.json | 8 +- public/language/az/notifications.json | 8 +- public/language/bg/notifications.json | 10 +- public/language/bn/notifications.json | 8 +- public/language/cs/notifications.json | 8 +- public/language/da/notifications.json | 8 +- public/language/de/notifications.json | 60 ++++----- public/language/el/notifications.json | 8 +- public/language/en-US/notifications.json | 8 +- .../language/en-x-pirate/notifications.json | 8 +- public/language/es/notifications.json | 8 +- public/language/et/notifications.json | 8 +- public/language/fa-IR/notifications.json | 8 +- public/language/fi/notifications.json | 8 +- public/language/fr/notifications.json | 122 +++++++++--------- public/language/gl/notifications.json | 8 +- public/language/he/notifications.json | 34 ++--- public/language/hr/notifications.json | 8 +- public/language/hu/notifications.json | 8 +- public/language/hy/notifications.json | 8 +- public/language/id/notifications.json | 8 +- public/language/it/notifications.json | 58 ++++----- public/language/ja/notifications.json | 8 +- public/language/ko/notifications.json | 8 +- public/language/lt/notifications.json | 8 +- public/language/lv/notifications.json | 8 +- public/language/ms/notifications.json | 8 +- public/language/nb/notifications.json | 8 +- public/language/nl/notifications.json | 8 +- public/language/nn-NO/notifications.json | 8 +- public/language/pl/notifications.json | 52 ++++---- public/language/pt-BR/notifications.json | 8 +- public/language/pt-PT/notifications.json | 8 +- public/language/ro/notifications.json | 8 +- public/language/ru/notifications.json | 8 +- public/language/rw/notifications.json | 8 +- public/language/sc/notifications.json | 8 +- public/language/sk/notifications.json | 8 +- public/language/sl/notifications.json | 8 +- public/language/sq-AL/notifications.json | 8 +- public/language/sr/notifications.json | 8 +- public/language/sv/notifications.json | 8 +- public/language/th/notifications.json | 8 +- public/language/tr/notifications.json | 8 +- public/language/uk/notifications.json | 8 +- public/language/ur/notifications.json | 8 +- public/language/vi/notifications.json | 44 +++---- public/language/zh-CN/notifications.json | 10 +- public/language/zh-TW/notifications.json | 8 +- 49 files changed, 359 insertions(+), 359 deletions(-) diff --git a/public/language/ar/notifications.json b/public/language/ar/notifications.json index 7fefcfe7e5..0270ed2b1f 100644 --- a/public/language/ar/notifications.json +++ b/public/language/ar/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/az/notifications.json b/public/language/az/notifications.json index dd0981a302..fbb5de5b4e 100644 --- a/public/language/az/notifications.json +++ b/public/language/az/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%2-dən %1 yeni mesaj", "new-message-in": "%1-də yeni mesaj", "new-messages-in": "%2-də %1 yeni mesaj", - "user-posted-in-public-room": "%1 %3-də yazdı", - "user-posted-in-public-room-dual": "%1%2 %4-də yazdı", - "user-posted-in-public-room-triple": "%1, %2%3 %5 ilə yazır", - "user-posted-in-public-room-multiple": "%1, %2 və %3 digərləri %5-də yazıblar", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/bg/notifications.json b/public/language/bg/notifications.json index cc3d4ce074..6c51589d7f 100644 --- a/public/language/bg/notifications.json +++ b/public/language/bg/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 нови съобщения от %2", "new-message-in": "Ново съобщение в %1", "new-messages-in": "%1 нови съобщения в %2", - "user-posted-in-public-room": "%1 писа в %3", - "user-posted-in-public-room-dual": "%1 и %2 писаха в %4", - "user-posted-in-public-room-triple": "%1, %2 и %3 писаха в %5", - "user-posted-in-public-room-multiple": "%1, %2 и %3 други писаха в %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 гласува положително за Вашата публикация в %2", "upvoted-your-post-in-dual": "%1 и %2 гласуваха положително за Вашата публикация в %3", "upvoted-your-post-in-triple": "%1, %2 и %3 гласуваха положително за Вашата публикация в %4", @@ -71,7 +71,7 @@ "users-csv-exported": "Потребителите са изнесени във формат „csv“, щракнете за сваляне", "post-queue-accepted": "Вашата публикация, която чакаше в опашката, беше приета. Натиснете тук, за да я видите.", "post-queue-rejected": "Вашата публикация, която чакаше в опашката, беше отхвърлена.", - "post-queue-rejected-for-reason": "Your queued post has been rejected for the following reason: \"%1\"", + "post-queue-rejected-for-reason": "Вашата публикация, която чакаше в опашката, беше отхвърлена поради следната причина: „%1“", "post-queue-notify": "Публикацията в опашката получи известие: „%1“", "email-confirmed": "Е-пощата беше потвърдена", "email-confirmed-message": "Благодарим Ви, че потвърдихте е-пощата си. Акаунтът Ви е вече напълно активиран.", diff --git a/public/language/bn/notifications.json b/public/language/bn/notifications.json index 0386dc6670..5328093d89 100644 --- a/public/language/bn/notifications.json +++ b/public/language/bn/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json index c7b4f30009..046732abc6 100644 --- a/public/language/cs/notifications.json +++ b/public/language/cs/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 nových zpráv od %2", "new-message-in": "Nová zpráva v %1", "new-messages-in": "%1 nových zpráv v%2", - "user-posted-in-public-room": "%1 napsal do %3", - "user-posted-in-public-room-dual": "%1 a %2napsali do %4", - "user-posted-in-public-room-triple": "%1, %2 a %3 napsali do %5", - "user-posted-in-public-room-multiple": "%1, %2 a %3 dalších napsali do %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/da/notifications.json b/public/language/da/notifications.json index c07c3dc8fa..b3acb98ac9 100644 --- a/public/language/da/notifications.json +++ b/public/language/da/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json index 2dc78e1c81..4cd8140720 100644 --- a/public/language/de/notifications.json +++ b/public/language/de/notifications.json @@ -22,41 +22,41 @@ "upvote": "Positive Bewertungen", "awards": "Auszeichnungen", "new-flags": "Neue Meldungen", - "my-flags": "My Flags", + "my-flags": "Meine Markierungen", "bans": "Verbannungen", "new-message-from": "Neue Nachricht von %1", "new-messages-from": "%1 neue Nachrichten von %2", "new-message-in": "Neue Nachricht in %1", "new-messages-in": "%1 neue Nachrichten in %2", - "user-posted-in-public-room": "%1 schrieb in %3", - "user-posted-in-public-room-dual": "%1 und %2 schrieben in %4", - "user-posted-in-public-room-triple": "%1, %2 und %3 schrieben in %5", - "user-posted-in-public-room-multiple": "%1, %2 und %3 andere schrieben in %5", - "upvoted-your-post-in": "%1 upvoted your post in %2", - "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", - "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", - "upvoted-your-post-in-multiple": "%1, %2 and %3 others upvoted your post in %4.", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "upvoted-your-post-in": "%1 hat deinen Beitrag in %2positiv bewertet", + "upvoted-your-post-in-dual": "%1 und %2 haben deinen Beitrag in %3 positiv bewertet.", + "upvoted-your-post-in-triple": "%1, %2 und %3 haben deinen Beitrag in %4 positiv bewertet.", + "upvoted-your-post-in-multiple": "%1, %2 und %3 andere haben deinen Beitrag in %4hochgestuft.", "moved-your-post": "%1 hat deinen Beitrag nach %2 verschoben.", "moved-your-topic": "%1 hat %2 verschoben.", - "user-flagged-post-in": "%1 flagged a post in %2", - "user-flagged-post-in-dual": "%1 and %2 flagged a post in %3", - "user-flagged-post-in-triple": "%1, %2 and %3 flagged a post in %4", - "user-flagged-post-in-multiple": "%1, %2 and %3 others flagged a post in %4", + "user-flagged-post-in": "%1 hat einen Beitrag in %2markiert", + "user-flagged-post-in-dual": "%1 und %2 haben einen Beitrag in %3 markiert", + "user-flagged-post-in-triple": "%1, %2 und %3 haben einen Beitrag in %4 markiert.", + "user-flagged-post-in-multiple": "%1, %2 und %3 andere haben einen Beitrag in %4 markiert.", "user-flagged-user": "%1 meldete ein Nutzerprofil (%2)", "user-flagged-user-dual": "%1 und %2 meldeten ein Nutzerprofil (%3)", "user-flagged-user-triple": "%1, %2 und %3 meldeten ein Benutzerprofil (%4)", "user-flagged-user-multiple": "%1, %2 und %3 andere meldeten ein Benutzerprofil (%4)", - "user-posted-to": "%1 posted a reply in %2", - "user-posted-to-dual": "%1 and %2 replied in %3", - "user-posted-to-triple": "%1, %2 and %3 replied in %4", - "user-posted-to-multiple": "%1, %2 and %3 others replied in %4", - "user-posted-topic": "%1 posted %2", - "user-edited-post": "%1 edited a post in %2", - "user-posted-topic-with-tag": "%1 posted %2 (tagged %3)", - "user-posted-topic-with-tag-dual": "%1 posted %2 (tagged %3 and %4)", - "user-posted-topic-with-tag-triple": "%1 posted %2 (tagged %3, %4, and %5)", - "user-posted-topic-with-tag-multiple": "%1 posted %2 (tagged %3)", - "user-posted-topic-in-category": "%1 posted %2 in %3", + "user-posted-to": "%1 hat eine Antwort in %2 gepostet", + "user-posted-to-dual": "%1 und %2 haben in %3geantwortet", + "user-posted-to-triple": "%1, %2 und %3 haben in %4 geantwortet", + "user-posted-to-multiple": "%1, %2 und %3 andere haben in %4 geantwortet", + "user-posted-topic": "%1 hat %2gepostet", + "user-edited-post": "%1 hat einen Beitrag in %2bearbeitet.", + "user-posted-topic-with-tag": "%1 hat %2gepostet (getagged mit %3)", + "user-posted-topic-with-tag-dual": "%1 hat %2gepostet (mit den Tags %3 und %4)", + "user-posted-topic-with-tag-triple": "%1 hat %2gepostet (mit den Tags %3, %4 und %5)", + "user-posted-topic-with-tag-multiple": "%1 hat %2gepostet (getagged mit %3)", + "user-posted-topic-in-category": "%1 hat %2in %3gepostet", "user-started-following-you": "%1 folgt dir jetzt.", "user-started-following-you-dual": "%1 und %2 folgen dir jetzt.", "user-started-following-you-triple": "%1, %2 und %3 folgen dir jetzt.", @@ -71,8 +71,8 @@ "users-csv-exported": "Benutzer im CSV-Format exportiert, zum Herunterladen klicken", "post-queue-accepted": "Ihr Post in der Warteschlange wurde akzeptiert. Klicken Sie hier, um Ihren Beitrag anzuzeigen.", "post-queue-rejected": "Ihr Post in der Warteschlange wurde abgelehnt.", - "post-queue-rejected-for-reason": "Your queued post has been rejected for the following reason: \"%1\"", - "post-queue-notify": "Queued post received a notification: \"%1\"", + "post-queue-rejected-for-reason": "Dein Beitrag in der Warteschlange wurde abgelehnt, weil: \"%1\"", + "post-queue-notify": "Der in der Warteschlange stehende Beitrag hat eine Benachrichtigung bekommen: „%1“", "email-confirmed": "E-Mail bestätigt", "email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.", "email-confirm-error-message": "Es gab ein Problem bei der Validierung Ihrer E-Mail-Adresse. Möglicherweise ist der Code ungültig oder abgelaufen.", @@ -99,8 +99,8 @@ "notificationType-new-post-flag": "Wenn ein Beitrag gemeldet wird", "notificationType-new-user-flag": "Wenn ein Benutzer gemeldet wird", "notificationType-new-reward": "Wenn du eine neue Auszeichnung erhältst", - "activitypub.announce": "%1 shared your post in %2 to their followers.", - "activitypub.announce-dual": "%1 and %2 shared your post in %3 to their followers.", - "activitypub.announce-triple": "%1, %2 and %3 shared your post in %4 to their followers.", - "activitypub.announce-multiple": "%1, %2 and %3 others shared your post in %4 to their followers." + "activitypub.announce": "%1 hat deinen Beitrag in %2 mit seinen Followern geteilt.", + "activitypub.announce-dual": "%1 und %2 haben deinen Beitrag in %3 mit ihren Followern geteilt.", + "activitypub.announce-triple": "%1, %2 und %3 haben deinen Beitrag in %4 mit ihren Followern geteilt.", + "activitypub.announce-multiple": "%1, %2 und %3 Andere haben deinen Beitrag in %4 mit ihren Followern geteilt." } \ No newline at end of file diff --git a/public/language/el/notifications.json b/public/language/el/notifications.json index 6ed64ec9e5..7ac5294b9d 100644 --- a/public/language/el/notifications.json +++ b/public/language/el/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/en-US/notifications.json b/public/language/en-US/notifications.json index c445120343..6b1e6f45b2 100644 --- a/public/language/en-US/notifications.json +++ b/public/language/en-US/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/en-x-pirate/notifications.json b/public/language/en-x-pirate/notifications.json index 844e6f9c17..84872e7923 100644 --- a/public/language/en-x-pirate/notifications.json +++ b/public/language/en-x-pirate/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/es/notifications.json b/public/language/es/notifications.json index 7c4cc6d2f5..703a1933b7 100644 --- a/public/language/es/notifications.json +++ b/public/language/es/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/et/notifications.json b/public/language/et/notifications.json index a05d7bca3b..7be74ec40d 100644 --- a/public/language/et/notifications.json +++ b/public/language/et/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/fa-IR/notifications.json b/public/language/fa-IR/notifications.json index bf57a3aad9..1207f6261c 100644 --- a/public/language/fa-IR/notifications.json +++ b/public/language/fa-IR/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/fi/notifications.json b/public/language/fi/notifications.json index 637f3aaec8..1b7719219f 100644 --- a/public/language/fi/notifications.json +++ b/public/language/fi/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 uutta viestiä lähteestä %2", "new-message-in": "New message in %1", "new-messages-in": "%1 uutta viestiä kohteessa %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/fr/notifications.json b/public/language/fr/notifications.json index cc08b7b941..ab4ef72727 100644 --- a/public/language/fr/notifications.json +++ b/public/language/fr/notifications.json @@ -1,11 +1,11 @@ { "title": "Notifications", - "no-notifs": "Vous n'avez aucune notification", + "no-notifs": "Vous n'avez aucune nouvelle notification", "see-all": "Toutes les notifications", "mark-all-read": "Marquer tout comme lu", "back-to-home": "Revenir à %1", "outgoing-link": "Lien sortant", - "outgoing-link-message": "Vous quittez %1", + "outgoing-link-message": "Vous quittez maintenant %1", "continue-to": "Continuer vers %1", "return-to": "Revenir à %1", "new-notification": "Vous avez une nouvelle notification", @@ -22,85 +22,85 @@ "upvote": "Votes positifs", "awards": "Récompenses", "new-flags": "Nouveaux signalements", - "my-flags": "My Flags", + "my-flags": "Mes signalements", "bans": "Bannissements", "new-message-from": "Nouveau message de %1", - "new-messages-from": "%1 new messages from %2", - "new-message-in": "New message in %1", - "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 a écrit dans %3", - "user-posted-in-public-room-dual": "%1 et %2 ont écrit dans %4", - "user-posted-in-public-room-triple": "%1, %2 et %3 ont écris dans %5", - "user-posted-in-public-room-multiple": "%1, %2 et %3 d'autres ont écrit dans %5", - "upvoted-your-post-in": "%1 upvoted your post in %2", - "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", - "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", - "upvoted-your-post-in-multiple": "%1, %2 and %3 others upvoted your post in %4.", - "moved-your-post": "%1 a déplacé votre message vers %2", + "new-messages-from": "%1 Nouveaux messages de %2", + "new-message-in": "Nouveau message dans %1", + "new-messages-in": "%1 Nouveaux messages dans %2", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "upvoted-your-post-in": "%1 a approuvé votre publication dans %2", + "upvoted-your-post-in-dual": "%1 et %2 ont approuvé votre publication dans %3", + "upvoted-your-post-in-triple": "%1, %2 et %3 ont approuvé votre publication dans %4", + "upvoted-your-post-in-multiple": "%1, %2 et %3 autres ont approuvé votre publication dans %4.", + "moved-your-post": "%1 a déplacé votre publication vers %2", "moved-your-topic": "%1 a déplacé %2.", - "user-flagged-post-in": "%1 flagged a post in %2", - "user-flagged-post-in-dual": "%1 and %2 flagged a post in %3", - "user-flagged-post-in-triple": "%1, %2 and %3 flagged a post in %4", - "user-flagged-post-in-multiple": "%1, %2 and %3 others flagged a post in %4", + "user-flagged-post-in": "%1 a signalé une publication dans %2", + "user-flagged-post-in-dual": "%1 et %2 ont signalé une publication dans %3", + "user-flagged-post-in-triple": "1, %2 et %3 ont signalé une publication dans %4", + "user-flagged-post-in-multiple": "%1, %2 et %3 autres ont signalé une publication dans %4", "user-flagged-user": "%1 a signalé un profil utilisateur (%2)", "user-flagged-user-dual": "%1 et %2 ont signalé un profil utilisateur (%3)", "user-flagged-user-triple": "%1, %2 et %3 ont signalé un profil utilisateur (%4)", "user-flagged-user-multiple": "%1, %2 et %3 autres ont signalé un profil utilisateur (%4)", - "user-posted-to": "%1 posted a reply in %2", - "user-posted-to-dual": "%1 and %2 replied in %3", - "user-posted-to-triple": "%1, %2 and %3 replied in %4", - "user-posted-to-multiple": "%1, %2 and %3 others replied in %4", - "user-posted-topic": "%1 posted %2", - "user-edited-post": "%1 edited a post in %2", - "user-posted-topic-with-tag": "%1 posted %2 (tagged %3)", - "user-posted-topic-with-tag-dual": "%1 posted %2 (tagged %3 and %4)", - "user-posted-topic-with-tag-triple": "%1 posted %2 (tagged %3, %4, and %5)", - "user-posted-topic-with-tag-multiple": "%1 posted %2 (tagged %3)", - "user-posted-topic-in-category": "%1 posted %2 in %3", - "user-started-following-you": "%1 vous suit.", - "user-started-following-you-dual": "%1 et %2 se sont abonnés à votre compte.", + "user-posted-to": "%1 a répondu dans %2", + "user-posted-to-dual": "%1 et %2 ont répondu dans %3", + "user-posted-to-triple": "%1, %2 et %3 ont répondu dans %4", + "user-posted-to-multiple": "%1, %2 and %3 autres ont répondu dans %4", + "user-posted-topic": "%1 a publié %2", + "user-edited-post": "%1 a édité une publication dans %2", + "user-posted-topic-with-tag": "%1 a publié %2 (a mentionné %3)", + "user-posted-topic-with-tag-dual": "%1 a posté %2 (a mentionné %3 et %4)", + "user-posted-topic-with-tag-triple": "%1 a publié %2 (a mentionné %3, %4, et %5)", + "user-posted-topic-with-tag-multiple": "%1 a publié %2 (a mentionné %3)", + "user-posted-topic-in-category": "%1 a publié %2 dans %3", + "user-started-following-you": "%1 a commencé à vous suivre", + "user-started-following-you-dual": "%1 et %2 ont commencé à vous suivre", "user-started-following-you-triple": "%1, %2 et %3 ont commencé à vous suivre.", "user-started-following-you-multiple": "%1, %2 et %3 autres ont commencé à vous suivre.", "new-register": "%1 a envoyé une demande d'incription.", - "new-register-multiple": "%1 inscription(s) est en attente de validation.", - "flag-assigned-to-you": "Signalement %1 vous a été assigné", + "new-register-multiple": "Il y a %1 demandes d'inscription en attente de validation.", + "flag-assigned-to-you": "Le signalement %1 vous a été assigné", "post-awaiting-review": "Message en attente de validation", "profile-exported": "%1 profil exporté, cliquez pour le télécharger", "posts-exported": "%1 messages exportés, cliquez pour les télécharger", "uploads-exported": "%1 envois exportés, cliquez pour les télécharger", "users-csv-exported": "Utilisateurs exportés en CSV, cliquer pour télécharger", - "post-queue-accepted": "Votre message a été accepté. Cliquez ici pour l'afficher.", - "post-queue-rejected": "Votre message a été rejeté.", - "post-queue-rejected-for-reason": "Your queued post has been rejected for the following reason: \"%1\"", - "post-queue-notify": "Queued post received a notification: \"%1\"", + "post-queue-accepted": "Votre publication a été acceptée. Cliquez ici pour l'afficher.", + "post-queue-rejected": "Votre publication a été rejetée.", + "post-queue-rejected-for-reason": "Votre publication en attente a été refusée pour le motif suivant : \"%1\"", + "post-queue-notify": "Une publication en attente a reçu une notification : \"%1\"", "email-confirmed": "E-mail vérifié", "email-confirmed-message": "Merci pour la validation de votre adresse e-mail. Votre compte est désormais activé.", "email-confirm-error-message": "Il y a un un problème dans la vérification de votre adresse e-mail. Le code est peut être invalide ou a expiré.", "email-confirm-sent": "E-mail de vérification envoyé.", "none": "aucun", - "notification-only": "Seulement une notification", - "email-only": "Seulement un e-mail", + "notification-only": "Notification uniquement", + "email-only": "E-mail uniquement", "notification-and-email": "Notification & E-mail", - "notificationType-upvote": "Lorsque quelqu'un a voté pour un de vos messages", - "notificationType-new-topic": "Lorsque quelqu'un que vous suivez publie un sujet", - "notificationType-new-topic-with-tag": "Lorsqu'un sujet est publié avec un mot-clé que vous suivez", - "notificationType-new-topic-in-category": "Lorsqu'un sujet est publié dans une catégorie que vous regardez", - "notificationType-new-reply": "Lorsqu'une nouvelle réponse est ajoutée dans un sujet que vous suivez", - "notificationType-post-edit": "Lorsqu'un article est modifié dans un sujet que vous regardez", - "notificationType-follow": "Lorsque quelqu'un commence à vous suivre", - "notificationType-new-chat": "Lorsque vous recevez un message du chat", - "notificationType-new-group-chat": "Lorsque vous recevez un message de discussion de groupe", - "notificationType-new-public-chat": "Lorsque vous recevez un message du groupe de discussion publique", - "notificationType-group-invite": "Lorsque vous recevez une invitation d'un groupe", - "notificationType-group-leave": "Lorsqu'un utilisateur quitte votre groupe", + "notificationType-upvote": "Quand quelqu'un a voté pour une de vos publication", + "notificationType-new-topic": "Quand quelqu'un que vous suivez publie un sujet", + "notificationType-new-topic-with-tag": "Quand un sujet est publié avec un mot-clé que vous suivez", + "notificationType-new-topic-in-category": "Quand un sujet est publié dans une catégorie que vous suivez", + "notificationType-new-reply": "Quand une nouvelle réponse est ajoutée dans un sujet que vous suivez", + "notificationType-post-edit": "Quand un article est modifié dans un sujet que vous suivez", + "notificationType-follow": "Quand quelqu'un commence à vous suivre", + "notificationType-new-chat": "Quand vous recevez un message du chat", + "notificationType-new-group-chat": "Quand vous recevez un message de discussion de groupe", + "notificationType-new-public-chat": "Quand vous recevez un message du groupe de discussion publique", + "notificationType-group-invite": "Quand vous recevez une invitation d'un groupe", + "notificationType-group-leave": "Quand un utilisateur quitte votre groupe", "notificationType-group-request-membership": "Quand quelqu'un demande à rejoindre un groupe que vous administrez", - "notificationType-new-register": "Lorsque quelqu'un est ajouté à la file d'attente d'inscription", - "notificationType-post-queue": "Lorsque un nouveau message est mis en file d'attente", - "notificationType-new-post-flag": "Lorsque un message est marqué", - "notificationType-new-user-flag": "Lorsque un utilisateur est marqué", - "notificationType-new-reward": "Lorsque vous gagnez une nouvelle récompense", - "activitypub.announce": "%1 a partagé votre publication à %2 personnes.", - "activitypub.announce-dual": "%1 et %2 ont partagé votre publication à %3 personnes.", - "activitypub.announce-triple": "%1, %2 et %3 ont partagé votre publication à %4 personnes.", - "activitypub.announce-multiple": "%1, %2 et %3 autres personnes ont partagé votre publication à %4 personnes." + "notificationType-new-register": "Quand quelqu'un est ajouté à la file d'attente d'inscription", + "notificationType-post-queue": "Quand un nouveau message est mis en file d'attente", + "notificationType-new-post-flag": "Quand un message est marqué", + "notificationType-new-user-flag": "Quand un utilisateur est signalé", + "notificationType-new-reward": "Quand vous gagnez une nouvelle récompense", + "activitypub.announce": "%1 a partagé votre publication dans %2 avec ses abonnés.", + "activitypub.announce-dual": "%1 et %2 ont partagé votre publication dans %3 avec leurs abonnés.", + "activitypub.announce-triple": "%1, %2 et %3 ont partagé votre publication dans %4 avec leurs abonnés.", + "activitypub.announce-multiple": "%1, %2 et %3 autres ont partagé votre publication à %4 avec leurs abonnés." } \ No newline at end of file diff --git a/public/language/gl/notifications.json b/public/language/gl/notifications.json index bba321c678..7e6154c9b8 100644 --- a/public/language/gl/notifications.json +++ b/public/language/gl/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/he/notifications.json b/public/language/he/notifications.json index 2c2e86d6a6..e5b45994a9 100644 --- a/public/language/he/notifications.json +++ b/public/language/he/notifications.json @@ -22,34 +22,34 @@ "upvote": "הצבעות בעד", "awards": "פרסים", "new-flags": "דיווחים חדשים", - "my-flags": "My Flags", + "my-flags": "דיווחים שהוקצו עבורי", "bans": "הרחקות", "new-message-from": "הודעה חדשה מ %1", "new-messages-from": "%1 הודעות חדשות מאת %2", "new-message-in": "הודעה חדשה ב%1", "new-messages-in": "%1 הודעות חדשות ב%2", - "user-posted-in-public-room": "%1 כתב ב%3", - "user-posted-in-public-room-dual": "%1 ו%2 כתבו ב%4", - "user-posted-in-public-room-triple": "%1, %2 ו%3 כתבו ב%5", - "user-posted-in-public-room-multiple": "%1, %2 ו-%3 אחרים כתבו ב%5", - "upvoted-your-post-in": "%1 upvoted your post in %2", - "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", - "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", - "upvoted-your-post-in-multiple": "%1, %2 and %3 others upvoted your post in %4.", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "upvoted-your-post-in": "%1 הצביע בעד הפוסט שלך %2", + "upvoted-your-post-in-dual": "%1 ו%2 הצביעו בעד הפוסט שלך ב%3", + "upvoted-your-post-in-triple": "%1, %2 ו%3 הצביעו בעד הפוסט שלך ב-%4.", + "upvoted-your-post-in-multiple": "%1, %2 ו-%3 אחרים הצביעו בעד הפוסט שלך ב-%4.", "moved-your-post": "%1 העביר את הפוסט שלך ל%2", "moved-your-topic": "%1 הזיז את %2", - "user-flagged-post-in": "%1 flagged a post in %2", - "user-flagged-post-in-dual": "%1 and %2 flagged a post in %3", - "user-flagged-post-in-triple": "%1, %2 and %3 flagged a post in %4", - "user-flagged-post-in-multiple": "%1, %2 and %3 others flagged a post in %4", + "user-flagged-post-in": "%1 דיווח על פוסט ב %2", + "user-flagged-post-in-dual": "%1 ו%2 סימנו פוסט ב%3", + "user-flagged-post-in-triple": "%1, %2 ו%3 דיווחו על פוסט ב-%4", + "user-flagged-post-in-multiple": "%1, %2 ו-%3 אחרים דיווחו על פוסט ב-%4", "user-flagged-user": "%1 דיווח על משתמש (%2)", "user-flagged-user-dual": "%1 ו - %2 דיווחו על משתמש (%3)", "user-flagged-user-triple": "%1, %2 ו%3 דיווחו על פרופיל משתמש (%4)", "user-flagged-user-multiple": "%1, %2 ו-%3 אחרים דיווחו על פרופיל משתמש (%4)", - "user-posted-to": "%1 posted a reply in %2", - "user-posted-to-dual": "%1 and %2 replied in %3", - "user-posted-to-triple": "%1, %2 and %3 replied in %4", - "user-posted-to-multiple": "%1, %2 and %3 others replied in %4", + "user-posted-to": "%1 פרסם תגובה ל: %2", + "user-posted-to-dual": "%1 ו%2 הגיבו ל: %3", + "user-posted-to-triple": "%1, %2 ו%3 הגיבו ל: %4", + "user-posted-to-multiple": "%1, %2 ו-%3 אחרים הגיבו ל: %4", "user-posted-topic": "%1 posted %2", "user-edited-post": "%1 edited a post in %2", "user-posted-topic-with-tag": "%1 posted %2 (tagged %3)", diff --git a/public/language/hr/notifications.json b/public/language/hr/notifications.json index 9aace0d982..884c670cca 100644 --- a/public/language/hr/notifications.json +++ b/public/language/hr/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/hu/notifications.json b/public/language/hu/notifications.json index ba96a71526..fa00f20966 100644 --- a/public/language/hu/notifications.json +++ b/public/language/hu/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/hy/notifications.json b/public/language/hy/notifications.json index 9e5e9ad716..55a570ff70 100644 --- a/public/language/hy/notifications.json +++ b/public/language/hy/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 գրել են %3 - ում", - "user-posted-in-public-room-dual": "%1 և %2 գրել են%4-ում", - "user-posted-in-public-room-triple": "%1, %2 և %3 գրել են %5 - ում", - "user-posted-in-public-room-multiple": "%1, %2 և %3 ուրիշները գրել են%5 - ում", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/id/notifications.json b/public/language/id/notifications.json index b7083c2324..4dc9a30c7a 100644 --- a/public/language/id/notifications.json +++ b/public/language/id/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index c639316df3..de128cdd45 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -22,41 +22,41 @@ "upvote": "Voti positivi", "awards": "Premi", "new-flags": "Nuove segnalazioni", - "my-flags": "My Flags", - "bans": "Espulsioni", + "my-flags": "Le mie segnalazioni", + "bans": "Bannati", "new-message-from": "Nuovo messaggio da %1", "new-messages-from": "%1 nuovi messaggi da %2", "new-message-in": "Nuovo messaggio in %1", "new-messages-in": "%1 nuovi messaggi in %2", - "user-posted-in-public-room": "%1 ha scritto in %3", - "user-posted-in-public-room-dual": "%1 e %2 hanno scritto in %4", - "user-posted-in-public-room-triple": "%1, %2 e %3 hanno scritto in %5", - "user-posted-in-public-room-multiple": "%1, %2 e %3 altri hanno scritto in %5", - "upvoted-your-post-in": "%1 upvoted your post in %2", - "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", - "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", - "upvoted-your-post-in-multiple": "%1, %2 and %3 others upvoted your post in %4.", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "upvoted-your-post-in": "%1 ha votato positivamente il tuo post in %2", + "upvoted-your-post-in-dual": "%1 e %2 hanno votato positivamente il tuo post in %3", + "upvoted-your-post-in-triple": "%1, %2 e %3 hanno votato positivamente il tuo post in %4", + "upvoted-your-post-in-multiple": "%1, %2 e %3 altri hanno votato positivamente il tuo post in %4.", "moved-your-post": "%1 ha spostato il tuo post su %2", "moved-your-topic": "%1 è stato spostato %2", - "user-flagged-post-in": "%1 flagged a post in %2", - "user-flagged-post-in-dual": "%1 and %2 flagged a post in %3", - "user-flagged-post-in-triple": "%1, %2 and %3 flagged a post in %4", - "user-flagged-post-in-multiple": "%1, %2 and %3 others flagged a post in %4", + "user-flagged-post-in": "%1 ha segnalato un post in %2", + "user-flagged-post-in-dual": "%1 e %2 hanno segnalato un post in %3", + "user-flagged-post-in-triple": "%1, %2 e %3 hanno segnalato un post in %4", + "user-flagged-post-in-multiple": "%1, %2 e %3 altri hanno segnalato un post in %4", "user-flagged-user": "%1 ha segnalato un utente (%2)", "user-flagged-user-dual": "%1 e %2 hanno segnalato un utente (%3)", "user-flagged-user-triple": "%1, %2 e %3 hanno segnalato un profilo utente (%4)", "user-flagged-user-multiple": "%1, %2 e %3 altri hanno segnalato un profilo utente (%4)", - "user-posted-to": "%1 posted a reply in %2", - "user-posted-to-dual": "%1 and %2 replied in %3", - "user-posted-to-triple": "%1, %2 and %3 replied in %4", - "user-posted-to-multiple": "%1, %2 and %3 others replied in %4", - "user-posted-topic": "%1 posted %2", - "user-edited-post": "%1 edited a post in %2", - "user-posted-topic-with-tag": "%1 posted %2 (tagged %3)", - "user-posted-topic-with-tag-dual": "%1 posted %2 (tagged %3 and %4)", - "user-posted-topic-with-tag-triple": "%1 posted %2 (tagged %3, %4, and %5)", - "user-posted-topic-with-tag-multiple": "%1 posted %2 (tagged %3)", - "user-posted-topic-in-category": "%1 posted %2 in %3", + "user-posted-to": "%1 ha postato una risposta in %2", + "user-posted-to-dual": "%1 e %2 hanno risposto in %3", + "user-posted-to-triple": "%1, %2 e %3 hanno risposto in %4", + "user-posted-to-multiple": "%1, %2 e %3 altri hanno risposto in %4", + "user-posted-topic": "%1 postato %2", + "user-edited-post": "%1 ha modificato un post in %2", + "user-posted-topic-with-tag": "%1 postato %2 (contrassegnato %3)", + "user-posted-topic-with-tag-dual": "%1 postato %2 (contrassegnato %3 e %4)", + "user-posted-topic-with-tag-triple": "%1 postato %2 (contrassegnato %3, %4, e %5)", + "user-posted-topic-with-tag-multiple": "%1 postato %2 (contrassegnato %3)", + "user-posted-topic-in-category": "%1 postato %2 in %3", "user-started-following-you": "%1 ha iniziato a seguirti.", "user-started-following-you-dual": "%1 e %2 hanno iniziato a seguirti.", "user-started-following-you-triple": "%1, %2 e %3 hanno iniziato a seguirti.", @@ -71,8 +71,8 @@ "users-csv-exported": "Utenti esportati in CSV, clicca per scaricare", "post-queue-accepted": "Il tuo post in coda è stato accettato. Clicca qui per vedere il tuo post.", "post-queue-rejected": "Il tuo post in coda è stato rifiutato.", - "post-queue-rejected-for-reason": "Your queued post has been rejected for the following reason: \"%1\"", - "post-queue-notify": "Queued post received a notification: \"%1\"", + "post-queue-rejected-for-reason": "Il tuo post in coda è stato rifiutato per il seguente motivo: \"%1\"", + "post-queue-notify": "Il post in coda ha ricevuto una notifica: \"%1\"", "email-confirmed": "Email Confermata", "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", @@ -84,7 +84,7 @@ "notificationType-upvote": "Quando il tuo post riceve un Mi Piace", "notificationType-new-topic": "Quando qualcuno che segui posta una discussione", "notificationType-new-topic-with-tag": "Quando una discussione viene postata con un tag che segui", - "notificationType-new-topic-in-category": "Quando una discussione viene pubblicata in una categoria che stai seguendo", + "notificationType-new-topic-in-category": "Quando una discussione viene postata in una categoria che stai seguendo", "notificationType-new-reply": "Quando viene postata una nuova risposta in una discussione che stai seguendo", "notificationType-post-edit": "Quando un post viene modificato in una discussione che stai guardando", "notificationType-follow": "Quando qualcuno inizia a seguirti", @@ -102,5 +102,5 @@ "activitypub.announce": "%1 ha condiviso il tuo post in %2 agli utenti che lo seguono.", "activitypub.announce-dual": "%1 e %2 hanno condiviso il tuo post in %3 agli utenti che li seguono.", "activitypub.announce-triple": "%1, %2 e %3 hanno condiviso il tuo post in %4 agli utenti che li seguono.", - "activitypub.announce-multiple": "%1, %2 e %3 hanno condiviso il tuo post in %4 agli utenti che li seguono." + "activitypub.announce-multiple": "%1, %2 e %3 altri hanno condiviso il tuo post in %4 agli utenti che li seguono." } \ No newline at end of file diff --git a/public/language/ja/notifications.json b/public/language/ja/notifications.json index 6504e2b38b..69b1ec5606 100644 --- a/public/language/ja/notifications.json +++ b/public/language/ja/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/ko/notifications.json b/public/language/ko/notifications.json index 841a45c2d6..2766564f09 100644 --- a/public/language/ko/notifications.json +++ b/public/language/ko/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%2님의 %1개의 새로운 메시지", "new-message-in": "%1에서 새로운 메시지", "new-messages-in": "%2에서 %1개의 새로운 메시지", - "user-posted-in-public-room": "%1님이 %3에 게시했습니다.", - "user-posted-in-public-room-dual": "%1%2님이 %4에 게시했습니다.", - "user-posted-in-public-room-triple": "%1, %2%3님이 %5에 게시했습니다.", - "user-posted-in-public-room-multiple": "%1, %2 및 다른 %3명이 %5에 게시했습니다.", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/lt/notifications.json b/public/language/lt/notifications.json index 85bdd407eb..77899ed417 100644 --- a/public/language/lt/notifications.json +++ b/public/language/lt/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/lv/notifications.json b/public/language/lv/notifications.json index 86a9d7267f..f4bdff05fb 100644 --- a/public/language/lv/notifications.json +++ b/public/language/lv/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/ms/notifications.json b/public/language/ms/notifications.json index 6faf4e069b..1594484920 100644 --- a/public/language/ms/notifications.json +++ b/public/language/ms/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/nb/notifications.json b/public/language/nb/notifications.json index 6f968c6fce..52d1f77e13 100644 --- a/public/language/nb/notifications.json +++ b/public/language/nb/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 nye meldinger fra %2", "new-message-in": "Ny melding i %1", "new-messages-in": "%1 nye meldinger i %2", - "user-posted-in-public-room": "%1 skrev i %3", - "user-posted-in-public-room-dual": "%1 og %2 skrev i %4", - "user-posted-in-public-room-triple": "%1, %2 og %3 skrev i %5", - "user-posted-in-public-room-multiple": "%1, %2 og %3 andre skrev i %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json index 6642cb8798..6b5a769147 100644 --- a/public/language/nl/notifications.json +++ b/public/language/nl/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/nn-NO/notifications.json b/public/language/nn-NO/notifications.json index dceaec1cfd..18db20bcba 100644 --- a/public/language/nn-NO/notifications.json +++ b/public/language/nn-NO/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 nye meldingar frå %2", "new-message-in": "Ny melding i %1", "new-messages-in": "%1 nye meldingar i %2", - "user-posted-in-public-room": "%1 skreiv i %3", - "user-posted-in-public-room-dual": "%1 og %2 skreiv i %4", - "user-posted-in-public-room-triple": "%1, %2 og %3 skreiv i %5", - "user-posted-in-public-room-multiple": "%1, %2 og %3 andre skreiv i %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/pl/notifications.json b/public/language/pl/notifications.json index 8b3910634a..ecb23d9ec0 100644 --- a/public/language/pl/notifications.json +++ b/public/language/pl/notifications.json @@ -22,41 +22,41 @@ "upvote": "Głosy za", "awards": "Nagrody", "new-flags": "Nowe flagi", - "my-flags": "My Flags", + "my-flags": "Moje flagi", "bans": "Bany", "new-message-from": "Nowa wiadomość od %1", "new-messages-from": "%1 nowych wiadomości od %2", "new-message-in": "Nowa wiadomość w %1", "new-messages-in": "%1 nowych wiadomości w %2", - "user-posted-in-public-room": "%1 napisał w %3", - "user-posted-in-public-room-dual": "%1 i %2 napisali w %4", - "user-posted-in-public-room-triple": "%1, %2 i %3 napisali w %5", - "user-posted-in-public-room-multiple": "%1, %2 i %3 innych napisali w %5", - "upvoted-your-post-in": "%1 upvoted your post in %2", - "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", - "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", - "upvoted-your-post-in-multiple": "%1, %2 and %3 others upvoted your post in %4.", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "upvoted-your-post-in": "%1 zagłosował na Twój post w %2", + "upvoted-your-post-in-dual": "%1 oraz %2 zagłosowali na Twój post w %3", + "upvoted-your-post-in-triple": "%1, %2 i%3 zagłosowali na Twój post w %4", + "upvoted-your-post-in-multiple": "%1, %2 i %3 innych zagłosowali na Twój post w %4", "moved-your-post": "%1 przeniósł Twój post do %2", "moved-your-topic": "%1 przeniósł %2", - "user-flagged-post-in": "%1 flagged a post in %2", - "user-flagged-post-in-dual": "%1 and %2 flagged a post in %3", - "user-flagged-post-in-triple": "%1, %2 and %3 flagged a post in %4", - "user-flagged-post-in-multiple": "%1, %2 and %3 others flagged a post in %4", + "user-flagged-post-in": "%1 oflagował post w %2", + "user-flagged-post-in-dual": "%1 oraz %2 oflagowali post w %3", + "user-flagged-post-in-triple": "%1, %2 oraz %3 oflagowali post w %4", + "user-flagged-post-in-multiple": "%1, %2 i %3 innych oflagowali post w %4", "user-flagged-user": "%1 oflagował profil użytkownika (%2)", "user-flagged-user-dual": "%1 oraz %2 oflagowali profil użytkownika (%3)", "user-flagged-user-triple": "%1, %2 i %3 oflagowali profil użytkownika (%4)", "user-flagged-user-multiple": "%1, %2 i %3 innych oflagowali profil użytkownika (%4)", - "user-posted-to": "%1 posted a reply in %2", - "user-posted-to-dual": "%1 and %2 replied in %3", - "user-posted-to-triple": "%1, %2 and %3 replied in %4", - "user-posted-to-multiple": "%1, %2 and %3 others replied in %4", - "user-posted-topic": "%1 posted %2", - "user-edited-post": "%1 edited a post in %2", - "user-posted-topic-with-tag": "%1 posted %2 (tagged %3)", - "user-posted-topic-with-tag-dual": "%1 posted %2 (tagged %3 and %4)", - "user-posted-topic-with-tag-triple": "%1 posted %2 (tagged %3, %4, and %5)", - "user-posted-topic-with-tag-multiple": "%1 posted %2 (tagged %3)", - "user-posted-topic-in-category": "%1 posted %2 in %3", + "user-posted-to": "%1 dodał odpowiedź w %2", + "user-posted-to-dual": "%1 i %2 odpowiedzieli w %3", + "user-posted-to-triple": "%1, %2 i %3 odpowiedzieli w %4", + "user-posted-to-multiple": "%1, %2 i %3 innych odpowiedzieli w %4", + "user-posted-topic": "%1 dodał %2", + "user-edited-post": "%1 edytował post w %2", + "user-posted-topic-with-tag": "%1 dodał %2 (tag %3)", + "user-posted-topic-with-tag-dual": "%1 dodał %2 (tagi %3 i %4)", + "user-posted-topic-with-tag-triple": "%1 dodał %2 (tagi %3, %4 i %5)", + "user-posted-topic-with-tag-multiple": "%1 dodał %2 (tag %3)", + "user-posted-topic-in-category": "%1 dodał %2 w %3", "user-started-following-you": "%1 zaczął Cię obserwować.", "user-started-following-you-dual": "%1 oraz %2 zaczęli Cię obserwować.", "user-started-following-you-triple": "%1, %2 i %3 zaczęli Cię obserwować.", @@ -71,8 +71,8 @@ "users-csv-exported": "Plik csv użytkowników wyeksportowany, kliknij aby pobrać", "post-queue-accepted": "Twój post oczekujący w kolejce został zaakceptowany. Kliknij tutaj, aby go zobaczyć.", "post-queue-rejected": "Twój post oczekujący w kolejce został odrzucony.", - "post-queue-rejected-for-reason": "Your queued post has been rejected for the following reason: \"%1\"", - "post-queue-notify": "Queued post received a notification: \"%1\"", + "post-queue-rejected-for-reason": "Twój post oczekujący został odrzucony z następującego powodu: \"%1\"", + "post-queue-notify": "Post oczekujący w kolejce otrzymał powiadomienie: \"%1\"", "email-confirmed": "E-mail potwierdzony", "email-confirmed-message": "Dziękujemy za potwierdzenie maila. Twoje konto zostało aktywowane.", "email-confirm-error-message": "Wystąpił problem przy aktywacji - kod jest błędny lub przestarzały", diff --git a/public/language/pt-BR/notifications.json b/public/language/pt-BR/notifications.json index ddc68ff548..9b994e6209 100644 --- a/public/language/pt-BR/notifications.json +++ b/public/language/pt-BR/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/pt-PT/notifications.json b/public/language/pt-PT/notifications.json index a23b9701a5..0a59ac1100 100644 --- a/public/language/pt-PT/notifications.json +++ b/public/language/pt-PT/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/ro/notifications.json b/public/language/ro/notifications.json index 305d5b5414..4eb574d8c2 100644 --- a/public/language/ro/notifications.json +++ b/public/language/ro/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json index a4e2db9c7d..dcc7c4b97b 100644 --- a/public/language/ru/notifications.json +++ b/public/language/ru/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/rw/notifications.json b/public/language/rw/notifications.json index 5625d74e5a..30c257731e 100644 --- a/public/language/rw/notifications.json +++ b/public/language/rw/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/sc/notifications.json b/public/language/sc/notifications.json index 84c1370919..5d9bee4d29 100644 --- a/public/language/sc/notifications.json +++ b/public/language/sc/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/sk/notifications.json b/public/language/sk/notifications.json index 13d5f0adbf..31cce7c4b2 100644 --- a/public/language/sk/notifications.json +++ b/public/language/sk/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/sl/notifications.json b/public/language/sl/notifications.json index 1d850a4498..573185bd44 100644 --- a/public/language/sl/notifications.json +++ b/public/language/sl/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/sq-AL/notifications.json b/public/language/sq-AL/notifications.json index 9a13c89ed2..9974d0b777 100644 --- a/public/language/sq-AL/notifications.json +++ b/public/language/sq-AL/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/sr/notifications.json b/public/language/sr/notifications.json index 3f844ba53e..5be7e51f7c 100644 --- a/public/language/sr/notifications.json +++ b/public/language/sr/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 је написао у %3", - "user-posted-in-public-room-dual": "%1 и %2 су написали у %4", - "user-posted-in-public-room-triple": "%1, %2 и %3 су написали у %5", - "user-posted-in-public-room-multiple": "%1, %2 и осталих %3 су написали у %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/sv/notifications.json b/public/language/sv/notifications.json index f748786e67..2a22fb75c2 100644 --- a/public/language/sv/notifications.json +++ b/public/language/sv/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 skrev i %3", - "user-posted-in-public-room-dual": "%1 och %2 skrev i %4", - "user-posted-in-public-room-triple": "%1, %2 och %3 skrev i %5", - "user-posted-in-public-room-multiple": "%1, %2 och %3 andra skrev i %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/th/notifications.json b/public/language/th/notifications.json index b3c1eaade7..1c9683c641 100644 --- a/public/language/th/notifications.json +++ b/public/language/th/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 ข้อความใหม่จาก %2", "new-message-in": "ข้อความใหม่ใน %1", "new-messages-in": "%1 ข้อความใหม่ใน %2", - "user-posted-in-public-room": "%1 ได้เขียนลงใน %3", - "user-posted-in-public-room-dual": "%1 และ %2 ได้เขียนลงใน %4", - "user-posted-in-public-room-triple": "%1, %2 และ %3 ได้เขียนลงใน %5", - "user-posted-in-public-room-multiple": "%1, %2 และอีก %3 คน ได้เขียนลงใน %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json index b88efbd89c..6f7a08fb4e 100644 --- a/public/language/tr/notifications.json +++ b/public/language/tr/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%2 kullanıcısından %1 yeni mesaj var", "new-message-in": "%1 odasında yeni mesaj var", "new-messages-in": "%2 odasında %1 yeni mesaj var", - "user-posted-in-public-room": "%1 şu odaya yazdı: %3", - "user-posted-in-public-room-dual": "%1 ve %2 şu odaya yazdı: %4", - "user-posted-in-public-room-triple": "%1, %2 ve %3 şu odaya yazdılar: %5", - "user-posted-in-public-room-multiple": "%1, %2 ve %3 diğer kullanıcı şu odaya yazdılar: %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/uk/notifications.json b/public/language/uk/notifications.json index 86212dddcb..3d0c77598e 100644 --- a/public/language/uk/notifications.json +++ b/public/language/uk/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/ur/notifications.json b/public/language/ur/notifications.json index 817914ede7..283e0551ad 100644 --- a/public/language/ur/notifications.json +++ b/public/language/ur/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%2 سے %1 نئے پیغامات", "new-message-in": "%1 میں نیا پیغام", "new-messages-in": "%2 میں %1 نئے پیغامات", - "user-posted-in-public-room": "%1 نے %3 میں لکھا", - "user-posted-in-public-room-dual": "%1 اور %2 نے %4 میں لکھا", - "user-posted-in-public-room-triple": "%1، %2 اور %3 نے %5 میں لکھا", - "user-posted-in-public-room-multiple": "%1، %2 اور %3 دیگر نے %5 میں لکھا", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json index c01856d57f..40fc392f0c 100644 --- a/public/language/vi/notifications.json +++ b/public/language/vi/notifications.json @@ -22,39 +22,39 @@ "upvote": "Ủng hộ", "awards": "Giải thưởng", "new-flags": "Cảnh báo mới", - "my-flags": "My Flags", + "my-flags": "Gắn Cờ Của Tôi", "bans": "Cấm", "new-message-from": "Tin nhắn mới từ %1", "new-messages-from": "%1 tin nhắn mới từ %2", "new-message-in": "Tin nhắn mới trong %1", "new-messages-in": "%1 tin nhắn mới trong %2", - "user-posted-in-public-room": "%1 đã viết vào %3", - "user-posted-in-public-room-dual": "%1%2 đã viết vào %4", - "user-posted-in-public-room-triple": "%1, %2%3 đã viết vào %5", - "user-posted-in-public-room-multiple": "%1, %2 và %3 người khác đã viết vào %5", - "upvoted-your-post-in": "%1 upvoted your post in %2", - "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", - "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", - "upvoted-your-post-in-multiple": "%1, %2 and %3 others upvoted your post in %4.", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "upvoted-your-post-in": "%1 đã ủng hộ bài của bạn trong %2", + "upvoted-your-post-in-dual": "%1%2 đã ủng hộ bài của bạn trong %3", + "upvoted-your-post-in-triple": "%1, %2%3 đã ủng hộ bài của bạn trong %4", + "upvoted-your-post-in-multiple": "%1, %2 và %3 người khác đã ủng hộ bài của bạn trong %4", "moved-your-post": "%1 đã chuyển bài viết của bạn tới %2", "moved-your-topic": "%1 đã chuyển %2", - "user-flagged-post-in": "%1 flagged a post in %2", - "user-flagged-post-in-dual": "%1 and %2 flagged a post in %3", - "user-flagged-post-in-triple": "%1, %2 and %3 flagged a post in %4", - "user-flagged-post-in-multiple": "%1, %2 and %3 others flagged a post in %4", + "user-flagged-post-in": "%1 đã gắn cờ một bài trong %2", + "user-flagged-post-in-dual": "%1%2 đã gắn cờ một bài trong %3", + "user-flagged-post-in-triple": "%1, %2%3 gắn cờ một bài trong %4", + "user-flagged-post-in-multiple": "%1, %2 và %3 người khác đã gắn cờ một bài trong %4", "user-flagged-user": "%1 đã gắn cờ một hồ sơ người dùng (%2)", "user-flagged-user-dual": "%1%2 đã gắn cờ một hồ sơ người dùng (%3)", "user-flagged-user-triple": "%1, %2%3 đã gắn cờ một hồ sơ người dùng (%4)", "user-flagged-user-multiple": "%1, %2 và %3 người khác đã gắn cờ một hồ sơ người dùng (%4)", - "user-posted-to": "%1 posted a reply in %2", - "user-posted-to-dual": "%1 and %2 replied in %3", - "user-posted-to-triple": "%1, %2 and %3 replied in %4", - "user-posted-to-multiple": "%1, %2 and %3 others replied in %4", - "user-posted-topic": "%1 posted %2", - "user-edited-post": "%1 edited a post in %2", - "user-posted-topic-with-tag": "%1 posted %2 (tagged %3)", - "user-posted-topic-with-tag-dual": "%1 posted %2 (tagged %3 and %4)", - "user-posted-topic-with-tag-triple": "%1 posted %2 (tagged %3, %4, and %5)", + "user-posted-to": "%1 đã đăng một trả lời trong %2", + "user-posted-to-dual": "%1%2 đã trả lời trong %3", + "user-posted-to-triple": "%1, %2%3 đã trả lời trong %4", + "user-posted-to-multiple": "%1, %2 và %3 người khác đã trả lời trong %4", + "user-posted-topic": "%1 đã đăng %2", + "user-edited-post": "%1 đã chỉnh sửa một bài đăng trong %2", + "user-posted-topic-with-tag": "%1 đã đăng %2 (đã gắn thẻ %3)", + "user-posted-topic-with-tag-dual": "%1 đã đăng %2 (đã gắn thẻ %3 và %4)", + "user-posted-topic-with-tag-triple": "%1 đã đăng %2 (đã gắn thẻ %3, %4, và %5)", "user-posted-topic-with-tag-multiple": "%1 posted %2 (tagged %3)", "user-posted-topic-in-category": "%1 posted %2 in %3", "user-started-following-you": "%1 bắt đầu theo dõi bạn.", diff --git a/public/language/zh-CN/notifications.json b/public/language/zh-CN/notifications.json index b361e71e34..6194661a2a 100644 --- a/public/language/zh-CN/notifications.json +++ b/public/language/zh-CN/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "来自 %2 的 %1 条新消息", "new-message-in": "%1的新消息", "new-messages-in": " %2的 %1 条新消息", - "user-posted-in-public-room": "%1%3发表", - "user-posted-in-public-room-dual": "%1%2%4发表", - "user-posted-in-public-room-triple": "%1, %2%3%5发表", - "user-posted-in-public-room-multiple": "%1, %2 和其余 %3 人在 %5发表", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 赞了帖子 %2", "upvoted-your-post-in-dual": "%1%2 赞了帖子 %3", "upvoted-your-post-in-triple": "%1, %2%3 赞了帖子 %4", @@ -71,7 +71,7 @@ "users-csv-exported": "用户列表 CSV 已导出,点击以下载", "post-queue-accepted": "您先前提交的帖子已通过查验,点击这里查看您的帖子。", "post-queue-rejected": "您先前提交的帖子已被拒绝", - "post-queue-rejected-for-reason": "Your queued post has been rejected for the following reason: \"%1\"", + "post-queue-rejected-for-reason": "您的待发布帖子因以下原因被拒绝:\"%1\"", "post-queue-notify": "队列中的帖子收到通知:\"%1\"", "email-confirmed": "电子邮箱已确认", "email-confirmed-message": "感谢您验证您的电子邮箱。您的帐户现已完全激活。", diff --git a/public/language/zh-TW/notifications.json b/public/language/zh-TW/notifications.json index d150073fdd..f97f52b7de 100644 --- a/public/language/zh-TW/notifications.json +++ b/public/language/zh-TW/notifications.json @@ -28,10 +28,10 @@ "new-messages-from": "%1 new messages from %2", "new-message-in": "New message in %1", "new-messages-in": "%1 new messages in %2", - "user-posted-in-public-room": "%1 wrote in %3", - "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", - "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", - "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", + "user-posted-in-public-room": "%1 wrote in %3", + "user-posted-in-public-room-dual": "%1 and %2 wrote in %4", + "user-posted-in-public-room-triple": "%1, %2 and %3 wrote in %5", + "user-posted-in-public-room-multiple": "%1, %2 and %3 others wrote in %5", "upvoted-your-post-in": "%1 upvoted your post in %2", "upvoted-your-post-in-dual": "%1 and %2 upvoted your post in %3", "upvoted-your-post-in-triple": "%1, %2 and %3 upvoted your post in %4", From caba37c911ed752d848207c43ebde0e8cd776e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 13:50:20 -0500 Subject: [PATCH 4249/4744] dont display bars on mobile --- src/views/partials/chats/system-message.tpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/partials/chats/system-message.tpl b/src/views/partials/chats/system-message.tpl index c94254303c..04cbfcf588 100644 --- a/src/views/partials/chats/system-message.tpl +++ b/src/views/partials/chats/system-message.tpl @@ -1,7 +1,7 @@ -
  • -
    +
  • +
    [[modules:chat.system.{messages.content}, {messages.fromUser.displayname}, {messages.timestampISO}]]
    -
    +
  • \ No newline at end of file From 9c5ffe361ca7b485d6977d7b92f80ac65bb65690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 14:13:33 -0500 Subject: [PATCH 4250/4744] fix: closes #14002, add max-height --- public/src/client/chats.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 9c0c7c0ddf..1c943e9889 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -577,9 +577,11 @@ define('forum/chats', [ 'z-index': 20000, flex: 0, top: 'inherit', + 'max-height': '250px', + overflow: 'auto', }, placement: 'top', - className: `chat-autocomplete-dropdown-${roomId} dropdown-menu textcomplete-dropdown`, + className: `chat-autocomplete-dropdown-${roomId} dropdown-menu textcomplete-dropdown ghost-scrollbar`, ...options, }, }; From 53927b4242f5bb5bff27b9dd46e382996446656c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 14:24:05 -0500 Subject: [PATCH 4251/4744] wait for images before scrolling to target msg --- public/src/client/chats.js | 6 +----- public/src/client/chats/messages.js | 9 +++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 1c943e9889..f418753e38 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -74,11 +74,7 @@ define('forum/chats', [ if (ajaxify.data.scrollToIndex) { messages.toggleScrollUpAlert(chatContentEl); const scrollToEl = chatContentEl.find(`[data-index="${ajaxify.data.scrollToIndex - 1}"]`); - if (scrollToEl.length) { - chatContentEl.scrollTop( - chatContentEl.scrollTop() - chatContentEl.offset().top + scrollToEl.offset().top - ); - } + messages.scrollToMessageAfterImageLoad(chatContentEl, scrollToEl); } else { messages.scrollToBottomAfterImageLoad(chatContentEl); } diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index 40c46960c3..c06e8c1c4d 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -157,6 +157,15 @@ define('forum/chats/messages', [ } }; + messages.scrollToMessageAfterImageLoad = function (containerEl, msgEl) { + if (containerEl.length && msgEl.length) { + const msgBodyEls = containerEl[0].querySelectorAll('[component="chat/message/body"]'); + imagesLoaded(msgBodyEls, () => { + msgEl[0].scrollIntoView(true); + }); + } + }; + messages.scrollToBottom = function (containerEl) { if (containerEl && containerEl.length) { containerEl.attr('data-ignore-next-scroll', 1); From 55d62c082fa6fd1d072d07eff551e1fc3ca7e782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 18 Feb 2026 16:59:11 -0500 Subject: [PATCH 4252/4744] add inline script to scroll to bottom --- src/views/partials/chats/message-window.tpl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/views/partials/chats/message-window.tpl b/src/views/partials/chats/message-window.tpl index 9dda8d45dc..71f6f10446 100644 --- a/src/views/partials/chats/message-window.tpl +++ b/src/views/partials/chats/message-window.tpl @@ -27,6 +27,12 @@
    +
    [[admin/advanced/jobs:next-run]] [[admin/advanced/jobs:last-duration]] [[admin/advanced/jobs:running]][[admin/advanced/jobs:active]]
    {./cronTimeHuman} ({./cronTime}) {./durationReadable}{{{ if ./running }}}Yes{{{ else }}}No{{{ end }}}{{{ if ./running }}}{{{ else }}}{{{ end }}}{{{ if ./active }}}{{{ else }}}{{{ end }}}
    + + + + + + + {{{ each blocklists }}} + + + + + + {{{ end }}} + + + + + + +
    [[admin/settings/activitypub:blocklists.url]][[admin/settings/activitypub:blocklists.count]]
    {./url}{./count} +
    + + + +
    +
    + +
    +
    + + +
    [[admin/settings/activitypub:server-filtering]]
    diff --git a/src/views/admin/partials/activitypub/blocklists.tpl b/src/views/admin/partials/activitypub/blocklists.tpl new file mode 100644 index 0000000000..76ad526397 --- /dev/null +++ b/src/views/admin/partials/activitypub/blocklists.tpl @@ -0,0 +1,10 @@ +

    [[admin/settings/activitypub:blocklists.add-help]]

    + +
    + + +
    + + +
    + \ No newline at end of file From 04b22a261ce54031bf034e4359528c0c3445fe79 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 25 Mar 2026 15:34:15 +0000 Subject: [PATCH 4704/4744] chore(i18n): fallback strings for new resources: nodebb.admin-settings-activitypub --- public/language/ar/admin/settings/activitypub.json | 11 +++++++++++ public/language/az/admin/settings/activitypub.json | 11 +++++++++++ public/language/bg/admin/settings/activitypub.json | 11 +++++++++++ public/language/bn/admin/settings/activitypub.json | 11 +++++++++++ public/language/cs/admin/settings/activitypub.json | 11 +++++++++++ public/language/da/admin/settings/activitypub.json | 11 +++++++++++ public/language/de/admin/settings/activitypub.json | 11 +++++++++++ public/language/el/admin/settings/activitypub.json | 11 +++++++++++ public/language/en-US/admin/settings/activitypub.json | 11 +++++++++++ .../en-x-pirate/admin/settings/activitypub.json | 11 +++++++++++ public/language/es/admin/settings/activitypub.json | 11 +++++++++++ public/language/et/admin/settings/activitypub.json | 11 +++++++++++ public/language/fa-IR/admin/settings/activitypub.json | 11 +++++++++++ public/language/fi/admin/settings/activitypub.json | 11 +++++++++++ public/language/fr/admin/settings/activitypub.json | 11 +++++++++++ public/language/gl/admin/settings/activitypub.json | 11 +++++++++++ public/language/he/admin/settings/activitypub.json | 11 +++++++++++ public/language/hr/admin/settings/activitypub.json | 11 +++++++++++ public/language/hu/admin/settings/activitypub.json | 11 +++++++++++ public/language/hy/admin/settings/activitypub.json | 11 +++++++++++ public/language/id/admin/settings/activitypub.json | 11 +++++++++++ public/language/it/admin/settings/activitypub.json | 11 +++++++++++ public/language/ja/admin/settings/activitypub.json | 11 +++++++++++ public/language/ko/admin/settings/activitypub.json | 11 +++++++++++ public/language/lt/admin/settings/activitypub.json | 11 +++++++++++ public/language/lv/admin/settings/activitypub.json | 11 +++++++++++ public/language/ms/admin/settings/activitypub.json | 11 +++++++++++ public/language/nb/admin/settings/activitypub.json | 11 +++++++++++ public/language/nl/admin/settings/activitypub.json | 11 +++++++++++ public/language/nn-NO/admin/settings/activitypub.json | 11 +++++++++++ public/language/pl/admin/settings/activitypub.json | 11 +++++++++++ public/language/pt-BR/admin/settings/activitypub.json | 11 +++++++++++ public/language/pt-PT/admin/settings/activitypub.json | 11 +++++++++++ public/language/ro/admin/settings/activitypub.json | 11 +++++++++++ public/language/ru/admin/settings/activitypub.json | 11 +++++++++++ public/language/rw/admin/settings/activitypub.json | 11 +++++++++++ public/language/sc/admin/settings/activitypub.json | 11 +++++++++++ public/language/sk/admin/settings/activitypub.json | 11 +++++++++++ public/language/sl/admin/settings/activitypub.json | 11 +++++++++++ public/language/sq-AL/admin/settings/activitypub.json | 11 +++++++++++ public/language/sr/admin/settings/activitypub.json | 11 +++++++++++ public/language/sv/admin/settings/activitypub.json | 11 +++++++++++ public/language/th/admin/settings/activitypub.json | 11 +++++++++++ public/language/tr/admin/settings/activitypub.json | 11 +++++++++++ public/language/uk/admin/settings/activitypub.json | 11 +++++++++++ public/language/ur/admin/settings/activitypub.json | 11 +++++++++++ public/language/vi/admin/settings/activitypub.json | 11 +++++++++++ public/language/zh-CN/admin/settings/activitypub.json | 11 +++++++++++ public/language/zh-TW/admin/settings/activitypub.json | 11 +++++++++++ 49 files changed, 539 insertions(+) diff --git a/public/language/ar/admin/settings/activitypub.json b/public/language/ar/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/ar/admin/settings/activitypub.json +++ b/public/language/ar/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/az/admin/settings/activitypub.json b/public/language/az/admin/settings/activitypub.json index 743357311d..ec71065a46 100644 --- a/public/language/az/admin/settings/activitypub.json +++ b/public/language/az/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtrlə", "count": "Bu NodeBB hazırda %1 server(lər)dən xəbərdardır", "server.filter-help": "NodeBB ilə federasiyaya mane olmaq istədiyiniz serverləri göstərin. Alternativ olaraq, bunun əvəzinə xüsusi serverlərlə federasiyaya seçimlə icazə verə bilərsiniz. Hər iki variant bir-birini istisna etsə də, dəstəklənir.", diff --git a/public/language/bg/admin/settings/activitypub.json b/public/language/bg/admin/settings/activitypub.json index 2bb55d5a3c..b685cadcae 100644 --- a/public/language/bg/admin/settings/activitypub.json +++ b/public/language/bg/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Активен", "relays.errors.invalid-url": "Моля, въведете правилен адрес", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Филтриране", "count": "Този NodeBB в момента знае за наличието на %1 сървър(а)", "server.filter-help": "Посочете сървърите, с които не искате Вашият NodeBB да осъществява връзка. Или можете вместо това да посочите конкретни сървъри, с които разрешавате връзката. И двете възможности са налични, но може да изберете само една от тях.", diff --git a/public/language/bn/admin/settings/activitypub.json b/public/language/bn/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/bn/admin/settings/activitypub.json +++ b/public/language/bn/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/cs/admin/settings/activitypub.json b/public/language/cs/admin/settings/activitypub.json index 6cb1acc7ad..c1bc80adf1 100644 --- a/public/language/cs/admin/settings/activitypub.json +++ b/public/language/cs/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Aktivní", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtrování", "count": "Tento NodeBB momentálně vi o %1 serveru/serverech.", "server.filter-help": "Zadejte servery, se kterými nechcete, aby vaše NodeBB federovalo. Alternativně můžete zvolit, že povolíte federaci pouze s vybranými servery. Obě možnosti jsou podporovány, ale vzájemně se vylučují.", diff --git a/public/language/da/admin/settings/activitypub.json b/public/language/da/admin/settings/activitypub.json index 649b7a2fd5..bfd6bf7241 100644 --- a/public/language/da/admin/settings/activitypub.json +++ b/public/language/da/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtrering", "count": "Denne NodeBB instans er lige nu bevidst om %1 server(e)", "server.filter-help": "Specificér servere, som du gerne vil stoppe fra at føderere med din NodeBB instans. Alternativt, kan du vælge at selektivt tillade føderation med udvalgte servere i stedet. Begge muligheder er understøttet, men man kan kun vælge en metode ad gangen.", diff --git a/public/language/de/admin/settings/activitypub.json b/public/language/de/admin/settings/activitypub.json index 0457b88314..2e48df5884 100644 --- a/public/language/de/admin/settings/activitypub.json +++ b/public/language/de/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Aktiv", "relays.errors.invalid-url": "Bitte gib eine gültige URL ein", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filterung", "count": "Dieses NodeBB kennt derzeit %1 Server", "server.filter-help": "Gib die Server an, die du von der Föderation mit deinem NodeBB ausschließen möchtest. Alternativ kannst du auch festlegen, dass die Föderation nur mit bestimmten Servern erlaubt ist. Beide Optionen werden unterstützt, schließen sich jedoch gegenseitig aus.", diff --git a/public/language/el/admin/settings/activitypub.json b/public/language/el/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/el/admin/settings/activitypub.json +++ b/public/language/el/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/en-US/admin/settings/activitypub.json b/public/language/en-US/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/en-US/admin/settings/activitypub.json +++ b/public/language/en-US/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/en-x-pirate/admin/settings/activitypub.json b/public/language/en-x-pirate/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/en-x-pirate/admin/settings/activitypub.json +++ b/public/language/en-x-pirate/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/es/admin/settings/activitypub.json b/public/language/es/admin/settings/activitypub.json index bff8ef9958..fd4e887d1a 100644 --- a/public/language/es/admin/settings/activitypub.json +++ b/public/language/es/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/et/admin/settings/activitypub.json b/public/language/et/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/et/admin/settings/activitypub.json +++ b/public/language/et/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/fa-IR/admin/settings/activitypub.json b/public/language/fa-IR/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/fa-IR/admin/settings/activitypub.json +++ b/public/language/fa-IR/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/fi/admin/settings/activitypub.json b/public/language/fi/admin/settings/activitypub.json index 83b3a399eb..97606c5c41 100644 --- a/public/language/fi/admin/settings/activitypub.json +++ b/public/language/fi/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/fr/admin/settings/activitypub.json b/public/language/fr/admin/settings/activitypub.json index f6fb57eb86..b9f6a5bf1b 100644 --- a/public/language/fr/admin/settings/activitypub.json +++ b/public/language/fr/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Actif", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtrage", "count": "Ce NodeBB connaît actuellement %1 serveur(s)", "server.filter-help": "Spécifiez les serveurs que vous souhaitez interdire de se fédérer avec votre NodeBB. Vous pouvez également autoriser sélectivement la fédération avec certains serveurs. Ces deux options sont possibles, mais incompatibles.", diff --git a/public/language/gl/admin/settings/activitypub.json b/public/language/gl/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/gl/admin/settings/activitypub.json +++ b/public/language/gl/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/he/admin/settings/activitypub.json b/public/language/he/admin/settings/activitypub.json index 6be5a095f0..54a466afc7 100644 --- a/public/language/he/admin/settings/activitypub.json +++ b/public/language/he/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "פעיל", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "סינון", "count": "NodeBB זה מודע כרגע ל-%1 שרתים", "server.filter-help": "ציין שרתים שברצונך למנוע מהתאחדות עם ה-NodeBB שלך. לחלופין, אתה יכול לבחור באופן סלקטיבי פדרציה מאושרים עם שרתים ספציפיים, במקום זאת. שתי האפשרויות נתמכות, אם כי הן סותרות זו את זו.", diff --git a/public/language/hr/admin/settings/activitypub.json b/public/language/hr/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/hr/admin/settings/activitypub.json +++ b/public/language/hr/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/hu/admin/settings/activitypub.json b/public/language/hu/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/hu/admin/settings/activitypub.json +++ b/public/language/hu/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/hy/admin/settings/activitypub.json b/public/language/hy/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/hy/admin/settings/activitypub.json +++ b/public/language/hy/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/id/admin/settings/activitypub.json b/public/language/id/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/id/admin/settings/activitypub.json +++ b/public/language/id/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/it/admin/settings/activitypub.json b/public/language/it/admin/settings/activitypub.json index 01b835de45..c190fe0463 100644 --- a/public/language/it/admin/settings/activitypub.json +++ b/public/language/it/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Attivo", "relays.errors.invalid-url": "Inserisci un URL valido", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtraggio", "count": "Questo NodeBB è attualmente a conoscenza di %1 server", "server.filter-help": "Specifica i server a cui desideri impedire la federazione con il tuo NodeBB. In alternativa, puoi scegliere di consentire in modo selettivo la federazione con server specifici. Entrambe le opzioni sono supportate, anche se si escludono a vicenda.", diff --git a/public/language/ja/admin/settings/activitypub.json b/public/language/ja/admin/settings/activitypub.json index 1f7b9347a6..d6438b7c8b 100644 --- a/public/language/ja/admin/settings/activitypub.json +++ b/public/language/ja/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "アクティブ", "relays.errors.invalid-url": "有効なURLを入力してください", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "フィルタリング", "count": "このNodeBBは現在%1台のサーバーを認識しています", "server.filter-help": "NodeBBとのフェデレーションから除外したいサーバーを指定してください。または、特定のサーバーとのフェデレーションを選択的に許可することもできます。両方のオプションがサポートされていますが、相互に排他的です。", diff --git a/public/language/ko/admin/settings/activitypub.json b/public/language/ko/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/ko/admin/settings/activitypub.json +++ b/public/language/ko/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/lt/admin/settings/activitypub.json b/public/language/lt/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/lt/admin/settings/activitypub.json +++ b/public/language/lt/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/lv/admin/settings/activitypub.json b/public/language/lv/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/lv/admin/settings/activitypub.json +++ b/public/language/lv/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/ms/admin/settings/activitypub.json b/public/language/ms/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/ms/admin/settings/activitypub.json +++ b/public/language/ms/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/nb/admin/settings/activitypub.json b/public/language/nb/admin/settings/activitypub.json index d012ab0428..68bb6d209b 100644 --- a/public/language/nb/admin/settings/activitypub.json +++ b/public/language/nb/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtrering", "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.", diff --git a/public/language/nl/admin/settings/activitypub.json b/public/language/nl/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/nl/admin/settings/activitypub.json +++ b/public/language/nl/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/nn-NO/admin/settings/activitypub.json b/public/language/nn-NO/admin/settings/activitypub.json index a74d1f5bf9..e63c583f41 100644 --- a/public/language/nn-NO/admin/settings/activitypub.json +++ b/public/language/nn-NO/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtrer etter", "count": "Denne NodeBB-en er for tida klar over %1 server(ar)", "server.filter-help": "Spesifiser serverar du ønskjer å hindre frå å føderere med din NodeBB. Alternativt kan du velje å tillate føderasjon berre med spesifikke serverar. Begge alternativ er støtta, men dei er gjensidig utelukkande.", diff --git a/public/language/pl/admin/settings/activitypub.json b/public/language/pl/admin/settings/activitypub.json index 1b7c990981..bc69a56ad2 100644 --- a/public/language/pl/admin/settings/activitypub.json +++ b/public/language/pl/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Aktywny", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtrowanie", "count": "NodeBB obecnie wykrywa 1% serwerów", "server.filter-help": "Określ serwery, z którymi nie chcesz spinać NodeBB w ramach fediverse. Alternatywnie możesz dobrać dozwolone serwery fediverse. Obie opcje są dostępne ale wybierz jedną z nich.", diff --git a/public/language/pt-BR/admin/settings/activitypub.json b/public/language/pt-BR/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/pt-BR/admin/settings/activitypub.json +++ b/public/language/pt-BR/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/pt-PT/admin/settings/activitypub.json b/public/language/pt-PT/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/pt-PT/admin/settings/activitypub.json +++ b/public/language/pt-PT/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/ro/admin/settings/activitypub.json b/public/language/ro/admin/settings/activitypub.json index f40ffb147d..7f0eaf9f12 100644 --- a/public/language/ro/admin/settings/activitypub.json +++ b/public/language/ro/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Activ", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/ru/admin/settings/activitypub.json b/public/language/ru/admin/settings/activitypub.json index ab61a8f523..0d0ca3f445 100644 --- a/public/language/ru/admin/settings/activitypub.json +++ b/public/language/ru/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Активный", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Фильтрация", "count": "В настоящее время NodeBB знает о %1 сервере(ах)", "server.filter-help": "Укажите серверы, для которых вы хотели бы запретить объединение с вашим NodeBB. В качестве альтернативы вы можете выборочно разрешить объединение с определенными серверами. Поддерживаются оба варианта, хотя они и являются взаимоисключающими.", diff --git a/public/language/rw/admin/settings/activitypub.json b/public/language/rw/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/rw/admin/settings/activitypub.json +++ b/public/language/rw/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/sc/admin/settings/activitypub.json b/public/language/sc/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/sc/admin/settings/activitypub.json +++ b/public/language/sc/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/sk/admin/settings/activitypub.json b/public/language/sk/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/sk/admin/settings/activitypub.json +++ b/public/language/sk/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/sl/admin/settings/activitypub.json b/public/language/sl/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/sl/admin/settings/activitypub.json +++ b/public/language/sl/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/sq-AL/admin/settings/activitypub.json b/public/language/sq-AL/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/sq-AL/admin/settings/activitypub.json +++ b/public/language/sq-AL/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/sr/admin/settings/activitypub.json b/public/language/sr/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/sr/admin/settings/activitypub.json +++ b/public/language/sr/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/sv/admin/settings/activitypub.json b/public/language/sv/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/sv/admin/settings/activitypub.json +++ b/public/language/sv/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/th/admin/settings/activitypub.json b/public/language/th/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/th/admin/settings/activitypub.json +++ b/public/language/th/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/tr/admin/settings/activitypub.json b/public/language/tr/admin/settings/activitypub.json index 01faba1fd4..1377eec588 100644 --- a/public/language/tr/admin/settings/activitypub.json +++ b/public/language/tr/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Etkin", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Filtreleme", "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.", diff --git a/public/language/uk/admin/settings/activitypub.json b/public/language/uk/admin/settings/activitypub.json index 677af75cd7..6c65323ce5 100644 --- a/public/language/uk/admin/settings/activitypub.json +++ b/public/language/uk/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "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.", diff --git a/public/language/ur/admin/settings/activitypub.json b/public/language/ur/admin/settings/activitypub.json index bdfe0ef6a0..d48d7b7db0 100644 --- a/public/language/ur/admin/settings/activitypub.json +++ b/public/language/ur/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "فلٹرنگ", "count": "یہ نوڈ بی بی فی الحال %1 سرور(ز) کے بارے میں جانتا ہے", "server.filter-help": "ان سرورز کی نشاندہی کریں جن کے ساتھ آپ نہیں چاہتے کہ آپ کا نوڈ بی بی رابطہ قائم کرے۔ یا آپ اس کے بجائے مخصوص سرورز کی نشاندہی کر سکتے ہیں جن کے ساتھ رابطہ کی اجازت ہے۔ دونوں اختیارات دستیاب ہیں، لیکن آپ صرف ایک کا انتخاب کر سکتے ہیں۔", diff --git a/public/language/vi/admin/settings/activitypub.json b/public/language/vi/admin/settings/activitypub.json index de3ec7976e..fc2205f5a5 100644 --- a/public/language/vi/admin/settings/activitypub.json +++ b/public/language/vi/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Kích hoạt", "relays.errors.invalid-url": "Vui lòng nhập URL hợp lệ", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "Lọc", "count": "NodeBB này hiện đã biết về %1 máy chủ", "server.filter-help": "Chỉ ra các máy chủ mà bạn muốn cấm liên kết với NodeBB của mình. Ngoài ra, bạn có thể chọn tham gia có chọn lọc cho phép liên kết có chọn lọc với các máy chủ cụ thể. Cả hai tùy chọn đều được hỗ trợ, mặc dù chúng loại trừ lẫn nhau.", diff --git a/public/language/zh-CN/admin/settings/activitypub.json b/public/language/zh-CN/admin/settings/activitypub.json index 3f654d41e6..c426f68713 100644 --- a/public/language/zh-CN/admin/settings/activitypub.json +++ b/public/language/zh-CN/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "已启用", "relays.errors.invalid-url": "请输入有效的 URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "过滤", "count": "该 NodeBB 目前可检测到 %1 台服务器", "server.filter-help": "指定您希望禁止与 NodeBB 联邦化的服务器。或者,您也可以选择性地 允许 与特定服务器联邦化。两者只能选其一。", diff --git a/public/language/zh-TW/admin/settings/activitypub.json b/public/language/zh-TW/admin/settings/activitypub.json index c22ed8f4c6..ea3837e7b3 100644 --- a/public/language/zh-TW/admin/settings/activitypub.json +++ b/public/language/zh-TW/admin/settings/activitypub.json @@ -41,6 +41,17 @@ "relays.state-2": "Active", "relays.errors.invalid-url": "Please enter a valid URL", + "blocklists": "Third-party Blocklists", + "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", + "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "URL", + "blocklists.count": "Domains", + "blocklists.add": "Add New Blocklist", + "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", + "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", + "blocklists.view.title": "View Blocklist", + "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "server-filtering": "過濾...", "count": "本 NodeBB 已發現 %1 台伺服器。", "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.", From 78bdc4a102cff7ce8395d96bb24b04a0dbd005e2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 25 Mar 2026 12:04:00 -0400 Subject: [PATCH 4705/4744] refactor: use topic data returned from getSortedTopics instead of getting topic data twice lol --- src/controllers/activitypub/topics.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 26008e416c..97574e1031 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -44,6 +44,8 @@ controller.list = async function (req, res) { query: req.query, tag: req.query.tag, targetUid: targetUid, + teaserPost: 'last-reply', + thumbsOnly: 1, }; const data = await categories.getCategoryById(cidQuery); delete data.children; @@ -53,6 +55,7 @@ controller.list = async function (req, res) { let tids; let topicCount; + let topicData; let { local } = req.query; local = parseInt(local, 10) === 1; if (req.query.sort === 'popular') { @@ -64,7 +67,7 @@ controller.list = async function (req, res) { followingOnly: !req.query.all || !parseInt(req.query.all, 10), }; delete cidQuery.cid; - ({ tids, topicCount } = await topics.getSortedTopics(cidQuery)); + ({ tids, topicCount, topics: topicData } = await topics.getSortedTopics(cidQuery)); tids = tids.slice(start, stop !== -1 ? stop + 1 : undefined); } else { cidQuery = { @@ -74,10 +77,14 @@ controller.list = async function (req, res) { followingOnly: !req.query.all || !parseInt(req.query.all, 10), }; delete cidQuery.cid; - ({ tids, topicCount } = await topics.getSortedTopics(cidQuery)); + ({ tids, topicCount, topics: topicData } = await topics.getSortedTopics(cidQuery)); + /** + * Use `after` if passed in (only on IS) to update `start`/`stop`, this is useful + * to prevent loading duplicate posts if the sorted topics have received new topics + * since the set was last loaded. + */ if (after) { - // Update start/stop with values inferred from `after` const index = tids.indexOf(utils.isNumber(after) ? parseInt(after, 10) : after); if (index && start - index < 1) { const count = stop - start; @@ -94,23 +101,18 @@ controller.list = async function (req, res) { stripTags: false, extraFields: ['bookmarks'], }); - const uniqTids = _.uniq(postData.map(p => p.tid)); - const [topicData, { upvotes }, bookmarkStatus] = await Promise.all([ - topics.getTopicsFields(uniqTids, ['tid', 'numThumbs', 'thumbs', 'mainPid']), + const [{ upvotes }, bookmarkStatus] = await Promise.all([ posts.getVoteStatusByPostIDs(mainPids, req.uid), posts.hasBookmarked(mainPids, req.uid), ]); - const thumbs = await topics.thumbs.load(topicData, { thumbsOnly: 1 }); - const tidToThumbs = _.zipObject(uniqTids, thumbs); - const teasers = await topics.getTeasers(postData.map(p => p.topic), { uid: req.uid }); postData.forEach((p, index) => { p.pid = encodeURIComponent(p.pid); if (p.topic) { p.topic = { ...p.topic }; - p.topic.thumbs = tidToThumbs[p.tid]; + p.topic.thumbs = topicData[index].thumbs; p.topic.postcount = Math.max(0, p.topic.postcount - 1); - p.topic.teaser = teasers[index]; + p.topic.teaser = topicData[index].teaser; } p.upvoted = upvotes[index]; p.bookmarked = bookmarkStatus[index]; From 82d380a38d6a5d8c625e82c938ad5392f32fd0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 25 Mar 2026 12:51:44 -0400 Subject: [PATCH 4706/4744] fix: ./nodebb upgrade on windows --- src/cli/upgrade-plugins.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index 92cc980fa2..4949ef619f 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -148,8 +148,11 @@ async function upgradePlugins(unattended = false) { if (['y', 'Y', 'yes', 'YES'].includes(result.upgrade)) { console.log('\nUpgrading packages...'); const args = packageManagerInstallArgs.concat(found.map(suggestObj => `${suggestObj.name}@${suggestObj.suggested}`)); - - cproc.execFileSync(packageManagerExecutable, args, { stdio: 'ignore' }); + const options = { stdio: 'ignore' }; + if (process.platform === 'win32') { + options.shell = true; + } + cproc.execFileSync(packageManagerExecutable, args, options); } else { console.log(`${chalk.yellow('Package upgrades skipped')}. Check for upgrades at any time by running "${chalk.green('./nodebb upgrade -p')}".`); } From d290aa563e632be00684b50b676e8ba7c4a1ce95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 25 Mar 2026 13:21:17 -0400 Subject: [PATCH 4707/4744] lint: remove unused --- src/controllers/activitypub/topics.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/controllers/activitypub/topics.js b/src/controllers/activitypub/topics.js index 97574e1031..473dca7a7d 100644 --- a/src/controllers/activitypub/topics.js +++ b/src/controllers/activitypub/topics.js @@ -1,7 +1,5 @@ 'use strict'; -const _ = require('lodash'); - const meta = require('../../meta'); const user = require('../../user'); const topics = require('../../topics'); From 445361ad33539217298b88cebf8be90cbabfc8ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:21:32 -0400 Subject: [PATCH 4708/4744] fix(deps): update dependency nodemailer to v8.0.4 (#14126) 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 bd522d6551..e9fd86ce13 100644 --- a/install/package.json +++ b/install/package.json @@ -114,7 +114,7 @@ "nodebb-theme-peace": "2.2.57", "nodebb-theme-persona": "14.2.33", "nodebb-widget-essentials": "7.0.43", - "nodemailer": "8.0.3", + "nodemailer": "8.0.4", "nprogress": "0.2.0", "passport": "0.7.0", "passport-http-bearer": "1.0.1", From a54c2ed71e34fc5262bef38248a8724dfb73c96d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:22:53 -0400 Subject: [PATCH 4709/4744] chore(deps): update redis docker tag to v8.6.2 (#14122) 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 b90984c2c3..00846e9c06 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: - 5432:5432 redis: - image: 'redis:8.6.1' + image: 'redis:8.6.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 368a9c8054..618f571f5c 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.6.1-alpine + image: redis:8.6.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 438f190f97..67e5fc4acc 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.6.1-alpine + image: redis:8.6.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 b639a559bd..7f00a1a549 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.6.1-alpine + image: redis:8.6.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 24bd002996c7d34c1c1368f51986f2b1cb0488a0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 25 Mar 2026 15:19:16 -0400 Subject: [PATCH 4710/4744] fix: avoid db calls in upgrade scripts, just add blocklists to db, no refresh --- src/upgrades/4.11.0/subscribe-to-default-blocklists.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/upgrades/4.11.0/subscribe-to-default-blocklists.js b/src/upgrades/4.11.0/subscribe-to-default-blocklists.js index feb0af76c7..4bfe8cde02 100644 --- a/src/upgrades/4.11.0/subscribe-to-default-blocklists.js +++ b/src/upgrades/4.11.0/subscribe-to-default-blocklists.js @@ -1,14 +1,14 @@ 'use strict'; -const activitypub = require('../../activitypub'); +const db = require('../../database'); module.exports = { name: 'Subscribe to IFTAS DNI and AUD denylists', timestamp: Date.UTC(2026, 2, 23), method: async () => { - await Promise.all([ - activitypub.blocklists.add('https://about.iftas.org/wp-content/uploads/2025/10/iftas-dni-latest.csv'), - activitypub.blocklists.add('https://about.iftas.org/wp-content/uploads/2025/10/iftas-abandoned-unmanaged-latest.csv'), + await db.sortedSetAdd('blocklists', [Date.now(), Date.now()], [ + 'https://about.iftas.org/wp-content/uploads/2025/10/iftas-dni-latest.csv', + 'https://about.iftas.org/wp-content/uploads/2025/10/iftas-abandoned-unmanaged-latest.csv', ]); }, }; From a5f87603bee57d8037cfceb94994815ed064a135 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:27:00 -0400 Subject: [PATCH 4711/4744] chore(deps): update dependency smtp-server to v3.18.3 (#14125) 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 e9fd86ce13..95370ad1db 100644 --- a/install/package.json +++ b/install/package.json @@ -179,7 +179,7 @@ "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", "nyc": "18.0.0", - "smtp-server": "3.18.2" + "smtp-server": "3.18.3" }, "optionalDependencies": { "sass-embedded": "1.98.0" From 74b702dfef6e031d5a0a605639af95a4f70c9f42 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 26 Mar 2026 09:07:47 +0000 Subject: [PATCH 4712/4744] Latest translations and fallbacks --- .../bg/admin/settings/activitypub.json | 20 ++-- .../de/admin/settings/activitypub.json | 16 +-- public/language/he/admin/advanced/jobs.json | 2 +- public/language/he/admin/manage/users.json | 2 +- .../he/admin/settings/activitypub.json | 22 ++-- .../language/he/admin/settings/general.json | 6 +- public/language/he/topic.json | 4 +- public/language/he/world.json | 14 +-- .../language/ja/admin/manage/privileges.json | 104 +++++++++--------- public/language/ja/error.json | 98 ++++++++--------- 10 files changed, 144 insertions(+), 144 deletions(-) diff --git a/public/language/bg/admin/settings/activitypub.json b/public/language/bg/admin/settings/activitypub.json index b685cadcae..0e6754d03e 100644 --- a/public/language/bg/admin/settings/activitypub.json +++ b/public/language/bg/admin/settings/activitypub.json @@ -41,16 +41,16 @@ "relays.state-2": "Активен", "relays.errors.invalid-url": "Моля, въведете правилен адрес", - "blocklists": "Third-party Blocklists", - "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", - "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", - "blocklists.url": "URL", - "blocklists.count": "Domains", - "blocklists.add": "Add New Blocklist", - "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", - "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", - "blocklists.view.title": "View Blocklist", - "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "blocklists": "Външни списъци със забранени домейни", + "blocklists-help": "С оглед на безопасността Ви и тази на потребителите Ви, е важно да поддържате списък със забранени домейни, особено когато работите със съдържание произхождащо извън обхвата на местните модератори. NodeBB идва с определени стандартни предложения, но всичко може да бъде персонализирано тук.", + "blocklists-default": "NodeBB идва с два списъка със забранени домейни по подразбиране – IFTAS Do Not Interact Denylist и IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists.url": "Адрес", + "blocklists.count": "Домейни", + "blocklists.add": "Добавяне на нов списък със забранени домейни", + "blocklists.add-help": "Въведете адреса на списъка със забранени домейни, който искате да добавите. NodeBB може да работи със списъци във формата на тези предоставени от IFTAS.", + "blocklists.refreshed": "Списъкът е обновен – вече съдържа %1 записа", + "blocklists.view.title": "Преглед на списъка", + "blocklists.view.intro": "Това са домейните (%1) блокирани от този списък със забранени домейни:", "server-filtering": "Филтриране", "count": "Този NodeBB в момента знае за наличието на %1 сървър(а)", diff --git a/public/language/de/admin/settings/activitypub.json b/public/language/de/admin/settings/activitypub.json index 2e48df5884..7398980efb 100644 --- a/public/language/de/admin/settings/activitypub.json +++ b/public/language/de/admin/settings/activitypub.json @@ -41,16 +41,16 @@ "relays.state-2": "Aktiv", "relays.errors.invalid-url": "Bitte gib eine gültige URL ein", - "blocklists": "Third-party Blocklists", - "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", - "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists": "Blocklisten von Drittanbietern", + "blocklists-help": "Zu deiner eigenen Sicherheit und der deiner Nutzer ist die Pflege einer Sperrliste ein wesentlicher Bestandteil aller Maßnahmen zur Vertrauens & sicherung, wenn du mit Inhalten arbeitest, die außerhalb des lokalen Moderationsbereichs liegen. NodeBB enthält einige empfohlene Standardeinstellungen, die du hier anpassen kannst.", + "blocklists-default": "NodeBB wird standardmäßig mit zwei Sperrlisten ausgeliefert: der IFTAS-Sperrliste „Nicht interagieren“ und der IFTAS-Sperrliste für verlassene und verwahrloste Domains.", "blocklists.url": "URL", "blocklists.count": "Domains", - "blocklists.add": "Add New Blocklist", - "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", - "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", - "blocklists.view.title": "View Blocklist", - "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "blocklists.add": "Neue Sperrliste hinzufügen", + "blocklists.add-help": "Gib die URL der Sperrliste ein, die du hinzufügen möchtest. NodeBB erkennt Sperrlisten, die dem von IFTAS.verwendeten Format entsprechen.", + "blocklists.refreshed": "Die Sperrliste wurde aktualisiert – sie enthält nun %1 Einträge", + "blocklists.view.title": "Sperrliste anzeigen", + "blocklists.view.intro": "Dies sind die %1 Domains, die von dieser Blockliste gesperrt wurden:", "server-filtering": "Filterung", "count": "Dieses NodeBB kennt derzeit %1 Server", diff --git a/public/language/he/admin/advanced/jobs.json b/public/language/he/admin/advanced/jobs.json index 5dcbe5bd7b..9c7dcaf986 100644 --- a/public/language/he/admin/advanced/jobs.json +++ b/public/language/he/admin/advanced/jobs.json @@ -5,5 +5,5 @@ "next-run": "ריצה הבאה", "last-duration": "זמן ביצוע אחרון", "running": "בהרצה", - "active": "Active" + "active": "פעיל" } \ No newline at end of file diff --git a/public/language/he/admin/manage/users.json b/public/language/he/admin/manage/users.json index 45f17924f0..4d59f84eb9 100644 --- a/public/language/he/admin/manage/users.json +++ b/public/language/he/admin/manage/users.json @@ -40,7 +40,7 @@ "250-per-page": "250 לעמוד", "500-per-page": "500 לעמוד", - "search.help": "Use "*" to make partial searches, for example "*query"", + "search.help": "השתמשו ב- "*" לביצוע חיפושים חלקיים, למשל "*query"", "search.uid": "לפי זהות משתמש (ID)", "search.uid-placeholder": "הזינו מזהה משתמש (ID) לחיפוש", "search.username": "לפי שם משתמש", diff --git a/public/language/he/admin/settings/activitypub.json b/public/language/he/admin/settings/activitypub.json index 54a466afc7..a3da6b4f85 100644 --- a/public/language/he/admin/settings/activitypub.json +++ b/public/language/he/admin/settings/activitypub.json @@ -39,18 +39,18 @@ "relays.state-0": "ממתין", "relays.state-1": "קבלה בלבד", "relays.state-2": "פעיל", - "relays.errors.invalid-url": "Please enter a valid URL", + "relays.errors.invalid-url": "אנא הזינו URL תקין.", - "blocklists": "Third-party Blocklists", - "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", - "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists": "רשימת חסימות של צד שלישי", + "blocklists-help": "למען הבטיחות שלכם ושל המשתמשים שלכם, שמירה על רשימת חסימות היא חלק חיוני מכל מאמץ אמון ובטיחות כאשר עובדים עם תוכן מחוץ לתחום הניהול המקומי. NodeBB נשלח עם כמה ברירות מחדל מומלצות, וניתן להתאים אותן כאן.", + "blocklists-default": "NodeBB נוצרת עם שתי רשימות חסימה כברירת מחדל, ה IFTAS Do Not Interact Denylist וה- IFTAS Abandoned and Unmanaged Domain Denylist.", "blocklists.url": "URL", - "blocklists.count": "Domains", - "blocklists.add": "Add New Blocklist", - "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", - "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", - "blocklists.view.title": "View Blocklist", - "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "blocklists.count": "דומיינים", + "blocklists.add": "הוספת רשימת חסימות חדשה", + "blocklists.add-help": "הזינו את כתובת האתר של רשימת החסימה שברצונכם להוסיף. NodeBB מבינה רשימת חסימות התואמות את הפורמט שבו משתמשת IFTAS.", + "blocklists.refreshed": "רשימת החסימות התעדכנה - היא מכילה כעת %1 ערכים", + "blocklists.view.title": "הצגת רשימת חסימות", + "blocklists.view.intro": "אלו הם %1 הדומיינים שנחסמו על ידי רשימת חסימות זו:", "server-filtering": "סינון", "count": "NodeBB זה מודע כרגע ל-%1 שרתים", @@ -63,5 +63,5 @@ "content.summary-limit-help": "כאשר תוכן המופץ החוצה חורג ממכסת תווים זו, ייווצר תקצירהמורכב מכל המשפטים השלמים שלפני המגבלה. (ברירת מחדל: 500)", "content.break-string": "מפריד הערת סיכום/מאמר", "content.break-string-help": "משתמשים מתקדמים יכולים להזין את המפריד הזה ידנית בעת כתיבת נושאים חדשים. הוא מורה ל-NodeBB להשתמש בתוכן שעד לנקודה זו כחלק מה- תקציר . אם לא נעשה שימוש במחרוזת זו, תופעל חלופת ספירת התווים. (ברירת מחדל: [...] )", - "content.world-default-cid": "Default category ID for "World" page composer" + "content.world-default-cid": "מזהה קטגוריה ברירת מחדל עבור "World" יוצר עמודים" } \ No newline at end of file diff --git a/public/language/he/admin/settings/general.json b/public/language/he/admin/settings/general.json index 1ab09d5bd4..b0efac39d9 100644 --- a/public/language/he/admin/settings/general.json +++ b/public/language/he/admin/settings/general.json @@ -18,7 +18,7 @@ "description": "תיאור האתר", "keywords": "מילות מפתח של האתר", "keywords-placeholder": "מילות מפתח המתארות את הקהילה שלך, מופרדות באמצעות פסיקים", - "logo-and-icons": "Media & Branding", + "logo-and-icons": "מדיה ומיתוג", "logo.image": "תמונה", "logo.image-placeholder": "נתב ללוגו שיראה בכותרת הפורום", "logo.upload": "העלאה", @@ -35,8 +35,8 @@ "touch-icon.help": "סמליל דף אינטרנט מופיע כאשר מישהו מסמן את דף האינטרנט שלך או מוסיף את דף האינטרנט שלך למסך הבית שלו, גודל ותבנית מומלצים: 512x512, תבנית PNG בלבד. אם לא הוגדר סמליל דף אינטרנט, NodeBB יחזור להשתמש בסמליל הפבאייקון.", "maskable-icon": "סמליל הניתן להסוואה (במסך הבית)", "maskable-icon.help": "סמליל הניתן להסוואה מופיע בדף הבית של הסוללרי, זהו תמונה אטומה עם מעט ריפוד שהיישום דף הבית שלך יוכל לחתוך אחר כך לצורה ולגודל הרצוי. עדיף לא להסתמך על צורה מסוימת, מכיוון שהצורה שנבחרה בסופו של דבר יכולה להשתנות לפי סוגי מסך בית ופלטפורמה. גודל ותבנית מומלצים: 512x512, תבנית PNG בלבד. אם לא הוגדר אייקון הניתן להסוואה, NodeBB יחזור להשתמש בסמליל דף האינטרנט.", - "screenshot": "Screenshot", - "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", + "screenshot": "צילום מסך", + "screenshot.help": "גודל ופורמט מומלצים: בין 320px ל-3480px, פורמט JPG ו-PNG בלבד. אם לא צוין צילום מסך, NodeBB יחזור לצילום מסך גנרי", "outgoing-links": "קישורים חיצוניים", "outgoing-links.warning-page": "שימוש בדף האזהרה לקישורים יוצאים", "search": "חיפוש", diff --git a/public/language/he/topic.json b/public/language/he/topic.json index e6c69f47c3..1e2ccc442f 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -92,8 +92,8 @@ "watch.title": "קבלת התראה כאשר יש תגובות חדשות בנושא זה", "unwatch.title": "הפסקת מעקב אחר נושא זה", "share-this-post": "שיתוף פוסט זה", - "share-mail-subject": "Check out this post on \"%1\"", - "share-mail-body": "I thought you might be interested in this post: %1", + "share-mail-subject": "בדקו את הפוסט הזה ב\"% 1\"", + "share-mail-body": "חשבתי שאולי תתעניין בפוסט הזה:% 1", "watching": "במעקב", "not-watching": "לא במעקב", "ignoring": "התעלמות", diff --git a/public/language/he/world.json b/public/language/he/world.json index ace8b5cad4..5202563ba3 100644 --- a/public/language/he/world.json +++ b/public/language/he/world.json @@ -1,8 +1,8 @@ { "name": "עולם", - "latest": "Latest", - "latest-local": "Latest (Local)", - "latest-all": "Latest (All)", + "latest": "אחרון", + "latest-local": "אחרון (מקומי)", + "latest-all": "האחרון (כולם)", "popular-day": "פופולרי (יומי)", "popular-week": "פופולרי (שבועי)", "popular-month": "פופולרי (חודשי)", @@ -18,10 +18,10 @@ "help.federating": "באופן דומה, אם משתמשים מחוץ לפורום זה מתחילים לעקוב אחריכם, אז הפוסטים שלכם יתחילו להופיע גם באפליקציות ובאתרים אלה.", "help.next-generation": "זהו הדור הבא של המדיה החברתית, התחלו לתרום עוד היום!", - "onboard.title": "A world of content at your fingertips…", - "onboard.what": "Think of this as your global discovery feed. It brings together interesting discussions from across the web and other communities, all in one place.", - "onboard.why": "While you can browse what's trending now, the best way to use this feed is to make it your own. By creating an account, you can follow specific creators and topics to filter out the noise and see only what matters to you.", - "onboard.how": "Ready to dive in? Create an account to start following others, get notified when people reply to you, and save your favorite finds.", + "onboard.title": "עולם של תוכן בקצות אצבעותיך...", + "onboard.what": "תחשובו על זה כעל הזנת התגליות הגלובלית שלכם. הוא מפגיש דיונים מעניינים מרחבי האינטרנט ומקהילות אחרות, והכל במקום אחד.", + "onboard.why": "אמנם תוכלו לעיין במה שפופולרי עכשיו, אבל הדרך הטובה ביותר להשתמש בפיד הזה היא להפוך אותו לשלכם. על ידי יצירת חשבון, תוכלו לעקוב אחר יוצרים ונושאים ספציפיים כדי לסנן את הרעש ולראות רק את מה שחשוב לכם.", + "onboard.how": "מוכנים לצלול פנימה? צרו חשבון כדי להתחיל לעקוב אחר אחרים, לקבל התראה כשאנשים עונים לכם ולשמור את הממצאים המועדפים עלכם.", "category-search": "מצא קטגוריה...", "see-more": "ראה עוד", diff --git a/public/language/ja/admin/manage/privileges.json b/public/language/ja/admin/manage/privileges.json index 0f4614cb9a..eb01b33b5a 100644 --- a/public/language/ja/admin/manage/privileges.json +++ b/public/language/ja/admin/manage/privileges.json @@ -1,67 +1,67 @@ { - "manage-privileges": "Manage Privileges", - "discard-changes": "Discard changes", + "manage-privileges": "権限を管理", + "discard-changes": "変更を破棄", "global": "グローバル", - "admin": "Admin", - "group-privileges": "Group Privileges", - "user-privileges": "User Privileges", - "edit-privileges": "Edit Privileges", - "select-clear-all": "Select/Clear All", + "admin": "管理者", + "group-privileges": "グループ権限", + "user-privileges": "ユーザー権限", + "edit-privileges": "権限を編集", + "select-clear-all": "すべて選択/解除", "chat": "チャット", - "chat-with-privileged": "Chat with Privileged", + "chat-with-privileged": "特権ユーザーとチャット", "upload-images": "画像をアップロード", "upload-files": "ファイルをアップロード", "signature": "署名", - "ban": "Ban", - "mute": "Mute", - "invite": "Invite", + "ban": "BAN", + "mute": "ミュート", + "invite": "招待", "search-content": "コンテンツを検索", - "search-users": "ユーザー検索", - "search-tags": "タグ検索", + "search-users": "ユーザーを検索", + "search-tags": "タグを検索", "view-users": "ユーザーを表示", "view-tags": "タグを表示", "view-groups": "グループを表示", "allow-local-login": "ローカルログイン", - "allow-group-creation": "グループを作成", - "view-users-info": "View Users Info", + "allow-group-creation": "グループ作成", + "view-users-info": "ユーザー情報を表示", "find-category": "カテゴリを検索", "access-category": "カテゴリにアクセス", - "access-topics": "トピックスにアクセス", - "create-topics": "トピックスを作成", - "reply-to-topics": "トピックスに返信", - "crosspost-topics": "Cross-post Topics", - "schedule-topics": "Schedule Topics", - "tag-topics": "Tag Topics", - "edit-posts": "Edit Posts", - "view-edit-history": "View Edit History", - "delete-posts": "Delete Posts", - "view-deleted": "View Deleted Posts", - "upvote-posts": "Upvote Posts", - "downvote-posts": "Downvote Posts", - "delete-topics": "Delete Topics", - "purge": "Purge", - "moderate": "Moderate", - "admin-dashboard": "Dashboard", - "admin-categories": "Categories", - "admin-privileges": "Privileges", - "admin-users": "Users", - "admin-admins-mods": "Admins & Mods", - "admin-groups": "Groups", - "admin-tags": "Tags", - "admin-settings": "Settings", + "access-topics": "スレッドにアクセス", + "create-topics": "スレッドを作成", + "reply-to-topics": "スレッドに返信", + "crosspost-topics": "スレッドをクロスポスト", + "schedule-topics": "スレッドを予約", + "tag-topics": "スレッドにタグ付け", + "edit-posts": "投稿を編集", + "view-edit-history": "編集履歴を表示", + "delete-posts": "投稿を削除", + "view-deleted": "削除済み投稿を表示", + "upvote-posts": "投稿に高評価", + "downvote-posts": "投稿に低評価", + "delete-topics": "スレッドを削除", + "purge": "完全削除", + "moderate": "モデレーション", + "admin-dashboard": "ダッシュボード", + "admin-categories": "カテゴリ", + "admin-privileges": "権限", + "admin-users": "ユーザー", + "admin-admins-mods": "管理者とモデレーター", + "admin-groups": "グループ", + "admin-tags": "タグ", + "admin-settings": "設定", - "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", - "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", - "alert.confirm-save": "Please confirm your intention to save these privileges", - "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", - "alert.discarded": "Privilege changes discarded", - "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", - "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", - "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", - "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", - "alert.no-undo": "This action cannot be undone.", - "alert.admin-warning": "Administrators implicitly get all privileges", - "alert.copyPrivilegesFrom-title": "Select a category to copy from", - "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", - "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." + "alert.confirm-moderate": "このユーザーグループにモデレーション権限を付与してもよろしいですか? このグループは公開されており、誰でも自由に参加できます。", + "alert.confirm-admins-mods": "このユーザー/グループに「管理者とモデレーター」権限を付与してもよろしいですか? この権限を持つユーザーは他のユーザーを特権ロールに昇格/降格でき、スーパー管理者も含みます", + "alert.confirm-save": "これらの権限を保存することを確認してください", + "alert.confirm-discard": "権限変更を破棄してもよろしいですか?", + "alert.discarded": "権限変更を破棄しました", + "alert.confirm-copyToAll": "この %1 の設定を すべてのカテゴリ に適用してもよろしいですか?", + "alert.confirm-copyToAllGroup": "このグループの %1 の設定を すべてのカテゴリ に適用してもよろしいですか?", + "alert.confirm-copyToChildren": "この %1 の設定を すべての子孫(子)カテゴリ に適用してもよろしいですか?", + "alert.confirm-copyToChildrenGroup": "このグループの %1 の設定を すべての子孫(子)カテゴリ に適用してもよろしいですか?", + "alert.no-undo": "この操作は取り消せません。", + "alert.admin-warning": "管理者は暗黙的にすべての権限を持ちます", + "alert.copyPrivilegesFrom-title": "コピー元カテゴリを選択", + "alert.copyPrivilegesFrom-warning": "選択したカテゴリから %1 をコピーします。", + "alert.copyPrivilegesFromGroup-warning": "選択したカテゴリからこのグループの %1 の設定をコピーします。" } \ No newline at end of file diff --git a/public/language/ja/error.json b/public/language/ja/error.json index 27b7f6c063..16e13c4efb 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -1,10 +1,10 @@ { "invalid-data": "無効なデータ", - "invalid-config-field-value": "Invalid value for config field \"%1\": %2", + "invalid-config-field-value": "設定フィールド \\\"%1\\\" の値が無効です: %2", "invalid-json": "無効なJSON", - "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", - "required-parameters-missing": "Required parameters were missing from this API call: %1", - "reserved-ip-address": "Network requests to reserved IP ranges are not allowed.", + "wrong-parameter-type": "プロパティ `%1` には型 %3 の値が期待されますが、%2 が渡されました", + "required-parameters-missing": "このAPI呼び出しに必要なパラメータが不足しています: %1", + "reserved-ip-address": "予約済みIPレンジへのネットワークリクエストは許可されていません。", "not-logged-in": "ログインしていません。", "account-locked": "あなたのアカウントは一時的にロックされています", "search-requires-login": "検索するにはアカウントが必要です - ログインするかアカウントを作成してください。", @@ -13,13 +13,13 @@ "invalid-tid": "無効なスレッドID", "invalid-pid": "無効な投稿ID", "invalid-uid": "無効なユーザーID", - "invalid-mid": "Invalid Chat Message ID", - "invalid-date": "A valid date must be provided", + "invalid-mid": "無効なチャットメッセージID", + "invalid-date": "有効な日付を指定してください", "invalid-username": "無効なユーザー名", "invalid-email": "無効なメール", - "invalid-fullname": "Invalid Fullname", - "invalid-location": "Invalid Location", - "invalid-birthday": "Invalid Birthday", + "invalid-fullname": "無効なフルネーム", + "invalid-location": "無効なロケーション", + "invalid-birthday": "無効な誕生日", "invalid-title": "無効なタイトル", "invalid-user-data": "無効なユーザーデータ", "invalid-password": "無効なパスワード", @@ -27,38 +27,38 @@ "invalid-username-or-password": "ユーザー名とパスワードの両方を指定してください", "invalid-search-term": "無効な検索ワード", "invalid-url": "無効なURL", - "invalid-event": "Invalid event: %1", + "invalid-event": "無効なイベント: %1", "local-login-disabled": "ローカルログインシステムは、非特権アカウントに対して無効になっています", "csrf-invalid": "セッションの期限切れと思われるため、私達はあなたのログイン状態を確認できませんでした。もう一度お試しください。", - "invalid-path": "Invalid path", - "folder-exists": "Folder exists", - "invalid-pagination-value": "無効なページネーション値です。%1 から%2の値でなければありません。", - "invalid-unread-cutoff": "Invalid unread cutoff value, must be at least 1 and at most %1", - "username-taken": "ユーザー名は既に使われています", - "email-taken": "Email address is already taken.", - "email-nochange": "The email entered is the same as the email already on file.", - "email-invited": "Email was already invited", - "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "invalid-path": "無効なパス", + "folder-exists": "フォルダが既に存在します", + "invalid-pagination-value": "無効なページネーション値です。%1以上%2以下で指定してください。", + "invalid-unread-cutoff": "未読カットオフ値が無効です。1以上%1以下で指定してください。", + "username-taken": "ユーザー名はすでに使われています", + "email-taken": "このメールアドレスはすでに使用されています。", + "email-nochange": "入力されたメールアドレスは既存のものと同じです。", + "email-invited": "このメールアドレスはすでに招待済みです", + "email-not-confirmed": "一部のカテゴリやスレッドへの投稿はメール確認後に有効になります。確認メールを送信するにはここをクリックしてください。", "email-not-confirmed-chat": "チャットを行うにはメールアドレスの確認を行う必要があります。メールアドレスを確認するためにはここをクリックしてください。", - "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", - "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", - "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-not-confirmed-email-sent": "メールアドレスはまだ確認されていません。確認メールをご確認ください。確認するまで一部のカテゴリへの投稿やチャットができません。", + "no-email-to-confirm": "アカウントにメールアドレスが設定されていません。アカウント復旧に必要です。メールアドレスを入力するにはここをクリックしてください。", + "user-doesnt-have-email": "ユーザー「%1」にメールアドレスが設定されていません。", "email-confirm-failed": "メールアドレスの確認が出来ませんでした。再度お試しください。", - "confirm-email-already-sent": "確認のメールは既に送信されています。再度送信するには、%1分後に再度お試しください。", - "confirm-email-expired": "Confirmation email expired", + "confirm-email-already-sent": "確認のメールはすでに送信されています。再度送信するには、%1分後に再度お試しください。", + "confirm-email-expired": "確認メールの有効期限が切れました", "sendmail-not-found": "Sendmailの実行ファイルが見つかりませんでした。インストールされ、ユーザーによってNodeBBが実行されていることを確認してください。", - "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "digest-not-enabled": "このユーザーはダイジェストが有効になっていないか、システムのデフォルトでダイジェスト送信が設定されていません", "username-too-short": "ユーザー名が短すぎます", "username-too-long": "ユーザー名が長すぎます", "password-too-long": "パスワードが長すぎます", - "reset-rate-limited": "パスワードのリセット要求が多すぎます。(料金制限あり)", - "reset-same-password": "Please use a password that is different from your current one", + "reset-rate-limited": "パスワードのリセット要求が多すぎます。(レート制限に達しました)", + "reset-same-password": "現在のパスワードとは異なるパスワードを使用してください", "user-banned": "ユーザーは停止されています", "user-banned-reason": "申し訳ありませんが、このアカウントは停止されています。 (理由: %1)", - "user-banned-reason-until": "申し訳ありませんが、このアカウントは%1(理由:%2)まで禁止されています。", + "user-banned-reason-until": "申し訳ありませんが、このアカウントは%1まで停止されています。(理由: %2)", "user-too-new": "申し訳ありません。登録後に投稿を行うには%1秒お待ち下さい。", "blacklisted-ip": "申し訳ありませんがあなたのIPアドレスは当コミュニティで停止されています。もし誤ったエラーだと思われる場合は管理者にお問い合わせください。", - "cant-blacklist-self-ip": "You can't blacklist your own IP", + "cant-blacklist-self-ip": "自分のIPアドレスをブラックリストに追加することはできません", "ban-expiry-missing": "この停止の終了日を入力してください。", "no-category": "カテゴリは存在しません", "no-topic": "スレッドは存在しません", @@ -66,19 +66,19 @@ "no-group": "グループは存在しません", "no-user": "ユーザーは存在しません", "no-teaser": "ティーザーが存在しません", - "no-flag": "Flag does not exist", - "no-chat-room": "Chat room does not exist", - "no-privileges": "あなたがこの行為する権利がありません。", - "category-disabled": "この板は無効された", - "post-deleted": "Post deleted", - "topic-locked": "Topic locked", - "post-edit-duration-expired": "あなたが%1秒後に投稿を編集する事が許されます", - "post-edit-duration-expired-minutes": "あなたは投稿後%1 分(s)後に編集できます。", - "post-edit-duration-expired-minutes-seconds": "あなたは投稿後%1 分(s) %2 秒(s)後に編集できます。", - "post-edit-duration-expired-hours": "あなたは投稿後%1 時間(s)後に編集できます。", - "post-edit-duration-expired-hours-minutes": "あなたは投稿後%1 時間(s) %2 分(s) 後に編集できます。", - "post-edit-duration-expired-days": "あなたは投稿後%1 日(s)後に編集できます。", - "post-edit-duration-expired-days-hours": "あなたは投稿後%1 日(s) %2 時間(s)後に編集できます。", + "no-flag": "フラグは存在しません", + "no-chat-room": "チャットルームは存在しません", + "no-privileges": "この操作を行う権限がありません。", + "category-disabled": "カテゴリは無効になっています", + "post-deleted": "投稿は削除されました", + "topic-locked": "スレッドはロックされています", + "post-edit-duration-expired": "投稿後%1秒間だけ投稿を編集できます。", + "post-edit-duration-expired-minutes": "投稿後%1分間だけ投稿を編集できます。", + "post-edit-duration-expired-minutes-seconds": "投稿後%1分%2秒間だけ投稿を編集できます。", + "post-edit-duration-expired-hours": "投稿後%1時間だけ投稿を編集できます。", + "post-edit-duration-expired-hours-minutes": "投稿後%1時間%2分間だけ投稿を編集できます。", + "post-edit-duration-expired-days": "投稿後%1日間だけ投稿を編集できます。", + "post-edit-duration-expired-days-hours": "投稿後%1日%2時間だけ投稿を編集できます。", "post-delete-duration-expired": "あなたは%1 秒(s)後に投稿を削除することが許可されています。", "post-delete-duration-expired-minutes": "あなたは%1 分(s)後に投稿を削除することが許可されています。", "post-delete-duration-expired-minutes-seconds": "あなたは%1 分(s) %2 秒(s) 後に投稿を削除することが許可されています。", @@ -261,11 +261,11 @@ "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", "api.503": "The route you are trying to call is not currently available due to a server configuration", "api.reauth-required": "The resource you are trying to access requires (re-)authentication.", - "activitypub.not-enabled": "Federation is not enabled on this server", - "activitypub.invalid-id": "Unable to resolve the input id, likely as it is malformed.", - "activitypub.get-failed": "Unable to retrieve the specified resource.", - "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", - "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", - "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" + "activitypub.not-enabled": "このサーバーではフェデレーションは有効になっていません", + "activitypub.invalid-id": "入力IDを解決できません。形式が正しくない可能性があります。", + "activitypub.get-failed": "指定されたリソースを取得できません。", + "activitypub.pubKey-not-found": "公開鍵を解決できないため、ペイロードの検証ができません。", + "activitypub.origin-mismatch": "受信したオブジェクトのオリジンが送信者のオリジンと一致しません", + "activitypub.actor-mismatch": "受信したアクティビティは、予期したアクターとは異なるアクターによって実行されています。", + "activitypub.not-implemented": "リクエストまたはその一部が受信サーバーで実装されていないため、拒否されました" } \ No newline at end of file From 781ed3447bac4e4df60312f27b73b5f4c20e9684 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 26 Mar 2026 10:30:28 -0400 Subject: [PATCH 4713/4744] feat: track user cids (#14114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: start tracking which cids a user has posted to, update account pages' topics/posts view to call this new sorted set re: #14113 * feat: upgrade script for #14113 * fix: cids unavailable in getPostsFields, duh * fix: update sortedSetIncrByBulk in mongo/psql to return early on empty data * fix: remove unused lodash require * test: sortedSetIncrBy and sortedSetIncrByBulk tests * test: who needs null checks anyway * fix: sortedSetIncrByBulk null response * test: aggregate zincrbulk data if there are alot of identical key/value pairs they will be combined into a single row * fix: key name * test: fix test name * lint: fix lint issues * test: negative values should work too * fix: add e11000 handler for incrByBulk * refactor: fix variable name * merge tests with existing zset test, remove dupes * test: return topicData for failing test * delete uid::cids on user delete --------- Co-authored-by: Barış Soner Uşaklı --- src/categories/recentreplies.js | 6 ++ src/controllers/accounts/posts.js | 6 +- src/database/helpers.js | 18 +++++ src/database/mongo/sorted.js | 27 ++++++- src/database/postgres/sorted.js | 11 ++- src/database/redis/sorted.js | 6 +- src/posts/delete.js | 7 +- src/posts/user.js | 7 ++ src/topics/fork.js | 4 + src/upgrades/4.11.0/backfill-user-cids.js | 38 ++++++++++ src/user/delete.js | 1 + src/user/posts.js | 1 + test/database/sorted.js | 89 ++++++++++++++++------- test/topics.js | 7 +- 14 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 src/upgrades/4.11.0/backfill-user-cids.js diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index 8e21647c70..92eed3be56 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -191,10 +191,15 @@ module.exports = function (Categories) { const bulkRemove = []; const bulkAdd = []; + const bulkIncr = []; postData.forEach((post) => { bulkRemove.push([`cid:${oldCid}:uid:${post.uid}:pids`, post.pid]); bulkRemove.push([`cid:${oldCid}:uid:${post.uid}:pids:votes`, post.pid]); bulkAdd.push([`cid:${cid}:uid:${post.uid}:pids`, post.timestamp, post.pid]); + bulkIncr.push( + [`uid:${post.uid}:cids`, -1, oldCid], + [`uid:${post.uid}:cids`, 1, cid], + ); if (post.votes > 0 || post.votes < 0) { bulkAdd.push([`cid:${cid}:uid:${post.uid}:pids:votes`, post.votes, post.pid]); } @@ -207,6 +212,7 @@ module.exports = function (Categories) { db.sortedSetAdd(`cid:${cid}:pids`, timestamps, postsToReAdd.map(p => p.pid)), db.sortedSetRemoveBulk(bulkRemove), db.sortedSetAddBulk(bulkAdd), + db.sortedSetIncrByBulk(bulkIncr), ]); }, { batch: 500 }); }; diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js index 9ce048bd28..1535ed0737 100644 --- a/src/controllers/accounts/posts.js +++ b/src/controllers/accounts/posts.js @@ -31,7 +31,8 @@ const templateToData = { noItemsFoundKey: '[[user:has-no-posts]]', crumb: '[[global:posts]]', getSets: async function (callerUid, userData) { - const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + let cids = await db.getSortedSetRangeByScore(`uid:${userData.uid}:cids`, 0, -1, 1, '+inf'); + cids = await privileges.categories.filterCids('topics:read', cids, callerUid); return cids.map(c => `cid:${c}:uid:${userData.uid}:pids`); }, }, @@ -143,7 +144,8 @@ const templateToData = { noItemsFoundKey: '[[user:has-no-topics]]', crumb: '[[global:topics]]', getSets: async function (callerUid, userData) { - const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + let cids = await db.getSortedSetRangeByScore(`uid:${userData.uid}:cids`, 0, -1, 1, '+inf'); + cids = await privileges.categories.filterCids('topics:read', cids, callerUid); return cids.map(c => `cid:${c}:uid:${userData.uid}:tids`); }, }, diff --git a/src/database/helpers.js b/src/database/helpers.js index eb8206f783..d66bfb87a6 100644 --- a/src/database/helpers.js +++ b/src/database/helpers.js @@ -42,3 +42,21 @@ helpers.globToRegex = function (match) { } return _match; }; + +helpers.aggregateIncrByBulk = function (data) { + const buckets = Object.create(null); + + for (const [key, incr, val] of data) { + buckets[key] = buckets[key] || {}; + buckets[key][val] = (buckets[key][val] || 0) + incr; + } + + const result = []; + for (const [key, vals] of Object.entries(buckets)) { + for (const [val, incr] of Object.entries(vals)) { + result.push([key, incr, val]); + } + } + + return result; +}; diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index c6f1ee90ce..71c4f4c273 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -459,16 +459,35 @@ module.exports = function (module) { }; module.sortedSetIncrByBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return []; + } + const aggregated = dbHelpers.aggregateIncrByBulk(data); const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); - data.forEach((item) => { + aggregated.forEach((item) => { bulk.find({ _key: item[0], value: helpers.valueToString(item[2]) }) .upsert() .update({ $inc: { score: parseFloat(item[1]) } }); }); - await bulk.execute(); + + try { + await bulk.execute(); + } catch (err) { + // retry failed e11000 operations + if (err.code === 11000 || (err.writeErrors && err.writeErrors.some(e => e.code === 11000))) { + const failedIndices = err.writeErrors.map(e => e.index); + const retryData = failedIndices.map(idx => aggregated[idx]); + await Promise.all(retryData.map( + item => module.sortedSetIncrBy(item[0], item[1], item[2]) + )); + } else { + throw err; + } + } + const result = await module.client.collection('objects').find({ - _key: { $in: _.uniq(data.map(i => i[0])) }, - value: { $in: _.uniq(data.map(i => i[2])) }, + _key: { $in: _.uniq(aggregated.map(i => i[0])) }, + value: { $in: _.uniq(aggregated.map(i => i[2])) }, }, { projection: { _id: 0, _key: 1, value: 1, score: 1 }, }).toArray(); diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 351fe3e059..6332907e43 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -1,8 +1,10 @@ 'use strict'; module.exports = function (module) { - const helpers = require('./helpers'); const util = require('util'); + + const helpers = require('./helpers'); + const dbHelpers = require('../helpers'); const Cursor = require('pg-cursor'); Cursor.prototype.readAsync = util.promisify(Cursor.prototype.read); const sleep = util.promisify(setTimeout); @@ -547,18 +549,19 @@ RETURNING "score" s`, }; module.sortedSetIncrByBulk = async function (data) { - if (!data.length) { + if (!Array.isArray(data) || !data.length) { return []; } + const aggregated = dbHelpers.aggregateIncrByBulk(data); return await module.transaction(async (client) => { - await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'zset'); + await helpers.ensureLegacyObjectsType(client, aggregated.map(item => item[0]), 'zset'); const values = []; const queryParams = []; let paramIndex = 1; - data.forEach(([key, increment, value]) => { + aggregated.forEach(([key, increment, value]) => { value = helpers.valueToString(value); increment = parseFloat(increment); values.push(key, value, increment); diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index 8433133f15..a29ef75a20 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -261,8 +261,12 @@ module.exports = function (module) { }; module.sortedSetIncrByBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return []; + } + const aggregated = dbHelpers.aggregateIncrByBulk(data); const multi = module.client.multi(); - data.forEach((item) => { + aggregated.forEach((item) => { multi.zIncrBy(item[0], item[1], String(item[2])); }); const result = await multi.exec(); diff --git a/src/posts/delete.js b/src/posts/delete.js index f6b7aafb37..9235f37111 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -96,14 +96,19 @@ module.exports = function (Posts) { async function deleteFromTopicUserNotification(postData) { const bulkRemove = []; + const bulkIncr = []; postData.forEach((p) => { bulkRemove.push([`tid:${p.tid}:posts`, p.pid]); bulkRemove.push([`tid:${p.tid}:posts:votes`, p.pid]); bulkRemove.push([`uid:${p.uid}:posts`, p.pid]); bulkRemove.push([`cid:${p.cid}:uid:${p.uid}:pids`, p.pid]); bulkRemove.push([`cid:${p.cid}:uid:${p.uid}:pids:votes`, p.pid]); + bulkIncr.push([`uid:${p.uid}:cids`, -1, p.cid]); }); - await db.sortedSetRemoveBulk(bulkRemove); + await Promise.all([ + db.sortedSetRemoveBulk(bulkRemove), + db.sortedSetIncrByBulk(bulkIncr), + ]); const localCount = postData.filter(p => utils.isNumber(p.pid)).length; const incrObjectBulk = [['global', { postCount: -localCount }]]; diff --git a/src/posts/user.js b/src/posts/user.js index d096b0fa50..cdb9836bdb 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -150,6 +150,7 @@ module.exports = function (Posts) { const bulkRemove = []; const bulkAdd = []; + const bulkIncr = []; let repChange = 0; const postsByUser = {}; postData.forEach((post, i) => { @@ -164,6 +165,11 @@ module.exports = function (Posts) { if (post.votes > 0 || post.votes < 0) { bulkAdd.push([`cid:${post.cid}:uid:${toUid}:pids:votes`, post.votes, post.pid]); } + + bulkIncr.push( + [`uid:${post.uid}:cids`, -1, post.cid], + [`uid:${toUid}:cids`, 1, post.cid], + ); postsByUser[post.uid] = postsByUser[post.uid] || []; postsByUser[post.uid].push(post); }); @@ -172,6 +178,7 @@ module.exports = function (Posts) { db.setObjectField(pids.map(pid => `post:${pid}`), 'uid', toUid), db.sortedSetRemoveBulk(bulkRemove), db.sortedSetAddBulk(bulkAdd), + db.sortedSetIncrByBulk(bulkIncr), user.incrementUserReputationBy(toUid, repChange), handleMainPidOwnerChange(postData, toUid), updateTopicPosters(postData, toUid), diff --git a/src/topics/fork.js b/src/topics/fork.js index fc8b030f63..22903d112e 100644 --- a/src/topics/fork.js +++ b/src/topics/fork.js @@ -169,6 +169,10 @@ module.exports = function (Topics) { db.sortedSetRemove(removeFrom, postData.pid), db.sortedSetAdd(`cid:${topicData[1].cid}:pids`, postData.timestamp, postData.pid), db.sortedSetAdd(`cid:${topicData[1].cid}:uid:${postData.uid}:pids`, postData.timestamp, postData.pid), + db.sortedSetIncrByBulk([ + [`uid:${postData.uid}:cids`, -1, topicData[0].cid], + [`uid:${postData.uid}:cids`, 1, topicData[1].cid], + ]), ]; if (postData.votes > 0 || postData.votes < 0) { tasks.push(db.sortedSetAdd(`cid:${topicData[1].cid}:uid:${postData.uid}:pids:votes`, postData.votes, postData.pid)); diff --git a/src/upgrades/4.11.0/backfill-user-cids.js b/src/upgrades/4.11.0/backfill-user-cids.js new file mode 100644 index 0000000000..58db2fc194 --- /dev/null +++ b/src/upgrades/4.11.0/backfill-user-cids.js @@ -0,0 +1,38 @@ +'use strict'; + +const db = require('../../database'); +const posts = require('../../posts'); +const batch = require('../../batch'); + +module.exports = { + name: 'Backfill user posted categories', + timestamp: Date.UTC(2026, 2, 20), + method: async function () { + const { progress } = this; + await batch.processSortedSet('posts:pid', async (pids) => { + const postData = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['uid']); + const cids = await posts.getCidsByPids(pids); + const uidPostCountByCid = Object.create(null); + postData.forEach((post, idx) => { + const cid = cids[idx]; + uidPostCountByCid[post.uid] = uidPostCountByCid[post.uid] || {}; + uidPostCountByCid[post.uid][cid] = (uidPostCountByCid[post.uid][cid] || 0) + 1; + }); + const bulkIncr = []; + Object.keys(uidPostCountByCid).forEach((uid) => { + Object.keys(uidPostCountByCid[uid]).forEach((cid) => { + bulkIncr.push([`uid:${uid}:cids`, uidPostCountByCid[uid][cid], cid]); + }); + }); + + if (bulkIncr.length) { + await db.sortedSetIncrByBulk(bulkIncr); + } + + progress.incr(pids.length); + }, { + batch: 500, + progress, + }); + }, +}; diff --git a/src/user/delete.js b/src/user/delete.js index 5c15c5c660..b13c197f7f 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -126,6 +126,7 @@ module.exports = function (User) { `user:${uid}:usernames`, `user:${uid}:emails`, `uid:${uid}:topics`, `uid:${uid}:posts`, + `uid:${uid}:cids`, `uid:${uid}:chats`, `uid:${uid}:chats:unread`, `uid:${uid}:chat:rooms`, `uid:${uid}:chat:rooms:unread`, diff --git a/src/user/posts.js b/src/user/posts.js index 30ddeaadfc..c8be6f81cd 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -101,6 +101,7 @@ module.exports = function (User) { `uid:${postData.uid}:posts`, `cid:${postData.cid}:uid:${postData.uid}:pids`, ], postData.timestamp, postData.pid); + await db.sortedSetIncrBy(`uid:${postData.uid}:cids`, 1, postData.cid); await User.updatePostCount(postData.uid); }; diff --git a/test/database/sorted.js b/test/database/sorted.js index fde2bafb0b..f1eca291bf 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -2,6 +2,7 @@ const assert = require('assert'); const db = require('../mocks/databasemock'); +const utils = require('../../src/utils'); describe('Sorted Set methods', () => { before(async () => { @@ -1070,31 +1071,19 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea }); }); - describe('sortedSetIncrBy()', () => { - it('should create a sorted set with a field set to 1', (done) => { - db.sortedSetIncrBy('sortedIncr', 1, 'field1', function (err, newValue) { - assert.equal(err, null); - assert.equal(arguments.length, 2); - assert.strictEqual(newValue, 1); - db.sortedSetScore('sortedIncr', 'field1', (err, score) => { - assert.equal(err, null); - assert.strictEqual(score, 1); - done(); - }); - }); + describe('sortedSetIncrBy()/sortedSetIncrByBulk()', () => { + it('should create a sorted set with a field set to 1', async () => { + const newValue = await db.sortedSetIncrBy('sortedIncr', 1, 'field1'); + assert.strictEqual(newValue, 1); + const score = await db.sortedSetScore('sortedIncr', 'field1'); + assert.strictEqual(score, 1); }); - it('should increment a field of a sorted set by 5', (done) => { - db.sortedSetIncrBy('sortedIncr', 5, 'field1', function (err, newValue) { - assert.equal(err, null); - assert.equal(arguments.length, 2); - assert.strictEqual(newValue, 6); - db.sortedSetScore('sortedIncr', 'field1', (err, score) => { - assert.equal(err, null); - assert.strictEqual(score, 6); - done(); - }); - }); + it('should increment a field of a sorted set by 5', async () => { + const newValue = await db.sortedSetIncrBy('sortedIncr', 5, 'field1'); + assert.strictEqual(newValue, 6); + const score = await db.sortedSetScore('sortedIncr', 'field1'); + assert.strictEqual(score, 6); }); it('should increment fields of sorted sets with a single call', async () => { @@ -1122,12 +1111,27 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea ); }); + it('should increment the same zset twice', async () => { + const zset = utils.generateUUID(); + const value1 = utils.generateUUID(); + const value2 = utils.generateUUID(); + await db.sortedSetIncrByBulk([ + [zset, 1, value1], + [zset, 1, value2], + ]); + const scores = await Promise.all([ + db.sortedSetScore(zset, value1), + db.sortedSetScore(zset, value2), + ]); + assert.deepStrictEqual(scores, [1, 1]); + }); + it('should increment the same field', async () => { - const data1 = await db.sortedSetIncrByBulk([ + await db.sortedSetIncrByBulk([ ['sortedIncrBulk5', 5, 'value5'], ]); - const data2 = await db.sortedSetIncrByBulk([ + await db.sortedSetIncrByBulk([ ['sortedIncrBulk5', 5, 'value5'], ]); assert.deepStrictEqual( @@ -1137,6 +1141,41 @@ NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:rea ], ); }); + + it('should return empty array', async function () { + const zset = utils.generateUUID(); + const response = await db.sortedSetIncrByBulk(zset, []); + assert(Array.isArray(response)); + assert.strictEqual(response.length, 0); + }); + + it('should aggregate increments to the same key/value pair', async function () { + const zset = utils.generateUUID(); + await db.sortedSetIncrByBulk([ + [zset, 1, 'baz'], + [zset, 1, 'baz'], + [zset, 7, 'baz'], + [zset, 1, 'foo'], + [zset, 3, 'foo'], + [zset, 4, 'foo'], + [zset, 2, 'fizz'], + [zset, 1, 'fizz'], + [zset, -3, 'fizz'], + ]); + const score = await db.sortedSetScores(zset, ['foo', 'baz', 'fizz']); + assert.deepStrictEqual(score, [8, 9, 0]); + }); + + it('should handle parallel increments with same key/value pairs', async function () { + const zset = utils.generateUUID(); + await Promise.all([ + db.sortedSetIncrByBulk([[zset, 1, 'baz']]), + db.sortedSetIncrByBulk([[zset, 1, 'baz']]), + db.sortedSetIncrByBulk([[zset, 1, 'baz']]), + ]); + const score = await db.sortedSetScore(zset, 'baz'); + assert.deepStrictEqual(score, 3); + }); }); diff --git a/test/topics.js b/test/topics.js index f67e0fc40e..767af84901 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1425,10 +1425,13 @@ describe('Topic\'s', () => { const unreadTids = await topics.getUnreadTids({ cid: 0, uid: uid }); await sleep(2000); - const _unreadTids = await topics.getUnreadTids({ cid: 0, uid: uid }); + const [_unreadTids, topicData] = await Promise.all([ + topics.getUnreadTids({ cid: 0, uid: uid }), + topics.getTopicData(result.topicData.tid), + ]); assert( !unreadTids.includes(result.topicData.tid), - JSON.stringify({ unreadTids, _unreadTids, tid: result.topicData.tid }) + JSON.stringify({ unreadTids, _unreadTids, topic: topicData }) ); }); }); From 835723482e2edcd8bd34f1390fbe6598d784f830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Mar 2026 11:50:55 -0400 Subject: [PATCH 4714/4744] feat: add unreadNids to /api/notifications --- public/openapi/read/notifications.yaml | 5 +++++ src/controllers/accounts/notifications.js | 2 ++ 2 files changed, 7 insertions(+) diff --git a/public/openapi/read/notifications.yaml b/public/openapi/read/notifications.yaml index 015b6fae33..8dd723aabf 100644 --- a/public/openapi/read/notifications.yaml +++ b/public/openapi/read/notifications.yaml @@ -71,6 +71,11 @@ get: type: boolean readClass: type: string + unreadNids: + type: array + description: An array of notification ids that are unread. + items: + type: string filters: $ref: ../components/schemas/NotificationFilters.yaml#/FiltersArray regularFilters: diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index 301851ca36..00b94f0acb 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -61,6 +61,7 @@ notificationsController.get = async function (req, res, next) { const data = await user.notifications.getAllWithCounts(req.uid, selectedFilter.filter); let notifications = await user.notifications.getNotifications(data.nids, req.uid); + const unreadNids = notifications.filter(n => n && n.nid && !n.read).map(n => n.nid); allFilters.forEach((filterData) => { if (filterData && filterData.filter) { filterData.count = data.counts[filterData.filter] || 0; @@ -72,6 +73,7 @@ notificationsController.get = async function (req, res, next) { res.render('notifications', { notifications: notifications, + unreadNids, pagination: pagination.create(page, pageCount, req.query), filters: allFilters, regularFilters: regularFilters, From 4d3211cabaf963c971019e13d4dfb7bc99fc14dc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 26 Mar 2026 12:04:40 -0400 Subject: [PATCH 4715/4744] fix: regression where topic moves during Announce(Create(Note)) stopped working, added test for #14040, fix broken AP test helper mock --- src/activitypub/inbox.js | 4 +-- src/activitypub/notes.js | 2 +- src/topics/tools.js | 4 +-- test/activitypub/helpers.js | 5 +-- test/activitypub/inbox.js | 72 +++++++++++++++++++++++++++++++------ 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index cd14579362..b8967cbef5 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -53,7 +53,7 @@ inbox.create = async (req) => { cid = Array.from(cids)[0]; } - const asserted = await activitypub.notes.assert(0, object, { cid }); + const asserted = await activitypub.notes.assert(req.uid || 0, object, { cid }); if (asserted) { await activitypub.feps.announce(object.id, req.body); // api.activitypub.add(req, { pid: object.id }); @@ -461,7 +461,7 @@ inbox.announce = async (req) => { return; } - const assertion = await activitypub.notes.assert(0, pid, { cid, skipChecks: true }); + const assertion = await activitypub.notes.assert(req.uid || 0, pid, { cid, skipChecks: true }); if (!assertion) { return; } diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 8522965826..1b570ea689 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -121,7 +121,7 @@ Notes.assert = async (uid, input, options = { skipChecks: false }) => { if (options.cid && cid === -1) { // Move topic if currently uncategorized - await api.topics.move({ uid: 'system' }, { tid, cid: options.cid }); + await topics.tools.move(tid, { cid: options.cid, uid: 'system' }); } const exists = await posts.exists(chain.map(p => p.pid)); diff --git a/src/topics/tools.js b/src/topics/tools.js index f5e9b13dc7..e43a7df208 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -233,7 +233,7 @@ module.exports = function (Topics) { }; topicTools.move = async function (tid, data) { - const cid = parseInt(data.cid, 10); + const cid = utils.isNumber(data.cid) ? parseInt(data.cid, 10) : data.cid; const topicData = await Topics.getTopicData(tid); if (!topicData) { throw new Error('[[error:no-topic]]'); @@ -241,7 +241,7 @@ module.exports = function (Topics) { if (cid === topicData.cid) { throw new Error('[[error:cant-move-topic-to-same-category]]'); } - if (!utils.isNumber(cid) || !utils.isNumber(topicData.cid)) { + if (data.uid !== 'system' && (!utils.isNumber(cid) || !utils.isNumber(topicData.cid))) { throw new Error('[[error:cant-move-topic-to-from-remote-categories]]'); } diff --git a/test/activitypub/helpers.js b/test/activitypub/helpers.js index c026f23e1d..eb3056d2af 100644 --- a/test/activitypub/helpers.js +++ b/test/activitypub/helpers.js @@ -55,12 +55,13 @@ Helpers.mocks.group = (override = {}) => { type: 'Group', ...override, }); + const { hostname } = new URL(id); activitypub._cache.set(`0;${id}`, actor); - activitypub.helpers._webfingerCache.set(`${actor.preferredUsername}@example.org`, { + activitypub.helpers._webfingerCache.set(`${actor.preferredUsername}@${hostname}`, { actorUri: id, username: id, - hostname: 'example.org', + hostname, }); return { id, actor }; diff --git a/test/activitypub/inbox.js b/test/activitypub/inbox.js index 4c812a873d..bfcdb690e9 100644 --- a/test/activitypub/inbox.js +++ b/test/activitypub/inbox.js @@ -183,20 +183,72 @@ describe('Inbox', () => { }); describe('(Create)', () => { - it('should create a new topic in a remote category if addressed', async () => { - const { id: remoteCid } = helpers.mocks.group(); - const { id, note } = helpers.mocks.note({ - audience: [remoteCid], + describe('newly-discovered topic', () => { + before(async function () { + const { id: remoteCid } = helpers.mocks.group(); + const { id, note } = helpers.mocks.note({ + audience: [remoteCid], + }); + this.id = id; + this.remoteCid = remoteCid; + let { activity } = helpers.mocks.create(note); + ({ activity } = helpers.mocks.announce({ actor: remoteCid, object: activity })); + + await activitypub.inbox.announce({ body: activity }); }); - let { activity } = helpers.mocks.create(note); - ({ activity } = helpers.mocks.announce({ actor: remoteCid, object: activity })); - await activitypub.inbox.announce({ body: activity }); + it('should create a new topic in a remote category if addressed', async function () { + assert(await posts.exists(this.id)); - assert(await posts.exists(id)); + const cid = await posts.getCidByPid(this.id); + assert.strictEqual(cid, this.remoteCid); + }); + }); - const cid = await posts.getCidByPid(id); - assert.strictEqual(cid, remoteCid); + describe.only('known topic in cid -1 (author domain != announcer domain)', async () => { + /** + * This happens if follower receives object from microblog user before the community announces it. + * It's probably more likely to occur because the Create(Note) is a single hop whereas the reflected + * Announce(Create(Note)) takes two hops. + * + * If the author and announcer domain are the same, the object should already be correctly classified. + */ + before(async function () { + const { id: remoteCid } = helpers.mocks.group({ + id: `https://example.social/${utils.generateUUID()}`, + }); + await activitypub.actors.assertGroup([remoteCid]); + const uid = await user.create({ username: utils.generateUUID().slice(0, 10) }); + + this.uid = uid; + this.remoteCid = remoteCid; + }); + + it('should create a topic in cid -1', async function () { + const { id, note } = helpers.mocks.note({ + to: [activitypub._constants.publicAddress, this.remoteCid], + }); + + const { activity } = helpers.mocks.create(note); + await activitypub.inbox.create({ uid: this.uid, body: activity }); + + this.id = id; + this.note = note; + this.activity = activity; + + const cid = await posts.getCidByPid(this.id); + assert.strictEqual(cid, -1); + }); + + it('should handle the Announce(Create) from the remote category', async function () { + const { activity } = helpers.mocks.announce({ actor: this.remoteCid, object: this.activity }); + await activitypub.inbox.announce({ uid: this.uid, body: activity }); + }); + + it('should be categorized in the remote category', async function () { + const cid = await posts.getCidByPid(this.id); + assert.strictEqual(cid, this.remoteCid); + }); }); }); From 1a0c2a21c7ffceb2a0f7d316bbe662c69b63d84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 26 Mar 2026 12:43:21 -0400 Subject: [PATCH 4716/4744] fix: align-center user and name on post queue --- src/views/post-queue.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/post-queue.tpl b/src/views/post-queue.tpl index 7366500f6e..569bfb68e3 100644 --- a/src/views/post-queue.tpl +++ b/src/views/post-queue.tpl @@ -100,9 +100,9 @@
    {{{ end }}} -
    +
    {{{ if posts.user.userslug}}} - {buildAvatar(posts.user, "24px", true, "not-responsive")} {posts.user.username} + {buildAvatar(posts.user, "24px", true, "not-responsive")} {posts.user.username} {{{ else }}} {posts.user.username} {{{ end }}} From 82fad39f8149f1eac82b290cec654c7bd8a2b385 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 27 Mar 2026 09:12:32 +0000 Subject: [PATCH 4717/4744] Latest translations and fallbacks --- public/language/ja/admin/manage/users.json | 24 ++-- public/language/ja/error.json | 144 ++++++++++----------- public/language/ja/flags.json | 112 ++++++++-------- public/language/ja/social.json | 24 ++-- public/language/ja/themes/harmony.json | 46 +++---- public/language/ja/topic.json | 28 ++-- 6 files changed, 189 insertions(+), 189 deletions(-) diff --git a/public/language/ja/admin/manage/users.json b/public/language/ja/admin/manage/users.json index 05ca18a673..a42a1a6853 100644 --- a/public/language/ja/admin/manage/users.json +++ b/public/language/ja/admin/manage/users.json @@ -4,11 +4,11 @@ "edit": "アクション", "make-admin": "管理者にする", "remove-admin": "管理者を削除", - "change-email": "Change Email", - "new-email": "New Email", - "validate-email": "電子メールの", + "change-email": "メールを変更", + "new-email": "新しいメール", + "validate-email": "メールを確認", "send-validation-email": "確認メールを送信", - "change-password": "Change Password", + "change-password": "パスワードを変更", "password-reset-email": "パスワードリセットメールを送信する", "force-password-reset": "パスワードのリセットとユーザーのログアウトを強制する", "ban": "Ban", @@ -24,8 +24,8 @@ "download-csv": "CSVでダウンロード", "custom-user-fields": "カスタムユーザーフィールド", "custom-reasons": "通知文", - "manage-groups": "Manage Groups", - "set-reputation": "Set Reputation", + "manage-groups": "グループを管理", + "set-reputation": "評価を設定", "add-group": "Add Group", "create": "Create User", "invite": "Invite by Email", @@ -40,7 +40,7 @@ "250-per-page": "1ページあたり250 件", "500-per-page": "1ページあたり500 件", - "search.help": "Use "*" to make partial searches, for example "*query"", + "search.help": "部分検索には\\\"*\\\"を使用してください。例:\\\"*query\\\"\"", "search.uid": "ユーザーID別", "search.uid-placeholder": "検索するユーザーIDを入力してください", "search.username": "ユーザー名別", @@ -56,7 +56,7 @@ "inactive.12-months": "12ヶ月", "users.uid": "ユーザーID", - "users.user-id": "User ID", + "users.user-id": "ユーザーID", "users.username": "ユーザー名", "users.email": "メール", "users.no-email": "(no email)", @@ -65,7 +65,7 @@ "users.validation-pending": "Validation Pending", "users.validation-expired": "Validation Expired", "users.ip": "IP", - "users.recent-ips": "Recent IPs", + "users.recent-ips": "最近のIP", "users.postcount": "投稿カウント", "users.reputation": "評価", "users.flags": "フラグ", @@ -81,11 +81,11 @@ "temp-ban.length": "Length", "temp-ban.reason": "理由(任意)", - "temp-ban.select-reason": "Select a reason", + "temp-ban.select-reason": "理由を選択してください", "temp-ban.hours": "時間", "temp-ban.days": "日", - "temp-ban.explanation": "禁止期間の長さを入力します。0にすると永久に禁止と解釈されますのでご注意ください。", - "temp-mute.explanation": "Enter the length of time for the mute. Note that a time of 0 will be a considered a permanent mute.", + "temp-ban.explanation": "停止期間の長さを入力してください。0を指定すると永久停止と見なされますのでご注意ください。", + "temp-mute.explanation": "ミュート期間の長さを入力してください。0を指定すると永久ミュートと見なされますのでご注意ください。", "alerts.confirm-ban": "あなたは本当にこのユーザーを永久に禁止しますか?", "alerts.confirm-ban-multi": "あなたは本当にこれらのユーザーを恒久的に禁止しますか?", diff --git a/public/language/ja/error.json b/public/language/ja/error.json index 16e13c4efb..cc4b404729 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -79,54 +79,54 @@ "post-edit-duration-expired-hours-minutes": "投稿後%1時間%2分間だけ投稿を編集できます。", "post-edit-duration-expired-days": "投稿後%1日間だけ投稿を編集できます。", "post-edit-duration-expired-days-hours": "投稿後%1日%2時間だけ投稿を編集できます。", - "post-delete-duration-expired": "あなたは%1 秒(s)後に投稿を削除することが許可されています。", - "post-delete-duration-expired-minutes": "あなたは%1 分(s)後に投稿を削除することが許可されています。", - "post-delete-duration-expired-minutes-seconds": "あなたは%1 分(s) %2 秒(s) 後に投稿を削除することが許可されています。", - "post-delete-duration-expired-hours": "あなたは%1 時間(s)後に投稿を削除することが許可されています。", - "post-delete-duration-expired-hours-minutes": "あなたは%1 時間(s) %2 分(s)後に投稿を削除することが許可されています。", - "post-delete-duration-expired-days": "あなたは%1 日(s)後に投稿を削除することが許可されています。", - "post-delete-duration-expired-days-hours": "あなたは%1 日(s) %2 時間(s)後に投稿を削除することが許可されています。", - "cant-delete-topic-has-reply": "応答待ちの場合、あなたのスレッドは削除できません。", - "cant-delete-topic-has-replies": "%1 の応答待ちの場合、あなたのスレッドは削除できません。", - "content-too-short": "より長く投稿を書いて下さい。投稿にはせめて%1文字が必要です。", - "content-too-long": "より短く投稿を書いて下さい。投稿が%1文字以上が許されません。", - "title-too-short": "より長くタイトルを書いて下さい。タイトルはせめて%1文字が必要です。", - "title-too-long": "より短くタイトルを書いて下さい。タイトルは%1文字以上が許されません。", + "post-delete-duration-expired": "投稿後%1秒間だけ投稿を削除できます。", + "post-delete-duration-expired-minutes": "投稿後%1分間だけ投稿を削除できます。", + "post-delete-duration-expired-minutes-seconds": "投稿後%1分%2秒間だけ投稿を削除できます。", + "post-delete-duration-expired-hours": "投稿後%1時間だけ投稿を削除できます。", + "post-delete-duration-expired-hours-minutes": "投稿後%1時間%2分間だけ投稿を削除できます。", + "post-delete-duration-expired-days": "投稿後%1日間だけ投稿を削除できます。", + "post-delete-duration-expired-days-hours": "投稿後%1日%2時間だけ投稿を削除できます。", + "cant-delete-topic-has-reply": "返信があるため、このスレッドは削除できません。", + "cant-delete-topic-has-replies": "%1件の返信があるため、このスレッドは削除できません。", + "content-too-short": "もっと長い投稿を入力してください。投稿には少なくとも%1文字必要です。", + "content-too-long": "もっと短い投稿を入力してください。投稿は%1文字以下で入力してください。", + "title-too-short": "もっと長いタイトルを入力してください。タイトルには少なくとも%1文字必要です。", + "title-too-long": "もっと短いタイトルを入力してください。タイトルは%1文字以下で入力してください。", "category-not-selected": "カテゴリが選択されていません。", - "too-many-posts": "あなたは%1秒間に一つの投稿しか許されます-少し待ってまた投稿してください", - "too-many-posts-newbie": "あなたは%2評判を得ているまで、新しいユーザーとしては、一度だけごとに%1秒を投稿することができます - 再び投稿する前にお待ちください", - "too-many-posts-newbie-minutes": "As a new user, you can only post once every %1 minute(s) until you have earned %2 reputation - please wait before posting again", - "already-posting": "You are already posting", - "tag-too-short": "%1文字(s)以上でタグを入力してください。", - "tag-too-long": "%1文字(s)以内でタグを入力してください。", - "tag-not-allowed": "Tag not allowed", - "not-enough-tags": "タグが足りません。スレッドはせめて%1のタグ(s)が必要です。", - "too-many-tags": "タグが多すぎます。スレッドは%1のタグ(s)以上が許されません。", - "cant-use-system-tag": "You can not use this system tag.", - "cant-remove-system-tag": "You can not remove this system tag.", + "too-many-posts": "投稿は%1秒に1回までです。再度投稿する前にしばらくお待ちください。", + "too-many-posts-newbie": "新規ユーザーは評価が%2に達するまで、%1秒に1回しか投稿できません。再度投稿する前にしばらくお待ちください。", + "too-many-posts-newbie-minutes": "新規ユーザーは%2の評価を得るまで%1分に1回しか投稿できません。再度投稿する前にしばらくお待ちください。", + "already-posting": "すでに投稿中です", + "tag-too-short": "%1文字以上でタグを入力してください。", + "tag-too-long": "%1文字以内でタグを入力してください。", + "tag-not-allowed": "このタグは使用できません", + "not-enough-tags": "タグが不足しています。スレッドには少なくとも%1個のタグが必要です。", + "too-many-tags": "タグが多すぎます。スレッドに設定できるタグは最大%1個です。", + "cant-use-system-tag": "このシステムタグは使用できません。", + "cant-remove-system-tag": "このシステムタグは削除できません。", "still-uploading": "アップロードが完成するまでお待ちください。", - "file-too-big": "%1kBより大きいサイズファイルが許されません-より小さいファイルをアップして下さい。", + "file-too-big": "許可される最大ファイルサイズは%1kBです。より小さいファイルをアップロードしてください。", "guest-upload-disabled": "ゲストさんからのアップを無効にしています", "cors-error": "CORSの設定が誤っているため、画像をアップロードできません", - "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", - "upload-error-fallback": "Unable to upload image — %1", - "scheduling-to-past": "Please select a date in the future.", - "invalid-schedule-date": "Please enter a valid date and time.", - "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", - "cant-merge-scheduled": "Scheduled topics cannot be merged.", - "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", - "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "upload-ratelimit-reached": "一度にアップロードしたファイルが多すぎます。しばらくしてから再度お試しください。", + "upload-error-fallback": "画像をアップロードできません — %1", + "scheduling-to-past": "将来の日付を選択してください。", + "invalid-schedule-date": "有効な日付と時刻を入力してください。", + "cant-pin-scheduled": "予約スレッドは固定/固定解除できません。", + "cant-merge-scheduled": "予約スレッドはマージできません。", + "cant-move-posts-to-scheduled": "予約スレッドに投稿を移動できません。", + "cant-move-from-scheduled-to-existing": "予約スレッドから既存のスレッドに投稿を移動できません。", "already-bookmarked": "あなたは、この投稿をすでにブックマークしています", - "already-unbookmarked": "あなたは、この投稿をすでにブックマークから外しています", + "already-unbookmarked": "この投稿のブックマークはすでに解除されています", "cant-ban-other-admins": "ほかの管理者を停止することはできません!", - "cant-mute-other-admins": "You can't mute other admins!", - "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", - "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", - "cant-make-banned-users-admin": "You can't make banned users admin.", - "cant-remove-last-admin": "あなたが唯一の管理者です。管理者としてあなた自身を削除する前に、管理者として別のユーザーを追加します。", - "account-deletion-disabled": "Account deletion is disabled", + "cant-mute-other-admins": "他の管理者をミュートすることはできません!", + "user-muted-for-hours": "ミュートされました。%1時間後に投稿できるようになります。", + "user-muted-for-minutes": "ミュートされました。%1分後に投稿できるようになります。", + "cant-make-banned-users-admin": "BANされたユーザーを管理者にすることはできません。", + "cant-remove-last-admin": "あなたが唯一の管理者です。自分の管理者権限を外す前に、別のユーザーを管理者に追加してください。", + "account-deletion-disabled": "アカウント削除は無効になっています", "cant-delete-admin": "削除する前に、このアカウントから管理者権限を削除してください。", - "already-deleting": "Already deleting", + "already-deleting": "削除処理中です", "invalid-image": "無効な画像", "invalid-image-type": "無効なイメージタイプです。許可された種類は: %1", "invalid-image-extension": "無効なイメージのエクステンション", @@ -136,41 +136,41 @@ "group-name-too-long": "グループ名が長すぎます", "group-already-exists": "グループ名はすでに存在しています", "group-name-change-not-allowed": "グループ名の修正はできません", - "group-already-member": "既にこのグループの一部であります", - "group-not-member": "このグループの一部ではありません", + "group-already-member": "すでにこのグループのメンバーです", + "group-not-member": "このグループのメンバーではありません", "group-needs-owner": "このグループには少なくとも一人のオーナーが必要です", - "group-already-invited": "このユーザーが既に招待されました", - "group-already-requested": "あなたのメンバーシップの要求が既に提出されました", - "group-join-disabled": "You are not able to join this group at this time", - "group-leave-disabled": "You are not able to leave this group at this time", - "group-user-not-pending": "User does not have a pending request to join this group.", - "gorup-user-not-invited": "User has not been invited to join this group.", - "post-already-deleted": "この投稿が既に削除されました", - "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": "スレッドのサムネイルが無効された", + "group-already-invited": "このユーザーがすでに招待されました", + "group-already-requested": "あなたのメンバーシップの要求がすでに提出されました", + "group-join-disabled": "現在このグループに参加できません", + "group-leave-disabled": "現在このグループを退会できません", + "group-user-not-pending": "このユーザーはこのグループへの参加リクエストを保留していません。", + "gorup-user-not-invited": "このユーザーはこのグループへの招待を受けていません。", + "post-already-deleted": "この投稿がすでに削除されました", + "post-already-restored": "この投稿がすでに復元されました", + "topic-already-deleted": "このスレッドはすでに削除されました", + "topic-already-restored": "このスレッドはすでに復元されました", + "topic-already-crossposted": "このスレッドはすでにそこにクロスポストされています。", + "cant-purge-main-post": "メインの投稿を削除することはできません。代わりにスレッドを削除してください。", + "topic-thumbnails-are-disabled": "スレッドのサムネイルは無効になっています", "invalid-file": "無効なファイル", - "uploads-are-disabled": "アップロードが無効された", - "signature-too-long": "申し訳ありませんが、あなたの署名が%1文字より長くすることができません。", - "about-me-too-long": "申し訳ありませんが、あなたの私についての項目が%1より長くすることができません。", + "uploads-are-disabled": "アップロードは無効になっています", + "signature-too-long": "署名は%1文字以下で入力してください。", + "about-me-too-long": "「自己紹介」は%1文字以下で入力してください。", "cant-chat-with-yourself": "自分にチャットすることはできません!", - "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", - "chat-allow-list-user-already-added": "This user is already in your allow list", - "chat-deny-list-user-already-added": "This user is already in your deny list", - "chat-user-blocked": "You have been blocked by this user.", - "chat-disabled": "Chat system disabled", - "too-many-messages": "You have sent too many messages, please wait awhile.", - "invalid-chat-message": "Invalid chat message", - "chat-message-too-long": "チャットメッセージは%1文字より長くすることはできません。", - "cant-edit-chat-message": "You are not allowed to edit this message", + "chat-restricted": "このユーザーはチャットを制限しています。チャットするにはフォローしてもらう必要があります。", + "chat-allow-list-user-already-added": "このユーザーはすでに許可リストに追加されています", + "chat-deny-list-user-already-added": "このユーザーはすでに拒否リストに追加されています", + "chat-user-blocked": "このユーザーにブロックされています。", + "chat-disabled": "チャットシステムは無効です", + "too-many-messages": "メッセージを送りすぎました。しばらくお待ちください。", + "invalid-chat-message": "無効なチャットメッセージ", + "chat-message-too-long": "チャットメッセージは%1文字以下で入力してください。", + "cant-edit-chat-message": "このメッセージを編集する権限がありません", "cant-delete-chat-message": "あなたはこのメッセージを削除する権限を持っていません。", - "chat-edit-duration-expired": "投稿後、あなたは %1秒間(s)だけチャットメッセージを編集することを許可されています", - "chat-delete-duration-expired": "投稿後、あなたは %1秒間(s)だけチャットメッセージを削除することを許可されています", - "chat-deleted-already": "このチャットメッセージは既に削除されています", - "chat-restored-already": "このチャットメッセージは既に削除されています", + "chat-edit-duration-expired": "投稿後%1秒間だけチャットメッセージを編集できます", + "chat-delete-duration-expired": "投稿後%1秒間だけチャットメッセージを削除できます", + "chat-deleted-already": "このチャットメッセージはすでに削除されています", + "chat-restored-already": "このチャットメッセージはすでに復元されています", "chat-room-does-not-exist": "Chat room does not exist.", "cant-add-users-to-chat-room": "Can't add users to chat room.", "cant-remove-users-from-chat-room": "Can't remove users from chat room.", diff --git a/public/language/ja/flags.json b/public/language/ja/flags.json index fa025366ae..69d525cc27 100644 --- a/public/language/ja/flags.json +++ b/public/language/ja/flags.json @@ -1,101 +1,101 @@ { "state": "状態", - "report": "Report", - "reports": "Reports", - "first-reported": "First Reported", + "report": "報告", + "reports": "報告", + "first-reported": "最初の報告", "no-flags": "おめでとう!フラグは見つかりませんでした。", - "x-flags-found": "%1 flag(s) found.", - "assignee": "譲受人", + "x-flags-found": "%1件のフラグが見つかりました。", + "assignee": "担当者", "update": "更新", "updated": "更新されました", - "resolved": "Resolved", - "report-added": "Added", - "report-rescinded": "Rescinded", - "target-purged": "このフラグが参照しているコンテンツは切り離されており、利用できません。", - "target-aboutme-empty": "This user has no "About Me" set.", + "resolved": "解決済み", + "report-added": "追加", + "report-rescinded": "取り消し", + "target-purged": "このフラグが参照していたコンテンツは完全削除され、利用できません。", + "target-aboutme-empty": "このユーザーは「私について」を設定していません。", - "graph-label": "Daily Flags", + "graph-label": "日別フラグ", "quick-filters": "クイックフィルター", - "filter-active": "このフラグのリストには1つまたは複数のフィルタが有効です。", + "filter-active": "このフラグリストに1つ以上のフィルターが適用されています", "filter-reset": "フィルターを削除", "filters": "フィルターオプション", - "filter-reporterId": "Reporter", - "filter-targetUid": "Reportee", + "filter-reporterId": "報告者", + "filter-targetUid": "被報告者", "filter-type": "フラグの種類", "filter-type-all": "すべてのコンテンツ", "filter-type-post": "投稿", - "filter-type-user": "User", + "filter-type-user": "ユーザー", "filter-state": "状態", - "filter-assignee": "Assignee", + "filter-assignee": "担当者", "filter-cid": "カテゴリ", - "filter-quick-mine": "私に割り当てられました", - "filter-cid-all": "全てのカテゴリ", - "apply-filters": "フィルターを追加", - "more-filters": "More Filters", - "fewer-filters": "Fewer Filters", + "filter-quick-mine": "自分に割り当てられた", + "filter-cid-all": "すべてのカテゴリ", + "apply-filters": "フィルターを適用", + "more-filters": "フィルターを追加表示", + "fewer-filters": "フィルターを減らす", - "quick-actions": "Quick Actions", + "quick-actions": "クイックアクション", "flagged-user": "フラグを立てたユーザー", "view-profile": "プロフィールを見る", "start-new-chat": "新しいチャットを開始", "go-to-target": "フラグのターゲットを表示", - "assign-to-me": "Assign To Me", - "delete-post": "Delete Post", - "purge-post": "Purge Post", - "restore-post": "Restore Post", - "delete": "Delete Flag", + "assign-to-me": "自分に割り当て", + "delete-post": "投稿を削除", + "purge-post": "投稿を完全削除", + "restore-post": "投稿を復元", + "delete": "フラグを削除", "user-view": "プロフィールを見る", "user-edit": "プロフィールを編集", "notes": "ノートにフラグをつける", "add-note": "ノートを追加", - "edit-note": "Edit Note", + "edit-note": "ノートを編集", "no-notes": "共有ノートはありません。", - "delete-note-confirm": "Are you sure you want to delete this flag note?", - "delete-flag-confirm": "Are you sure you want to delete this flag?", + "delete-note-confirm": "このフラグノートを削除してもよろしいですか?", + "delete-flag-confirm": "このフラグを削除してもよろしいですか?", "note-added": "ノートが追加されました", - "note-deleted": "Note Deleted", - "flag-deleted": "Flag Deleted", + "note-deleted": "ノートが削除されました", + "flag-deleted": "フラグが削除されました", - "history": "Account & Flag History", + "history": "アカウントとフラグ履歴", "no-history": "フラグ履歴がありません", - "state-all": "全ての状態", + "state-all": "すべての状態", "state-open": "新規/開く", "state-wip": "進行中の作業", "state-resolved": "解決済み", "state-rejected": "拒否済", "no-assignee": "割り当てられていない", - "sort": "Sort by", - "sort-newest": "Newest first", - "sort-oldest": "Oldest first", - "sort-reports": "Most reports", - "sort-all": "All flag types...", - "sort-posts-only": "Posts only...", - "sort-downvotes": "Most downvotes", - "sort-upvotes": "Most upvotes", - "sort-replies": "Most replies", + "sort": "並び替え基準", + "sort-newest": "新しい順", + "sort-oldest": "古い順", + "sort-reports": "報告数順", + "sort-all": "すべてのフラグタイプ…", + "sort-posts-only": "投稿のみ…", + "sort-downvotes": "低評価順", + "sort-upvotes": "高評価順", + "sort-replies": "返信数順", - "modal-title": "Report Content", + "modal-title": "コンテンツを報告", "modal-body": "レビューのために%1 %2 にフラグを付ける理由を指定してください。または必要に応じてクイックレポートボタンの1つを使用します。", "modal-reason-spam": "スパム", - "modal-reason-offensive": "攻撃", - "modal-reason-other": "Other (specify below)", + "modal-reason-offensive": "不快なコンテンツ", + "modal-reason-other": "その他(以下に指定)", "modal-reason-custom": "このコンテンツを報告する理由...", - "modal-notify-remote": "Forward this report to %1", + "modal-notify-remote": "この報告を%1に転送", "modal-submit": "レポートを提出", "modal-submit-success": "コンテンツはモデレーションにフラグ付けされています。", - "modal-confirm-rescind": "Rescind Report?", + "modal-confirm-rescind": "報告を取り消しますか?", - "bulk-actions": "Bulk Actions", - "bulk-resolve": "Resolve Flag(s)", - "confirm-purge": "Are you sure you want to permanently delete these flags?", - "purge-cancelled": "Flag Purge Cancelled", - "bulk-purge": "Purge Flag(s)", - "bulk-success": "%1 flags updated", - "flagged-timeago": "Flagged ", - "auto-flagged": "[Auto Flagged] Received %1 downvotes." + "bulk-actions": "一括操作", + "bulk-resolve": "フラグを解決", + "confirm-purge": "これらのフラグを完全に削除してもよろしいですか?", + "purge-cancelled": "フラグの完全削除をキャンセルしました", + "bulk-purge": "フラグを完全削除", + "bulk-success": "%1件のフラグを更新しました", + "flagged-timeago": "フラグ付け ", + "auto-flagged": "[自動フラグ] %1件の低評価を受けました。" } \ No newline at end of file diff --git a/public/language/ja/social.json b/public/language/ja/social.json index 5b8dd99a46..bd206b30df 100644 --- a/public/language/ja/social.json +++ b/public/language/ja/social.json @@ -1,14 +1,14 @@ { - "sign-in-with-twitter": "Sign in with Twitter", - "sign-up-with-twitter": "Sign up with Twitter", - "sign-in-with-github": "Sign in with Github", - "sign-up-with-github": "Sign up with Github", - "sign-in-with-google": "Sign in with Google", - "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-twitter": "Twitterでサインイン", + "sign-up-with-twitter": "Twitterでサインアップ", + "sign-in-with-github": "Githubでサインイン", + "sign-up-with-github": "Githubでサインアップ", + "sign-in-with-google": "Googleでサインイン", + "sign-up-with-google": "Googleでサインアップ", + "log-in-with-facebook": "Facebookでログイン", + "continue-with-facebook": "Facebookで続行", + "sign-in-with-linkedin": "LinkedInでサインイン", + "sign-up-with-linkedin": "LinkedInでサインアップ", + "sign-in-with-wordpress": "WordPressでサインイン", + "sign-up-with-wordpress": "WordPressでサインアップ" } \ No newline at end of file diff --git a/public/language/ja/themes/harmony.json b/public/language/ja/themes/harmony.json index 7143d1b56d..5f1e0a6a6e 100644 --- a/public/language/ja/themes/harmony.json +++ b/public/language/ja/themes/harmony.json @@ -1,25 +1,25 @@ { - "theme-name": "Harmony Theme", - "skins": "Skins", - "light": "Light", - "dark": "Dark", - "collapse": "Collapse", - "expand": "Expand", - "sidebar-toggle": "Sidebar Toggle", - "login-register-to-search": "Login or register to search.", - "settings.title": "Theme settings", - "settings.enableQuickReply": "Enable quick reply", - "settings.enableBreadcrumbs": "Show breadcrumbs in Category and Topic pages", - "settings.enableBreadcrumbs.why": "Breadcrumbs are visible in most pages for ease-of-navigation. The base design of the category and topic pages has alternative means to link back to parent pages, but the breadcrumb can be toggled off to reduce clutter.", - "settings.centerHeaderElements": "Center header elements", - "settings.mobileTopicTeasers": "Show topic teasers on mobile", - "settings.stickyToolbar": "Sticky toolbar", - "settings.stickyToolbar.help": "The toolbar on topic and category pages will stick to the top of the page", - "settings.topicSidebarTools": "Topic sidebar tools", - "settings.topicSidebarTools.help": "This option will move the topic tools to the sidebar on desktop", - "settings.autohideBottombar": "Auto hide mobile navigation bar", - "settings.autohideBottombar.help": "The mobile bar will be hidden when the page is scrolled down", - "settings.topMobilebar": "Move the mobile navigation bar to the top", - "settings.openSidebars": "Open sidebars", - "settings.chatModals": "Enable chat modals" + "theme-name": "Harmonyテーマ", + "skins": "スキン", + "light": "ライト", + "dark": "ダーク", + "collapse": "折りたたむ", + "expand": "展開", + "sidebar-toggle": "サイドバー切り替え", + "login-register-to-search": "検索するにはログインまたは登録してください", + "settings.title": "テーマ設定", + "settings.enableQuickReply": "クイック返信を有効にする", + "settings.enableBreadcrumbs": "カテゴリとスレッドページにパンくずリストを表示", + "settings.enableBreadcrumbs.why": "パンくずリストはほとんどのページでナビゲーションを容易にするために表示されます。カテゴリとスレッドページの基本デザインには親ページへのリンクの代替手段がありますが、パンくずリストはオフにして煩雑さを減らすことができます。", + "settings.centerHeaderElements": "ヘッダー要素を中央揃え", + "settings.mobileTopicTeasers": "モバイルでスレッドティーザーを表示", + "settings.stickyToolbar": "固定ツールバー", + "settings.stickyToolbar.help": "スレッドとカテゴリページのツールバーがページの上部に固定されます", + "settings.topicSidebarTools": "スレッドサイドバーツール", + "settings.topicSidebarTools.help": "このオプションで、デスクトップのスレッドツールがサイドバーに移動します", + "settings.autohideBottombar": "モバイルナビゲーションバーを自動非表示", + "settings.autohideBottombar.help": "ページを下にスクロールするとモバイルバーが非表示になります", + "settings.topMobilebar": "モバイルナビゲーションバーを上部に移動", + "settings.openSidebars": "サイドバーを開く", + "settings.chatModals": "チャットモーダルを有効にする" } \ No newline at end of file diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json index 2542ecff75..a67b1f651b 100644 --- a/public/language/ja/topic.json +++ b/public/language/ja/topic.json @@ -226,18 +226,18 @@ "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", - "post-quick-reply": "Quick reply", - "post-quick-create": "Quick post", - "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)", - "guest-cta.title": "Hello! It looks like you're interested in this conversation, but you don't have an account yet.", - "guest-cta.message": "Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.", - "guest-cta.closing": "With your input, this post could be even better 💗" + "post-quick-reply": "クイック返信", + "post-quick-create": "クイック投稿", + "navigator.index": "1件目の投稿", + "navigator.unread": "1件未読", + "upvote-post": "投稿に高評価", + "downvote-post": "投稿に低評価", + "post-tools": "投稿ツール", + "unread-posts-link": "未読投稿リンク", + "thumb-image": "スレッドのサムネイル画像", + "announcers": "共有", + "announcers-x": "共有 (%1)", + "guest-cta.title": "こんにちは!この会話に興味があるようですが、まだアカウントをお持ちでないようです。", + "guest-cta.message": "毎回同じ投稿をスクロールするのにうんざりしていませんか?アカウントを登録すると、いつでも前回の続きから閲覧でき、新しい返信を通知(メールまたはプッシュ通知)で受け取れます。ブックマークの保存や投稿への高評価で、他のコミュニティメンバーに感謝を示すこともできます。", + "guest-cta.closing": "あなたの参加で、この投稿はさらに良くなるかもしれません 💗" } \ No newline at end of file From 202a99bb488e523ac93c94b216762269fa569205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Mar 2026 10:29:02 -0400 Subject: [PATCH 4718/4744] perf: use $or instead of double $in, in sortedSetIncrByBulk --- src/database/mongo/sorted.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 71c4f4c273..9940cf2677 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -1,6 +1,5 @@ 'use strict'; -const _ = require('lodash'); const utils = require('../../utils'); module.exports = function (module) { @@ -463,11 +462,15 @@ module.exports = function (module) { return []; } const aggregated = dbHelpers.aggregateIncrByBulk(data); + const findQuery = { $or: [] }; + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); - aggregated.forEach((item) => { - bulk.find({ _key: item[0], value: helpers.valueToString(item[2]) }) + aggregated.forEach(([key, incr, value]) => { + const query = { _key: key, value: value }; + findQuery.$or.push(query); + bulk.find(query) .upsert() - .update({ $inc: { score: parseFloat(item[1]) } }); + .update({ $inc: { score: parseFloat(incr) } }); }); try { @@ -475,28 +478,29 @@ module.exports = function (module) { } catch (err) { // retry failed e11000 operations if (err.code === 11000 || (err.writeErrors && err.writeErrors.some(e => e.code === 11000))) { - const failedIndices = err.writeErrors.map(e => e.index); + const failedIndices = err.writeErrors.filter(e => e.code === 11000).map(e => e.index); const retryData = failedIndices.map(idx => aggregated[idx]); await Promise.all(retryData.map( - item => module.sortedSetIncrBy(item[0], item[1], item[2]) + ([key, incr, value]) => module.sortedSetIncrBy(key, incr, value) )); } else { throw err; } } - const result = await module.client.collection('objects').find({ - _key: { $in: _.uniq(aggregated.map(i => i[0])) }, - value: { $in: _.uniq(aggregated.map(i => i[2])) }, - }, { - projection: { _id: 0, _key: 1, value: 1, score: 1 }, - }).toArray(); + const result = await module.client.collection('objects').find( + findQuery, + { projection: { _id: 0, _key: 1, value: 1, score: 1 } }, + ).toArray(); - const map = {}; + const map = Object.create(null); result.forEach((item) => { - map[`${item._key}:${item.value}`] = item.score; + if (!map[item._key]) { + map[item._key] = Object.create(null); + } + map[item._key][item.value] = item.score; }); - return data.map(item => map[`${item[0]}:${item[2]}`]); + return data.map(([key, , value]) => (map[key] && map[key][value]) || 0); }; module.getSortedSetRangeByLex = async function (key, min, max, start, count) { From 25fb2969cf3fa9a7ac77098599ae9e5123cb5ae1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 27 Mar 2026 10:50:12 -0400 Subject: [PATCH 4719/4744] test: remove leftover .only --- test/activitypub/inbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/activitypub/inbox.js b/test/activitypub/inbox.js index bfcdb690e9..712b182c36 100644 --- a/test/activitypub/inbox.js +++ b/test/activitypub/inbox.js @@ -205,7 +205,7 @@ describe('Inbox', () => { }); }); - describe.only('known topic in cid -1 (author domain != announcer domain)', async () => { + describe('known topic in cid -1 (author domain != announcer domain)', async () => { /** * This happens if follower receives object from microblog user before the community announces it. * It's probably more likely to occur because the Create(Note) is a single hop whereas the reflected From 8547fa9e481499acaf81fa320d428aa00493fb07 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 27 Mar 2026 11:06:26 -0400 Subject: [PATCH 4720/4744] fix: broken test --- test/activitypub/inbox.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/activitypub/inbox.js b/test/activitypub/inbox.js index 712b182c36..f70d10db79 100644 --- a/test/activitypub/inbox.js +++ b/test/activitypub/inbox.js @@ -155,15 +155,16 @@ describe('Inbox', () => { }); it('should create a new topic in cid -1 if a non-same origin remote category is addressed', async function () { + const uid = await user.create({ username: utils.generateUUID() }); const { id: remoteCid } = helpers.mocks.group({ id: `https://example.com/${utils.generateUUID()}`, }); const { note, id } = helpers.mocks.note({ - audience: [remoteCid], + to: [remoteCid, activitypub._constants.publicAddress], }); const { activity } = helpers.mocks.create(note); try { - await activitypub.inbox.create({ body: activity }); + await activitypub.inbox.create({ uid, body: activity }); } catch (err) { assert(false); } From 837d984b8483624b083b11eac05d0cec2327141f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Mar 2026 11:24:43 -0400 Subject: [PATCH 4721/4744] refactor: some cleanup of dbal code --- src/database/mongo/main.js | 16 ++++----- src/database/mongo/sets.js | 12 +++---- src/database/mongo/sorted.js | 52 ++++++++++++----------------- src/database/mongo/sorted/remove.js | 6 ++-- test/database/keys.js | 6 ++++ 5 files changed, 42 insertions(+), 50 deletions(-) diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 77f1f1409c..aa5892389b 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -24,12 +24,8 @@ module.exports = function (module) { _key: { $in: key }, }, { _id: 0, _key: 1 }).toArray(); - const map = Object.create(null); - data.forEach((item) => { - map[item._key] = true; - }); - - return key.map(key => !!map[key]); + const foundKeys = new Set(data.map(item => item._key)); + return key.map(key => foundKeys.has(key)); } const item = await module.client.collection('objects').findOne({ @@ -76,7 +72,9 @@ module.exports = function (module) { return; } - const objectData = await module.client.collection('objects').findOne({ _key: key }, { projection: { _id: 0 } }); + const objectData = await module.client.collection('objects').findOne( + { _key: key }, { projection: { _id: 0 } } + ); // fallback to old field name 'value' for backwards compatibility #6340 let value = null; @@ -100,12 +98,12 @@ module.exports = function (module) { { projection: { _id: 0 } } ).toArray(); - const map = {}; + const map = Object.create(null); data.forEach((d) => { map[d._key] = d.data; }); - return keys.map(k => (map.hasOwnProperty(k) ? map[k] : null)); + return keys.map(k => map[k] !== undefined ? map[k] : null); }; module.set = async function (key, value) { diff --git a/src/database/mongo/sets.js b/src/database/mongo/sets.js index 66b4baebab..32e81f37b1 100644 --- a/src/database/mongo/sets.js +++ b/src/database/mongo/sets.js @@ -128,7 +128,7 @@ module.exports = function (module) { const item = await module.client.collection('objects').findOne({ _key: key, members: value, }, { - projection: { _id: 0, _key: 1}, + projection: { _id: 0, _key: 1 }, }); return item !== null && item !== undefined; @@ -177,12 +177,8 @@ module.exports = function (module) { projection: { _id: 0, _key: 1 }, }).toArray(); - const map = {}; - result.forEach((item) => { - map[item._key] = true; - }); - - return sets.map(set => !!map[set]); + const foundMembers = new Set(result.map(item => item._key)); + return sets.map(set => foundMembers.has(set)); }; module.getSetMembers = async function (key) { @@ -208,7 +204,7 @@ module.exports = function (module) { projection: { _id: 0 }, }).toArray(); - const sets = {}; + const sets = Object.create(null); data.forEach((set) => { sets[set._key] = set.members || []; }); diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 9940cf2677..b5de27ef6b 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -271,7 +271,10 @@ module.exports = function (module) { return null; } value = helpers.valueToString(value); - const result = await module.client.collection('objects').findOne({ _key: key, value: value }, { projection: { _id: 0, _key: 0, value: 0 } }); + const result = await module.client.collection('objects').findOne( + { _key: key, value: value }, + { projection: { _id: 0, _key: 0, value: 0 } } + ); return result ? result.score : null; }; @@ -280,15 +283,15 @@ module.exports = function (module) { return []; } value = helpers.valueToString(value); - const result = await module.client.collection('objects').find({ _key: { $in: keys }, value: value }, { projection: { _id: 0, value: 0 } }).toArray(); - const map = {}; + const result = await module.client.collection('objects').find( + { _key: { $in: keys }, value: value }, + { projection: { _id: 0, value: 0 } } + ).toArray(); + const scoreMap = Object.create(null); result.forEach((item) => { - if (item) { - map[item._key] = item; - } + scoreMap[item._key] = item.score; }); - - return keys.map(key => (map[key] ? map[key].score : null)); + return keys.map(key => (scoreMap[key] !== undefined ? scoreMap[key] : null)); }; module.sortedSetScores = async function (key, values) { @@ -299,13 +302,14 @@ module.exports = function (module) { return []; } values = values.map(helpers.valueToString); - const result = await module.client.collection('objects').find({ _key: key, value: { $in: values } }, { projection: { _id: 0, _key: 0 } }).toArray(); + const result = await module.client.collection('objects').find( + { _key: key, value: { $in: values } }, + { projection: { _id: 0, _key: 0 } } + ).toArray(); - const valueToScore = {}; + const valueToScore = Object.create(null); result.forEach((item) => { - if (item) { - valueToScore[item.value] = item.score; - } + valueToScore[item.value] = item.score; }); return values.map(v => (utils.isNumber(valueToScore[v]) ? valueToScore[v] : null)); @@ -338,14 +342,8 @@ module.exports = function (module) { projection: { _id: 0, value: 1 }, }).toArray(); - const isMember = {}; - results.forEach((item) => { - if (item) { - isMember[item.value] = true; - } - }); - - return values.map(value => !!isMember[value]); + const foundMembers = new Set(results.map(item => item.value)); + return values.map(value => foundMembers.has(value)); }; module.isMemberOfSortedSets = async function (keys, value) { @@ -359,14 +357,8 @@ module.exports = function (module) { projection: { _id: 0, _key: 1, value: 1 }, }).toArray(); - const isMember = {}; - results.forEach((item) => { - if (item) { - isMember[item._key] = true; - } - }); - - return keys.map(key => !!isMember[key]); + const keysWithMember = new Set(results.map(item => item._key)); + return keys.map(key => keysWithMember.has(key)); }; module.getSortedSetMembers = async function (key) { @@ -411,7 +403,7 @@ module.exports = function (module) { data.map(item => item.value), ]; } - const sets = {}; + const sets = Object.create(null); data.forEach((item) => { sets[item._key] = sets[item._key] || []; if (withScores) { diff --git a/src/database/mongo/sorted/remove.js b/src/database/mongo/sorted/remove.js index d6bf96fa7e..80545d56fe 100644 --- a/src/database/mongo/sorted/remove.js +++ b/src/database/mongo/sorted/remove.js @@ -56,8 +56,8 @@ module.exports = function (module) { if (!Array.isArray(data) || !data.length) { return; } - const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); - data.forEach(item => bulk.find({ _key: item[0], value: String(item[1]) }).delete()); - await bulk.execute(); + await module.client.collection('objects').deleteMany({ + $or: data.map(([key, value]) => ({ _key: key, value: String(value) })), + }); }; }; diff --git a/test/database/keys.js b/test/database/keys.js index 673d083f98..84a3a7d495 100644 --- a/test/database/keys.js +++ b/test/database/keys.js @@ -44,6 +44,12 @@ describe('Key methods', () => { assert.deepStrictEqual(await db.mget(null), []); }); + it('should return 0 if value of key is 0', async () => { + await db.set('zeroKey', 0); + const value = await db.mget(['zeroKey']); + assert.strictEqual(value[0], 0); + }); + it('should return true if key exist', (done) => { db.exists('testKey', function (err, exists) { assert.ifError(err); From 833899d091c389baf6147674cbff1c2394b851ce Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 27 Mar 2026 11:40:20 -0400 Subject: [PATCH 4722/4744] fix: failing test --- test/topics/tools.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/topics/tools.js b/test/topics/tools.js index 5c5d91f76f..962863c0f8 100644 --- a/test/topics/tools.js +++ b/test/topics/tools.js @@ -6,6 +6,7 @@ const db = require('../mocks/databasemock'); const user = require('../../src/user'); const categories = require('../../src/categories'); +const groups = require('../../src/groups'); const topics = require('../../src/topics'); const utils = require('../../src/utils'); @@ -68,7 +69,7 @@ describe('Topic tools', () => { let tid1; let tid2; - before(async () => { + before(async function () { const helpers = require('../activitypub/helpers'); ({ id: remoteCid } = helpers.mocks.group()); ({ cid: localCid } = await categories.create({ name: utils.generateUUID().slice(0, 8) })); @@ -84,23 +85,26 @@ describe('Topic tools', () => { content: utils.generateUUID(), }); tid2 = topicData.tid; + + this.adminUid = await user.create({ username: utils.generateUUID() }); + await groups.join('administrators', this.adminUid); }); - it('should throw when attempting to move a topic from a remote category', async () => { + it('should throw when attempting to move a topic from a remote category', async function () { await assert.rejects( topics.tools.move(tid1, { cid: localCid, - uid: 'system', + uid: this.adminUid, }), { message: '[[error:no-topic]]' } ); }); - it('should throw when attempting to move a topic to a remote category', async () => { + it('should throw when attempting to move a topic to a remote category', async function () { await assert.rejects( topics.tools.move(tid2, { cid: remoteCid, - uid: 'system', + uid: this.adminUid, }), { message: '[[error:cant-move-topic-to-from-remote-categories]]' } ); From 28fafcdc5678168671ee719b93458888eb08bc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Mar 2026 11:48:45 -0400 Subject: [PATCH 4723/4744] retrt e11000 in incrObjectField --- src/database/mongo/hash.js | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index 59ea48e1c9..8dabf11831 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -240,7 +240,22 @@ module.exports = function (module) { key.forEach((key) => { bulk.find({ _key: key }).upsert().update({ $inc: increment }); }); - await bulk.execute(); + + try { + await bulk.execute(); + } catch (err) { + // retry failed e11000 operations + if (err.code === 11000 || (err.writeErrors && err.writeErrors.some(e => e.code === 11000))) { + const failedIndices = err.writeErrors.filter(e => e.code === 11000).map(e => e.index); + const retryData = failedIndices.map(idx => key[idx]); + await Promise.all(retryData.map( + key => module.incrObjectFieldBy(key, field, value) + )); + } else { + throw err; + } + } + cache.del(key); const result = await module.getObjectsFields(key, [field]); return result.map(data => data && data[field]); @@ -284,7 +299,18 @@ module.exports = function (module) { } bulk.find({ _key: item[0] }).upsert().update({ $inc: increment }); }); - await bulk.execute(); + try { + await bulk.execute(); + } catch (err) { + // retry failed e11000 operations + if (err.code === 11000 || (err.writeErrors && err.writeErrors.some(e => e.code === 11000))) { + const failedIndices = err.writeErrors.filter(e => e.code === 11000).map(e => e.index); + const retryData = failedIndices.map(idx => data[idx]); + await module.incrObjectFieldByBulk(retryData); + } else { + throw err; + } + } cache.del(data.map(item => item[0])); }; }; From deca5e6715f638aad4cc6c2844b48c669b4e78de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Mar 2026 12:22:06 -0400 Subject: [PATCH 4724/4744] fix: redis/psql --- test/database/keys.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/database/keys.js b/test/database/keys.js index 84a3a7d495..ead0a3ba86 100644 --- a/test/database/keys.js +++ b/test/database/keys.js @@ -47,7 +47,7 @@ describe('Key methods', () => { it('should return 0 if value of key is 0', async () => { await db.set('zeroKey', 0); const value = await db.mget(['zeroKey']); - assert.strictEqual(value[0], 0); + assert.strictEqual(String(value[0]), '0'); }); it('should return true if key exist', (done) => { From 2327cae7647fd05922c8d66de08f93675f07d4ed Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 27 Mar 2026 13:20:52 -0400 Subject: [PATCH 4725/4744] fix: #14045, automatically open category selector dropdown on move topic modal --- public/src/client/category/tools.js | 2 -- public/src/client/topic/move.js | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/public/src/client/category/tools.js b/public/src/client/category/tools.js index bd15ff459b..17be8c65c2 100644 --- a/public/src/client/category/tools.js +++ b/public/src/client/category/tools.js @@ -89,8 +89,6 @@ define('forum/category/tools', [ } move.init(tids, null, onCommandComplete); }); - - return false; }); components.get('topic/move-all').on('click', function () { diff --git a/public/src/client/topic/move.js b/public/src/client/topic/move.js index 08ce3e9d5c..22c693bb29 100644 --- a/public/src/client/topic/move.js +++ b/public/src/client/topic/move.js @@ -2,8 +2,8 @@ define('forum/topic/move', [ - 'categorySelector', 'alerts', 'hooks', 'api', -], function (categorySelector, alerts, hooks, api) { + 'categorySelector', 'alerts', 'hooks', 'api', 'bootstrap', +], function (categorySelector, alerts, hooks, api, bootstrap) { const Move = {}; let modal; let selectedCategory; @@ -37,6 +37,9 @@ define('forum/topic/move', [ localOnly: true, }); + const dropdown = new bootstrap.Dropdown(dropdownEl.find('button')); + dropdown.show(); + modal.find('#move_thread_commit').on('click', onCommitClicked); modal.find('#move_topic_cancel').on('click', closeMoveModal); }); From 3c4117115120ec4a787130d43f95dcf965454e0e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 27 Mar 2026 13:50:49 -0400 Subject: [PATCH 4726/4744] fix: make 'show more' button not overlap existing text --- src/views/partials/feed/item.tpl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/views/partials/feed/item.tpl b/src/views/partials/feed/item.tpl index 31cfe758a6..daf69dfd44 100644 --- a/src/views/partials/feed/item.tpl +++ b/src/views/partials/feed/item.tpl @@ -58,9 +58,7 @@ {./content}
    -
    - -
    +
    {humanReadableNumber(./topic.postcount)} From 4b503db49701e0f815e5c751435e3c2142fca5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Mar 2026 16:44:10 -0400 Subject: [PATCH 4727/4744] refactor: break long line --- src/database/mongo/sorted/add.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/database/mongo/sorted/add.js b/src/database/mongo/sorted/add.js index bc3a8bc8ec..82539ee8c8 100644 --- a/src/database/mongo/sorted/add.js +++ b/src/database/mongo/sorted/add.js @@ -83,7 +83,9 @@ module.exports = function (module) { if (!utils.isNumber(item[1])) { throw new Error(`[[error:invalid-score, ${item[1]}]]`); } - bulk.find({ _key: item[0], value: String(item[2]) }).upsert().updateOne({ $set: { score: parseFloat(item[1]) } }); + bulk.find({ _key: item[0], value: String(item[2]) }) + .upsert() + .updateOne({ $set: { score: parseFloat(item[1]) } }); }); await bulk.execute(); }; From 6c4e9284822e37b5f77100705a8441e09b1854a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 27 Mar 2026 16:45:20 -0400 Subject: [PATCH 4728/4744] fix: on exit, dont write analytics data on all nodes if you are running 4 nodebbs each one was calling writeData which could trigger duplicate key errors --- src/analytics.js | 14 +++++++++----- src/start.js | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/analytics.js b/src/analytics.js index e054e2e733..f64c651687 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -38,11 +38,7 @@ Analytics.init = async function () { runOnAllNodes: true, onTick: async () => { if (Analytics.pause) return; - publishLocalAnalytics(); - if (runJobs) { - await sleep(2000); - await Analytics.writeData(); - } + await Analytics.writeLocalData(); }, }); @@ -63,6 +59,14 @@ Analytics.init = async function () { } }; +Analytics.writeLocalData = async function () { + publishLocalAnalytics(); + if (runJobs) { + await sleep(2000); + await Analytics.writeData(); + } +}; + function publishLocalAnalytics() { pubsub.publish('analytics:publish', { local: local, diff --git a/src/start.js b/src/start.js index 00a129e33f..89a1683703 100644 --- a/src/start.js +++ b/src/start.js @@ -149,7 +149,7 @@ async function shutdown(code) { try { await require('./webserver').destroy(); winston.info('[app] Web server closed to connections.'); - await require('./analytics').writeData(); + await require('./analytics').writeLocalData(); winston.info('[app] Live analytics saved.'); const db = require('./database'); await db.delete('locks'); From f5d8ff90b860203120784d4ab411f4e2a6607d1c Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 28 Mar 2026 09:08:38 +0000 Subject: [PATCH 4729/4744] Latest translations and fallbacks --- .../it/admin/settings/activitypub.json | 18 ++-- .../language/ja/admin/settings/general.json | 96 +++++++++---------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/public/language/it/admin/settings/activitypub.json b/public/language/it/admin/settings/activitypub.json index c190fe0463..1be5f2d52a 100644 --- a/public/language/it/admin/settings/activitypub.json +++ b/public/language/it/admin/settings/activitypub.json @@ -41,16 +41,16 @@ "relays.state-2": "Attivo", "relays.errors.invalid-url": "Inserisci un URL valido", - "blocklists": "Third-party Blocklists", - "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", - "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists": "Liste di blocchi di terze parti", + "blocklists-help": "Per la sicurezza tua e dei tuoi utenti, la gestione di una lista di blocchi è una componente fondamentale di qualsiasi strategia volta a garantire la fiducia e la sicurezza quando si lavora con contenuti che esulano dall'ambito della moderazione locale. NodeBB include alcune impostazioni predefinite consigliate, che possono essere personalizzate qui.", + "blocklists-default": "NodeBB include di default due liste di blocco, la IFTAS Do Not Interact Denylist e la IFTAS Abandoned and Unmanaged Domain Denylist.", "blocklists.url": "URL", - "blocklists.count": "Domains", - "blocklists.add": "Add New Blocklist", - "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", - "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", - "blocklists.view.title": "View Blocklist", - "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "blocklists.count": "Domini", + "blocklists.add": "Aggiungi nuova lista di blocchi", + "blocklists.add-help": "Inserisci l'URL della lista di blocco che desideri aggiungere. NodeBB supporta le liste di blocco che corrispondono al formato usato da IFTAS.", + "blocklists.refreshed": "Lista di blocco aggiornata — ora contiene %1 voci", + "blocklists.view.title": "Visualizza lista dei blocchi", + "blocklists.view.intro": "Questi sono i %1 dominio(i) bloccati da questa lista di blocco:", "server-filtering": "Filtraggio", "count": "Questo NodeBB è attualmente a conoscenza di %1 server", diff --git a/public/language/ja/admin/settings/general.json b/public/language/ja/admin/settings/general.json index f3f623f970..50581d32ce 100644 --- a/public/language/ja/admin/settings/general.json +++ b/public/language/ja/admin/settings/general.json @@ -1,65 +1,65 @@ { - "general-settings": "General Settings", - "on-this-page": "On this page:", + "general-settings": "一般設定", + "on-this-page": "このページ:", "site-settings": "サイト設定", "title": "サイトタイトル", - "title.short": "Short Title", - "title.short-placeholder": "If no short title is specified, the site title will be used", - "title.url": "Title Link URL", + "title.short": "短いタイトル", + "title.short-placeholder": "短いタイトルが指定されていない場合、サイトタイトルが使用されます", + "title.url": "タイトルリンクURL", "title.url-placeholder": "サイトタイトルのURL", - "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index. Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", - "title.name": "あなたのコミュニティ名", + "title.url-help": "タイトルをクリックしたとき、ユーザーをこのアドレスに送信します。空白のままにすると、フォーラムのインデックスに送信されます。注意: メール等で使用される外部URLではありません。それはconfig.jsonのurlプロパティで設定されます", + "title.name": "コミュニティ名", "title.show-in-header": "ヘッダーにサイトタイトルを表示する", - "browser-title": "ブラウザ", - "browser-title-help": "ブラウザのタイトルが指定されていない場合、サイトのタイトルが使用されます。", - "title-layout": "タイトル配置", - "title-layout-help": "ブラウザのタイトルがどのように構成されるかを定義します。{pageTitle} | {browserTitle}", - "description.placeholder": "あなたのコミュニティについての簡単な説明", - "description": "Site Description", + "browser-title": "ブラウザタイトル", + "browser-title-help": "ブラウザタイトルが指定されていない場合、サイトタイトルが使用されます", + "title-layout": "タイトルレイアウト", + "title-layout-help": "ブラウザタイトルの構成を定義します。例: {pageTitle} | {browserTitle}", + "description.placeholder": "コミュニティについての簡単な説明", + "description": "サイトの説明", "keywords": "サイトのキーワード", - "keywords-placeholder": "あなたのコミュニティを記述するキーワード、カンマ区切り", - "logo-and-icons": "Media & Branding", + "keywords-placeholder": "コミュニティを説明するキーワード(カンマ区切り)", + "logo-and-icons": "メディアとブランディング", "logo.image": "画像", "logo.image-placeholder": "フォーラムのヘッダーに表示するロゴのパス", "logo.upload": "アップロード", - "logo.url": "Logo Link URL", + "logo.url": "ロゴリンクURL", "logo.url-placeholder": "サイトロゴのURL", - "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
    Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", - "logo.alt-text": "全てのテキスト:", + "logo.url-help": "ロゴをクリックしたとき、ユーザーをこのアドレスに送信します。空白のままにすると、フォーラムのインデックスに送信されます。
    注意: メール等で使用される外部URLではありません。それはconfig.jsonのurlプロパティで設定されます。", + "logo.alt-text": "代替テキスト", "log.alt-text-placeholder": "アクセシビリティのための代替テキスト", - "favicon": "お気に入りアイコン", + "favicon": "ファビコン", "favicon.upload": "アップロード", - "pwa": "Progressive Web App", - "touch-icon": "Touch Icon", + "pwa": "プログレッシブウェブアプリ", + "touch-icon": "タッチアイコン", "touch-icon.upload": "アップロード", - "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", - "maskable-icon": "Maskable (Homescreen) Icon", - "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", - "screenshot": "Screenshot", - "screenshot.help": "Recommended size and format: between 320px and 3480px, JPG and PNG format only. If no screenshot is specified, NodeBB will fall back to a generic screenshot", - "outgoing-links": "外部サイトへのリンク", - "outgoing-links.warning-page": "送信リンクの警告ページを使用", - "search": "Search", - "search-default-in": "Search In", - "search-default-in-quick": "Quick Search In", - "search-default-sort-by": "Sort by", - "outgoing-links.whitelist": "警告ページをバイパスするためのホワイトリストへのドメイン", - "site-colors": "Site Color Metadata", - "theme-color": "Theme Color", - "background-color": "Background Color", - "background-color-help": "Color used for splash screen background when website is installed as a PWA", - "undo-timeout": "Undo Timeout", - "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", - "topic-tools": "Topic Tools", + "touch-icon.help": "推奨サイズと形式: 512x512、PNG形式のみ。タッチアイコンが指定されていない場合、NodeBBはファビコンを使用します。", + "maskable-icon": "切り抜き対応のホーム画面アイコン", + "maskable-icon.help": "推奨サイズと形式: 512x512、PNG形式のみ。端末側で円形や角丸などに切り抜かれても見切れにくいアイコンです。指定しない場合、NodeBBはタッチアイコンを使用します。", + "screenshot": "スクリーンショット", + "screenshot.help": "推奨サイズと形式: 320px〜3480px、JPGおよびPNG形式のみ。スクリーンショットが指定されていない場合、NodeBBは汎用スクリーンショットを使用します。", + "outgoing-links": "外部リンク", + "outgoing-links.warning-page": "外部リンク警告ページを使用する", + "search": "検索", + "search-default-in": "検索対象", + "search-default-in-quick": "クイック検索対象", + "search-default-sort-by": "並び替え", + "outgoing-links.whitelist": "警告ページをスキップするためのホワイトリストドメイン", + "site-colors": "サイトカラーメタデータ", + "theme-color": "テーマカラー", + "background-color": "背景色", + "background-color-help": "ウェブサイトがPWAとしてインストールされたときのスプラッシュ画面の背景に使用される色", + "undo-timeout": "元に戻すタイムアウト", + "undo-timeout-help": "トピックの移動などの一部の操作では、モデレーターが一定時間内に操作を取り消すことができます。完全に無効にするには0に設定してください。", + "topic-tools": "トピックツール", "home-page": "ホームページ", - "home-page-route": "ホームページのルート", - "home-page-description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "ホームページルート", + "home-page-description": "ユーザーがフォーラムのルートURLにアクセスしたときに表示するページを選択します。", "custom-route": "カスタムルート", - "allow-user-home-pages": "ユーザーホームページを有効にする", - "home-page-title": "Title of the home page (default \"Home\")", - "default-language": "デフォルトの言語", - "auto-detect": "ゲストの自動検出言語設定", - "default-language-help": "デフォルトの言語は、フォーラムにアクセスしているすべてのユーザーの言語表示を決定します。
    個々のユーザーは、アカウント設定ページでデフォルトの言語を上書きできます。", + "allow-user-home-pages": "ユーザーホームページを許可する", + "home-page-title": "ホームページのタイトル(デフォルト「ホーム」)", + "default-language": "デフォルト言語", + "auto-detect": "ゲストの言語を自動検出する", + "default-language-help": "デフォルト言語は、フォーラムにアクセスするすべてのユーザーの言語設定を決定します。
    個々のユーザーは、アカウント設定ページでデフォルト言語を上書きできます。", "post-sharing": "投稿共有", - "info-plugins-additional": "プラグインは投稿を共有するために追加のネットワークを設定することができます" + "info-plugins-additional": "プラグインは投稿共有用の追加ネットワークを追加できます。" } \ No newline at end of file From b8fd88fba955db240dd3b8bd473ab9b9f19098aa Mon Sep 17 00:00:00 2001 From: Michele Di Maria Date: Sat, 28 Mar 2026 18:24:34 +0100 Subject: [PATCH 4730/4744] Fix the saving of the statistics on PosgreSQL #14124 (#14129) * fix: deduplicate postgres sorted set bulk ops to prevent pkey violation sortedSetIncrByBulk and sortedSetAddBulk did not deduplicate (key, value) pairs before INSERT, causing "duplicate key value violates unique constraint legacy_zset_pkey" errors since PostgreSQL ON CONFLICT only resolves against existing table rows, not within-statement duplicates. Also adds missing pageviews:ap metrics to analyticsKeys sorted set. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use upsert with RETURNING to prevent postgres analytics write failures Replace the INSERT ON CONFLICT DO NOTHING + separate SELECT verification pattern with INSERT ON CONFLICT DO UPDATE RETURNING. The old pattern had an unreliable gap between INSERT and SELECT causing random "failed to insert keys for objects" errors that blocked all analytics writes. The no-op upsert (DO UPDATE SET type = existing type) guarantees every row is returned via RETURNING, eliminating the need for a separate SELECT and the "missing keys" check entirely. Also deduplicates the keys array to prevent "cannot affect row a second time" errors with DO UPDATE. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/analytics.js | 6 ++++ src/database/postgres/helpers.js | 44 ++++++++--------------------- src/database/postgres/sorted.js | 21 +++++++++++--- src/database/postgres/sorted/add.js | 4 ++- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/analytics.js b/src/analytics.js index f64c651687..e8e60c5e68 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -189,6 +189,12 @@ Analytics.writeData = async function () { incrByBulk.push(['analytics:pageviews:ap', total.apPageViews, today.getTime()]); incrByBulk.push(['analytics:pageviews:ap:month', total.apPageViews, month.getTime()]); total.apPageViews = 0; + if (!metrics.includes('pageviews:ap')) { + metrics.push('pageviews:ap'); + } + if (!metrics.includes('pageviews:ap:month')) { + metrics.push('pageviews:ap:month'); + } } if (total.uniquevisitors > 0) { diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js index 85e0b63d07..8b92d3fe50 100644 --- a/src/database/postgres/helpers.js +++ b/src/database/postgres/helpers.js @@ -27,31 +27,25 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - await db.query({ - name: 'ensureLegacyObjectType1', + const res = await db.query({ + name: 'ensureLegacyObjectType_upsert', text: ` INSERT INTO "legacy_object" ("_key", "type") VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) - ON CONFLICT - DO NOTHING`, + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "type"`, values: [key, type], }); - const res = await db.query({ - name: 'ensureLegacyObjectType2', - text: ` -SELECT "type" - FROM "legacy_object_live" - WHERE "_key" = $1::TEXT`, - values: [key], - }); - if (res.rows[0].type !== type) { throw new Error(`database: cannot insert ${JSON.stringify(key)} as ${type} because it already exists as ${res.rows[0].type}`); } }; helpers.ensureLegacyObjectsType = async function (db, keys, type) { + keys = [...new Set(keys)]; + await db.query({ name: 'ensureLegacyObjectTypeBefore', text: ` @@ -60,38 +54,24 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - await db.query({ - name: 'ensureLegacyObjectsType1', + const res = await db.query({ + name: 'ensureLegacyObjectsType_upsert', text: ` INSERT INTO "legacy_object" ("_key", "type") SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE FROM UNNEST($1::TEXT[]) k - ON CONFLICT - DO NOTHING`, + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "_key", "type"`, values: [keys, type], }); - const res = await db.query({ - name: 'ensureLegacyObjectsType2', - text: ` -SELECT "_key", "type" - FROM "legacy_object_live" - WHERE "_key" = ANY($1::TEXT[])`, - values: [keys], - }); - const invalid = res.rows.filter(r => r.type !== type); if (invalid.length) { const parts = invalid.map(r => `${JSON.stringify(r._key)} is ${r.type}`); throw new Error(`database: cannot insert multiple objects as ${type} because they already exist: ${parts.join(', ')}`); } - - const missing = keys.filter(k => !res.rows.some(r => r._key === k)); - - if (missing.length) { - throw new Error(`database: failed to insert keys for objects: ${JSON.stringify(missing)}`); - } }; helpers.noop = function () {}; diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 351fe3e059..50581a6ed4 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -551,16 +551,29 @@ RETURNING "score" s`, return []; } + // Deduplicate by (key, value) pair, summing increments for duplicates + const seen = new Map(); + const deduped = []; + data.forEach(([key, increment, value]) => { + value = helpers.valueToString(value); + increment = parseFloat(increment); + const mapKey = `${key}\0${value}`; + if (seen.has(mapKey)) { + deduped[seen.get(mapKey)][1] += increment; + } else { + seen.set(mapKey, deduped.length); + deduped.push([key, increment, value]); + } + }); + return await module.transaction(async (client) => { - await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'zset'); + await helpers.ensureLegacyObjectsType(client, deduped.map(item => item[0]), 'zset'); const values = []; const queryParams = []; let paramIndex = 1; - data.forEach(([key, increment, value]) => { - value = helpers.valueToString(value); - increment = parseFloat(increment); + deduped.forEach(([key, increment, value]) => { values.push(key, value, increment); queryParams.push(`($${paramIndex}::TEXT, $${paramIndex + 1}::TEXT, $${paramIndex + 2}::NUMERIC)`); paramIndex += 3; diff --git a/src/database/postgres/sorted/add.js b/src/database/postgres/sorted/add.js index 6f87416089..3db65cfde9 100644 --- a/src/database/postgres/sorted/add.js +++ b/src/database/postgres/sorted/add.js @@ -114,8 +114,10 @@ INSERT INTO "legacy_zset" ("_key", "value", "score") } keys.push(item[0]); scores.push(item[1]); - values.push(item[2]); + values.push(helpers.valueToString(item[2])); }); + const compositeKeys = keys.map((k, i) => `${k}\0${values[i]}`); + helpers.removeDuplicateValues(compositeKeys, keys, values, scores); await module.transaction(async (client) => { await helpers.ensureLegacyObjectsType(client, keys, 'zset'); await client.query({ From 09183ac0620c3b419ee939a786e907290bc0657d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 28 Mar 2026 13:35:47 -0400 Subject: [PATCH 4731/4744] test: dont create users parallel --- test/socket.io.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/socket.io.js b/test/socket.io.js index 45ffb43dd8..209f8dc056 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -30,20 +30,14 @@ describe('socket.io', () => { let regularUid; before(async () => { - const data = await Promise.all([ - user.create({ username: 'admin', password: 'adminpwd' }), - user.create({ username: 'regular', password: 'regularpwd', email: 'regular@test.com' }, { emailVerification: 'verify' }), - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - }), - ]); - adminUid = data[0]; + adminUid = await user.create({ username: 'admin', password: 'adminpwd' }); await groups.join('administrators', adminUid); + regularUid = await user.create({ username: 'regular', password: 'regularpwd', email: 'regular@test.com' }, { emailVerification: 'verify' }); + ({ cid } = await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + })); - regularUid = data[1]; - - cid = data[2].cid; await topics.post({ uid: adminUid, cid: cid, From b04976ed3553aac681d2462c3d67902dbdcd0c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 28 Mar 2026 13:35:47 -0400 Subject: [PATCH 4732/4744] test: dont create users parallel --- test/socket.io.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/socket.io.js b/test/socket.io.js index 45ffb43dd8..209f8dc056 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -30,20 +30,14 @@ describe('socket.io', () => { let regularUid; before(async () => { - const data = await Promise.all([ - user.create({ username: 'admin', password: 'adminpwd' }), - user.create({ username: 'regular', password: 'regularpwd', email: 'regular@test.com' }, { emailVerification: 'verify' }), - categories.create({ - name: 'Test Category', - description: 'Test category created by testing script', - }), - ]); - adminUid = data[0]; + adminUid = await user.create({ username: 'admin', password: 'adminpwd' }); await groups.join('administrators', adminUid); + regularUid = await user.create({ username: 'regular', password: 'regularpwd', email: 'regular@test.com' }, { emailVerification: 'verify' }); + ({ cid } = await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + })); - regularUid = data[1]; - - cid = data[2].cid; await topics.post({ uid: adminUid, cid: cid, From bc5457ef18025284ecdaf831020dabcace367647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 28 Mar 2026 13:37:24 -0400 Subject: [PATCH 4733/4744] lint: remove unused --- src/activitypub/notes.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index 1b570ea689..73d864db6e 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -14,7 +14,6 @@ const notifications = require('../notifications'); const user = require('../user'); const topics = require('../topics'); const posts = require('../posts'); -const api = require('../api'); const ttlCache = require('../cache/ttl'); const websockets = require('../socket.io'); const utils = require('../utils'); From 988f513655c7312890805a140494d0967cdca30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 28 Mar 2026 14:32:04 -0400 Subject: [PATCH 4734/4744] fix: try upsert type if it fails --- src/database/postgres/helpers.js | 36 ++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js index 8b92d3fe50..35c41012de 100644 --- a/src/database/postgres/helpers.js +++ b/src/database/postgres/helpers.js @@ -27,14 +27,14 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - const res = await db.query({ + const res = await tryUpsert(db, { name: 'ensureLegacyObjectType_upsert', text: ` -INSERT INTO "legacy_object" ("_key", "type") -VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) - ON CONFLICT ("_key") - DO UPDATE SET "type" = "legacy_object"."type" - RETURNING "type"`, + INSERT INTO "legacy_object" ("_key", "type") + VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "type"`, values: [key, type], }); @@ -54,15 +54,15 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - const res = await db.query({ + const res = await tryUpsert(db, { name: 'ensureLegacyObjectsType_upsert', text: ` INSERT INTO "legacy_object" ("_key", "type") SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE - FROM UNNEST($1::TEXT[]) k - ON CONFLICT ("_key") - DO UPDATE SET "type" = "legacy_object"."type" - RETURNING "_key", "type"`, +FROM UNNEST($1::TEXT[]) k + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "_key", "type"`, values: [keys, type], }); @@ -74,4 +74,18 @@ SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE } }; +async function tryUpsert(db, queryConfig) { + let res; + try { + res = await db.query(queryConfig); + } catch (err) { + if (err.code === '23505') { // retry if failed due to error: unique constraint + res = await db.query(queryConfig); + } else { + throw err; + } + } + return res; +} + helpers.noop = function () {}; From 991e9778130d5b114b9c7955296d7b71eb03c5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 28 Mar 2026 14:32:04 -0400 Subject: [PATCH 4735/4744] fix: try upsert type if it fails --- src/database/postgres/helpers.js | 36 ++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js index 8b92d3fe50..35c41012de 100644 --- a/src/database/postgres/helpers.js +++ b/src/database/postgres/helpers.js @@ -27,14 +27,14 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - const res = await db.query({ + const res = await tryUpsert(db, { name: 'ensureLegacyObjectType_upsert', text: ` -INSERT INTO "legacy_object" ("_key", "type") -VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) - ON CONFLICT ("_key") - DO UPDATE SET "type" = "legacy_object"."type" - RETURNING "type"`, + INSERT INTO "legacy_object" ("_key", "type") + VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "type"`, values: [key, type], }); @@ -54,15 +54,15 @@ DELETE FROM "legacy_object" AND "expireAt" <= CURRENT_TIMESTAMP`, }); - const res = await db.query({ + const res = await tryUpsert(db, { name: 'ensureLegacyObjectsType_upsert', text: ` INSERT INTO "legacy_object" ("_key", "type") SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE - FROM UNNEST($1::TEXT[]) k - ON CONFLICT ("_key") - DO UPDATE SET "type" = "legacy_object"."type" - RETURNING "_key", "type"`, +FROM UNNEST($1::TEXT[]) k + ON CONFLICT ("_key") + DO UPDATE SET "type" = "legacy_object"."type" + RETURNING "_key", "type"`, values: [keys, type], }); @@ -74,4 +74,18 @@ SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE } }; +async function tryUpsert(db, queryConfig) { + let res; + try { + res = await db.query(queryConfig); + } catch (err) { + if (err.code === '23505') { // retry if failed due to error: unique constraint + res = await db.query(queryConfig); + } else { + throw err; + } + } + return res; +} + helpers.noop = function () {}; From 2185f22a55aa6ac342afcd5040b58e5f2beb2345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 28 Mar 2026 15:09:03 -0400 Subject: [PATCH 4736/4744] fix: try a save point in retry --- src/database/postgres/helpers.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js index 35c41012de..e3df40bad5 100644 --- a/src/database/postgres/helpers.js +++ b/src/database/postgres/helpers.js @@ -45,7 +45,9 @@ DELETE FROM "legacy_object" helpers.ensureLegacyObjectsType = async function (db, keys, type) { keys = [...new Set(keys)]; - + if (!keys.length) { + return; + } await db.query({ name: 'ensureLegacyObjectTypeBefore', text: ` @@ -76,11 +78,18 @@ FROM UNNEST($1::TEXT[]) k async function tryUpsert(db, queryConfig) { let res; + const savepoint = `upsert_${Math.random().toString(36).substring(7)}`; try { + await db.query(`SAVEPOINT ${savepoint}`); res = await db.query(queryConfig); + await db.query(`RELEASE SAVEPOINT ${savepoint}`); } catch (err) { if (err.code === '23505') { // retry if failed due to error: unique constraint + // Roll back to the savepoint to prevent + // error: current transaction is aborted, commands ignored until end of transaction block + await db.query(`ROLLBACK TO SAVEPOINT ${savepoint}`); res = await db.query(queryConfig); + await db.query(`RELEASE SAVEPOINT ${savepoint}`); } else { throw err; } From 203f4cc7ff412dd8df5b806bb7fd572cc55904ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 28 Mar 2026 15:09:03 -0400 Subject: [PATCH 4737/4744] fix: try a save point in retry --- src/database/postgres/helpers.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js index 35c41012de..e3df40bad5 100644 --- a/src/database/postgres/helpers.js +++ b/src/database/postgres/helpers.js @@ -45,7 +45,9 @@ DELETE FROM "legacy_object" helpers.ensureLegacyObjectsType = async function (db, keys, type) { keys = [...new Set(keys)]; - + if (!keys.length) { + return; + } await db.query({ name: 'ensureLegacyObjectTypeBefore', text: ` @@ -76,11 +78,18 @@ FROM UNNEST($1::TEXT[]) k async function tryUpsert(db, queryConfig) { let res; + const savepoint = `upsert_${Math.random().toString(36).substring(7)}`; try { + await db.query(`SAVEPOINT ${savepoint}`); res = await db.query(queryConfig); + await db.query(`RELEASE SAVEPOINT ${savepoint}`); } catch (err) { if (err.code === '23505') { // retry if failed due to error: unique constraint + // Roll back to the savepoint to prevent + // error: current transaction is aborted, commands ignored until end of transaction block + await db.query(`ROLLBACK TO SAVEPOINT ${savepoint}`); res = await db.query(queryConfig); + await db.query(`RELEASE SAVEPOINT ${savepoint}`); } else { throw err; } From 9b7d62be5e9b43250fcb85cb72923d2f4cfd02bf Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 29 Mar 2026 09:15:59 +0000 Subject: [PATCH 4738/4744] Latest translations and fallbacks --- .../ja/admin/appearance/customise.json | 18 +++++++++--------- public/language/ja/admin/appearance/skins.json | 18 +++++++++--------- .../zh-CN/admin/settings/activitypub.json | 18 +++++++++--------- public/language/zh-CN/topic.json | 4 ++-- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/public/language/ja/admin/appearance/customise.json b/public/language/ja/admin/appearance/customise.json index a6611d99ff..e2eaecab4b 100644 --- a/public/language/ja/admin/appearance/customise.json +++ b/public/language/ja/admin/appearance/customise.json @@ -1,20 +1,20 @@ { - "customise": "Customise", + "customise": "カスタマイズ", "custom-css": "Custom CSS/SASS", - "custom-css.description": "Enter your own CSS/SASS declarations here, which will be applied after all other styles.", - "custom-css.enable": "Enable Custom CSS/SASS", + "custom-css.description": "ここに独自のCSS/SASS宣言を入力すると、他のすべてのスタイルの後に適用されます。", + "custom-css.enable": "カスタムCSS/SASSを有効にする", - "custom-js": "Custom Javascript", - "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", - "custom-js.enable": "Enable Custom Javascript", + "custom-js": "カスタムJavaScript", + "custom-js.description": "ここに独自のJavaScriptを入力してください。ページが完全に読み込まれた後に実行されます。", + "custom-js.enable": "カスタムJavaScriptを有効にする", "custom-header": "カスタムヘッダー", - "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.description": "ここにカスタムHTML(例: メタタグなど)を入力すると、フォーラムのマークアップの<head>セクションに追加されます。スクリプトタグは許可されていますが、カスタムJavaScriptタブが利用可能なため、推奨されません。", "custom-header.enable": "カスタムヘッダーを有効にする", "custom-css.livereload": "ライブリロードを有効にする", "custom-css.livereload.description": "これを有効にすると、保存ボタンをクリックするたびにアカウントのすべてのデバイスのすべてのセッションが強制的に更新されます。", "bsvariables": "_variables.scss", - "bsvariables.description": "Override bootstrap variables here. You can also use a tool like bootstrap.build and paste the output here.
    Changes require a rebuild & restart.", - "bsvariables.enable": "Enable _variables.scss" + "bsvariables.description": "ここでBootstrap変数を上書きできます。bootstrap.buildなどのツールを使用して出力をここに貼り付けることもできます。
    変更には再ビルドと再起動が必要です。", + "bsvariables.enable": "_variables.scssを有効にする" } \ No newline at end of file diff --git a/public/language/ja/admin/appearance/skins.json b/public/language/ja/admin/appearance/skins.json index ece69f3932..27fb76d699 100644 --- a/public/language/ja/admin/appearance/skins.json +++ b/public/language/ja/admin/appearance/skins.json @@ -1,16 +1,16 @@ { - "skins": "Skins", - "bootswatch-skins": "Bootswatch Skins", - "custom-skins": "Custom Skins", - "add-skin": "Add Skin", - "save-custom-skins": "Save Custom Skins", - "save-custom-skins-success": "Custom skins saved successfully", - "custom-skin-name": "Custom Skin Name", - "custom-skin-variables": "Custom Skin Variables", + "skins": "スキン", + "bootswatch-skins": "Bootswatchスキン", + "custom-skins": "カスタムスキン", + "add-skin": "スキンを追加", + "save-custom-skins": "カスタムスキンを保存", + "save-custom-skins-success": "カスタムスキンを正常に保存しました", + "custom-skin-name": "カスタムスキン名", + "custom-skin-variables": "カスタムスキン変数", "loading": "スキンを読み込んでいます...", "homepage": "ホームページ", "select-skin": "スキン選択", - "revert-skin": "Revert Skin", + "revert-skin": "スキンを元に戻す", "current-skin": "現在のスキン", "skin-updated": "スキンがアップデートされました", "applied-success": "スキン %1 が正常に適用されました", diff --git a/public/language/zh-CN/admin/settings/activitypub.json b/public/language/zh-CN/admin/settings/activitypub.json index c426f68713..31f066c7d2 100644 --- a/public/language/zh-CN/admin/settings/activitypub.json +++ b/public/language/zh-CN/admin/settings/activitypub.json @@ -41,16 +41,16 @@ "relays.state-2": "已启用", "relays.errors.invalid-url": "请输入有效的 URL", - "blocklists": "Third-party Blocklists", - "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", - "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", + "blocklists": "第三方黑名单", + "blocklists-help": "为了您和用户的安全,在处理本地审核范围之外的内容时,维护黑名单是任何信任与安全措施的重要组成部分。NodeBB 默认提供了一些推荐设置,您可以在这里进行自定义。", + "blocklists-default": "NodeBB 默认附带两个黑名单:IFTAS “禁止交互”黑名单IFTAS “废弃及无人管理域名”。", "blocklists.url": "URL", - "blocklists.count": "Domains", - "blocklists.add": "Add New Blocklist", - "blocklists.add-help": "Enter the URL of the blocklist you would like to add. NodeBB understands blocklists that match the format used by IFTAS.", - "blocklists.refreshed": "Blocklist refreshed — it now contains %1 entries", - "blocklists.view.title": "View Blocklist", - "blocklists.view.intro": "These are the %1 domain(s) blocked by this blocklist:", + "blocklists.count": "域名", + "blocklists.add": "添加新黑名单", + "blocklists.add-help": "请输入您要添加的封禁列表的 URL。NodeBB 支持符合 IFTAS 所用格式的封禁列表。", + "blocklists.refreshed": "黑名单已更新——现在包含 %1 条记录", + "blocklists.view.title": "查看黑名单", + "blocklists.view.intro": "以下是该屏蔽列表所屏蔽的 %1 个域名:", "server-filtering": "过滤", "count": "该 NodeBB 目前可检测到 %1 台服务器", diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json index 7febb739f5..11d011b450 100644 --- a/public/language/zh-CN/topic.json +++ b/public/language/zh-CN/topic.json @@ -92,8 +92,8 @@ "watch.title": "当此主题有新回复时,通知我", "unwatch.title": "取消关注此主题", "share-this-post": "分享此帖子", - "share-mail-subject": "Check out this post on \"%1\"", - "share-mail-body": "I thought you might be interested in this post: %1", + "share-mail-subject": "查看这篇关于“%1”的帖子", + "share-mail-body": "我想您可能会对这篇帖子感兴趣:%1", "watching": "关注中", "not-watching": "未关注", "ignoring": "忽略中", From f3eeec93df5d5d681d734685290499095b2b0501 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Sun, 29 Mar 2026 21:19:38 -0400 Subject: [PATCH 4739/4744] fix: #14130, set addressee on follow, undo(follow), and accept(follow) --- src/activitypub/inbox.js | 2 ++ src/activitypub/out.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index b8967cbef5..bfa19d7823 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -529,6 +529,7 @@ inbox.follow = async (req) => { activitypub.send('uid', id, actor, { id: `${nconf.get('url')}/${type}/${id}#activity/accept:follow/${handle}/${Date.now()}`, type: 'Accept', + to: [actor], object: { id: followId, type: 'Follow', @@ -556,6 +557,7 @@ inbox.follow = async (req) => { activitypub.send('cid', id, actor, { id: `${nconf.get('url')}/${type}/${id}#activity/accept:follow/${handle}/${Date.now()}`, type: 'Accept', + to: [actor], object: { id: followId, type: 'Follow', diff --git a/src/activitypub/out.js b/src/activitypub/out.js index 5242e6df6d..d92c8dc29c 100644 --- a/src/activitypub/out.js +++ b/src/activitypub/out.js @@ -65,6 +65,7 @@ Out.follow = enabledCheck(async (type, id, actor) => { await activitypub.send(type, id, [actor], { id: `${nconf.get('url')}/${type}/${id}#activity/follow/${encodeURIComponent(actor)}/${timestamp}`, type: 'Follow', + to: [actor], object: actor, }); } catch (e) { @@ -462,6 +463,7 @@ Out.undo.follow = enabledCheck(async (type, id, actor) => { await activitypub.send(type, id, [actor], { id: `${nconf.get('url')}/${type}/${id}#activity/undo:follow/${encodeURIComponent(actor)}/${timestamp}`, type: 'Undo', + to: [actor], actor: object.actor, object, }); From b629b8cfb527a2228abdb597de29f4335c37083d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 30 Mar 2026 09:07:38 +0000 Subject: [PATCH 4740/4744] Latest translations and fallbacks --- .../ja/admin/settings/navigation.json | 12 ++-- .../language/ja/admin/settings/uploads.json | 62 +++++++++---------- public/language/ja/notifications.json | 14 ++--- public/language/ja/recent.json | 8 +-- public/language/ja/tags.json | 22 +++---- public/language/ja/top.json | 4 +- public/language/ja/user.json | 32 +++++----- .../vi/admin/settings/activitypub.json | 2 +- public/language/vi/topic.json | 4 +- 9 files changed, 80 insertions(+), 80 deletions(-) diff --git a/public/language/ja/admin/settings/navigation.json b/public/language/ja/admin/settings/navigation.json index 52567e3d04..782e72ea68 100644 --- a/public/language/ja/admin/settings/navigation.json +++ b/public/language/ja/admin/settings/navigation.json @@ -1,19 +1,19 @@ { - "navigation": "Navigation", + "navigation": "ナビゲーション", "icon": "アイコン:", "change-icon": "変更", "route": "ルート:", "tooltip": "ツールチップ:", "text": "テキスト:", - "text-class": " テキストのClass:任意", - "class": "Class: optional", + "text-class": " テキストのクラス:任意", + "class": "クラス: 任意", "id": "ID: 任意", "properties": "プロパティ:", - "show-to-groups": "Show to Groups:", + "show-to-groups": "表示するグループ:", "open-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>", + "dropdown": "ドロップダウン", + "dropdown-placeholder": "ドロップダウンメニュー項目を以下に配置、例:
    <li><a class="dropdown-item" href="https://myforum.com">リンク1</a></li>", "btn.delete": "削除", "btn.disable": "無効", diff --git a/public/language/ja/admin/settings/uploads.json b/public/language/ja/admin/settings/uploads.json index 481ebfe60d..42c4a526ed 100644 --- a/public/language/ja/admin/settings/uploads.json +++ b/public/language/ja/admin/settings/uploads.json @@ -1,52 +1,52 @@ { "posts": "投稿", - "orphans": "Orphaned Files", + "orphans": "孤立ファイル", "private": "アップロードしたファイルを非公開にする", "strip-exif-data": "EXIFデータを削除", - "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", - "orphanExpiryDays": "Days to keep orphaned files", - "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
    Set 0 or leave blank to disable.", + "preserve-orphaned-uploads": "投稿が完全削除された後もアップロードされたファイルをディスクに保持", + "orphanExpiryDays": "孤立ファイルを保持する日数", + "orphanExpiryDays-help": "この日数が経過すると、孤立したアップロードはファイルシステムから削除されます。
    0または空白で無効。", "private-extensions": "非公開にするファイル拡張子", - "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-image-width-threshold-help": "(in pixels, default: 2000 pixels, set to 0 to disable)", - "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-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": "最大ファイルサイズ(KB)", - "max-file-size-help": "(キロバイト,デフォルト:2048 KB)", - "reject-image-width": "Maximum Image Width (in pixels)", - "reject-image-width-help": "Images wider than this value will be rejected.", - "reject-image-height": "Maximum Image Height (in pixels)", - "reject-image-height-help": "Images taller than this value will be rejected.", - "convert-pasted-images-to": "Convert pasted images to:", - "convert-pasted-images-to-default": "No Conversion (Keep Original Format)", + "private-uploads-extensions-help": "ここで非公開にするファイル拡張子のカンマ区切りリストを入力(例: pdf,xls,doc)。空のリストはすべてのファイルが非公開であることを意味します。", + "resize-image-width-threshold": "指定した幅より広い画像をリサイズする", + "resize-image-width-threshold-help": "(ピクセル、デフォルト: 2000ピクセル、0で無効)", + "resize-image-width": "画像を指定幅にリサイズ", + "resize-image-width-help": "(ピクセル、デフォルト: 760ピクセル、0で無効)", + "resize-image-keep-original": "リサイズ後に元の画像を保持", + "resize-image-quality": "画像リサイズ時に使用する品質", + "resize-image-quality-help": "リサイズした画像のファイルサイズを減らすには、より低い品質設定を使用してください。", + "max-file-size": "最大ファイルサイズ(キビバイト)", + "max-file-size-help": "(キビバイト、デフォルト: 2048 KiB)", + "reject-image-width": "最大画像幅(ピクセル)", + "reject-image-width-help": "この値より幅が広い画像は拒否されます。", + "reject-image-height": "最大画像高さ(ピクセル)", + "reject-image-height-help": "この値より高さが高い画像は拒否されます。", + "convert-pasted-images-to": "貼り付け画像の変換先:", + "convert-pasted-images-to-default": "変換しない(元の形式を保持)", "convert-pasted-images-to-png": "PNG", "convert-pasted-images-to-jpeg": "JPEG", "convert-pasted-images-to-webp": "WebP", "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 )。空のリストは、すべての拡張が許可されていることを意味します。", - "upload-limit-threshold": "Rate limit user uploads to:", - "upload-limit-threshold-per-minute": "Per %1 Minute", - "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "allowed-file-extensions": "許可するファイル拡張子", + "allowed-file-extensions-help": "ここにファイル拡張子のカンマ区切りリストを入力してください(例: pdf,xls,doc)。空のリストはすべての拡張子が許可されることを意味します。", + "upload-limit-threshold": "ユーザーアップロードのレート制限:", + "upload-limit-threshold-per-minute": "%1分あたり", + "upload-limit-threshold-per-minutes": "%1分あたり", "profile-avatars": "プロフィールの顔写真", "allow-profile-image-uploads": "ユーザーがプロフィール画像をアップロードできるようにする。", "convert-profile-image-png": "プロフィール画像のアップロードをPNGに変換する", "default-avatar": "カスタムデフォルトアバター", "upload": "アップロード", - "profile-image-dimension": "プロファイル画像の寸法", - "profile-image-dimension-help": "(in pixels, default: 200 pixels)", + "profile-image-dimension": "プロフィール画像のサイズ", + "profile-image-dimension-help": "(ピクセル、デフォルト: 200ピクセル)", "max-profile-image-size": "プロフィール画像の最大ファイルサイズ", - "max-profile-image-size-help": "(キロバイト単位,デフォルト:256 KB)", + "max-profile-image-size-help": "(キビバイト、デフォルト: 256 KiB)", "max-cover-image-size": "カバー画像の最大サイズ", - "max-cover-image-size-help": "(キロバイト,デフォルト:2,048 KB)", + "max-cover-image-size-help": "(キビバイト、デフォルト: 2,048 KiB)", "keep-all-user-images": "古いバージョンのアバターとプロファイルカバーをサーバーに保管", "profile-covers": "プロフィールのカバー", "default-covers": "デフォルトのカバー画像", - "default-covers-help": "アップロードされたカバー画像を持たないアカウントのカンマ区切りのデフォルト表紙画像を追加する" + "default-covers-help": "カバー画像をアップロードしていないアカウント用の、カンマ区切りのデフォルトカバー画像を追加" } diff --git a/public/language/ja/notifications.json b/public/language/ja/notifications.json index 7b6f77031b..c37186ec23 100644 --- a/public/language/ja/notifications.json +++ b/public/language/ja/notifications.json @@ -93,15 +93,15 @@ "notificationType-new-group-chat": "When you receive a group chat message", "notificationType-new-public-chat": "When you receive a public group chat message", "notificationType-group-invite": "グループ招待を受けたとき", - "notificationType-group-leave": "When a user leaves your group", - "notificationType-group-request-membership": "When a user requests to join a group you own", + "notificationType-group-leave": "ユーザーがあなたのグループを退会したとき", + "notificationType-group-request-membership": "誰かがあなたのグループへの参加を要求したとき", "notificationType-new-register": "誰かが登録キューに追加されたとき", "notificationType-post-queue": "新しい投稿がキューに入ったとき", "notificationType-new-post-flag": "投稿にフラグが立てられたとき", "notificationType-new-user-flag": "ユーザーにフラグが立てられたとき", - "notificationType-new-reward": "When you earn a new reward", - "activitypub.announce": "%1 shared your post in %2 to their followers.", - "activitypub.announce-dual": "%1 and %2 shared your post in %3 to their followers.", - "activitypub.announce-triple": "%1, %2 and %3 shared your post in %4 to their followers.", - "activitypub.announce-multiple": "%1, %2 and %3 others shared your post in %4 to their followers." + "notificationType-new-reward": "新しい報酬を獲得したとき", + "activitypub.announce": "%1%2のあなたの投稿をフォロワーに共有しました。", + "activitypub.announce-dual": "%1%2%3のあなたの投稿をフォロワーに共有しました。", + "activitypub.announce-triple": "%1%2%3%4のあなたの投稿をフォロワーに共有しました。", + "activitypub.announce-multiple": "%1%2、他%3人が%4のあなたの投稿をフォロワーに共有しました。" } \ No newline at end of file diff --git a/public/language/ja/recent.json b/public/language/ja/recent.json index 4dd49a5214..d17f52b96c 100644 --- a/public/language/ja/recent.json +++ b/public/language/ja/recent.json @@ -4,10 +4,10 @@ "week": "1週間以内", "month": "1ヶ月以内", "year": "年", - "alltime": "全て", + "alltime": "すべて", "no-recent-topics": "最近のスレッドはありません。", "no-popular-topics": "人気スレッドはありません。", - "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." + "load-new-posts": "新しい投稿を読み込む", + "uncategorized.title": "既知のすべてのスレッド", + "uncategorized.intro": "このページには、このフォーラムが受信したすべてのスレッドの時系列一覧が表示されます。
    以下のスレッドで表明された見解や意見はモデレートされておらず、このウェブサイトの見解や意見を代表するものではない場合があります。" } \ No newline at end of file diff --git a/public/language/ja/tags.json b/public/language/ja/tags.json index 4bd611e139..c3b297a392 100644 --- a/public/language/ja/tags.json +++ b/public/language/ja/tags.json @@ -1,17 +1,17 @@ { - "all-tags": "All tags", + "all-tags": "すべてのタグ", "no-tag-topics": "このタグに関連するスレッドはありません。", - "no-tags-found": "No tags found", + "no-tags-found": "タグが見つかりません", "tags": "タグ", - "enter-tags-here": "Enter tags, %1 - %2 characters.", + "enter-tags-here": "タグを入力、%1〜%2文字。", "enter-tags-here-short": "タグを入れます…", "no-tags": "タグがありません", - "select-tags": "Select Tags", - "tag-whitelist": "Tag Whitelist", - "watching": "Watching", - "not-watching": "Not Watching", - "watching.description": "Notify me of new topics.", - "not-watching.description": "Do not notify me of new topics.", - "following-tag.message": "You will now be receiving notifications when somebody posts a topic with this tag.", - "not-following-tag.message": "You will not receive notifications when somebody posts a topic with this tag." + "select-tags": "タグを選択", + "tag-whitelist": "タグホワイトリスト", + "watching": "ウォッチ中", + "not-watching": "ウォッチしていない", + "watching.description": "新しいスレッドの通知を受け取る。", + "not-watching.description": "新しいスレッドの通知を受け取らない。", + "following-tag.message": "このタグでスレッドが投稿されると通知を受け取ります。", + "not-following-tag.message": "このタグでスレッドが投稿されても通知を受け取りません。" } \ No newline at end of file diff --git a/public/language/ja/top.json b/public/language/ja/top.json index 6e1e05674e..27e4fe8c21 100644 --- a/public/language/ja/top.json +++ b/public/language/ja/top.json @@ -1,4 +1,4 @@ { - "title": "Top", - "no-top-topics": "No top topics" + "title": "トップ", + "no-top-topics": "トップスレッドがありません" } \ No newline at end of file diff --git a/public/language/ja/user.json b/public/language/ja/user.json index f9884cbafd..e4fde66a1f 100644 --- a/public/language/ja/user.json +++ b/public/language/ja/user.json @@ -1,7 +1,7 @@ { "user-menu": "ユーザーメニュー", - "banned": "BANされた", - "unbanned": "BAN解除", + "banned": "利用停止された", + "unbanned": "利用停止解除", "muted": "ミュート中", "unmuted": "ミュート解除", "offline": "オフライン", @@ -13,9 +13,9 @@ "confirm-email": "メールアドレスを確認", "account-info": "アカウント情報", "admin-actions-label": "管理操作", - "ban-account": "BANアカウント", - "ban-account-confirm": "本当にこのユーザーをBANしますか?", - "unban-account": "BAN解除", + "ban-account": "アカウントを利用停止", + "ban-account-confirm": "本当にこのユーザーを利用停止しますか?", + "unban-account": "利用停止を解除", "mute-account": "アカウントをミュート", "unmute-account": "ミュートを解除", "delete-account": "アカウントを削除", @@ -181,24 +181,24 @@ "sso.dissociate-confirm": "%1 からアカウントの関連付けを解除してもよろしいですか?", "info.invited-by": "招待者", "info.latest-flags": "最近のフラグ", - "info.profile": "Profile", - "info.post": "Post", - "info.view-flag": "View flag", - "info.reported-by": "Reported by:", + "info.profile": "プロフィール", + "info.post": "投稿", + "info.view-flag": "フラグを表示", + "info.reported-by": "報告者:", "info.no-flags": "フラグのついた投稿はありません", "info.ban-history": "最近停止した履歴", "info.no-ban-history": "このユーザーは停止されていません", "info.banned-until": "%1まで停止", - "info.banned-expiry": "Expiry", - "info.ban-expired": "Ban expired", + "info.banned-expiry": "有効期限", + "info.ban-expired": "利用停止期限切れ", "info.banned-permanently": "永久に停止", "info.banned-reason-label": "理由", "info.banned-no-reason": "理由なし。", - "info.mute-history": "Recent Mute History", - "info.no-mute-history": "This user has never been muted", - "info.muted-until": "Muted until %1", - "info.muted-expiry": "Expiry", - "info.muted-no-reason": "No reason given.", + "info.mute-history": "最近のミュート履歴", + "info.no-mute-history": "このユーザーはミュートされたことがありません", + "info.muted-until": "%1までミュート", + "info.muted-expiry": "有効期限", + "info.muted-no-reason": "理由なし。", "info.username-history": "ユーザー名の履歴", "info.email-history": "Eメール履歴", "info.moderation-note": "モデレーションノート", diff --git a/public/language/vi/admin/settings/activitypub.json b/public/language/vi/admin/settings/activitypub.json index fc2205f5a5..56dfb4dc0e 100644 --- a/public/language/vi/admin/settings/activitypub.json +++ b/public/language/vi/admin/settings/activitypub.json @@ -41,7 +41,7 @@ "relays.state-2": "Kích hoạt", "relays.errors.invalid-url": "Vui lòng nhập URL hợp lệ", - "blocklists": "Third-party Blocklists", + "blocklists": "Danh Sách Chặn Của Bên Thứ Ba", "blocklists-help": "For the safety of you and your users, maintaining a blocklist is an essential part of any trust & safety effort when working with content from outside the local moderation scope. NodeBB ships with some recommended defaults, and these can be customized here.", "blocklists-default": "NodeBB ships with two blocklists by default, the IFTAS Do Not Interact Denylist and the IFTAS Abandoned and Unmanaged Domain Denylist.", "blocklists.url": "URL", diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index cc0d2f89a6..a227a17aa0 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -92,8 +92,8 @@ "watch.title": "Thông báo trả lời mới trong chủ đề này", "unwatch.title": "Ngừng xem chủ đề này", "share-this-post": "Chia sẻ bài viết này", - "share-mail-subject": "Check out this post on \"%1\"", - "share-mail-body": "I thought you might be interested in this post: %1", + "share-mail-subject": "Hãy xem bài đăng này về \"%1\"", + "share-mail-body": "Tôi nghĩ bạn có thể quan tâm đến bài đăng này: %1", "watching": "Đang xem", "not-watching": "Chưa Xem", "ignoring": "Bỏ qua", From af0e3d96898eb4ca51917f49d94bf894bac1a3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 30 Mar 2026 09:45:07 -0400 Subject: [PATCH 4741/4744] fix: closes #14133, don't modify displayName for system groups added a helper to just modify it for front end --- install/package.json | 6 +++--- public/src/modules/helpers.common.js | 5 +++++ src/groups/data.js | 3 --- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/install/package.json b/install/package.json index 42a5dda294..01011ea150 100644 --- a/install/package.json +++ b/install/package.json @@ -108,10 +108,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.7", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.62", + "nodebb-theme-harmony": "2.2.63", "nodebb-theme-lavender": "7.1.21", - "nodebb-theme-peace": "2.2.57", - "nodebb-theme-persona": "14.2.33", + "nodebb-theme-peace": "2.2.58", + "nodebb-theme-persona": "14.2.34", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.3", "nprogress": "0.2.0", diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js index 4893cea2fd..94a2d25307 100644 --- a/public/src/modules/helpers.common.js +++ b/public/src/modules/helpers.common.js @@ -17,6 +17,7 @@ module.exports = function (utils, Benchpress, relative_path) { generateCategoryBackground, generateChildrenCategories, generateTopicClass, + generateGroupDisplayName, membershipBtn, spawnPrivilegeStates, localeToHTML, @@ -167,6 +168,10 @@ module.exports = function (utils, Benchpress, relative_path) { return fields.filter(field => !!topic[field]).join(' '); } + function generateGroupDisplayName(group) { + return group.system ? group.displayName.replace(/-/g, ' ') : group.displayName; + } + // Groups helpers function membershipBtn(groupObj, btnClass = '') { if (groupObj.isMember && groupObj.name !== 'administrators') { diff --git a/src/groups/data.js b/src/groups/data.js index bef4d82fec..e585b653b5 100644 --- a/src/groups/data.js +++ b/src/groups/data.js @@ -133,9 +133,6 @@ module.exports = function (Groups) { if (hasField('name')) { group.nameEncoded = encodeURIComponent(group.name); group.displayName = validator.escape(String(group.name)); - if (Groups.systemGroups.includes(group.name)) { - group.displayName = group.displayName.replace(/-/g, ' '); - } } if (hasField('description')) { group.description = validator.escape(String(group.description || '')); From c37a9103c8406694f8d631f2115a60affb81c4d7 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 30 Mar 2026 10:20:35 -0400 Subject: [PATCH 4742/4744] fix: ActivityPub.fetchPublicKey to better handle key IDs that return CryptographicKey objects, #14130 --- src/activitypub/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/activitypub/index.js b/src/activitypub/index.js index d2f299ecfc..43e06b8664 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -197,11 +197,15 @@ ActivityPub.fetchPublicKey = async (uri) => { // Used for retrieving the public key from the passed-in keyId uri const body = await ActivityPub.get('uid', 0, uri); - if (!body.hasOwnProperty('publicKey')) { - throw new Error('[[error:activitypub.pubKey-not-found]]'); + if (body.hasOwnProperty('publicKeyPem')) { + // CryptographicKey returned (correct) + return body.publicKeyPem; + } else if (body.hasOwnProperty('publicKey') && body?.publicKey?.publicKeyPem) { + // Actor object returned (less correct) + return body.publicKey.publicKeyPem; } - return body.publicKey; + throw new Error('[[error:activitypub.pubKey-not-found]]'); }; ActivityPub.sign = async ({ key, keyId }, url, payload) => { @@ -288,7 +292,7 @@ ActivityPub.verify = async (req) => { // Retrieve public key from remote instance ActivityPub.helpers.log(`[activitypub/verify] Retrieving pubkey for ${keyId}`); - const { publicKeyPem } = await ActivityPub.fetchPublicKey(keyId); + const publicKeyPem = await ActivityPub.fetchPublicKey(keyId); const verify = createVerify('sha256'); verify.update(signed_string); From be347e674bf85b9f9b15fd3000766da671d6a74e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 30 Mar 2026 10:29:23 -0400 Subject: [PATCH 4743/4744] fix: loosen actor-matching check in Undo activity --- public/language/en-GB/error.json | 1 - src/activitypub/inbox.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 7ef3978faa..3a99dba7e1 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -307,6 +307,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } diff --git a/src/activitypub/inbox.js b/src/activitypub/inbox.js index bfa19d7823..c9dbf5192a 100644 --- a/src/activitypub/inbox.js +++ b/src/activitypub/inbox.js @@ -627,10 +627,6 @@ inbox.undo = async (req) => { const { actor, object } = req.body; const { type } = object; - if (actor !== object.actor) { - throw new Error('[[error:activitypub.actor-mismatch]]'); - } - const assertion = await activitypub.actors.assert(actor); if (!assertion) { throw new Error('[[error:activitypub.invalid-id]]'); From b98c06414640fb7334f9562151b471f59c35baa1 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Mon, 30 Mar 2026 14:29:52 +0000 Subject: [PATCH 4744/4744] chore(i18n): fallback strings for new resources: nodebb.error --- 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 deletions(-) diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 2471bc4452..aa71554400 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/az/error.json b/public/language/az/error.json index a63ba99a7f..277313cab4 100644 --- a/public/language/az/error.json +++ b/public/language/az/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Müəyyən edilmiş resursu əldə etmək mümkün deyil.", "activitypub.pubKey-not-found": "Açıq açarı həll etmək mümkün deyil, ona görə də faydalı yükün yoxlanılması həyata keçirilə bilməz.", "activitypub.origin-mismatch": "Alınan obyektin mənşəyi göndərənin mənşəyi ilə uyğun gəlmir", - "activitypub.actor-mismatch": "Alınan fəaliyyət gözlənildiyindən fərqli icraçı tərəfindən həyata keçirilir.", "activitypub.not-implemented": "Sorğu rədd edildi, çünki o və ya onun bir aspekti alıcı server tərəfindən icra olunmur" } \ No newline at end of file diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 0df31d3df2..716854bd66 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Посоченият материал не може да бъде получен.", "activitypub.pubKey-not-found": "Публичният ключ не може да бъде разпознат, така че потвърждението на данните не може да бъде извършено.", "activitypub.origin-mismatch": "Произходът на получения обект не съвпада с произхода на подателя", - "activitypub.actor-mismatch": "Полученото действие се изпълнява от източник, който е различен от очаквания.", "activitypub.not-implemented": "Заявката беше отказана, тъй като тя или част от нея не се поддържа от сървъра, към който е насочена" } \ No newline at end of file diff --git a/public/language/bn/error.json b/public/language/bn/error.json index adabb0bc9f..605d6b20ab 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 29e104ec70..d7bf19f953 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Nelze získat zadaný zdroj.", "activitypub.pubKey-not-found": "Nelze ověřit veřejný klíč, proto není možné provést ověření obsahu.", "activitypub.origin-mismatch": "Původ přijatého objektu neodpovídá původu odesílatele", - "activitypub.actor-mismatch": "Přijatou aktivitu provedl někdo jiný, než bylo očekáváno.", "activitypub.not-implemented": "Požadavek byl zamítnut, protože cílový server tuto funkci nepodporuje" } \ No newline at end of file diff --git a/public/language/da/error.json b/public/language/da/error.json index d0dc1bbc1a..11b03d740f 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/de/error.json b/public/language/de/error.json index c7e7fcf4eb..a69179d2ac 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Die angegebene Ressource kann nicht abgerufen werden.", "activitypub.pubKey-not-found": "Der öffentliche Schlüssel kann nicht aufgelöst werden, daher ist eine Überprüfung des Payloads nicht möglich.", "activitypub.origin-mismatch": "Der Ursprung des empfangenen Objekts stimmt nicht mit dem Ursprung des Absenders überein", - "activitypub.actor-mismatch": "Die empfangene Aktivität wird von einem anderen Akteur ausgeführt als erwartet", "activitypub.not-implemented": "Die Anfrage wurde abgelehnt, weil sie oder ein Teil davon vom empfangenden Server nicht implementiert ist" } \ No newline at end of file diff --git a/public/language/el/error.json b/public/language/el/error.json index e05e0556b0..d4f842bfb6 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index e2d479d164..afc35630da 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json index e2d479d164..afc35630da 100644 --- a/public/language/en-x-pirate/error.json +++ b/public/language/en-x-pirate/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/es/error.json b/public/language/es/error.json index 588063dc5c..2013c904d7 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "No se ha podido recuperar el recurso especificado. ", "activitypub.pubKey-not-found": "No se pudo resolver la llave pública, la verificación de payload no se ha concluido. ", "activitypub.origin-mismatch": "El origen del objeto recibido no coincide con el origen del emisario", - "activitypub.actor-mismatch": "La actividad recibida está siendo llevada por un actor que es diferente del esperado.", "activitypub.not-implemented": "La petición fue rechazada porque el servidor destinatario no implementa un aspecto esperado" } \ No newline at end of file diff --git a/public/language/et/error.json b/public/language/et/error.json index f98884ac0b..b5704f13cb 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json index bde54a18fb..16483da178 100644 --- a/public/language/fa-IR/error.json +++ b/public/language/fa-IR/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/fi/error.json b/public/language/fi/error.json index 7f23cddcd0..a3fbd80898 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/fr/error.json b/public/language/fr/error.json index 585541611a..e8e1892350 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Impossible de récupérer la ressource spécifiée", "activitypub.pubKey-not-found": "La clé publique est introuvable, la vérification de la charge utile est impossible.", "activitypub.origin-mismatch": "L’origine de l’objet reçu est différente de celle de l’expéditeur", - "activitypub.actor-mismatch": "L’activité reçue est effectuée par un acteur différent de celui attendu", "activitypub.not-implemented": "La requête a été rejetée : elle n’est pas prise en charge par le serveur destinataire." } \ No newline at end of file diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 2d690b0593..4d25479fe5 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/he/error.json b/public/language/he/error.json index 65636047cf..f530709242 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "לא ניתן לאחזר את המשאב שצוין.", "activitypub.pubKey-not-found": "לא ניתן לפתור מפתח ציבורי, ולכן לא ניתן לבצע אימות מטען.", "activitypub.origin-mismatch": "מקור האובייקט שהתקבל אינו תואם את מקור השולח", - "activitypub.actor-mismatch": "הפעילות המתקבלת מתבצעת על ידי שחקן שונה מהמצופה.", "activitypub.not-implemented": "הבקשה נדחתה מכיוון שהיא או היבט שלה אינם מיושמים על ידי שרת הנמען" } \ No newline at end of file diff --git a/public/language/hr/error.json b/public/language/hr/error.json index aedaf363ea..790951f65d 100644 --- a/public/language/hr/error.json +++ b/public/language/hr/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/hu/error.json b/public/language/hu/error.json index 46bf267a06..aea4ea8eba 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/hy/error.json b/public/language/hy/error.json index 624c261244..f11e521646 100644 --- a/public/language/hy/error.json +++ b/public/language/hy/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/id/error.json b/public/language/id/error.json index c1fa0a7ab4..6f971be4a0 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/it/error.json b/public/language/it/error.json index 9b8d425f37..c4f2da587b 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Impossibile recuperare la risorsa specificata.", "activitypub.pubKey-not-found": "Impossibile risolvere la chiave pubblica, quindi la verifica del payload non può avvenire.", "activitypub.origin-mismatch": "L'origine dell'oggetto ricevuto non corrisponde all'origine del mittente.", - "activitypub.actor-mismatch": "L'attività ricevuta viene eseguita da un partecipante diverso da quello previsto.", "activitypub.not-implemented": "La richiesta è stata rifiutata perché la richiesta o un suo aspetto non è implementato dal server del destinatario." } \ No newline at end of file diff --git a/public/language/ja/error.json b/public/language/ja/error.json index cc4b404729..87d3246708 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "指定されたリソースを取得できません。", "activitypub.pubKey-not-found": "公開鍵を解決できないため、ペイロードの検証ができません。", "activitypub.origin-mismatch": "受信したオブジェクトのオリジンが送信者のオリジンと一致しません", - "activitypub.actor-mismatch": "受信したアクティビティは、予期したアクターとは異なるアクターによって実行されています。", "activitypub.not-implemented": "リクエストまたはその一部が受信サーバーで実装されていないため、拒否されました" } \ No newline at end of file diff --git a/public/language/ko/error.json b/public/language/ko/error.json index c2338ddba4..0fb4eaf4e5 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/lt/error.json b/public/language/lt/error.json index b70c8a2fe3..22cd2528c4 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/lv/error.json b/public/language/lv/error.json index d251b3c149..fe904a61a4 100644 --- a/public/language/lv/error.json +++ b/public/language/lv/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/ms/error.json b/public/language/ms/error.json index 340a14711b..721884d657 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/nb/error.json b/public/language/nb/error.json index ed5e55d722..82e364c2fe 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Kan ikke hente den angitte ressursen.", "activitypub.pubKey-not-found": "Kan ikke løse opp offentlig nøkkel, så verifisering av nyttelasten kan ikke gjennomføres.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 77f8914364..d97c83e6bb 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/nn-NO/error.json b/public/language/nn-NO/error.json index 38b0946299..ae6c50205d 100644 --- a/public/language/nn-NO/error.json +++ b/public/language/nn-NO/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Kan ikkje hente den spesifiserte ressursen.", "activitypub.pubKey-not-found": " Kan ikkje løyse opp den offentlege nøkkelen, så verifisering av nyttelasta kan ikkje gjennomførast.", "activitypub.origin-mismatch": "Den mottatte objektet sin opphavsstad samsvarar ikkje med avsendaren sin opphavsstad.", - "activitypub.actor-mismatch": " Den mottatte aktiviteten blir utført av ein aktør som er annleis enn forventa.", "activitypub.not-implemented": "Førespurnaden blei avvist fordi han, eller ein del av han, ikkje er implementert av mottakarserveren." } \ No newline at end of file diff --git a/public/language/pl/error.json b/public/language/pl/error.json index dc28f25f27..0d0a7f5d49 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Nie udało się pobrać zadanego zasobu.", "activitypub.pubKey-not-found": "Nie udało się uzyskać klucza publicznego, więc nie może zajść weryfikacja zawartości.", "activitypub.origin-mismatch": "Odebrany obiekt nie zgadza się z tym co nadano.", - "activitypub.actor-mismatch": "Odebrana aktywność różni się od tego czego się spodziewano.", "activitypub.not-implemented": "Zapytanie napotkało odmowę z racji braku obsługi po stronie serwera odbiorczego." } \ No newline at end of file diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json index 58da962dcc..fa837e7d93 100644 --- a/public/language/pt-BR/error.json +++ b/public/language/pt-BR/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json index 23331c928d..cf763c7c18 100644 --- a/public/language/pt-PT/error.json +++ b/public/language/pt-PT/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/ro/error.json b/public/language/ro/error.json index 98f21dce51..e3878da28d 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/ru/error.json b/public/language/ru/error.json index bb0f988115..6895505eb9 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/rw/error.json b/public/language/rw/error.json index 6708efbb44..537c86f5b7 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/sc/error.json b/public/language/sc/error.json index e2d479d164..afc35630da 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/sk/error.json b/public/language/sk/error.json index bb14e6f77d..e00c74ad31 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/sl/error.json b/public/language/sl/error.json index ce9a320ffe..951d446585 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/sq-AL/error.json b/public/language/sq-AL/error.json index ead88b572c..84f8b7b954 100644 --- a/public/language/sq-AL/error.json +++ b/public/language/sq-AL/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/sr/error.json b/public/language/sr/error.json index 9c50f92230..65d113c7aa 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/sv/error.json b/public/language/sv/error.json index cda7701e74..2ddc9e90c6 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/th/error.json b/public/language/th/error.json index 41f40f9770..0a278d3edf 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/tr/error.json b/public/language/tr/error.json index f6eeebcb03..f33522ecb5 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 81b65bd963..5a2fb1f338 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file diff --git a/public/language/ur/error.json b/public/language/ur/error.json index 5c61957dff..a34f67c7a3 100644 --- a/public/language/ur/error.json +++ b/public/language/ur/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "مخصوص مواد حاصل نہیں کیا جا سکا۔", "activitypub.pubKey-not-found": "عوامی کلید کو تسلیم نہیں کیا جا سکا، اس لیے ڈیٹا کی تصدیق نہیں کی جا سکی۔", "activitypub.origin-mismatch": "موصول شدہ آبجیکٹ کا اصل بھیجنے والے کے اصل سے مطابقت نہیں رکھتا", - "activitypub.actor-mismatch": "موصول شدہ عمل کو متوقع سے مختلف ذریعہ سے انجام دیا جا رہا ہے۔", "activitypub.not-implemented": "درخواست کو مسترد کر دیا گیا کیونکہ اس کا یا اس کے کسی حصے کو اس سرور کے ذریعے سپورٹ نہیں کیا جاتا جس کی طرف یہ ہدایت کی گئی ہے" } \ No newline at end of file diff --git a/public/language/vi/error.json b/public/language/vi/error.json index 76ab019087..e03faef327 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Không thể truy xuất tài nguyên được chỉ định.", "activitypub.pubKey-not-found": "Không thể giải quyết khóa công khai, do đó việc xác minh payload không thể diễn ra.", "activitypub.origin-mismatch": "Nguồn gốc của đối tượng nhận được không khớp với nguồn gốc của người gửi", - "activitypub.actor-mismatch": "Hoạt động nhận được đang được thực hiện bởi một tác nhân khác với dự kiến.", "activitypub.not-implemented": "Yêu cầu bị từ chối vì nó hoặc một khía cạnh của nó không được máy chủ người nhận triển khai" } \ No newline at end of file diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index f34a214df8..009395b087 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "无法检索指定资源。", "activitypub.pubKey-not-found": "无法解析公钥,因此无法进行有效载荷验证。", "activitypub.origin-mismatch": "接收对象的原点与发送者的原点不一致", - "activitypub.actor-mismatch": "接收活动的行为者与预期的不同。", "activitypub.not-implemented": "请求被拒绝的原因是接收方服务器没有执行该请求或其中的某个方面" } \ No newline at end of file diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json index 5a2cbc4652..13504617c1 100644 --- a/public/language/zh-TW/error.json +++ b/public/language/zh-TW/error.json @@ -266,6 +266,5 @@ "activitypub.get-failed": "Unable to retrieve the specified resource.", "activitypub.pubKey-not-found": "Unable to resolve public key, so payload verification cannot take place.", "activitypub.origin-mismatch": "The received object's origin does not match the sender's origin", - "activitypub.actor-mismatch": "The received activity is being carried out by an actor that is different from expected.", "activitypub.not-implemented": "The request was denied because it or an aspect of it is not implemented by the recipient server" } \ No newline at end of file