From 18c27d1ad35f89973743e541d3633df649d41a79 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 25 Oct 2023 12:02:28 -0400 Subject: [PATCH 001/201] fix: suppress chat message notifications for users who are known to be in the chat room (uid is present in the corresponding socket.io room) --- src/messaging/notifications.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 2dc78a03d4..81a784944f 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -84,7 +84,7 @@ module.exports = function (Messaging) { Messaging.getRoomData(roomId), ]); const roomDefault = roomData.notificationSetting; - const uidsToNotify = []; + let uidsToNotify = []; const { ALLMESSAGES } = Messaging.notificationSettings; await batch.processSortedSet(`chat:room:${roomId}:uids:online`, async (uids) => { uids = uids.filter( @@ -99,6 +99,10 @@ module.exports = function (Messaging) { interval: 100, }); + // Suppress notifications for users who are literally present in the room + const realtimeUids = await io.getUidsInRoom(`chat_room_${roomId}`); + uidsToNotify = uidsToNotify.filter(uid => !realtimeUids.includes(parseInt(uid, 10))); + if (uidsToNotify.length) { const { displayname } = messageObj.fromUser; const isGroupChat = await Messaging.isGroupChat(roomId); From 6a696c43a3e33b91f04115ba08045bb18086f0aa Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 25 Oct 2023 12:11:37 -0400 Subject: [PATCH 002/201] fix: handle public chat rooms too --- src/messaging/notifications.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 81a784944f..f20af5468c 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -70,13 +70,13 @@ module.exports = function (Messaging) { } try { - await sendNotification(fromUid, roomId, messageObj); + await sendNotification(fromUid, roomId, isPublic, messageObj); } catch (err) { winston.error(`[messaging/notifications] Unabled to send notification\n${err.stack}`); } }; - async function sendNotification(fromUid, roomId, messageObj) { + async function sendNotification(fromUid, roomId, isPublic, messageObj) { fromUid = parseInt(fromUid, 10); const [settings, roomData] = await Promise.all([ @@ -100,7 +100,7 @@ module.exports = function (Messaging) { }); // Suppress notifications for users who are literally present in the room - const realtimeUids = await io.getUidsInRoom(`chat_room_${roomId}`); + const realtimeUids = await io.getUidsInRoom(`chat_room_${isPublic ? 'public_' : ''}${roomId}`); uidsToNotify = uidsToNotify.filter(uid => !realtimeUids.includes(parseInt(uid, 10))); if (uidsToNotify.length) { From dabc282dde3d9834bf1b2f3209f90c6e06250a2c 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 Oct 2023 13:25:55 -0400 Subject: [PATCH 003/201] refactor: move async call to parallel --- src/messaging/notifications.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index f20af5468c..e2ad6addb0 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -76,20 +76,22 @@ module.exports = function (Messaging) { } }; - async function sendNotification(fromUid, roomId, isPublic, messageObj) { + async function sendNotification(fromUid, roomId, messageObj) { fromUid = parseInt(fromUid, 10); - const [settings, roomData] = await Promise.all([ + const [settings, roomData, realtimeUids] = await Promise.all([ db.getObject(`chat:room:${roomId}:notification:settings`), Messaging.getRoomData(roomId), + io.getUidsInRoom(`chat_room_${roomId}`), ]); const roomDefault = roomData.notificationSetting; - let uidsToNotify = []; + const uidsToNotify = []; const { ALLMESSAGES } = Messaging.notificationSettings; await batch.processSortedSet(`chat:room:${roomId}:uids:online`, async (uids) => { uids = uids.filter( uid => (parseInt((settings && settings[uid]) || roomDefault, 10) === ALLMESSAGES) && - fromUid !== parseInt(uid, 10) + fromUid !== parseInt(uid, 10) && + !realtimeUids.includes(parseInt(uid, 10)) ); const hasRead = await Messaging.hasRead(uids, roomId); uidsToNotify.push(...uids.filter((uid, index) => !hasRead[index])); @@ -99,10 +101,6 @@ module.exports = function (Messaging) { interval: 100, }); - // Suppress notifications for users who are literally present in the room - const realtimeUids = await io.getUidsInRoom(`chat_room_${isPublic ? 'public_' : ''}${roomId}`); - uidsToNotify = uidsToNotify.filter(uid => !realtimeUids.includes(parseInt(uid, 10))); - if (uidsToNotify.length) { const { displayname } = messageObj.fromUser; const isGroupChat = await Messaging.isGroupChat(roomId); From e5a60dc85784f2510de752b718d6712f74a85bcb 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 Oct 2023 13:40:15 -0400 Subject: [PATCH 004/201] fix: param --- src/messaging/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index e2ad6addb0..503382cf01 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -70,7 +70,7 @@ module.exports = function (Messaging) { } try { - await sendNotification(fromUid, roomId, isPublic, messageObj); + await sendNotification(fromUid, roomId, messageObj); } catch (err) { winston.error(`[messaging/notifications] Unabled to send notification\n${err.stack}`); } From 075cd598d1b24261c1a75570fa0d40436feb911e 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 Oct 2023 19:06:29 -0400 Subject: [PATCH 005/201] fix: closes #12126, fix language keys --- src/controllers/admin/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index 9ecf08548d..61ccd6a091 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -49,7 +49,7 @@ settingsController.user = async (req, res) => { const notificationTypes = await notifications.getAllNotificationTypes(); const notificationSettings = notificationTypes.map(type => ({ name: type, - label: `[[notifications:${type}]]`, + label: `[[notifications:${type.replace(/_/g, '-')}]]`, })); res.render('admin/settings/user', { title: '[[admin/menu:settings/user]]', From 6c7e6144175dc12621b03a5ab06ff9d89d68c82a 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 Oct 2023 10:23:27 -0400 Subject: [PATCH 006/201] feat: batch.processSortedSet min/max (#12129) * feat: batch.processSortedSet min/max * test if this works --- src/batch.js | 14 +++++++++++++- src/database/mongo/sorted.js | 12 +++++++++++- src/database/postgres/sorted.js | 6 +++++- test/batch.js | 21 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/batch.js b/src/batch.js index edc4038055..48c6571cd4 100644 --- a/src/batch.js +++ b/src/batch.js @@ -39,11 +39,23 @@ exports.processSortedSet = async function (setKey, process, options) { if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { process = util.promisify(process); } + const method = options.reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'; + const isByScore = (options.min && options.min !== '-inf') || (options.max && options.max !== '+inf'); + const byScore = isByScore ? 'ByScore' : ''; + const withScores = options.withScores ? 'WithScores' : ''; let iteration = 1; + const getFn = db[`${method}${byScore}${withScores}`]; while (true) { /* eslint-disable no-await-in-loop */ - const ids = await db[`${method}${options.withScores ? 'WithScores' : ''}`](setKey, start, stop); + const ids = await getFn( + setKey, + start, + isByScore ? stop - start + 1 : stop, + options.reverse ? options.max : options.min, + options.reverse ? options.min : options.max, + ); + if (!ids.length || options.doneIf(start, stop, ids)) { return; } diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index eec65ef982..0b5036b064 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -568,7 +568,17 @@ module.exports = function (module) { if (!options.withScores) { project.score = 0; } - const cursor = await module.client.collection('objects').find({ _key: setKey }, { projection: project }) + const query = { _key: setKey }; + if (options.min && options.min !== '-inf') { + query.score = { $gte: parseFloat(options.min) }; + } + if (options.max && options.max !== '+inf') { + query.score = query.score || {}; + query.score.$lte = parseFloat(options.max); + } + + const cursor = await module.client.collection('objects') + .find(query, { projection: project }) .sort({ score: sort }) .batchSize(options.batch); diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 2b707a3a7d..5e3b6a65aa 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -665,6 +665,8 @@ SELECT z."value", const client = await module.pool.connect(); const batchSize = (options || {}).batch || 100; const sort = options.reverse ? 'DESC' : 'ASC'; + const min = options.min && options.min !== '-inf' ? options.min : null; + const max = options.max && options.max !== '+inf' ? options.max : null; const cursor = client.query(new Cursor(` SELECT z."value", z."score" FROM "legacy_object_live" o @@ -672,7 +674,9 @@ SELECT z."value", z."score" ON o."_key" = z."_key" AND o."type" = z."type" WHERE o."_key" = $1::TEXT - ORDER BY z."score" ${sort}, z."value" ${sort}`, [setKey])); + AND (z."score" >= $2::NUMERIC OR $2::NUMERIC IS NULL) + AND (z."score" <= $3::NUMERIC OR $3::NUMERIC IS NULL) + ORDER BY z."score" ${sort}, z."value" ${sort}`, [setKey, min, max])); if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { process = util.promisify(process); diff --git a/test/batch.js b/test/batch.js index fe11b72883..f7dad73f5a 100644 --- a/test/batch.js +++ b/test/batch.js @@ -77,6 +77,27 @@ describe('batch', () => { assert.strictEqual(total, 490); }); + it('should process sorted set with min/max scores', async () => { + await db.sortedSetAddBulk([ + ['processByScore', 1, 'item1'], + ['processByScore', 2, 'item2'], + ['processByScore', 3, 'item3'], + ['processByScore', 3, 'item4'], + ['processByScore', 4, 'item5'], + ['processByScore', 5, 'item6'], + ]); + const result = []; + await batch.processSortedSet('processByScore', async (items) => { + result.push(...items); + }, { + min: 3, + max: 4, + }); + assert(result.includes('item3')); + assert(result.includes('item4')); + assert(result.includes('item5')); + }); + it('should process array with callbacks', (done) => { let total = 0; batch.processArray(scores, (nums, next) => { From b351c00a7d42b75606708f4eacd1891f54ad33cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:23:47 -0400 Subject: [PATCH 007/201] fix(deps): update dependency sass to v1.69.5 (#12128) 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 bb3014c057..26d37700ae 100644 --- a/install/package.json +++ b/install/package.json @@ -125,7 +125,7 @@ "rss": "1.2.2", "rtlcss": "4.1.1", "sanitize-html": "2.11.0", - "sass": "1.69.4", + "sass": "1.69.5", "semver": "7.5.4", "serve-favicon": "2.5.0", "sharp": "0.32.6", From 9e8a21167bf76af2ec7821c0c5a7bf2fe18eaa27 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:23:55 -0400 Subject: [PATCH 008/201] chore(deps): update dependency sass-embedded to v1.69.5 (#12127) 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 26d37700ae..b586c14227 100644 --- a/install/package.json +++ b/install/package.json @@ -172,7 +172,7 @@ "smtp-server": "3.13.0" }, "optionalDependencies": { - "sass-embedded": "1.69.4" + "sass-embedded": "1.69.5" }, "resolutions": { "*/jquery": "3.7.1" From 9bed7646d2f266ae8fcbaa667ef02317b9f97a8b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:24:10 -0400 Subject: [PATCH 009/201] fix(deps): update dependency workerpool to v8 (#12121) 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 b586c14227..5b7b2fde65 100644 --- a/install/package.json +++ b/install/package.json @@ -146,7 +146,7 @@ "webpack": "5.89.0", "webpack-merge": "5.10.0", "winston": "3.11.0", - "workerpool": "6.5.1", + "workerpool": "8.0.0", "xml": "1.0.1", "xregexp": "5.1.1", "yargs": "17.7.2", From 022fa0e75f4801006e1c7f95581e51e5f177b19e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 26 Oct 2023 11:00:43 -0400 Subject: [PATCH 010/201] fix: don't count internal links towards link count when restricting new users from posting links --- src/posts/queue.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/posts/queue.js b/src/posts/queue.js index 8d6550b7fd..23419d2ee3 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -96,9 +96,16 @@ module.exports = function (Posts) { if (!isPrivileged && reputation < meta.config['min:rep:post-links']) { const parsed = await plugins.hooks.fire('filter:parse.raw', String(content)); - if (parsed.match(/]*>([^<]+)<\/a>/g)) { - return false; + const matches = parsed.matchAll(/]*href="([^"]+)"[^>]*>/g); + let external = 0; + for (const [, href] of matches) { + const internal = utils.isInternalURI(new URL(href, nconf.get('url')), new URL(nconf.get('url')), nconf.get('relative_path')); + if (!internal) { + external += 1; + } } + + return external === 0; } return true; }; From 87a859aa938639e5bb59f76c05d9f137b6cb026f 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 Oct 2023 19:27:00 -0400 Subject: [PATCH 011/201] feat: closes #5584, setup winston to output to file on upgrade --- src/cli/upgrade.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js index 5ad7e772e6..4b6d769b69 100644 --- a/src/cli/upgrade.js +++ b/src/cli/upgrade.js @@ -71,6 +71,21 @@ async function runSteps(tasks) { } async function runUpgrade(upgrades, options) { + const winston = require('winston'); + const path = require('path'); + winston.configure({ + transports: [ + new winston.transports.File({ + filename: path.join(__dirname, '../../', nconf.get('logFile') || 'logs/output.log'), + }), + ], + }); + const stdoutWrite = process.stdout.write; + process.stdout.write = function (...args) { + winston.info(args[0]); + stdoutWrite.apply(process.stdout, args); + }; + console.log(chalk.cyan('\nUpdating NodeBB...')); options = options || {}; // disable mongo timeouts during upgrade From 3210ace6aeccb8b6024755bf0387a18ec6c43211 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 28 Oct 2023 09:18:17 +0000 Subject: [PATCH 012/201] Latest translations and fallbacks --- public/language/de/admin/extend/widgets.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/de/admin/extend/widgets.json b/public/language/de/admin/extend/widgets.json index 8e92c632be..73e7853806 100644 --- a/public/language/de/admin/extend/widgets.json +++ b/public/language/de/admin/extend/widgets.json @@ -30,6 +30,6 @@ "start-date": "Anfangsdatum", "end-date": "Enddatum", "hide-on-mobile": "Auf dem Handy verstecken", - "hide-drafts": "Hide drafts", - "show-drafts": "Show drafts" + "hide-drafts": "Entwürfe ausblenden", + "show-drafts": "Entwürfe anzeigen" } \ No newline at end of file From d3fb339c370b682271cfb7cf86ef98c8c240d8ca 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 Oct 2023 20:44:51 -0400 Subject: [PATCH 013/201] derp --- src/cli/upgrade.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js index 4b6d769b69..ff3487388c 100644 --- a/src/cli/upgrade.js +++ b/src/cli/upgrade.js @@ -75,16 +75,14 @@ async function runUpgrade(upgrades, options) { const path = require('path'); winston.configure({ transports: [ + new winston.transports.Console({ + handleExceptions: true, + }), new winston.transports.File({ filename: path.join(__dirname, '../../', nconf.get('logFile') || 'logs/output.log'), }), ], }); - const stdoutWrite = process.stdout.write; - process.stdout.write = function (...args) { - winston.info(args[0]); - stdoutWrite.apply(process.stdout, args); - }; console.log(chalk.cyan('\nUpdating NodeBB...')); options = options || {}; From 6b28f1dc1a41ae5f6fb63fae7cb5a9d7cdade4bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:47:51 -0400 Subject: [PATCH 014/201] fix(deps): update dependency ace-builds to v1.31.1 (#12135) 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 b96dea923a..6e09247d2a 100644 --- a/install/package.json +++ b/install/package.json @@ -34,7 +34,7 @@ "@fortawesome/fontawesome-free": "6.4.2", "@isaacs/ttlcache": "1.4.1", "@popperjs/core": "2.11.8", - "ace-builds": "1.31.0", + "ace-builds": "1.31.1", "archiver": "6.0.1", "async": "3.2.4", "autoprefixer": "10.4.16", From 2fb3af3cfebbec03fc25b71867ac4af4db1d0265 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:48:07 -0400 Subject: [PATCH 015/201] fix(deps): update dependency cron to v3.1.6 (#12132) 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 6e09247d2a..204400a5b9 100644 --- a/install/package.json +++ b/install/package.json @@ -58,7 +58,7 @@ "connect-pg-simple": "9.0.0", "connect-redis": "7.1.0", "cookie-parser": "1.4.6", - "cron": "3.1.4", + "cron": "3.1.6", "cropperjs": "1.6.1", "csrf-sync": "4.0.1", "daemon": "1.1.0", From cdb407194a0f897ae37eabb9aa3d991806fc9055 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:48:44 -0400 Subject: [PATCH 016/201] fix(deps): update dependency nodebb-plugin-composer-default to v10.2.25 (#12136) 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 204400a5b9..88c3b2d97d 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.24", + "nodebb-plugin-composer-default": "10.2.25", "nodebb-plugin-dbsearch": "6.2.2", "nodebb-plugin-emoji": "5.1.13", "nodebb-plugin-emoji-android": "4.0.0", From 2337d641945bc6f468d5730a170d75ddea13435b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:36:29 +0000 Subject: [PATCH 017/201] fix(deps): update dependency spdx-license-list to v6.8.0 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 88c3b2d97d..cc152e2e1c 100644 --- a/install/package.json +++ b/install/package.json @@ -134,7 +134,7 @@ "socket.io-client": "4.7.2", "@socket.io/redis-adapter": "8.2.1", "sortablejs": "1.15.0", - "spdx-license-list": "6.7.0", + "spdx-license-list": "6.8.0", "spider-detector": "2.0.1", "terser-webpack-plugin": "5.3.9", "textcomplete": "0.18.2", From 29a59b90a26fb8597280e2dd57fa4cbbadc71fdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:40:14 -0400 Subject: [PATCH 018/201] fix(deps): update dependency nodebb-theme-peace to v2.1.24 (#12139) 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 cc152e2e1c..7e76065ef3 100644 --- a/install/package.json +++ b/install/package.json @@ -104,7 +104,7 @@ "nodebb-rewards-essentials": "1.0.0", "nodebb-theme-harmony": "1.1.91", "nodebb-theme-lavender": "7.1.5", - "nodebb-theme-peace": "2.1.23", + "nodebb-theme-peace": "2.1.24", "nodebb-theme-persona": "13.2.42", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", From a3452c8f39cb2b85b57ddeb9d990736e88c5291b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:40:34 -0400 Subject: [PATCH 019/201] fix(deps): update dependency nodebb-theme-harmony to v1.1.92 (#12131) 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 7e76065ef3..388259a843 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.91", + "nodebb-theme-harmony": "1.1.92", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.24", "nodebb-theme-persona": "13.2.42", From 97016f47804534d708736b9e24e5df4350caf73a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:40:55 -0400 Subject: [PATCH 020/201] chore(deps): update commitlint monorepo to v18 (#12105) 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 388259a843..bcdcbd4217 100644 --- a/install/package.json +++ b/install/package.json @@ -154,8 +154,8 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@commitlint/cli": "17.8.1", - "@commitlint/config-angular": "17.8.1", + "@commitlint/cli": "18.2.0", + "@commitlint/config-angular": "18.1.0", "coveralls": "3.1.1", "eslint": "8.52.0", "eslint-config-nodebb": "0.2.1", From 91e45fa26d6a593cf40c0abc7548ffe764d273f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:41:07 -0400 Subject: [PATCH 021/201] fix(deps): update dependency nodebb-theme-persona to v13.2.43 (#12140) 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 bcdcbd4217..7354135e4d 100644 --- a/install/package.json +++ b/install/package.json @@ -105,7 +105,7 @@ "nodebb-theme-harmony": "1.1.92", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.24", - "nodebb-theme-persona": "13.2.42", + "nodebb-theme-persona": "13.2.43", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", From 7a8c27bf905b9e0a570c2a9ea4e9af96b62fffcc Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 31 Oct 2023 09:18:34 +0000 Subject: [PATCH 022/201] Latest translations and fallbacks --- public/language/de/notifications.json | 2 +- public/language/he/admin/extend/widgets.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json index c032f0e955..0fe3b1354d 100644 --- a/public/language/de/notifications.json +++ b/public/language/de/notifications.json @@ -48,7 +48,7 @@ "user-posted-to": "%1 hat auf %2 geantwortet.", "user-posted-to-dual": "%1 und %2 haben auf %3 geantwortet.", "user-posted-to-triple": "%1, %2 und %3 haben geantwortet auf: %4", - "user-posted-to-multiple": "%1, %2 und %3 andere haben geantwortet auf :%4", + "user-posted-to-multiple": "%1, %2 und %3 andere haben geantwortet auf: %4", "user-posted-topic": "%1 hat ein neues Thema erstellt: %2", "user-edited-post": "%1 hat einen Post in %2 bearbeitet", "user-posted-topic-with-tag": "%1 hat ein neues Thema mit dem Tag %2 erstellt", diff --git a/public/language/he/admin/extend/widgets.json b/public/language/he/admin/extend/widgets.json index 11643eecd6..0a41084f8d 100644 --- a/public/language/he/admin/extend/widgets.json +++ b/public/language/he/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 From 4c4f3ac9835f223f2190863a1da1d5e7876b59b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 31 Oct 2023 10:15:06 -0400 Subject: [PATCH 023/201] feat: add direct message link (#12138) * feat: add direct message link /message/:mid add /:index? to chat routes add copy link to chat messages add messageCount to each room object add infinitescroll in both directions to chat * fix more tests * test: more text fixes * test: fix tests * remove async * dont crash if element not in dom clamp scrollToIndex values to 0, msgCount --- public/language/en-GB/modules.json | 1 + public/openapi/components/schemas/Chats.yaml | 3 + public/openapi/read.yaml | 6 + public/openapi/read/chats/roomid.yaml | 6 + public/openapi/read/message/mid.yaml | 19 +++ .../read/user/userslug/chats/roomid.yaml | 15 +++ public/src/client/chats.js | 121 +++++++++++++----- public/src/client/chats/messages.js | 4 + public/src/modules/chat.js | 2 +- src/api/chats.js | 20 ++- src/controllers/accounts/chats.js | 37 +++++- src/controllers/write/chats.js | 5 +- src/messaging/create.js | 1 + src/messaging/index.js | 8 +- src/messaging/rooms.js | 4 +- src/routes/user.js | 6 +- src/upgrades/3.6.0/chat_message_counts.js | 20 +++ test/api.js | 2 +- 18 files changed, 236 insertions(+), 44 deletions(-) create mode 100644 public/openapi/read/message/mid.yaml create mode 100644 src/upgrades/3.6.0/chat_message_counts.js diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 72fac9447f..3307b1c213 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", diff --git a/public/openapi/components/schemas/Chats.yaml b/public/openapi/components/schemas/Chats.yaml index be0dfb681b..8dc6349a6d 100644 --- a/public/openapi/components/schemas/Chats.yaml +++ b/public/openapi/components/schemas/Chats.yaml @@ -16,6 +16,9 @@ RoomObject: userCount: type: number description: number of users in this chat room + messageCount: + type: number + description: number of messages sent in this chat room groups: type: array description: list of groups that can access the room diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index e8567b5a1f..1e1b721d6f 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -310,8 +310,14 @@ paths: $ref: 'read/notifications.yaml' "/api/user/{userslug}/chats/{roomid}": $ref: 'read/user/userslug/chats/roomid.yaml' + "/api/user/{userslug}/chats/{roomid}/{index}": + $ref: 'read/user/userslug/chats/roomid.yaml' "/api/chats/{roomid}": $ref: 'read/chats/roomid.yaml' + "/api/chats/{roomid}/{index}": + $ref: 'read/chats/roomid.yaml' + "/api/message/{mid}": + $ref: 'read/message/mid.yaml' /api/groups: $ref: 'read/groups.yaml' "/api/groups/{slug}": diff --git a/public/openapi/read/chats/roomid.yaml b/public/openapi/read/chats/roomid.yaml index 0780b774cb..91b7586fda 100644 --- a/public/openapi/read/chats/roomid.yaml +++ b/public/openapi/read/chats/roomid.yaml @@ -10,6 +10,12 @@ get: schema: type: string example: 1 + - name: index + in: path + required: true + schema: + type: string + example: 1 responses: "200": description: "Chat identifier resolved" diff --git a/public/openapi/read/message/mid.yaml b/public/openapi/read/message/mid.yaml new file mode 100644 index 0000000000..aa51827eba --- /dev/null +++ b/public/openapi/read/message/mid.yaml @@ -0,0 +1,19 @@ +get: + tags: + - shorthand + summary: Access a specific chat message + description: This route comes in handy when all you have is the `mid`, and you want to redirect users to the canonical URL for the chat message, with the appropriate user slug and room id + parameters: + - name: mid + in: path + required: true + schema: + type: string + example: 1 + responses: + "200": + description: "Canonical URL of chat message" + content: + text/plain: + schema: + type: string \ 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 24bd0cb90b..bd0ae278fd 100644 --- a/public/openapi/read/user/userslug/chats/roomid.yaml +++ b/public/openapi/read/user/userslug/chats/roomid.yaml @@ -15,6 +15,12 @@ get: schema: type: string example: 1 + - name: index + in: path + required: true + schema: + type: string + example: 1 responses: "200": description: "" @@ -32,6 +38,11 @@ get: type: boolean userCount: type: number + messageCount: + type: number + scrollToIndex: + type: number + nullable: true icon: type: string groups: @@ -187,6 +198,8 @@ get: type: boolean userCount: type: number + messageCount: + type: number groups: type: array timestamp: @@ -238,6 +251,8 @@ get: teaser: type: object properties: + roomId: + type: number fromuid: type: number content: diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 53a2835e9a..0ad676dc12 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -69,9 +69,19 @@ define('forum/chats', [ } Chats.initialised = true; - const changeContentEl = $('[component="chat/message/content"]'); - messages.wrapImagesInLinks(changeContentEl); - messages.scrollToBottomAfterImageLoad(changeContentEl); + const chatContentEl = $('[component="chat/message/content"]'); + messages.wrapImagesInLinks(chatContentEl); + 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 + ); + } + } else { + messages.scrollToBottomAfterImageLoad(chatContentEl); + } create.init(); hooks.fire('action:chat.loaded', $('.chats-full')); @@ -90,12 +100,13 @@ define('forum/chats', [ Chats.addLeaveHandler(roomId, chatControls.find('[data-action="leave"]')); Chats.addDeleteHandler(roomId, chatControls.find('[data-action="delete"]')); Chats.addScrollHandler(roomId, ajaxify.data.uid, chatMessageContent); - Chats.addScrollBottomHandler(chatMessageContent); + Chats.addScrollBottomHandler(roomId, chatMessageContent); Chats.addParentHandler(mainWrapper); Chats.addCharactersLeftHandler(mainWrapper); Chats.addTextareaResizeHandler(mainWrapper); Chats.addTypingHandler(mainWrapper, roomId); Chats.addIPHandler(mainWrapper); + Chats.addCopyLinkHandler(mainWrapper); Chats.createAutoComplete(roomId, $('[component="chat/input"]')); Chats.addUploadHandler({ dragDropAreaEl: $('.chats-full'), @@ -230,6 +241,19 @@ define('forum/chats', [ }); }; + Chats.addCopyLinkHandler = function (container) { + container.off('click', '[data-action="copy-link"]') + .on('click', '[data-action="copy-link"]', function () { + const copyEl = $(this); + const mid = copyEl.attr('data-mid'); + if (mid) { + navigator.clipboard.writeText(`${window.location.origin}/message/${mid}`); + copyEl.find('i').addClass('fa-check').removeClass('fa-link'); + setTimeout(() => copyEl.find('i').removeClass('fa-check').addClass('fa-link'), 2000); + } + }); + }; + Chats.addPopoutHandler = function () { $('[data-action="pop-out"]').on('click', function () { const text = components.get('chat/input').val(); @@ -252,49 +276,82 @@ define('forum/chats', [ Chats.addScrollHandler = function (roomId, uid, el) { let loading = false; + let previousScrollTop = el.scrollTop(); + let currentScrollTop = previousScrollTop; el.off('scroll').on('scroll', utils.debounce(function () { + if (parseInt(el.attr('data-ignore-next-scroll'), 10) === 1) { + el.removeAttr('data-ignore-next-scroll'); + previousScrollTop = el.scrollTop(); + return; + } messages.toggleScrollUpAlert(el); if (loading) { return; } + currentScrollTop = el.scrollTop(); - const top = (el[0].scrollHeight - el.height()) * 0.1; - if (el.scrollTop() >= top) { + const direction = currentScrollTop > previousScrollTop ? 1 : -1; + previousScrollTop = currentScrollTop; + const scrollPercent = 100 * (currentScrollTop / (el[0].scrollHeight - el.height())); + const top = 15; + const bottom = 85; + + if (direction === 1 && !ajaxify.data.scrollToIndex) { + // dont trigger infinitescroll if there is no /index in url return; } - loading = true; - const start = parseInt(el.children('[data-mid]').length, 10); - api.get(`/chats/${roomId}/messages`, { uid, start }).then((data) => { - data = data.messages; - if (!data) { - loading = false; - return; - } - data = data.filter(function (chatMsg) { - return !$('[component="chat/message"][data-mid="' + chatMsg.messageId + '"]').length; - }); - if (!data.length) { - loading = false; - return; - } - messages.parseMessage(data, function (html) { - const currentScrollTop = el.scrollTop(); - const previousHeight = el[0].scrollHeight; - el.prepend(html); - messages.onMessagesAddedToDom(html); - el.scrollTop((el[0].scrollHeight - previousHeight) + currentScrollTop); - loading = false; - }); - }).catch(alerts.error); + if ((scrollPercent < top && direction === -1) || (scrollPercent > bottom && direction === 1)) { + loading = true; + + const msgEls = el.children('[data-mid]').not('.new'); + const afterEl = direction > 0 ? msgEls.last() : msgEls.first(); + const start = parseInt(afterEl.attr('data-index'), 10) || 0; + + api.get(`/chats/${roomId}/messages`, { uid, start, direction }).then((data) => { + let messageData = data.messages; + if (!messageData) { + loading = false; + return; + } + messageData = messageData.filter(function (chatMsg) { + const msgOnDom = el.find('[component="chat/message"][data-mid="' + chatMsg.messageId + '"]'); + msgOnDom.removeClass('new'); + return !msgOnDom.length; + }); + if (!messageData.length) { + loading = false; + return; + } + messages.parseMessage(messageData, function (html) { + el.attr('data-ignore-next-scroll', 1); + if (direction > 0) { + html.insertAfter(afterEl); + messages.onMessagesAddedToDom(html); + } else { + const currentScrollTop = el.scrollTop(); + const previousHeight = el[0].scrollHeight; + el.prepend(html); + messages.onMessagesAddedToDom(html); + el.scrollTop((el[0].scrollHeight - previousHeight) + currentScrollTop); + } + + loading = false; + }); + }).catch(alerts.error); + } }, 100)); }; - Chats.addScrollBottomHandler = function (chatContent) { + Chats.addScrollBottomHandler = function (roomId, chatContent) { chatContent.parents('[component="chat/message/window"]') .find('[component="chat/messages/scroll-up-alert"]') .off('click').on('click', function () { - messages.scrollToBottom(chatContent); + if (ajaxify.data.scrollToIndex && parseInt(ajaxify.data.roomId, 10) === parseInt(roomId, 10)) { + Chats.switchChat(roomId); + } else { + messages.scrollToBottom(chatContent); + } }); }; diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index d6d301d66d..2322ed8955 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -83,6 +83,7 @@ define('forum/chats/messages', [ if (!Array.isArray(data)) { data.newSet = data.toMid || lastSpeaker !== parseInt(data.fromuid, 10) || parseInt(data.timestamp, 10) > parseInt(lasttimestamp, 10) + (1000 * 60 * 3); + data.index = parseInt(lastMsgEl.attr('data-index'), 10) + 1; } messages.parseMessage(data, function (html) { @@ -93,6 +94,7 @@ define('forum/chats/messages', [ function onMessagesParsed(chatContentEl, html, msgData) { const newMessage = $(html); const isAtBottom = messages.isAtBottom(chatContentEl); + newMessage.addClass('new'); newMessage.appendTo(chatContentEl); messages.onMessagesAddedToDom(newMessage); if (isAtBottom || msgData.self) { @@ -102,6 +104,7 @@ define('forum/chats/messages', [ if (chatMsgEls.length > 150) { const removeCount = chatMsgEls.length - 150; chatMsgEls.slice(0, removeCount).remove(); + chatContentEl.find('[data-mid].new').removeClass('new'); } } @@ -150,6 +153,7 @@ define('forum/chats/messages', [ messages.scrollToBottom = function (containerEl) { if (containerEl && containerEl.length) { + containerEl.attr('data-ignore-next-scroll', 1); containerEl.scrollTop(containerEl[0].scrollHeight - containerEl.height()); containerEl.parents('[component="chat/message/window"]') .find('[component="chat/messages/scroll-up-alert"]') diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 234a596261..de090c7365 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -399,7 +399,7 @@ define('chat', [ Chats.createAutoComplete(roomId, chatModal.find('[component="chat/input"]')); Chats.addScrollHandler(roomId, data.uid, chatModal.find('[component="chat/message/content"]')); - Chats.addScrollBottomHandler(chatModal.find('[component="chat/message/content"]')); + Chats.addScrollBottomHandler(roomId, chatModal.find('[component="chat/message/content"]')); Chats.addParentHandler(chatModal.find('[component="chat/message/content"]')); Chats.addCharactersLeftHandler(chatModal); Chats.addTextareaResizeHandler(chatModal); diff --git a/src/api/chats.js b/src/api/chats.js index 326737f3fb..d3788ea9e1 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -244,13 +244,29 @@ chatsAPI.kick = async (caller, data) => { return chatsAPI.users(caller, data); }; -chatsAPI.listMessages = async (caller, { uid, roomId, start }) => { +chatsAPI.listMessages = async (caller, { uid, roomId, start, direction = null }) => { + const count = 50; + let stop = start + count - 1; + if (direction === 1 || direction === -1) { + const msgCount = await db.getObjectField(`chat:room:${roomId}`, 'messageCount'); + start = msgCount - start; + if (direction === 1) { + start -= count + 1; + } + stop = start + count - 1; + start = Math.max(0, start); + if (stop <= -1) { + return { messages: [] }; + } + stop = Math.max(0, stop); + } + const messages = await messaging.getMessages({ callerUid: caller.uid, uid, roomId, start, - count: 50, + count: stop - start + 1, }); return { messages }; diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index fa0234f058..d0b99b4041 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -48,7 +48,19 @@ chatsController.get = async function (req, res, next) { return res.render('chats', payload); } - const room = await messaging.loadRoom(req.uid, { uid: uid, roomId: req.params.roomid }); + const { index } = req.params; + let start = 0; + payload.scrollToIndex = null; + if (index) { + const msgCount = await db.getObjectField(`chat:room:${req.params.roomid}`, 'messageCount'); + start = Math.max(0, parseInt(msgCount, 10) - index - 49); + payload.scrollToIndex = Math.min(msgCount, Math.max(0, parseInt(index, 10) || 1)); + } + const room = await messaging.loadRoom(req.uid, { + uid: uid, + roomId: req.params.roomid, + start: start, + }); if (!room) { return next(); } @@ -72,5 +84,26 @@ chatsController.redirectToChat = async function (req, res, next) { return next(); } const roomid = parseInt(req.params.roomid, 10); - helpers.redirect(res, `/user/${userslug}/chats${roomid ? `/${roomid}` : ''}`); + const index = parseInt(req.params.index, 10); + helpers.redirect(res, `/user/${userslug}/chats${roomid ? `/${roomid}` : ''}${index ? `/${index}` : ''}`); +}; + +chatsController.redirectToMessage = async function (req, res, next) { + const mid = parseInt(req.params.mid, 10); + if (!mid) { + return next(); + } + const [userslug, roomId] = await Promise.all([ + user.getUserField(req.uid, 'userslug'), + messaging.getMessageField(mid, 'roomId'), + ]); + if (!userslug || !roomId) { + return next(); + } + const index = await db.sortedSetRank(`chat:room:${roomId}:mids`, mid); + if (!(parseInt(index, 10) >= 0)) { + return next(); + } + + helpers.redirect(res, `/user/${userslug}/chats/${roomId}${index ? `/${index + 1}` : ''}`, true); }; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index ce624ae30e..5dd86be325 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -110,7 +110,10 @@ Chats.messages.list = async (req, res) => { const uid = req.query.uid || req.uid; const { roomId } = req.params; const start = parseInt(req.query.start, 10) || 0; - const { messages } = await api.chats.listMessages(req, { uid, roomId, start }); + const direction = parseInt(req.query.direction, 10) || null; + const { messages } = await api.chats.listMessages(req, { + uid, roomId, start, direction, + }); helpers.formatApiResponse(200, res, { messages }); }; diff --git a/src/messaging/create.js b/src/messaging/create.js index f40002b801..2d7063a0bc 100644 --- a/src/messaging/create.js +++ b/src/messaging/create.js @@ -127,5 +127,6 @@ module.exports = function (Messaging) { Messaging.addMessageToRoom = async (roomId, mid, timestamp) => { await db.sortedSetAdd(`chat:room:${roomId}:mids`, timestamp, mid); + await db.incrObjectField(`chat:room:${roomId}`, 'messageCount'); }; }; diff --git a/src/messaging/index.js b/src/messaging/index.js index c773d547f5..f91367bbfc 100644 --- a/src/messaging/index.js +++ b/src/messaging/index.js @@ -43,13 +43,17 @@ Messaging.getMessages = async (params) => { if (!ok) { return; } - const mids = await getMessageIds(roomId, uid, start, stop); + const [mids, messageCount] = await Promise.all([ + getMessageIds(roomId, uid, start, stop), + db.getObjectField(`chat:room:${roomId}`, 'messageCount'), + ]); if (!mids.length) { return []; } + const count = parseInt(messageCount, 10) || 0; const indices = {}; mids.forEach((mid, index) => { - indices[mid] = start + index; + indices[mid] = count - start - index - 1; }); mids.reverse(); diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 6fad579cff..27b4e27f40 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -21,7 +21,7 @@ const roomUidCache = cacheCreate({ }); const intFields = [ - 'roomId', 'timestamp', 'userCount', + 'roomId', 'timestamp', 'userCount', 'messageCount', ]; module.exports = function (Messaging) { @@ -86,6 +86,7 @@ module.exports = function (Messaging) { roomId: roomId, timestamp: now, notificationSetting: data.notificationSetting, + messageCount: 0, }; if (data.hasOwnProperty('roomName') && data.roomName) { @@ -500,6 +501,7 @@ module.exports = function (Messaging) { Messaging.getUsersInRoomFromSet(`chat:room:${roomId}:uids:online`, roomId, 0, 39, true), Messaging.getMessages({ callerUid: uid, + start: data.start || 0, uid: data.uid || uid, roomId: roomId, isNew: false, diff --git a/src/routes/user.js b/src/routes/user.js index 3c953fa34e..49f551dc59 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -48,6 +48,8 @@ module.exports = function (app, name, middleware, controllers) { setupPageRoute(app, `/${name}/:userslug/sessions`, accountMiddlewares, controllers.accounts.sessions.get); setupPageRoute(app, '/notifications', [middleware.ensureLoggedIn], controllers.accounts.notifications.get); - setupPageRoute(app, `/${name}/:userslug/chats/:roomid?`, [middleware.exposeUid, middleware.canViewUsers], controllers.accounts.chats.get); - setupPageRoute(app, '/chats/:roomid?', [middleware.ensureLoggedIn], controllers.accounts.chats.redirectToChat); + setupPageRoute(app, `/${name}/:userslug/chats/:roomid?/:index?`, [middleware.exposeUid, middleware.canViewUsers], controllers.accounts.chats.get); + setupPageRoute(app, '/chats/:roomid?/:index?', [middleware.ensureLoggedIn], controllers.accounts.chats.redirectToChat); + + setupPageRoute(app, `/message/:mid`, [middleware.ensureLoggedIn], controllers.accounts.chats.redirectToMessage); }; diff --git a/src/upgrades/3.6.0/chat_message_counts.js b/src/upgrades/3.6.0/chat_message_counts.js new file mode 100644 index 0000000000..ab2eba82aa --- /dev/null +++ b/src/upgrades/3.6.0/chat_message_counts.js @@ -0,0 +1,20 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Set messageCount on chat rooms', + timestamp: Date.UTC(2023, 6, 27), + method: async function () { + const { progress } = this; + const allRoomIds = await db.getSortedSetRange(`chat:rooms`, 0, -1); + progress.total = allRoomIds.length; + for (const roomId of allRoomIds) { + const count = await db.sortedSetCard(`chat:room:${roomId}:mids`); + await db.setObject(`chat:room:${roomId}`, { messageCount: count }); + progress.incr(1); + } + }, +}; diff --git a/test/api.js b/test/api.js index 7abfec005e..de94973086 100644 --- a/test/api.js +++ b/test/api.js @@ -281,7 +281,7 @@ describe('API', async () => { 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(1, { uids: [2] }); + await messaging.newRoom(adminUid, { uids: [unprivUid] }); // 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 8393113851ca8640f67a501aa26ffceaa0f19175 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 31 Oct 2023 14:15:30 +0000 Subject: [PATCH 024/201] chore(i18n): fallback strings for new resources: nodebb.modules --- public/language/ar/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/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 + 46 files changed, 46 insertions(+) diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json index 68cedc6c3f..ddaace29f8 100644 --- a/public/language/ar/modules.json +++ b/public/language/ar/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index 715ec35e02..1a753d8b19 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "В тази стая", "chat.kick": "Изгонване", "chat.show-ip": "Показване на IP адреса", + "chat.copy-link": "Copy link", "chat.owner": "Собственик на стаята", "chat.grant-rescind-ownership": "Даване/отнемане на собственост", "chat.system.user-join": "%1 се присъедини към стаята ", diff --git a/public/language/bn/modules.json b/public/language/bn/modules.json index 0acbf5265d..f75974dc76 100644 --- a/public/language/bn/modules.json +++ b/public/language/bn/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json index 08bcc63390..2956e3de31 100644 --- a/public/language/cs/modules.json +++ b/public/language/cs/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "V této místnosti", "chat.kick": "Vykopnout", "chat.show-ip": "Zobrazit IP", + "chat.copy-link": "Copy link", "chat.owner": "Majitel místnosti", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/da/modules.json b/public/language/da/modules.json index 4639c9f1ab..ccc0e8f08d 100644 --- a/public/language/da/modules.json +++ b/public/language/da/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/de/modules.json b/public/language/de/modules.json index 2e88897593..207d450380 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In diesem Chat-Room", "chat.kick": "Rauswerfen", "chat.show-ip": "IP anzeigen", + "chat.copy-link": "Copy link", "chat.owner": "Raumbesitzer", "chat.grant-rescind-ownership": "Erteilung/Aufhebung des Eigentums", "chat.system.user-join": "%1 hat den Raum betreten ", diff --git a/public/language/el/modules.json b/public/language/el/modules.json index 98a6041312..0bd634c214 100644 --- a/public/language/el/modules.json +++ b/public/language/el/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/en-US/modules.json b/public/language/en-US/modules.json index 98a6041312..0bd634c214 100644 --- a/public/language/en-US/modules.json +++ b/public/language/en-US/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/en-x-pirate/modules.json b/public/language/en-x-pirate/modules.json index b2ce6a6339..96bdb6b14e 100644 --- a/public/language/en-x-pirate/modules.json +++ b/public/language/en-x-pirate/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/es/modules.json b/public/language/es/modules.json index 22ded29cc9..119aab5b91 100644 --- a/public/language/es/modules.json +++ b/public/language/es/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "En esta sala", "chat.kick": "Expulsar", "chat.show-ip": "Mostrar IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/et/modules.json b/public/language/et/modules.json index d50f5ea9c2..f8eed1d1d0 100644 --- a/public/language/et/modules.json +++ b/public/language/et/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/fa-IR/modules.json b/public/language/fa-IR/modules.json index 32fb993062..ce76938e40 100644 --- a/public/language/fa-IR/modules.json +++ b/public/language/fa-IR/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "در این چت روم", "chat.kick": "اخراج", "chat.show-ip": "نشان دادن IP", + "chat.copy-link": "Copy link", "chat.owner": "مدیر چت روم", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/fi/modules.json b/public/language/fi/modules.json index 802e6f4036..c6beaaa3cd 100644 --- a/public/language/fi/modules.json +++ b/public/language/fi/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json index e9356d7081..5e7ae37ef4 100644 --- a/public/language/fr/modules.json +++ b/public/language/fr/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Dans cet espace de discussion", "chat.kick": "Exclure", "chat.show-ip": "Voir IP", + "chat.copy-link": "Copy link", "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/gl/modules.json b/public/language/gl/modules.json index dfbbaf172e..478d2e227d 100644 --- a/public/language/gl/modules.json +++ b/public/language/gl/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/he/modules.json b/public/language/he/modules.json index 06c6e81045..88ce48a87d 100644 --- a/public/language/he/modules.json +++ b/public/language/he/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "בתוך חדר זה", "chat.kick": "הוצא", "chat.show-ip": "הצג IP", + "chat.copy-link": "Copy link", "chat.owner": "מנהלי החדר", "chat.grant-rescind-ownership": "הענק/בטל בעלות", "chat.system.user-join": "%1 הצטרף לחדר ", diff --git a/public/language/hr/modules.json b/public/language/hr/modules.json index 758078f326..535ca6816b 100644 --- a/public/language/hr/modules.json +++ b/public/language/hr/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/hu/modules.json b/public/language/hu/modules.json index 03811c99c7..6a7692cbc9 100644 --- a/public/language/hu/modules.json +++ b/public/language/hu/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Ebben a szobában", "chat.kick": "Kirúgás", "chat.show-ip": "IP cím mutatása", + "chat.copy-link": "Copy link", "chat.owner": "Szoba tulajdonos", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/hy/modules.json b/public/language/hy/modules.json index c4773cf8e6..f9706f24c5 100644 --- a/public/language/hy/modules.json +++ b/public/language/hy/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Այս սենյակում", "chat.kick": "Kick", "chat.show-ip": "Ցույց տալ IP", + "chat.copy-link": "Copy link", "chat.owner": "Սենյակի սեփականատեր", "chat.grant-rescind-ownership": "Տրամադրել/վերացնել սեփականության իրավունքը", "chat.system.user-join": "%1-ը միացել է սենյակին ", diff --git a/public/language/id/modules.json b/public/language/id/modules.json index b027ef70c1..97849e4b85 100644 --- a/public/language/id/modules.json +++ b/public/language/id/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/it/modules.json b/public/language/it/modules.json index 53f06ad8c0..0ca700cedc 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In questa stanza", "chat.kick": "Butta fuori", "chat.show-ip": "Mostra indirizzo IP", + "chat.copy-link": "Copy link", "chat.owner": "Propietario stanza", "chat.grant-rescind-ownership": "Concedi/Revoca Proprietà", "chat.system.user-join": "%1 si è unito alla stanza ", diff --git a/public/language/ja/modules.json b/public/language/ja/modules.json index 4ae805be4d..b44d722a68 100644 --- a/public/language/ja/modules.json +++ b/public/language/ja/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "この部屋内", "chat.kick": "キック", "chat.show-ip": "IP表示", + "chat.copy-link": "Copy link", "chat.owner": "部屋の管理者", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json index 44e872ddd5..ae568ff649 100644 --- a/public/language/ko/modules.json +++ b/public/language/ko/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "채팅 참여자", "chat.kick": "추방", "chat.show-ip": "IP 보이기", + "chat.copy-link": "Copy link", "chat.owner": "채팅 관리자", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json index 8d3d6d2b15..11180da6f0 100644 --- a/public/language/lt/modules.json +++ b/public/language/lt/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/lv/modules.json b/public/language/lv/modules.json index afdcec9197..66032799e1 100644 --- a/public/language/lv/modules.json +++ b/public/language/lv/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Šajā tērzētavā", "chat.kick": "Izslēgt", "chat.show-ip": "Rādīt IP adresi", + "chat.copy-link": "Copy link", "chat.owner": "Tērzētavas īpašnieks", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ms/modules.json b/public/language/ms/modules.json index 18c4865e54..bc9e268850 100644 --- a/public/language/ms/modules.json +++ b/public/language/ms/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json index e3a941f366..f565ef1348 100644 --- a/public/language/nb/modules.json +++ b/public/language/nb/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json index 2cf149930a..4254d47faf 100644 --- a/public/language/nl/modules.json +++ b/public/language/nl/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In deze chat room", "chat.kick": "Schop", "chat.show-ip": "Geef IP weer", + "chat.copy-link": "Copy link", "chat.owner": "Chatroom-eigenaar", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json index 032da38b4e..82429f519b 100644 --- a/public/language/pl/modules.json +++ b/public/language/pl/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "W tym pokoju", "chat.kick": "Wyrzuć", "chat.show-ip": "Pokaż IP", + "chat.copy-link": "Copy link", "chat.owner": "Właściciel pokoju", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/pt-BR/modules.json b/public/language/pt-BR/modules.json index 3ae0e95eae..5a9547f678 100644 --- a/public/language/pt-BR/modules.json +++ b/public/language/pt-BR/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Nesta sala", "chat.kick": "Expulsar", "chat.show-ip": "Mostrar IP", + "chat.copy-link": "Copy link", "chat.owner": "Dono da Sala", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/pt-PT/modules.json b/public/language/pt-PT/modules.json index b97f848659..fd2ed13e64 100644 --- a/public/language/pt-PT/modules.json +++ b/public/language/pt-PT/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Participantes nesta sala", "chat.kick": "Expulsar", "chat.show-ip": "Mostrar IP", + "chat.copy-link": "Copy link", "chat.owner": "Dono da Sala", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ro/modules.json b/public/language/ro/modules.json index ede5dea94f..923a63764c 100644 --- a/public/language/ro/modules.json +++ b/public/language/ro/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json index 69769d997e..c4c30f03fb 100644 --- a/public/language/ru/modules.json +++ b/public/language/ru/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "В этой комнате", "chat.kick": "Исключить", "chat.show-ip": "Показать IP", + "chat.copy-link": "Copy link", "chat.owner": "Владелец комнаты", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/rw/modules.json b/public/language/rw/modules.json index eb2599e1fd..86a2ee9757 100644 --- a/public/language/rw/modules.json +++ b/public/language/rw/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sc/modules.json b/public/language/sc/modules.json index 1675ac1fb6..622f68e00d 100644 --- a/public/language/sc/modules.json +++ b/public/language/sc/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json index 91ecfb427f..525dbf2206 100644 --- a/public/language/sk/modules.json +++ b/public/language/sk/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "V tejto miestnosti", "chat.kick": "Vykopnúť", "chat.show-ip": "Zobraziť IP adresu", + "chat.copy-link": "Copy link", "chat.owner": "Majiteľ miestnosti", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sl/modules.json b/public/language/sl/modules.json index 07fd6024d2..33cb814c8b 100644 --- a/public/language/sl/modules.json +++ b/public/language/sl/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Pokaži IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sq-AL/modules.json b/public/language/sq-AL/modules.json index 390133f050..b8eec66eb8 100644 --- a/public/language/sq-AL/modules.json +++ b/public/language/sq-AL/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Në këtë dhomë", "chat.kick": "Largo", "chat.show-ip": "Shfaq IP", + "chat.copy-link": "Copy link", "chat.owner": "Administratori i hapësirës", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sr/modules.json b/public/language/sr/modules.json index 79ddec2c79..e6de178576 100644 --- a/public/language/sr/modules.json +++ b/public/language/sr/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "У овој соби", "chat.kick": "Избаци", "chat.show-ip": "Прикажи IP", + "chat.copy-link": "Copy link", "chat.owner": "Власник собе", "chat.grant-rescind-ownership": "Додели/поништи власништво", "chat.system.user-join": "%1 се придружио соби ", diff --git a/public/language/sv/modules.json b/public/language/sv/modules.json index 0335e67f7b..3d498c4760 100644 --- a/public/language/sv/modules.json +++ b/public/language/sv/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "I detta rum", "chat.kick": "Sparka ut", "chat.show-ip": "Visa IP", + "chat.copy-link": "Copy link", "chat.owner": "Rummets ägare", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/th/modules.json b/public/language/th/modules.json index bc46f7729e..633a0efa03 100644 --- a/public/language/th/modules.json +++ b/public/language/th/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "ในห้องนี้", "chat.kick": "Kick", "chat.show-ip": "Show IP", + "chat.copy-link": "Copy link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json index 719464d5f2..7e5a72bf35 100644 --- a/public/language/tr/modules.json +++ b/public/language/tr/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Bu odada", "chat.kick": "Dışarı At", "chat.show-ip": "IP Göster", + "chat.copy-link": "Copy link", "chat.owner": "Oda Sahibi", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 odaya katıldı ", diff --git a/public/language/uk/modules.json b/public/language/uk/modules.json index b7e592222f..87be11241a 100644 --- a/public/language/uk/modules.json +++ b/public/language/uk/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "У цій кімнаті", "chat.kick": "Штурхнути", "chat.show-ip": "Показати IP", + "chat.copy-link": "Copy link", "chat.owner": "Власник кімнати", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 823ac679a7..de9ad3c35f 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "Trong phòng này", "chat.kick": "Loại ra", "chat.show-ip": "Hiện IP", + "chat.copy-link": "Copy link", "chat.owner": "Chủ Phòng", "chat.grant-rescind-ownership": "Cấp/Hủy bỏ Quyền sở hữu", "chat.system.user-join": "%1 đã tham gia phòng ", diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json index e071c0bb7b..49fbfbae6e 100644 --- a/public/language/zh-CN/modules.json +++ b/public/language/zh-CN/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "在此房间", "chat.kick": "踢出", "chat.show-ip": "显示 IP", + "chat.copy-link": "Copy link", "chat.owner": "房间所有者", "chat.grant-rescind-ownership": "给予/撤销所有权", "chat.system.user-join": "%1 加入了房间", diff --git a/public/language/zh-TW/modules.json b/public/language/zh-TW/modules.json index e1c5cb7257..486c562316 100644 --- a/public/language/zh-TW/modules.json +++ b/public/language/zh-TW/modules.json @@ -68,6 +68,7 @@ "chat.in-room": "在此房間", "chat.kick": "踢出", "chat.show-ip": "顯示 IP", + "chat.copy-link": "Copy link", "chat.owner": "房間所有者", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", From 52b78e83a89dbaa7e5e84b4a4a3c3143fa5e725e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Oct 2023 15:01:01 -0400 Subject: [PATCH 025/201] refactor(socket.io): deprecate categories.getRecentReplies in favour of api.categories.getPosts --- src/api/categories.js | 2 ++ src/controllers/write/categories.js | 5 +++++ src/routes/write/categories.js | 2 ++ src/socket.io/categories.js | 6 +++++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/api/categories.js b/src/api/categories.js index c37e287221..394db8f95d 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -61,6 +61,8 @@ categoriesAPI.delete = async function (caller, { cid }) { }); }; +categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4); + categoriesAPI.getPrivileges = async (caller, { cid }) => { await hasAdminPrivilege(caller.uid, 'privileges'); diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index d84f2bddfb..714816ef9f 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -31,6 +31,11 @@ Categories.delete = async (req, res) => { helpers.formatApiResponse(200, res); }; +Categories.getPosts = async (req, res) => { + const posts = await api.categories.getPosts(req, { ...req.params }); + helpers.formatApiResponse(200, res, posts); +}; + Categories.getPrivileges = async (req, res) => { const privilegeSet = await api.categories.getPrivileges(req, { cid: req.params.cid }); helpers.formatApiResponse(200, res, privilegeSet); diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index ed3ffd2dce..3b96556d20 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -15,6 +15,8 @@ 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/posts', [...middlewares], controllers.write.categories.getPosts); + setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 169e207b0d..1db5358354 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -4,13 +4,17 @@ const categories = require('../categories'); const privileges = require('../privileges'); const user = require('../user'); const topics = require('../topics'); +const api = require('../api'); + +const sockets = require('.'); const SocketCategories = module.exports; require('./categories/search')(SocketCategories); SocketCategories.getRecentReplies = async function (socket, cid) { - return await categories.getRecentReplies(cid, socket.uid, 0, 4); + sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid/posts'); + return await api.categories.getPosts(socket, { cid }); }; SocketCategories.get = async function (socket) { From 96046373da823ce9a57e8bdd4cb41fa69d9c5080 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Oct 2023 10:21:51 -0400 Subject: [PATCH 026/201] refactor(socket.io): deprecate categories.get in favour of api.categories.list --- src/api/categories.js | 16 ++++++++++++++++ src/controllers/write/categories.js | 4 ++++ src/routes/write/categories.js | 1 + src/socket.io/categories.js | 12 +++--------- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/api/categories.js b/src/api/categories.js index 394db8f95d..47cb0ca8f6 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -15,6 +15,22 @@ const hasAdminPrivilege = async (uid, privilege = 'categories') => { } }; +categoriesAPI.list = async (caller) => { + async function getCategories() { + const cids = await categories.getCidsByPrivilege('categories:cid', caller.uid, 'find'); + return await categories.getCategoriesData(cids); + } + + const [isAdmin, categoriesData] = await Promise.all([ + user.isAdministrator(caller.uid), + getCategories(), + ]); + + return { + categories: categoriesData.filter(category => category && (!category.disabled || isAdmin)), + }; +}; + categoriesAPI.get = async function (caller, data) { const [userPrivileges, category] = await Promise.all([ privileges.categories.get(data.cid, caller.uid), diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 714816ef9f..52408a5400 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -7,6 +7,10 @@ const helpers = require('../helpers'); const Categories = module.exports; +Categories.list = async (req, res) => { + helpers.formatApiResponse(200, res, await api.categories.list(req)); +}; + Categories.get = async (req, res) => { helpers.formatApiResponse(200, res, await api.categories.get(req, req.params)); }; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 3b96556d20..35957b440b 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -10,6 +10,7 @@ const { setupApiRoute } = routeHelpers; module.exports = function () { const middlewares = [middleware.ensureLoggedIn]; + setupApiRoute(router, 'get', '/', [...middlewares], 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); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 1db5358354..8168176656 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -18,15 +18,9 @@ SocketCategories.getRecentReplies = async function (socket, cid) { }; SocketCategories.get = async function (socket) { - async function getCategories() { - const cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'find'); - return await categories.getCategoriesData(cids); - } - const [isAdmin, categoriesData] = await Promise.all([ - user.isAdministrator(socket.uid), - getCategories(), - ]); - return categoriesData.filter(category => category && (!category.disabled || isAdmin)); + sockets.warnDeprecated(socket, 'GET /api/v3/categories'); + const { categories } = await api.categories.list(socket); + return categories; }; SocketCategories.getWatchedCategories = async function (socket) { From c442b6e662a875ca26f88e8c252ee38c5b1f700d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Oct 2023 11:26:00 -0400 Subject: [PATCH 027/201] refactor(socket.io): deprecate categories.getTopicCount in favour of api.categories.getTopicCount --- public/src/admin/manage/category.js | 32 ++++++++++++++++++----------- public/src/client/category.js | 14 +++++-------- public/src/sockets.js | 4 ++-- src/api/categories.js | 5 +++++ src/controllers/write/categories.js | 4 ++++ src/routes/write/categories.js | 1 + src/socket.io/categories.js | 6 +++++- src/socket.io/index.js | 6 +++++- 8 files changed, 47 insertions(+), 25 deletions(-) diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 9bbc9c47df..b6255fa251 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -109,11 +109,14 @@ define('admin/manage/category', [ callback: function () { modal.find('.modal-footer button').prop('disabled', true); - const intervalId = setInterval(function () { - socket.emit('categories.getTopicCount', ajaxify.data.category.cid, function (err, count) { - if (err) { - return alerts.error(err); - } + const intervalId = setInterval(async () => { + if (!ajaxify.data.category) { + // Already navigated away + return; + } + + try { + const { count } = await api.get(`/categories/${ajaxify.data.category.cid}/count`); let percent = 0; if (ajaxify.data.category.topic_count > 0) { @@ -121,16 +124,21 @@ define('admin/manage/category', [ } modal.find('.progress-bar').css({ width: percent + '%' }); - }); + } catch (err) { + clearInterval(intervalId); + alerts.error(err); + } }, 1000); api.del('/categories/' + ajaxify.data.category.cid).then(() => { - if (intervalId) { - clearInterval(intervalId); - } - modal.modal('hide'); - alerts.success('[[admin/manage/categories:alert.purge-success]]'); - ajaxify.go('admin/manage/categories'); + setTimeout(() => { + if (intervalId) { + clearInterval(intervalId); + } + modal.modal('hide'); + alerts.success('[[admin/manage/categories:alert.purge-success]]'); + ajaxify.go('admin/manage/categories'); + }, 5000); }).catch(alerts.error); return false; diff --git a/public/src/client/category.js b/public/src/client/category.js index a69ef64616..a89620cb34 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -9,7 +9,8 @@ define('forum/category', [ 'categorySelector', 'hooks', 'alerts', -], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts) { + 'api', +], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts, api) { const Category = {}; $(window).on('action:ajaxify.start', function (ev, data) { @@ -118,14 +119,9 @@ define('forum/category', [ navigator.scrollTop(0); }; - Category.toBottom = function () { - socket.emit('categories.getTopicCount', ajaxify.data.cid, function (err, count) { - if (err) { - return alerts.error(err); - } - - navigator.scrollBottom(count - 1); - }); + Category.toBottom = async () => { + const { count } = await api.get(`/categories/${ajaxify.data.category.cid}/count`); + navigator.scrollBottom(count - 1); }; function loadTopicsAfter(after, direction, callback) { diff --git a/public/src/sockets.js b/public/src/sockets.js index 1a87e57646..e4ef8273e1 100644 --- a/public/src/sockets.js +++ b/public/src/sockets.js @@ -111,8 +111,8 @@ app = window.app || {}; alerts.alert(params); }); }); - socket.on('event:deprecated_call', function (data) { - console.warn('[socket.io] ', data.eventName, 'is now deprecated in favour of', data.replacement); + socket.on('event:deprecated_call', (data) => { + console.warn('[socket.io]', data.eventName, 'is now deprecated', data.replacement ? `in favour of ${data.replacement}` : 'with no alternative planned.'); }); socket.on('event:livereload', function () { diff --git a/src/api/categories.js b/src/api/categories.js index 47cb0ca8f6..28b2539a3a 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -77,6 +77,11 @@ categoriesAPI.delete = async function (caller, { cid }) { }); }; +categoriesAPI.getTopicCount = async (caller, { cid }) => { + const count = await categories.getCategoryField(cid, 'topic_count'); + return { count }; +}; + categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4); categoriesAPI.getPrivileges = async (caller, { cid }) => { diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 52408a5400..8d4c0aebcf 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -35,6 +35,10 @@ Categories.delete = async (req, res) => { helpers.formatApiResponse(200, res); }; +Categories.getTopicCount = async (req, res) => { + helpers.formatApiResponse(200, res, await api.categories.getTopicCount(req, { ...req.params })); +}; + Categories.getPosts = async (req, res) => { const posts = await api.categories.getPosts(req, { ...req.params }); helpers.formatApiResponse(200, res, posts); diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 35957b440b..44a3020863 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -16,6 +16,7 @@ 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], controllers.write.categories.getTopicCount); setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 8168176656..126bf5e7d6 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -24,6 +24,8 @@ SocketCategories.get = async function (socket) { }; SocketCategories.getWatchedCategories = async function (socket) { + sockets.warnDeprecated(socket); + const [categoriesData, ignoredCids] = await Promise.all([ categories.getCategoriesByPrivilege('cid:0:children', socket.uid, 'find'), user.getIgnoredCategories(socket.uid), @@ -81,7 +83,9 @@ SocketCategories.loadMore = async function (socket, data) { }; SocketCategories.getTopicCount = async function (socket, cid) { - return await categories.getCategoryField(cid, 'topic_count'); + sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid'); + const { count } = await api.categories.getTopicCount(socket, { cid }); + return count; }; SocketCategories.getCategoriesByPrivilege = async function (socket, privilege) { diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 8f03eb2a9d..c10f271585 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -333,5 +333,9 @@ Sockets.warnDeprecated = (socket, replacement) => { replacement: replacement, }); } - winston.warn(`[deprecated]\n ${new Error('-').stack.split('\n').slice(2, 5).join('\n')}\n use ${replacement}`); + winston.warn([ + '[deprecated]', + `${new Error('-').stack.split('\n').slice(2, 5).join('\n')}`, + ` ${replacement ? `use ${replacement}` : 'there is no replacement for this call.'}`, + ].join('\n')); }; From f1dbfaa2834f31218c9f95a01669baddf7179f3b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Oct 2023 11:47:10 -0400 Subject: [PATCH 028/201] chore(socket.io): deprecate categories.(isModerator|ignore|watch|getSelectCategories|getMoveCategories|getCategoriesByPrivilege) --- src/socket.io/categories.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 126bf5e7d6..aea08cdfa2 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -84,19 +84,26 @@ SocketCategories.loadMore = async function (socket, data) { SocketCategories.getTopicCount = async function (socket, cid) { sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid'); + const { count } = await api.categories.getTopicCount(socket, { cid }); return count; }; SocketCategories.getCategoriesByPrivilege = async function (socket, privilege) { + sockets.warnDeprecated(socket); + return await categories.getCategoriesByPrivilege('categories:cid', socket.uid, privilege); }; SocketCategories.getMoveCategories = async function (socket, data) { + sockets.warnDeprecated(socket); + return await SocketCategories.getSelectCategories(socket, data); }; SocketCategories.getSelectCategories = async function (socket) { + sockets.warnDeprecated(socket); + const [isAdmin, categoriesData] = await Promise.all([ user.isAdministrator(socket.uid), categories.buildForSelect(socket.uid, 'find', ['disabled', 'link']), @@ -114,10 +121,14 @@ SocketCategories.setWatchState = async function (socket, data) { }; SocketCategories.watch = async function (socket, data) { + sockets.warnDeprecated(socket); + return await ignoreOrWatch(user.watchCategory, socket, data); }; SocketCategories.ignore = async function (socket, data) { + sockets.warnDeprecated(socket); + return await ignoreOrWatch(user.ignoreCategory, socket, data); }; @@ -146,6 +157,8 @@ async function ignoreOrWatch(fn, socket, data) { } SocketCategories.isModerator = async function (socket, cid) { + sockets.warnDeprecated(socket); + return await user.isModerator(socket.uid, cid); }; From d7c6b3d60e684b6e06801e5b248d74cd7fab5e4e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 23 Oct 2023 12:11:34 -0400 Subject: [PATCH 029/201] refactor(socket.io): deprecate categories.setWatchState in favour of api.categories.setWatchState --- public/src/client/account/categories.js | 36 +++++++++++-------------- public/src/client/category.js | 2 +- src/api/categories.js | 26 ++++++++++++++++++ src/controllers/write/categories.js | 19 +++++++++++++ src/middleware/assert.js | 9 +++++++ src/routes/write/categories.js | 3 +++ src/socket.io/categories.js | 10 ++++--- 7 files changed, 81 insertions(+), 24 deletions(-) diff --git a/public/src/client/account/categories.js b/public/src/client/account/categories.js index 8e162db809..bb6849b166 100644 --- a/public/src/client/account/categories.js +++ b/public/src/client/account/categories.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/account/categories', ['forum/account/header', 'alerts'], function (header, alerts) { +define('forum/account/categories', ['forum/account/header', 'alerts', 'api'], function (header, alerts, api) { const Categories = {}; Categories.init = function () { @@ -11,36 +11,32 @@ define('forum/account/categories', ['forum/account/header', 'alerts'], function handleIgnoreWatch(category.cid); }); - $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { + $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => { const cids = []; - const state = $(this).attr('data-state'); + const state = e.currentTarget.getAttribute('data-state'); + const { uid } = ajaxify.data; $('[data-parent-cid="0"]').each(function (index, el) { cids.push($(el).attr('data-cid')); }); - socket.emit('categories.setWatchState', { cid: cids, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) { - if (err) { - return alerts.error(err); - } - updateDropdowns(modified_cids, state); - }); + let modified_cids = await Promise.all(cids.map(async cid => api.put(`/categories/${cid}/watch`, { state, uid }))); + modified_cids = modified_cids + .reduce((memo, cur) => memo.concat(cur.modified), []) + .filter((cid, idx, arr) => arr.indexOf(cid) === idx); + + updateDropdowns(modified_cids, state); }); }; function handleIgnoreWatch(cid) { const category = $('[data-cid="' + cid + '"]'); - category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { - const $this = $(this); - const state = $this.attr('data-state'); + category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => { + const state = e.currentTarget.getAttribute('data-state'); + const { uid } = ajaxify.data; - socket.emit('categories.setWatchState', { cid: cid, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) { - if (err) { - return alerts.error(err); - } - updateDropdowns(modified_cids, state); - - alerts.success('[[category:' + state + '.message]]'); - }); + const { modified } = await api.put(`/categories/${cid}/watch`, { state, uid }); + updateDropdowns(modified, state); + alerts.success('[[category:' + state + '.message]]'); }); } diff --git a/public/src/client/category.js b/public/src/client/category.js index a89620cb34..3aff45420e 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -69,7 +69,7 @@ define('forum/category', [ const $this = $(this); const state = $this.attr('data-state'); - socket.emit('categories.setWatchState', { cid: cid, state: state }, function (err) { + api.put(`/categories/${cid}/watch`, { state }, (err) => { if (err) { return alerts.error(err); } diff --git a/src/api/categories.js b/src/api/categories.js index 28b2539a3a..442131b4fd 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -1,6 +1,7 @@ 'use strict'; const categories = require('../categories'); +const topics = require('../topics'); const events = require('../events'); const user = require('../user'); const groups = require('../groups'); @@ -84,6 +85,31 @@ categoriesAPI.getTopicCount = async (caller, { cid }) => { categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4); +categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => { + let targetUid = caller.uid; + const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)]; + if (uid) { + targetUid = uid; + } + await user.isAdminOrGlobalModOrSelf(caller.uid, targetUid); + const allCids = await categories.getAllCidsFromSet('categories:cid'); + const categoryData = await categories.getCategoriesFields(allCids, ['cid', 'parentCid']); + + // filter to subcategories of cid + let cat; + do { + cat = categoryData.find(c => !cids.includes(c.cid) && cids.includes(c.parentCid)); + if (cat) { + cids.push(cat.cid); + } + } while (cat); + + await user.setCategoryWatchState(targetUid, cids, state); + await topics.pushUnreadCount(targetUid); + + return { cids }; +}; + categoriesAPI.getPrivileges = async (caller, { cid }) => { await hasAdminPrivilege(caller.uid, 'privileges'); diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 8d4c0aebcf..e987b433ce 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -1,6 +1,7 @@ 'use strict'; const categories = require('../../categories'); +const meta = require('../../meta'); const api = require('../../api'); const helpers = require('../helpers'); @@ -44,6 +45,24 @@ Categories.getPosts = async (req, res) => { helpers.formatApiResponse(200, res, posts); }; +Categories.setWatchState = async (req, res) => { + const { cid } = req.params; + let { uid, state } = req.body; + + if (req.method === 'DELETE') { + // DELETE is always setting state to system default in acp + state = categories.watchStates[meta.config.categoryWatchState]; + } else if (Object.keys(categories.watchStates).includes(state)) { + state = categories.watchStates[state]; // convert to integer for backend processing + } else { + throw new Error('[[error:invalid-data]]'); + } + + const { cids: modified } = await api.categories.setWatchState(req, { cid, state, uid }); + + helpers.formatApiResponse(200, res, { modified }); +}; + Categories.getPrivileges = async (req, res) => { const privilegeSet = await api.categories.getPrivileges(req, { cid: req.params.cid }); helpers.formatApiResponse(200, res, privilegeSet); diff --git a/src/middleware/assert.js b/src/middleware/assert.js index 553114f870..6c0f5ef72f 100644 --- a/src/middleware/assert.js +++ b/src/middleware/assert.js @@ -11,6 +11,7 @@ const nconf = require('nconf'); const file = require('../file'); const user = require('../user'); const groups = require('../groups'); +const categories = require('../categories'); const topics = require('../topics'); const posts = require('../posts'); const messaging = require('../messaging'); @@ -39,6 +40,14 @@ Assert.group = helpers.try(async (req, res, next) => { next(); }); +Assert.category = helpers.try(async (req, res, next) => { + if (!await categories.exists(req.params.cid)) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-category]]')); + } + + next(); +}); + Assert.topic = helpers.try(async (req, res, next) => { if (!await topics.exists(req.params.tid)) { return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-topic]]')); diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 44a3020863..f1e2f39504 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -19,6 +19,9 @@ module.exports = function () { setupApiRoute(router, 'get', '/:cid/count', [...middlewares], controllers.write.categories.getTopicCount); setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); + 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); + setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index aea08cdfa2..e2b647f71a 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -112,12 +112,16 @@ SocketCategories.getSelectCategories = async function (socket) { }; SocketCategories.setWatchState = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT/DELETE /api/v3/categories/:cid/watch'); + if (!data || !data.cid || !data.state) { throw new Error('[[error:invalid-data]]'); } - return await ignoreOrWatch(async (uid, cids) => { - await user.setCategoryWatchState(uid, cids, categories.watchStates[data.state]); - }, socket, data); + + data.state = categories.watchStates[data.state]; + + await api.categories.setWatchState(socket, data); + return data.cid; }; SocketCategories.watch = async function (socket, data) { From 010727f5cb8f9bc170bacd221d01111e19ec2a85 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 23 Oct 2023 15:08:52 -0400 Subject: [PATCH 030/201] refactor(socket.io): deprecate categories.loadMoreSubCategories in favour of api.categories.getChildren --- public/src/client/category.js | 34 ++++++++++++----------------- src/api/categories.js | 21 ++++++++++++++++++ src/controllers/write/categories.js | 6 +++++ src/routes/write/categories.js | 5 +++-- src/socket.io/categories.js | 16 +++++--------- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/public/src/client/category.js b/public/src/client/category.js index 3aff45420e..b14a80d0c9 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -89,28 +89,22 @@ define('forum/category', [ } function handleLoadMoreSubcategories() { - $('[component="category/load-more-subcategories"]').on('click', function () { + $('[component="category/load-more-subcategories"]').on('click', async function () { const btn = $(this); - socket.emit('categories.loadMoreSubCategories', { - cid: ajaxify.data.cid, - start: ajaxify.data.nextSubCategoryStart, - }, function (err, data) { - if (err) { - return alerts.error(err); - } - btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage); - if (!data.length) { - return; - } - app.parseAndTranslate('category', 'children', { children: data }, function (html) { - html.find('.timeago').timeago(); - $('[component="category/subcategory/container"]').append(html); - ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage; - ajaxify.data.subCategoriesLeft -= data.length; - btn.toggleClass('hidden', ajaxify.data.subCategoriesLeft <= 0) - .translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]'); - }); + const { categories: data } = await api.get(`/categories/${ajaxify.data.cid}/children?start=${ajaxify.data.nextSubCategoryStart}`); + btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage); + if (!data.length) { + return; + } + app.parseAndTranslate('category', 'children', { children: data }, function (html) { + html.find('.timeago').timeago(); + $('[component="category/subcategory/container"]').append(html); + ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage; + ajaxify.data.subCategoriesLeft -= data.length; + btn.toggleClass('hidden', ajaxify.data.subCategoriesLeft <= 0) + .translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]'); }); + return false; }); } diff --git a/src/api/categories.js b/src/api/categories.js index 442131b4fd..7cb7a17f69 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -85,6 +85,27 @@ categoriesAPI.getTopicCount = async (caller, { cid }) => { categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4); +categoriesAPI.getChildren = async (caller, { cid, start }) => { + if (!start || start < 0) { + start = 0; + } + start = parseInt(start, 10); + + const allowed = await privileges.categories.can('read', cid, caller.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + + const category = await categories.getCategoryData(cid); + await categories.getChildrenTree(category, caller.uid); + const allCategories = []; + categories.flattenCategories(allCategories, category.children); + await categories.getRecentTopicReplies(allCategories, caller.uid); + + const payload = category.children.slice(start, start + category.subCategoriesPerPage); + return { categories: payload }; +}; + categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => { let targetUid = caller.uid; const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)]; diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index e987b433ce..e068f06305 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -45,6 +45,12 @@ Categories.getPosts = async (req, res) => { helpers.formatApiResponse(200, res, posts); }; +Categories.getChildren = async (req, res) => { + const { cid } = req.params; + const { start } = req.query; + helpers.formatApiResponse(200, res, await api.categories.getChildren(req, { cid, start })); +}; + Categories.setWatchState = async (req, res) => { const { cid } = req.params; let { uid, state } = req.body; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index f1e2f39504..9121ee9a7f 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -16,8 +16,9 @@ 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], controllers.write.categories.getTopicCount); - setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); + 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, 'put', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); setupApiRoute(router, 'delete', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index e2b647f71a..a62d55edfe 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -167,20 +167,14 @@ SocketCategories.isModerator = async function (socket, cid) { }; SocketCategories.loadMoreSubCategories = async function (socket, data) { + sockets.warnDeprecated(socket, `GET /api/v3/categories/:cid/children`); + if (!data || !data.cid || !(parseInt(data.start, 10) >= 0)) { throw new Error('[[error:invalid-data]]'); } - const allowed = await privileges.categories.can('read', data.cid, socket.uid); - if (!allowed) { - throw new Error('[[error:no-privileges]]'); - } - const category = await categories.getCategoryData(data.cid); - await categories.getChildrenTree(category, socket.uid); - const allCategories = []; - categories.flattenCategories(allCategories, category.children); - await categories.getRecentTopicReplies(allCategories, socket.uid); - const start = parseInt(data.start, 10); - return category.children.slice(start, start + category.subCategoriesPerPage); + + const { categories: children } = await api.categories.getChildren(socket, data); + return children; }; require('../promisify')(SocketCategories); From f279bca0382d1ee2c1189492b029fba4e4fba932 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 24 Oct 2023 13:54:19 -0400 Subject: [PATCH 031/201] docs(socket.io): added schema for new routes --- public/openapi/write.yaml | 4 +++ public/openapi/write/categories.yaml | 22 +++++++++++++++ .../openapi/write/categories/cid/count.yaml | 28 +++++++++++++++++++ .../openapi/write/categories/cid/posts.yaml | 28 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 public/openapi/write/categories/cid/count.yaml create mode 100644 public/openapi/write/categories/cid/posts.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index b1dabb778b..e4723397b4 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -112,6 +112,10 @@ paths: $ref: 'write/categories.yaml' /categories/{cid}: $ref: 'write/categories/cid.yaml' + /categories/{cid}/count: + $ref: 'write/categories/cid/count.yaml' + /categories/{cid}/posts: + $ref: 'write/categories/cid/posts.yaml' /categories/{cid}/privileges: $ref: 'write/categories/cid/privileges.yaml' /categories/{cid}/privileges/{privilege}: diff --git a/public/openapi/write/categories.yaml b/public/openapi/write/categories.yaml index 5c26b53633..9c08994759 100644 --- a/public/openapi/write/categories.yaml +++ b/public/openapi/write/categories.yaml @@ -1,3 +1,25 @@ +get: + tags: + - categories + summary: list categories + description: This operation returns a flat list of categories available to the calling user + responses: + '200': + description: categories successfully listed + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../components/schemas/Status.yaml#/Status + response: + type: object + properties: + categories: + type: array + items: + $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject post: tags: - categories diff --git a/public/openapi/write/categories/cid/count.yaml b/public/openapi/write/categories/cid/count.yaml new file mode 100644 index 0000000000..886152add9 --- /dev/null +++ b/public/openapi/write/categories/cid/count.yaml @@ -0,0 +1,28 @@ +get: + tags: + - categories + summary: get topic count + description: This operation returns the count of topics in a given category (excluding its subcategories) + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + responses: + '200': + description: categories count successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + count: + type: number \ No newline at end of file diff --git a/public/openapi/write/categories/cid/posts.yaml b/public/openapi/write/categories/cid/posts.yaml new file mode 100644 index 0000000000..0e46ef67a8 --- /dev/null +++ b/public/openapi/write/categories/cid/posts.yaml @@ -0,0 +1,28 @@ +get: + tags: + - categories + summary: get topic posts + description: This operation returns a list of posts in the category, across all topics in that category (excluding its subcategories) + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + responses: + '200': + description: categories posts successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + posts: + $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject \ No newline at end of file From 54000aabf5d397cba9f5500e4869d637ae8e03a6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 24 Oct 2023 13:55:00 -0400 Subject: [PATCH 032/201] fix(socket.io): update getPosts controller to return object containing posts instead of straight array --- src/controllers/write/categories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index e068f06305..00b90d47dd 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -42,7 +42,7 @@ Categories.getTopicCount = async (req, res) => { Categories.getPosts = async (req, res) => { const posts = await api.categories.getPosts(req, { ...req.params }); - helpers.formatApiResponse(200, res, posts); + helpers.formatApiResponse(200, res, { posts }); }; Categories.getChildren = async (req, res) => { From 5399e86af125cd0d255c3eed39b3c1776428e142 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 24 Oct 2023 14:19:55 -0400 Subject: [PATCH 033/201] docs(socket.io): openapi schema for remaining added routes --- public/openapi/write.yaml | 4 + .../write/categories/cid/children.yaml | 36 +++++++ .../openapi/write/categories/cid/watch.yaml | 102 ++++++++++++++++++ src/controllers/write/categories.js | 1 + 4 files changed, 143 insertions(+) create mode 100644 public/openapi/write/categories/cid/children.yaml create mode 100644 public/openapi/write/categories/cid/watch.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index e4723397b4..f62d1faca0 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -116,6 +116,10 @@ paths: $ref: 'write/categories/cid/count.yaml' /categories/{cid}/posts: $ref: 'write/categories/cid/posts.yaml' + /categories/{cid}/children: + $ref: 'write/categories/cid/children.yaml' + /categories/{cid}/watch: + $ref: 'write/categories/cid/watch.yaml' /categories/{cid}/privileges: $ref: 'write/categories/cid/privileges.yaml' /categories/{cid}/privileges/{privilege}: diff --git a/public/openapi/write/categories/cid/children.yaml b/public/openapi/write/categories/cid/children.yaml new file mode 100644 index 0000000000..de65fa1449 --- /dev/null +++ b/public/openapi/write/categories/cid/children.yaml @@ -0,0 +1,36 @@ +get: + tags: + - categories + summary: get subcategories + description: | + This operation returns the requested category's children (aka subcategories). + + It is important to note that the number of subcategories returned is dependent on the configured value for that category. + If a lower number is specified than there are children, then the list will be truncated to that number. + + This is defined by the `subCategoriesPerPage` key in the category's hash. + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + responses: + '200': + description: categories count successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + categories: + type: array + items: + $ref: ../../../components/schemas/CategoryObject.yaml#/CategoryObject \ No newline at end of file diff --git a/public/openapi/write/categories/cid/watch.yaml b/public/openapi/write/categories/cid/watch.yaml new file mode 100644 index 0000000000..06fc399dbe --- /dev/null +++ b/public/openapi/write/categories/cid/watch.yaml @@ -0,0 +1,102 @@ +put: + tags: + - categories + summary: update watch state + description: | + This operation changes the watch state for the category. + + Note that a category can be watched, not watched, or ignored: + + * A category that is watched will have topics that show up in both `/unread` and `/recent` + * A category that is *not* watched will have topics that show up in `/recent` but not `/unread` + * A category that is ignored will not have topics that show up in either route. + + This API call does not pertain to notifications for new topics in categories. + That behaviour is handled by a third-party plugin — nodebb-plugin-category-notifications + + N.B. When a category's watch state is updated, all of that category's children also have their watch states updated. + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + uid: + type: number + description: This value is optional, it allows privileged uids to use this call to affect other user accounts. + example: 1 + state: + type: string + enum: ['watching', 'notwatching', 'ignoring'] + example: 'watching' + responses: + '200': + description: categories watch state successfully updated + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + modified: + type: array + description: A list of cids that have had their watch states modified. + items: + type: number +delete: + tags: + - categories + summary: update watch state + description: | + Like the corresponding `PUT` method, this operation changes the watch state for the category. + However, it does not take a `state` parameter. It is assumed to be whatever the system default is (`categoryWatchState`). + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + uid: + type: number + description: This value is optional, it allows privileged uids to use this call to affect other user accounts. + example: 1 + responses: + '200': + description: categories watch state successfully updated + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + modified: + type: array + description: A list of cids that have had their watch states modified. + items: + type: number \ No newline at end of file diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 00b90d47dd..133ec2c055 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -61,6 +61,7 @@ Categories.setWatchState = async (req, res) => { } else if (Object.keys(categories.watchStates).includes(state)) { state = categories.watchStates[state]; // convert to integer for backend processing } else { + console.log('throwing', cid, uid, state); throw new Error('[[error:invalid-data]]'); } From 1ce4ca54da1cc44885f5777fee26307ef1d4a592 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 26 Oct 2023 16:51:46 -0400 Subject: [PATCH 034/201] refactor(socket.io): deprecate categories.loadMore in favour of api.categories.getTopics --- public/openapi/write.yaml | 2 + .../openapi/write/categories/cid/topics.yaml | 75 +++++++++++++++++++ public/src/client/category.js | 2 +- public/src/client/infinitescroll.js | 6 +- src/api/categories.js | 42 +++++++++++ src/controllers/write/categories.js | 7 ++ src/routes/write/categories.js | 1 + src/socket.io/categories.js | 39 +--------- 8 files changed, 135 insertions(+), 39 deletions(-) create mode 100644 public/openapi/write/categories/cid/topics.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index f62d1faca0..63c52430b8 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -118,6 +118,8 @@ paths: $ref: 'write/categories/cid/posts.yaml' /categories/{cid}/children: $ref: 'write/categories/cid/children.yaml' + /categories/{cid}/topics: + $ref: 'write/categories/cid/topics.yaml' /categories/{cid}/watch: $ref: 'write/categories/cid/watch.yaml' /categories/{cid}/privileges: diff --git a/public/openapi/write/categories/cid/topics.yaml b/public/openapi/write/categories/cid/topics.yaml new file mode 100644 index 0000000000..a14664b20c --- /dev/null +++ b/public/openapi/write/categories/cid/topics.yaml @@ -0,0 +1,75 @@ +get: + tags: + - categories + summary: get topics + description: | + This operation returns a set of topics in the requested category. + + The number of topics returned is defined by the "Topics per Page" (`topicsPerPage`) setting under ACP > Settings > Pagination. + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + - in: query + name: 'query' + schema: + type: string + required: false + description: Likely unused — a URI-encoded JSON string containing values that are passed to `getCategoryTopics`. + example: '' + - in: query + name: 'after' + schema: + type: string + required: false + description: The index to start at when querying for the next set of topics. This parameter would be more aptly named `start`. + example: '0' + - in: query + name: 'sort' + schema: + type: string + required: false + description: Likely deprecated — the sorting method of topics (use `categoryTopicSort` instead.) + example: '' + - in: query + name: 'categoryTopicSort' + schema: + type: string + required: false + description: The sorting method of topics + example: 'newest_to_oldest' + - in: query + name: 'direction' + schema: + type: string + required: false + description: The sorting of returned results (if you scroll up you want the topics reversed). Set to "-1" for reversed results. + example: '1' + responses: + '200': + description: categories topics successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + topics: + type: array + items: + $ref: ../../../components/schemas/TopicObject.yaml#/TopicObject + nextStart: + type: number + privileges: + type: object + additionalProperties: + type: boolean + description: A set of privileges with either true or false \ No newline at end of file diff --git a/public/src/client/category.js b/public/src/client/category.js index b14a80d0c9..e1ab97431f 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -123,7 +123,7 @@ define('forum/category', [ hooks.fire('action:topics.loading'); const params = utils.params(); - infinitescroll.loadMore('categories.loadMore', { + infinitescroll.loadMore(`/categories/${ajaxify.data.cid}/topics`, { cid: ajaxify.data.cid, after: after, direction: direction, diff --git a/public/src/client/infinitescroll.js b/public/src/client/infinitescroll.js index bd6f98d178..838f164f32 100644 --- a/public/src/client/infinitescroll.js +++ b/public/src/client/infinitescroll.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/infinitescroll', ['hooks', 'alerts'], function (hooks, alerts) { +define('forum/infinitescroll', ['hooks', 'alerts', 'api'], function (hooks, alerts, api) { const scroll = {}; let callback; let previousScrollTop = 0; @@ -72,7 +72,9 @@ define('forum/infinitescroll', ['hooks', 'alerts'], function (hooks, alerts) { const hookData = { method: method, data: data }; hooks.fire('action:infinitescroll.loadmore', hookData); - socket.emit(hookData.method, hookData.data, function (err, data) { + const call = hookData.method.startsWith('/') ? api.get : socket.emit; + + call(hookData.method, hookData.data, function (err, data) { if (err) { loadingMore = false; return alerts.error(err); diff --git a/src/api/categories.js b/src/api/categories.js index 7cb7a17f69..774091fd61 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -1,5 +1,6 @@ 'use strict'; +const meta = require('../meta'); const categories = require('../categories'); const topics = require('../topics'); const events = require('../events'); @@ -106,6 +107,47 @@ categoriesAPI.getChildren = async (caller, { cid, start }) => { return { categories: payload }; }; +categoriesAPI.getTopics = async (caller, data) => { + data.query = data.query || {}; + const [userPrivileges, settings, targetUid] = await Promise.all([ + privileges.categories.get(data.cid, caller.uid), + user.getSettings(caller.uid), + user.getUidByUserslug(data.query.author), + ]); + + if (!userPrivileges.read) { + throw new Error('[[error:no-privileges]]'); + } + + const infScrollTopicsPerPage = 20; + const sort = data.sort || data.categoryTopicSort || meta.config.categoryTopicSort || 'newest_to_oldest'; + + let start = Math.max(0, parseInt(data.after || 0, 10)); + + if (data.direction === -1) { + start -= infScrollTopicsPerPage; + } + + let stop = start + infScrollTopicsPerPage - 1; + + start = Math.max(0, start); + stop = Math.max(0, stop); + const result = await categories.getCategoryTopics({ + uid: caller.uid, + cid: data.cid, + start, + stop, + sort, + settings, + query: data.query, + tag: data.query.tag, + targetUid, + }); + categories.modifyTopicsByPrivilege(result.topics, userPrivileges); + + return { ...result, privileges: userPrivileges }; +}; + categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => { let targetUid = caller.uid; const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)]; diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 133ec2c055..80ee961fbf 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -51,6 +51,13 @@ Categories.getChildren = async (req, res) => { helpers.formatApiResponse(200, res, await api.categories.getChildren(req, { cid, start })); }; +Categories.getTopics = async (req, res) => { + const { cid } = req.params; + const result = await api.categories.getTopics(req, { ...req.query, cid }); + + helpers.formatApiResponse(200, res, result); +}; + Categories.setWatchState = async (req, res) => { const { cid } = req.params; let { uid, state } = req.body; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 9121ee9a7f..ca149a54da 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -19,6 +19,7 @@ module.exports = function () { 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, 'put', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); setupApiRoute(router, 'delete', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index a62d55edfe..934defaeac 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -1,7 +1,6 @@ 'use strict'; const categories = require('../categories'); -const privileges = require('../privileges'); const user = require('../user'); const topics = require('../topics'); const api = require('../api'); @@ -38,47 +37,15 @@ SocketCategories.loadMore = async function (socket, data) { throw new Error('[[error:invalid-data]]'); } data.query = data.query || {}; - const [userPrivileges, settings, targetUid] = await Promise.all([ - privileges.categories.get(data.cid, socket.uid), - user.getSettings(socket.uid), - user.getUidByUserslug(data.query.author), - ]); - if (!userPrivileges.read) { - throw new Error('[[error:no-privileges]]'); - } + const result = await api.categories.getTopics(socket, data); - const infScrollTopicsPerPage = 20; - const sort = data.sort || data.categoryTopicSort; - - let start = Math.max(0, parseInt(data.after, 10)); - - if (data.direction === -1) { - start -= infScrollTopicsPerPage; - } - - let stop = start + infScrollTopicsPerPage - 1; - - start = Math.max(0, start); - stop = Math.max(0, stop); - const result = await categories.getCategoryTopics({ - uid: socket.uid, - cid: data.cid, - start: start, - stop: stop, - sort: sort, - settings: settings, - query: data.query, - tag: data.query.tag, - targetUid: targetUid, - }); - categories.modifyTopicsByPrivilege(result.topics, userPrivileges); - - result.privileges = userPrivileges; + // Backwards compatibility — unsure of current usage. result.template = { category: true, name: 'category', }; + return result; }; From 00de9d5b077812810563dd71ea93d1a871dca1f9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 27 Oct 2023 15:19:49 -0400 Subject: [PATCH 035/201] refactor(socket.io): deprecate categories.categorySearch in favour of api.search.categories THIS IS WIP --- src/api/index.js | 1 + src/api/search.js | 104 +++++++++++++++++++++++++++++ src/controllers/write/index.js | 1 + src/controllers/write/search.js | 10 +++ src/routes/write/index.js | 1 + src/routes/write/search.js | 19 ++++++ src/socket.io/categories/search.js | 98 ++------------------------- 7 files changed, 141 insertions(+), 93 deletions(-) create mode 100644 src/api/search.js create mode 100644 src/controllers/write/search.js create mode 100644 src/routes/write/search.js diff --git a/src/api/index.js b/src/api/index.js index 9e5446c325..c454de93a5 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -9,6 +9,7 @@ module.exports = { posts: require('./posts'), chats: require('./chats'), categories: require('./categories'), + search: require('./search'), flags: require('./flags'), files: require('./files'), utils: require('./utils'), diff --git a/src/api/search.js b/src/api/search.js new file mode 100644 index 0000000000..8dad8eb44e --- /dev/null +++ b/src/api/search.js @@ -0,0 +1,104 @@ +'use strict'; + +const _ = require('lodash'); + +const categories = require('../categories'); +const privileges = require('../privileges'); +const meta = require('../meta'); +const plugins = require('../plugins'); + +const controllersHelpers = require('../controllers/helpers'); + +const searchApi = module.exports; + +searchApi.categories = async (caller, data) => { + // used by categorySearch module + + let cids = []; + let matchedCids = []; + const privilege = data.privilege || 'topics:read'; + data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map( + state => categories.watchStates[state] + ); + + if (data.search) { + ({ cids, matchedCids } = await findMatchedCids(caller.uid, data)); + } else { + cids = await loadCids(caller.uid, data.parentCid); + } + + const visibleCategories = await controllersHelpers.getVisibleCategories({ + cids, uid: caller.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid, + }); + + if (Array.isArray(data.selectedCids)) { + data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10)); + } + + let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid); + categoriesData = categoriesData.slice(0, 200); + + categoriesData.forEach((category) => { + category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false; + if (matchedCids.includes(category.cid)) { + category.match = true; + } + }); + const result = await plugins.hooks.fire('filter:categories.categorySearch', { + categories: categoriesData, + ...data, + uid: caller.uid, + }); + + return { categories: result.categories }; +}; + +async function findMatchedCids(uid, data) { + const result = await categories.search({ + uid: uid, + query: data.search, + qs: data.query, + paginate: false, + }); + + let matchedCids = result.categories.map(c => c.cid); + // no need to filter if all 3 states are used + const filterByWatchState = !Object.values(categories.watchStates) + .every(state => data.states.includes(state)); + + if (filterByWatchState) { + const states = await categories.getWatchState(matchedCids, uid); + matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index])); + } + + const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids)))); + const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids)))); + + return { + cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)), + matchedCids: matchedCids, + }; +} + +async function loadCids(uid, parentCid) { + let resultCids = []; + async function getCidsRecursive(cids) { + const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']); + const cidToData = _.zipObject(cids, categoryData); + await Promise.all(cids.map(async (cid) => { + const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`); + if (allChildCids.length) { + const childCids = await privileges.categories.filterCids('find', allChildCids, uid); + resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage)); + await getCidsRecursive(childCids); + } + })); + } + + const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`); + const rootCids = await privileges.categories.filterCids('find', allRootCids, uid); + const pageCids = rootCids.slice(0, meta.config.categoriesPerPage); + resultCids = pageCids; + await getCidsRecursive(pageCids); + return resultCids; +} diff --git a/src/controllers/write/index.js b/src/controllers/write/index.js index 46a8dd8110..26c74128d8 100644 --- a/src/controllers/write/index.js +++ b/src/controllers/write/index.js @@ -10,6 +10,7 @@ Write.tags = require('./tags'); Write.posts = require('./posts'); Write.chats = require('./chats'); Write.flags = require('./flags'); +Write.search = require('./search'); Write.admin = require('./admin'); Write.files = require('./files'); Write.utilities = require('./utilities'); diff --git a/src/controllers/write/search.js b/src/controllers/write/search.js new file mode 100644 index 0000000000..a6acd0a59a --- /dev/null +++ b/src/controllers/write/search.js @@ -0,0 +1,10 @@ +'use strict'; + +const api = require('../../api'); +const helpers = require('../helpers'); + +const Search = module.exports; + +Search.categories = async (req, res) => { + helpers.formatApiResponse(200, res, await api.search.categories(req, req.query)); +}; diff --git a/src/routes/write/index.js b/src/routes/write/index.js index 8e29c3ddd1..2ebec74ce1 100644 --- a/src/routes/write/index.js +++ b/src/routes/write/index.js @@ -41,6 +41,7 @@ Write.reload = async (params) => { router.use('/api/v3/posts', require('./posts')()); router.use('/api/v3/chats', require('./chats')()); router.use('/api/v3/flags', require('./flags')()); + router.use('/api/v3/search', require('./search')()); router.use('/api/v3/admin', require('./admin')()); router.use('/api/v3/files', require('./files')()); router.use('/api/v3/utilities', require('./utilities')()); diff --git a/src/routes/write/search.js b/src/routes/write/search.js new file mode 100644 index 0000000000..01b98cdeed --- /dev/null +++ b/src/routes/write/search.js @@ -0,0 +1,19 @@ +'use strict'; + +const router = require('express').Router(); +// const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + // const middlewares = []; + + // maybe redirect to /search/posts? + // setupApiRoute(router, 'post', '/', [...middlewares], controllers.write.search.TBD); + + setupApiRoute(router, 'get', '/categories', [], controllers.write.search.categories); + + return router; +}; diff --git a/src/socket.io/categories/search.js b/src/socket.io/categories/search.js index ad04c20edf..dbb355ce89 100644 --- a/src/socket.io/categories/search.js +++ b/src/socket.io/categories/search.js @@ -1,101 +1,13 @@ 'use strict'; -const _ = require('lodash'); - -const meta = require('../../meta'); -const categories = require('../../categories'); -const privileges = require('../../privileges'); -const controllersHelpers = require('../../controllers/helpers'); -const plugins = require('../../plugins'); +const sockets = require('..'); +const api = require('../../api'); module.exports = function (SocketCategories) { - // used by categorySearch module SocketCategories.categorySearch = async function (socket, data) { - let cids = []; - let matchedCids = []; - const privilege = data.privilege || 'topics:read'; - data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map( - state => categories.watchStates[state] - ); + sockets.warnDeprecated(socket, 'GET /api/v3/search/categories'); - if (data.search) { - ({ cids, matchedCids } = await findMatchedCids(socket.uid, data)); - } else { - cids = await loadCids(socket.uid, data.parentCid); - } - - const visibleCategories = await controllersHelpers.getVisibleCategories({ - cids, uid: socket.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid, - }); - - if (Array.isArray(data.selectedCids)) { - data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10)); - } - - let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid); - categoriesData = categoriesData.slice(0, 200); - - categoriesData.forEach((category) => { - category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false; - if (matchedCids.includes(category.cid)) { - category.match = true; - } - }); - const result = await plugins.hooks.fire('filter:categories.categorySearch', { - categories: categoriesData, - ...data, - uid: socket.uid, - }); - return result.categories; + const { categories } = await api.search.categories(socket, data); + return categories; }; - - async function findMatchedCids(uid, data) { - const result = await categories.search({ - uid: uid, - query: data.search, - qs: data.query, - paginate: false, - }); - - let matchedCids = result.categories.map(c => c.cid); - // no need to filter if all 3 states are used - const filterByWatchState = !Object.values(categories.watchStates) - .every(state => data.states.includes(state)); - - if (filterByWatchState) { - const states = await categories.getWatchState(matchedCids, uid); - matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index])); - } - - const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids)))); - const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids)))); - - return { - cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)), - matchedCids: matchedCids, - }; - } - - async function loadCids(uid, parentCid) { - let resultCids = []; - async function getCidsRecursive(cids) { - const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']); - const cidToData = _.zipObject(cids, categoryData); - await Promise.all(cids.map(async (cid) => { - const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`); - if (allChildCids.length) { - const childCids = await privileges.categories.filterCids('find', allChildCids, uid); - resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage)); - await getCidsRecursive(childCids); - } - })); - } - - const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`); - const rootCids = await privileges.categories.filterCids('find', allRootCids, uid); - const pageCids = rootCids.slice(0, meta.config.categoriesPerPage); - resultCids = pageCids; - await getCidsRecursive(pageCids); - return resultCids; - } }; From 581516c88ef30784a14849163ec000e768d6bcb2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 31 Oct 2023 11:08:59 -0400 Subject: [PATCH 036/201] fix: made parentCid optional in api.search.categories --- src/api/search.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/search.js b/src/api/search.js index 8dad8eb44e..18bd9fa160 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -20,6 +20,7 @@ searchApi.categories = async (caller, data) => { data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map( state => categories.watchStates[state] ); + data.parentCid = parseInt(data.parentCid || 0, 10); if (data.search) { ({ cids, matchedCids } = await findMatchedCids(caller.uid, data)); From 4ffe041732582d5124e8a6062b1505a080731253 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 31 Oct 2023 11:09:12 -0400 Subject: [PATCH 037/201] docs: openapi schema for api.search.categories --- public/openapi/write.yaml | 2 + public/openapi/write/search/categories.yaml | 96 +++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 public/openapi/write/search/categories.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 63c52430b8..1c4dfd44a4 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -208,6 +208,8 @@ paths: $ref: 'write/flags/flagId/notes.yaml' /flags/{flagId}/notes/{datetime}: $ref: 'write/flags/flagId/notes/datetime.yaml' + /search/categories: + $ref: 'write/search/categories.yaml' /admin/settings/{setting}: $ref: 'write/admin/settings/setting.yaml' /admin/analytics: diff --git a/public/openapi/write/search/categories.yaml b/public/openapi/write/search/categories.yaml new file mode 100644 index 0000000000..86d9115235 --- /dev/null +++ b/public/openapi/write/search/categories.yaml @@ -0,0 +1,96 @@ +get: + tags: + - search + summary: find categories by keyword + description: | + This operation returns a set of categories matching the keyword search. + + A number of filtering options are available, and can be passed in via query string. + parameters: + - in: query + name: 'search' + schema: + type: string + required: false + description: The keyword used in the category search + example: 'announcements' + - in: query + name: 'query' + schema: + type: string + required: false + description: Likely unused — a URI-encoded JSON string containing values that are passed to `getRecentTopicReplies`. + example: '' + - in: query + name: 'parentCid' + schema: + type: array + required: false + description: A list of category IDs. The values received are simply reflected back in the results. Matching cids will have "selected" set to true. + example: '0' + - in: query + name: 'selectedCids' + schema: + type: array + required: false + description: Likely deprecated — the sorting method of topics (use `categoryTopicSort` instead.) + example: '' + - in: query + name: 'categoryTopicSort' + schema: + type: string + required: false + description: The sorting method of topics + example: 'newest_to_oldest' + - in: query + name: 'direction' + schema: + type: string + required: false + description: The sorting of returned results (if you scroll up you want the topics reversed). Set to "-1" for reversed results. + example: '1' + responses: + '200': + description: matching categories successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + categories: + type: array + items: + type: object + properties: + cid: + type: number + description: A category identifier assigned upon category creation (this value cannot be changed) + name: + type: string + description: The category's name/title + level: + type: number + icon: + type: string + description: A FontAwesome icon string + example: fa-comments-o + bgColor: + type: string + description: Theme-related, a six-character hexadecimal string representing the background colour of the category + color: + type: string + description: Theme-related, a six-character hexadecimal string representing the foreground/text colour of the category + parentCid: + type: number + description: The category identifier for the category that is the immediate ancestor of the current category + imageClass: + type: string + enum: [auto, cover, contain] + description: The `background-position` of the category background image, if one is set + selected: + type: boolean \ No newline at end of file From d8d26c9f5732b2918694b022b6c159952a9af94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 31 Oct 2023 11:32:43 -0400 Subject: [PATCH 038/201] feat: add new lang string for minutes --- public/language/en-GB/error.json | 1 + src/user/posts.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index a01bdb473a..5fbc1bfb14 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -104,6 +104,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/src/user/posts.js b/src/user/posts.js index 26b0f348bf..b2e2b57be2 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -58,7 +58,11 @@ module.exports = function (User) { meta.config.newbiePostDelayThreshold > userData.reputation && now - lasttime < meta.config.newbiePostDelay * 1000 ) { - throw new Error(`[[error:too-many-posts-newbie, ${meta.config.newbiePostDelay}, ${meta.config.newbiePostDelayThreshold}]]`); + if (meta.config.newbiewPostDelay % 60 === 0) { + throw new Error(`[[error:too-many-posts-newbie-minutes, ${Math.floor(meta.config.newbiePostDelay / 60)}, ${meta.config.newbiePostDelayThreshold}]]`); + } else { + throw new Error(`[[error:too-many-posts-newbie, ${meta.config.newbiePostDelay}, ${meta.config.newbiePostDelayThreshold}]]`); + } } else if (now - lasttime < meta.config.postDelay * 1000) { throw new Error(`[[error:too-many-posts, ${meta.config.postDelay}]]`); } From 7d8f700f5688a49a33c6e92d335f74769a353115 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 31 Oct 2023 15:33:11 +0000 Subject: [PATCH 039/201] chore(i18n): fallback strings for new resources: nodebb.error --- public/language/ar/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/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 + 46 files changed, 46 insertions(+) diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 5459769ed7..ccd2d6a6aa 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "يسمح لك بالنشر مرة كل %1 ثانية - يرجى الإنتظار قبل النشر مجدداً", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 6074275791..0e8a20d80c 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -91,6 +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", "already-posting": "В момента публикувате", "tag-too-short": "Моля, въведете по-дълъг етикет. Етикетите трябва да съдържат поне %1 символ(а)", "tag-too-long": "Моля, въведете по-кратък етикет. Етикетите трябва да съдържат не повече от %1 символ(а)", diff --git a/public/language/bn/error.json b/public/language/bn/error.json index c2e704fb2d..dde2fe1266 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 711b99ee13..9cd0598a1c 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Nebyla vybrána kategorie.", "too-many-posts": "Můžete přispívat jednou za %1 sekund - vyčkejte tedy, než vytvoříte další příspěvek", "too-many-posts-newbie": "Jako nový uživatel, můžete přispívat jednou za %1 sekund, dokud nezískáte pověst %2 - vyčkejte tedy, než vytvoříte další příspěvek", + "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": "Zadejte delší značku. Značky by měli mít alespoň %1 znaků", "tag-too-long": "Zadejte kratší značku. Značky nesmí být delší než %1 znaků", diff --git a/public/language/da/error.json b/public/language/da/error.json index a84a6bb169..0c7e6c80bb 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "Du kan højest skrive et indlæg hver %1 sekund(er) - venligst vent et øjeblik før næste indlæg", "too-many-posts-newbie": "Som ny bruger kan du kun skrive et indlæg engang hvert %1. sekund() indtil du har optjent %2 omdømme point - venligst vent et øjeblik før næste indlæg.", + "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": "Indtast et længere tag. Tags skal indeholde mindst %1 karakter(er).", "tag-too-long": "Indtast et længere tag. Tags kan ikke være længere end %1 karakter(er).", diff --git a/public/language/de/error.json b/public/language/de/error.json index bfc79125c4..5fd9b5a767 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategorie nicht ausgewählt", "too-many-posts": "Du kannst nur einen Beitrag innerhalb von %1 Sekunden erstellen - Bitte warte bevor Du erneut einen Beitrag erstellst.", "too-many-posts-newbie": "Als neuer Benutzer kannst du nur einmal alle %1 Sekunde(n) posten, bis du %2 Reputation erworben hast - bitte warte, bevor du erneut postest", + "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": "Du bist bereits am Posten", "tag-too-short": "Bitte gebe ein längeres Schlagwort ein. Schlagworte sollten mindestens %1 Zeichen enthalten.", "tag-too-long": "Bitte gebe ein kürzeres Schlagwort ein. Schlagworte können nicht länger als %1 Zeichen sein.", diff --git a/public/language/el/error.json b/public/language/el/error.json index 1ada023f90..c0276445ed 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index 9836084180..94c074ea3a 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json index 9836084180..94c074ea3a 100644 --- a/public/language/en-x-pirate/error.json +++ b/public/language/en-x-pirate/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/es/error.json b/public/language/es/error.json index b6a00299a1..e355f3e11a 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Categoría no seleccionada.", "too-many-posts": "Solo puedes publicar una vez cada %1 segundo(s) - por favor espere antes de volver a publicar", "too-many-posts-newbie": "Como nuevo usuario, solo puedes publicar una vez cada %1 segundo(s) hasta hayas ganado una reputación de %2 - por favor espera antes de volver a publicar", + "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": "Por favor introduce una etiqueta más larga. Las etiquetas deben contener por lo menos %1 caractere(s)", "tag-too-long": "Por favor introduce una etiqueta más corta. Las etiquetas no pueden exceder los %1 caractere(s)", diff --git a/public/language/et/error.json b/public/language/et/error.json index 92b414b328..21737f741b 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "Te saate postitada %1 sekundi tagant - palun oodake enne uue postituse tegemist.", "too-many-posts-newbie": "Uue kasutajana saadte postitada vaid iga %1 sekundi tagant, seniks kuni olete teeninud vähemalt %2 reputatsiooni - palun oodake enne uue postituse tegemist.", + "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": "Palun sisestage pikem märksõna. Märksõna pikkus peab olema vähemalt %1 tähemärk(i).", "tag-too-long": "Palun sisestage lühem märksõna. Märksõna pikkus peab olema vähem kui %1 tähemärk(i).", diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json index 582befbbd0..28255147f4 100644 --- a/public/language/fa-IR/error.json +++ b/public/language/fa-IR/error.json @@ -91,6 +91,7 @@ "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": "قبلا ارسال کرده‌اید", "tag-too-short": "لطفا برچسب بلندتری وارد کنید. برچسبها باید حداقل %1 کاراکتر داشته باشند.", "tag-too-long": "لطفا برچسب کوتاه تری وارد کنید . برچسب ها نباید بیشتر از %1 کاراکتر داشته باشند", diff --git a/public/language/fi/error.json b/public/language/fi/error.json index 84ff43c2a3..deefef7725 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index 99c0132942..dc4f714c1b 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -91,6 +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", "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/gl/error.json b/public/language/gl/error.json index 03b15269b2..9b8a178993 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Categoría non seleccionada", "too-many-posts": "Só podes postear unha vez cada %1 segundo(s) - por favor agarda antes de publicar de novo.", "too-many-posts-newbie": "Como novo usuario, só podes publicar unha vez cada %1 segundo(s) ata que acades %2 de reputación -por favor, agarda para publicar de novo.", + "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": "Por favor, introduce unha etiqueta máis longa. As etiquetas deben conter %1 carácter(es) como mínimo.", "tag-too-long": "Por favor, introduce unha etiqueta máis curta. As etiquetas non poden conter máis de %1 carácter(es).", diff --git a/public/language/he/error.json b/public/language/he/error.json index beb845cd4a..9b83ca0d38 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -91,6 +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", "already-posting": "אתם כבר כותבים פוסט", "tag-too-short": "נא להזין תגית ארוכה יותר. על התגית להכיל לפחות %1 תווים", "tag-too-long": "נא להזין תגית קצרה יותר. תגיות לא יכולות להיות ארוכות מ-%1 תווים", diff --git a/public/language/hr/error.json b/public/language/hr/error.json index 84af5d8b50..61c76c992a 100644 --- a/public/language/hr/error.json +++ b/public/language/hr/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategorija nije odabrana.", "too-many-posts": "Možete objavljivati svakih %1 skeundi, pričekajte prije ponovne objave", "too-many-posts-newbie": "Kao novi korisnik, možete objavljivati svakih %1 sekundi dok ne steknete reputaciju %2 - molimo pričekajte prije ponovne objave", + "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": "Unesite dužu oznaku. Oznake moraju sadržavati najmanje %1 znak(ova)", "tag-too-long": "Unesite kraću oznaku. Oznake me mogu imati više od %1 znak(ova)", diff --git a/public/language/hu/error.json b/public/language/hu/error.json index 448a023ea5..5f3185374d 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -91,6 +91,7 @@ "category-not-selected": "A kategória nincs kiválasztva.", "too-many-posts": "Csak %1 másodpercenként hozhatsz létre új bejegyzést - kérlek várj egy kicsit mielőtt új bejegyzést tennél közzé", "too-many-posts-newbie": "Új felhasználóként csak egyszer készíthetsz bejegyzést %1 másodpercen belül, amíg el nem éred a %2 szintet - kérlek várj egy kicsit mielőtt új bejegyzést tennél közzé", + "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": "Már írsz egy bejegyzést", "tag-too-short": "Kérlek hosszabb címkét adj meg. A címke legalább %1 karaktert kell, hogy tartalmazzon", "tag-too-long": "Kérlek rövidebb címkét adj meg. A címkék nem lehetnek hosszabbak %1 karakternél", diff --git a/public/language/hy/error.json b/public/language/hy/error.json index 3eb20e4f20..a50b469cd5 100644 --- a/public/language/hy/error.json +++ b/public/language/hy/error.json @@ -91,6 +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", "already-posting": "Դուք արդեն հրապարակում եք", "tag-too-short": "Խնդրում ենք մուտքագրել ավելի երկար թեգ: Թեգերը պետք է պարունակեն առնվազն %1 նիշ(ներ)", "tag-too-long": "Խնդրում ենք մուտքագրել ավելի կարճ թեգ: Թեգերը չեն կարող ավելի երկար լինել, քան %1 նիշ(ներ)", diff --git a/public/language/id/error.json b/public/language/id/error.json index 13cd16a448..3417679b95 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "Anda hanya dapat memposting sekali setiap %1 detik() - harap tunggu sebelum memposting lagi", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/it/error.json b/public/language/it/error.json index 279b147aa8..5025660038 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Categoria non selezionata.", "too-many-posts": "Puoi postare solo una volta ogni %1 secondo(i) - attendi prima di postare di nuovo", "too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondo(i) finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di postare nuovamente", + "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": "Stai già postando", "tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.", "tag-too-long": "Per favore inserisci un tag più corto. I tags non dovrebbero essere più lunghi di %1 caratteri", diff --git a/public/language/ja/error.json b/public/language/ja/error.json index d5779e3813..f4b155c382 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -91,6 +91,7 @@ "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)以内でタグを入力してください。", diff --git a/public/language/ko/error.json b/public/language/ko/error.json index 1b29891a9f..e86031bd78 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -91,6 +91,7 @@ "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자 이상이어야 합니다.", "tag-too-long": "태그가 너무 깁니다. 태그는 최대 %1자 이내로 사용 가능합니다.", diff --git a/public/language/lt/error.json b/public/language/lt/error.json index 0071349540..fc6a4aee61 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Nepasirinkta kategorija.", "too-many-posts": "Jus galite rašyti kas %1 sekunde(s) - prašome palaukti prieš rašant dar kartą", "too-many-posts-newbie": "Kadangi esate naujas narys, jūs galite tik rašyti kas %1 sekunde(s) kol jūs pasieksite %2 reputacija - prašome palaukti prieš rašant dar kartą", + "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": "Prašome įvesti ilgesnę žymą. Žyma turi sudaryti mažiausiai %1 simboli(us)", "tag-too-long": "Prašome įvesti trumpesnę žymą. Žyma turi būti ne ilgesni negu %1 simboli(us)", diff --git a/public/language/lv/error.json b/public/language/lv/error.json index 2d03eb0cba..0ef88697ab 100644 --- a/public/language/lv/error.json +++ b/public/language/lv/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategorija nav atlasīta.", "too-many-posts": "Var publicēt tikai vienu rakstu katras %1 sekundes - lūdzu, uzgaidi, pirms publicē vēlreiz", "too-many-posts-newbie": "Jauni lietotāji var ievietot tikai vienu rakstu katras %1 sekundes, līdz ir nopelnīti %2 ranga punkti - lūdzu, uzgaidi, pirms publicē vēlreiz", + "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": "Lūdzu, ievadi garāku birku. Birkā jāsatur vismaz %1 rakstzīmes.", "tag-too-long": "Lūdzu, ievadi īsāku birku. Birkā nevar būt vairāk kā %1 rakstzīmes.", diff --git a/public/language/ms/error.json b/public/language/ms/error.json index db3908cd7d..144c180198 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "Anda hanya boleh mengirim sekali setiap %1 saat() - sila tunggu sebelum kiriman seterusnya", "too-many-posts-newbie": "Sebagai pengguna baru, anda hanya boleh mengirim sekali setiap %1 saat() sehinnga anda mendapat %2 reputasi - sila tunggu sebelum kiriman seterusnya", + "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": "Sila masukkan tag yang lebih panjang. Tag mesti mengandungi sekurang-kurangnya %1 aksara()", "tag-too-long": "Sila masukkan tag yang lebih pendek. Tag mesti mengandungi tidak lebih %1 aksara()", diff --git a/public/language/nb/error.json b/public/language/nb/error.json index 9b7d99e02c..5c371bdb20 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategori ikke valgt", "too-many-posts": "Du kan bare poste en gang per %1 sekund(er) – vennligst vent før du poster igjen", "too-many-posts-newbie": "Som ny bruker kan du bare poste en gang per %1. sekund(er), før du har opparbeidet %2 i omdømme – vennligst vent før du poster igjen", + "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": "Vennligst skriv et lengre emneord. Disse må være på minst %1 tegn", "tag-too-long": "Vennligst skriv et kortere emneord. Disse kan ikke være lengre enn %1 tegn", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 611231c616..f918aec5b7 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Categorie niet geselecteerd", "too-many-posts": "Het is slechts toegestaan iedere %1 seconde(n) een bericht te plaatsen - wacht even voordat opnieuw een bericht verzonden wordt", "too-many-posts-newbie": "Nieuwe gebruikersaccounts zoals deze zijn begrensd en mogen slechts iedere %1 seconde(n) berichten plaatsen, tot het moment dat %2 reputatie verdiend is - wacht daarom even met opnieuw een bericht te plaatsten", + "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": "Geef een tag op die uit meer tekens bestaat. Tags dienen uit minimaal %1 teken(s) te bestaan.", "tag-too-long": "Geef een kortere tag op. Tags mogen niet langer dan %1 teken(s) zijn", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 40f9124fdc..90f035591f 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Nie wybrano kategorii.", "too-many-posts": "Możesz publikować posty raz na %1 sekund – poczekaj, zanim dodasz kolejny post", "too-many-posts-newbie": "Jako nowy użytkownik możesz publikować posty raz na %1 sekund, dopóki nie zdobędziesz reputacji na poziomie %2 – poczekaj, zanim dodasz kolejny post", + "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": "Wprowadź dłuższy tag. Tagi muszą mieć przynajmniej %1 znak(-ów)", "tag-too-long": "Wprowadź krótszy tag. Tagi nie mogą mieć więcej niż %1 znak(-ów)", diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json index d8c53686a9..4b5153a357 100644 --- a/public/language/pt-BR/error.json +++ b/public/language/pt-BR/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Categoria não escolhida.", "too-many-posts": "Você pode postar uma vez a cada %1 segundo(s) - por favor aguarde antes de postar novamente", "too-many-posts-newbie": "Como novo usuário, você só pode postar uma vez a cada %1 segundo(s) até que você tenha, pelo menos, %2 de reputação. Por favor, aguarde antes de postar novamente.", + "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": "Por favor digite uma tag maior. Tags devem conter pelo menos %1 caractere(s)", "tag-too-long": "Por favor digite uma tag menor. Tags não podem conter mais que %1 caractere(s)", diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json index f519ffed3c..0c6b2622f2 100644 --- a/public/language/pt-PT/error.json +++ b/public/language/pt-PT/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Categoria não selecionada.", "too-many-posts": "Só podes publicar a cada %1 segundo(s) - por favor espera até poderes publicar outra vez", "too-many-posts-newbie": "Como novo utilizador, só podes publicar a cada %1 segundo(s) até teres conquistado %2 de reputação - por favor espera até poderes publicar outra vez", + "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": "Por favor introduz um marcador maior. Os marcadores devem ter pelo menos %1 caracter(s)", "tag-too-long": "Por favor introduz um marcador mais curto. Os marcadores devem ter no máximo %1 caracter(es)", diff --git a/public/language/ro/error.json b/public/language/ro/error.json index 7086854993..b58ac300a4 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index f2fb4e1871..9dedaa23c0 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -91,6 +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", "already-posting": "Вы уже опубликовали запись", "tag-too-short": "Слишком короткая метка. Минимум %1 символов.", "tag-too-long": "Слишком длинная метка. Максимум %1 символов.", diff --git a/public/language/rw/error.json b/public/language/rw/error.json index c5e79f81ed..923c9e90b1 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "Wemerewe kugira icyo ushyiraho rimwe mu masegonda (isegonda) %1. Ba utegerejeho gato kugirango wongere", "too-many-posts-newbie": "Nk'umuntu mushya, wemerewe gushyiraho ikintu rimwe mu masegonda (isegonda) %1 kugeza igihe ugize amanota agera kuri %2. Ba utegerejeho gato kugirango wongere", + "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": "Gerageza ukoreshe akamenyetso kagizwe n'inyuguti (cyangwa ibimenyetso) nibura zigera kuri %1", "tag-too-long": "Gerageza ukoreshe akamenyetso kagizwe n'inyuguti (cyangwa ibimenyetso) zitarenze %1", diff --git a/public/language/sc/error.json b/public/language/sc/error.json index 9836084180..94c074ea3a 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "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": "Please enter a longer tag. Tags should contain at least %1 character(s)", "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", diff --git a/public/language/sk/error.json b/public/language/sk/error.json index 2a963661cf..d65ea0d1fe 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategória nebola vybratá.", "too-many-posts": "Môžete uverejniť príspevok každých %1 sekúnd(y) - prosím počkajte pred opätovným zverejnením", "too-many-posts-newbie": "Ako nový užívateľ, môžete uverejniť príspevok raz za %1 sekúnd(y) pokiaľ nezískate %2 reputáciu - prosím, počkajte pred ďalším uverejnením", + "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": "Prosím, zadajte dlhšiu značku. Značky by mali obsahovať najmenej %1 znak(ov)", "tag-too-long": "Prosím, zadajte kratšiu značku. Značky nemôžu obsahovať viac ako %1 znak(ov)", diff --git a/public/language/sl/error.json b/public/language/sl/error.json index 24afe03917..2c1fbd65f7 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Category not selected.", "too-many-posts": "Objavljate lahko na %1 s - prosimo, počakajte pred novo objavo.", "too-many-posts-newbie": "Kot nov uporabnik lahko objavljate le na %1 s, dokler ne dosežete ugled vsaj %2 - prosimo, počakajte pred novo objavo.", + "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": "Prosimo, vnesite daljšo oznako. Obvezno število znakov: vsaj %1.", "tag-too-long": "Prosimo, vnesite krajšo oznako. Največje število znakov: %1.", diff --git a/public/language/sq-AL/error.json b/public/language/sq-AL/error.json index 1a31a1513d..eacea9b780 100644 --- a/public/language/sq-AL/error.json +++ b/public/language/sq-AL/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategoria nuk është zgjedhur.", "too-many-posts": "Mund të postoni vetëm një herë në %1 sekond(a) - ju lutemi prisni përpara se të postoni përsëri", "too-many-posts-newbie": "Si përdorues i ri, ju mund të postoni vetëm një herë në %1 sekond(a) derisa të keni fituar %2 reputacion - ju lutemi prisni përpara se të postoni përsëri", + "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": "Ju lutemi vendosni një tag më të gjatë. Tag-et duhet të përmbajnë të paktën %1 karakter(e)", "tag-too-long": "Ju lutemi vendosni një tag më të shkurtër. Tag-et nuk mund të jenë më të gjata se %1 karakter(e)", diff --git a/public/language/sr/error.json b/public/language/sr/error.json index 08cf3fc22e..b192346d6b 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -91,6 +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", "already-posting": "Већ објављујете", "tag-too-short": "Унесите дужу ознаку. Ознаке морају садржати најмање %1 знак(ов)а.", "tag-too-long": "Унесите краћу ознаку. Ознаке не смеју бити дуже од %1 знак(ов)а.", diff --git a/public/language/sv/error.json b/public/language/sv/error.json index 263e4cf95c..d24da9429a 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategori Ej vald.", "too-many-posts": "Du måste vänta minst %1 sekund(er) mellan varje inlägg", "too-many-posts-newbie": "Som ny användare måste du vänta %1 sekund(er) mellan varje inlägg tills dess du har %2 förtroende", + "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": "Fyll i en längre tagg. Taggar måste vara minst %1 tecken långa", "tag-too-long": "Fyll i en kortare tagg. Taggar kan ej vara längre än %1 tecken långa", diff --git a/public/language/th/error.json b/public/language/th/error.json index 671c76c6b3..b29f8c33d4 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -91,6 +91,7 @@ "category-not-selected": "ไม่มีการเลือกหมวดหมู่", "too-many-posts": "คุณสามารถโพสต์ได้เพียงครั้งเดียวเท่านั้นในทุกๆ %1 วินาที(s) - โปรดรอสักครู่ก่อนการโพสต์อีกครั้ง", "too-many-posts-newbie": "เนื่องด้วยการเป็นผู้ใช้งานใหม่ คุณสามารถโพสต์ได้เพียงครั้งเดียวเท่านั้นในทุกๆ %1 วินาที(s) จนกว่าคุณจะได้รับ %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", "already-posting": "You are already posting", "tag-too-short": "กรุณากรอกแท็กให้ยาวขึ้น แท็กควรมีข้อความอย่างน้อย %1 ตัวอักษร(s)", "tag-too-long": "กรุณากรอกแท็กให้สั้นลง แท็กไม่สามารถยาวกว่า %1 ตัวอักษร(s)", diff --git a/public/language/tr/error.json b/public/language/tr/error.json index 7c8eb39864..e00d64067a 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Kategori bulunamadı. Lütfen bir kategori seçiniz.", "too-many-posts": "%1 saniye içinde yalnızca bir ileti gönderebilirsiniz - lütfen tekrar ileti göndermeden önce bekleyiniz.", "too-many-posts-newbie": "Yeni bir kullanıcı olarak, %2 saygınlık puanı kazanana kadar %1 saniye içinde bir ileti gönderebilirsiniz - lütfen tekrar ileti göndermeden önce bekleyiniz.", + "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": "Halihazırda ileti gönderiyorsunuz...", "tag-too-short": "Lütfen daha uzun bir etiket girin. Etiketler en az %1 karakter içermelidir.", "tag-too-long": "Lütfen daha kısa bir etiket girin. Etiketler %1 karakterden uzun olamaz.", diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 23292eeab1..2f4b941ac0 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -91,6 +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", "already-posting": "You are already posting", "tag-too-short": "Введіть, будь ласка, довший тег. Мінімальна довжина тегу %1 символ(ів)", "tag-too-long": "Введіть, будь ласка, коротший тег. Максимальна довжина тегу %1 символ(ів)", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index ca7da16629..ec394ade91 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -91,6 +91,7 @@ "category-not-selected": "Danh mục không được chọn.", "too-many-posts": "Bạn chỉ có đăng bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.", "too-many-posts-newbie": "Là người dùng mới, bạn chỉ có thể đăng %1 giây một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại", + "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": "Vui lòng nhập tag dài hơn. Tag phải có tối thiểu %1 ký tự.", "tag-too-long": "Vui lòng nhập tag ngắn hơn. Tag chỉ có thể có tối đa %1 ký tự.", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index 3c48a63ebb..3b63a0da67 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -91,6 +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", "already-posting": "您已在发布帖子", "tag-too-short": "请输入一个更长的标签。标签应当包含不少于 %1 个字符", "tag-too-long": "请输入一个更短的标签。标签不能超过 %1 个字符", diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json index 1c0bf8bc74..7862ec6dfc 100644 --- a/public/language/zh-TW/error.json +++ b/public/language/zh-TW/error.json @@ -91,6 +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", "already-posting": "You are already posting", "tag-too-short": "標籤太短,不能少於 %1 個字元", "tag-too-long": "標籤太長,不能超過 %1 個字元", From cefd4061ca1036a73480ee7cfd25a2394a448639 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 31 Oct 2023 16:41:32 -0400 Subject: [PATCH 040/201] fix: #12141, use apiv3 for category search module --- public/src/modules/categorySearch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/src/modules/categorySearch.js b/public/src/modules/categorySearch.js index 5dccf8308a..ed9788ca3e 100644 --- a/public/src/modules/categorySearch.js +++ b/public/src/modules/categorySearch.js @@ -1,6 +1,6 @@ 'use strict'; -define('categorySearch', ['alerts', 'bootstrap'], function (alerts, bootstrap) { +define('categorySearch', ['alerts', 'bootstrap', 'api'], function (alerts, bootstrap, api) { const categorySearch = {}; categorySearch.init = function (el, options) { @@ -70,7 +70,7 @@ define('categorySearch', ['alerts', 'bootstrap'], function (alerts, bootstrap) { }); function loadList(search, callback) { - socket.emit('categories.categorySearch', { + api.get('/search/categories', { search: search, query: utils.params(), parentCid: options.parentCid || 0, @@ -78,7 +78,7 @@ define('categorySearch', ['alerts', 'bootstrap'], function (alerts, bootstrap) { privilege: options.privilege, states: options.states, showLinks: options.showLinks, - }, function (err, categories) { + }, function (err, { categories }) { if (err) { return alerts.error(err); } From 4a5ab081f69e8d555a136c6e255b2d48e6200e74 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 1 Nov 2023 09:18:51 +0000 Subject: [PATCH 041/201] Latest translations and fallbacks --- public/language/bg/error.json | 2 +- public/language/bg/modules.json | 2 +- public/language/de/error.json | 2 +- public/language/de/modules.json | 2 +- public/language/he/error.json | 2 +- public/language/he/modules.json | 2 +- public/language/it/admin/settings/email.json | 2 +- public/language/it/admin/settings/general.json | 8 ++++---- public/language/it/admin/settings/notifications.json | 2 +- public/language/it/admin/settings/post.json | 2 +- public/language/it/error.json | 2 +- public/language/it/modules.json | 2 +- public/language/it/topic.json | 4 ++-- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 0e8a20d80c..7da76bf1ee 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/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/bg/modules.json b/public/language/bg/modules.json index 1a753d8b19..bc1c329b62 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -68,7 +68,7 @@ "chat.in-room": "В тази стая", "chat.kick": "Изгонване", "chat.show-ip": "Показване на IP адреса", - "chat.copy-link": "Copy link", + "chat.copy-link": "Копиране на връзката", "chat.owner": "Собственик на стаята", "chat.grant-rescind-ownership": "Даване/отнемане на собственост", "chat.system.user-join": "%1 се присъедини към стаята ", diff --git a/public/language/de/error.json b/public/language/de/error.json index 5fd9b5a767..f053bfcd1a 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -91,7 +91,7 @@ "category-not-selected": "Kategorie nicht ausgewählt", "too-many-posts": "Du kannst nur einen Beitrag innerhalb von %1 Sekunden erstellen - Bitte warte bevor Du erneut einen Beitrag erstellst.", "too-many-posts-newbie": "Als neuer Benutzer kannst du nur einmal alle %1 Sekunde(n) posten, bis du %2 Reputation erworben hast - bitte warte, bevor du erneut postest", - "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": "Als neuer Benutzer kannst du nur einmal alle %1 Minute(n) posten, bis du %2 Ansehen erworben hast - bitte warte, bevor du erneut postest", "already-posting": "Du bist bereits am Posten", "tag-too-short": "Bitte gebe ein längeres Schlagwort ein. Schlagworte sollten mindestens %1 Zeichen enthalten.", "tag-too-long": "Bitte gebe ein kürzeres Schlagwort ein. Schlagworte können nicht länger als %1 Zeichen sein.", diff --git a/public/language/de/modules.json b/public/language/de/modules.json index 207d450380..d01efdb91b 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -68,7 +68,7 @@ "chat.in-room": "In diesem Chat-Room", "chat.kick": "Rauswerfen", "chat.show-ip": "IP anzeigen", - "chat.copy-link": "Copy link", + "chat.copy-link": "Link kopieren", "chat.owner": "Raumbesitzer", "chat.grant-rescind-ownership": "Erteilung/Aufhebung des Eigentums", "chat.system.user-join": "%1 hat den Raum betreten ", diff --git a/public/language/he/error.json b/public/language/he/error.json index 9b83ca0d38..6593829a9a 100644 --- a/public/language/he/error.json +++ b/public/language/he/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": "כמשתמש חדש (עד שתצבור %2 מוניטין), אתה יכול לפרסם פוסט רק פעם אחת כל %1 דקות - אנא המתן לפני שתפרסם שוב", "already-posting": "אתם כבר כותבים פוסט", "tag-too-short": "נא להזין תגית ארוכה יותר. על התגית להכיל לפחות %1 תווים", "tag-too-long": "נא להזין תגית קצרה יותר. תגיות לא יכולות להיות ארוכות מ-%1 תווים", diff --git a/public/language/he/modules.json b/public/language/he/modules.json index 88ce48a87d..cc63b93c9d 100644 --- a/public/language/he/modules.json +++ b/public/language/he/modules.json @@ -68,7 +68,7 @@ "chat.in-room": "בתוך חדר זה", "chat.kick": "הוצא", "chat.show-ip": "הצג IP", - "chat.copy-link": "Copy link", + "chat.copy-link": "העתק לינק", "chat.owner": "מנהלי החדר", "chat.grant-rescind-ownership": "הענק/בטל בעלות", "chat.system.user-join": "%1 הצטרף לחדר ", diff --git a/public/language/it/admin/settings/email.json b/public/language/it/admin/settings/email.json index 97d7d90e74..901ab74d79 100644 --- a/public/language/it/admin/settings/email.json +++ b/public/language/it/admin/settings/email.json @@ -14,7 +14,7 @@ "smtp-transport.service": "Seleziona un servizio", "smtp-transport.service-custom": "Servizio personalizzato", "smtp-transport.service-help": "Selezionare il nome di un servizio per utilizzare le informazioni note su di esso. In alternativa, selezionare "Servizio personalizzato" e inserire i dettagli qui sotto.", - "smtp-transport.gmail-warning1": "Se si utilizza GMail come provider di posta elettronica, è necessario generare una "Password dell'app" affinché NodeBB possa autenticarsi con successo. Puoi generarne una alla pagina Password dell'app .", + "smtp-transport.gmail-warning1": "Se si utilizza GMail come provider email, è necessario generare una "Password dell'app" affinché NodeBB possa autenticarsi con successo. Puoi generarne una alla pagina Password dell'app .", "smtp-transport.gmail-warning2": "Per ulteriori informazioni su questa soluzione alternativa, si prega di consultare questo articolo NodeMailer sulla questione. Un'alternativa sarebbe utilizzare un plug-in di posta elettronica di terze parti come SendGrid, Mailgun, ecc. Sfoglia i plugin disponibili qui.", "smtp-transport.auto-enable-toast": "Sembra che tu stia configurando un trasporto SMTP. Abbiamo abilitato l'opzione \"Trasporto SMTP\" per te.", "smtp-transport.host": "Host SMTP", diff --git a/public/language/it/admin/settings/general.json b/public/language/it/admin/settings/general.json index 487414a983..e268106a0a 100644 --- a/public/language/it/admin/settings/general.json +++ b/public/language/it/admin/settings/general.json @@ -5,7 +5,7 @@ "title": "Titolo Sito", "title.short": "Titolo abbreviato", "title.short-placeholder": "Se non specifichi un titolo abbreviato, verrà utilizzato il titolo completo", - "title.url": "Link URL Titolo", + "title.url": "URL del link del titolo", "title.url-placeholder": "L'URL del titolo del sito", "title.url-help": "Quando si fa clic sul titolo, inviare gli utenti a questo indirizzo. Se lasciato vuoto, l'utente sarà inviato all'indice del forum. Nota: Non si tratta dell'URL esterno utilizzato nelle email, ecc. Questo è impostato dalla proprietà url in config.json", "title.name": "Il Nome della Comunità", @@ -22,7 +22,7 @@ "logo.image": "Immagine", "logo.image-placeholder": "Percorso del logo da visualizzare sull'intestazione del forum", "logo.upload": "Carica", - "logo.url": "Link URL Logo", + "logo.url": "URL del link del logo", "logo.url-placeholder": "L'URL del logo del sito", "logo.url-help": "Quando il logo viene cliccato, invia gli utenti a questo indirizzo. Se lasciato vuoto, l'utente sarà inviato all'indice del forum.
Nota: Questo non è l'URL esterno usato nelle email, ecc. Questo è impostato dalla proprietà url in config.json", "logo.alt-text": "Testo alternativo", @@ -35,8 +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.", - "outgoing-links": "Collegamenti in uscita", - "outgoing-links.warning-page": "Usa pagina di avviso collegamenti in uscita", + "outgoing-links": "Link in uscita", + "outgoing-links.warning-page": "Usa pagina di avviso per i link in uscita", "search": "Cerca", "search-default-in": "Cerca in", "search-default-in-quick": "Ricerca rapida in", diff --git a/public/language/it/admin/settings/notifications.json b/public/language/it/admin/settings/notifications.json index 9574902ce0..2230b647ac 100644 --- a/public/language/it/admin/settings/notifications.json +++ b/public/language/it/admin/settings/notifications.json @@ -1,7 +1,7 @@ { "notifications": "Notifiche", "welcome-notification": "Notifica di benvenuto", - "welcome-notification-link": "Collegamento a Notifica di benvenuto", + "welcome-notification-link": "Link a Notifica di benvenuto", "welcome-notification-uid": "Notifica di benvenuto utente (UID)", "post-queue-notification-uid": "Coda post utente (UID)" } \ No newline at end of file diff --git a/public/language/it/admin/settings/post.json b/public/language/it/admin/settings/post.json index 8cf48f4dc2..6a46886fe3 100644 --- a/public/language/it/admin/settings/post.json +++ b/public/language/it/admin/settings/post.json @@ -47,7 +47,7 @@ "recent.categoryFilter.disable": "Disabilita il filtro delle discussioni nelle categorie ignorate nella /pagina recente", "signature": "Impostazioni della Firma", "signature.disable": "Disabilita le firme", - "signature.no-links": "Disabilita i collegamenti nelle firme", + "signature.no-links": "Disabilita i link nelle firme", "signature.no-images": "Disabilita le immagini nelle firme", "signature.hide-duplicates": "Nascondi firme duplicate nelle discussioni", "signature.max-length": "Lunghezza massima della firma", diff --git a/public/language/it/error.json b/public/language/it/error.json index 5025660038..949c70c693 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -91,7 +91,7 @@ "category-not-selected": "Categoria non selezionata.", "too-many-posts": "Puoi postare solo una volta ogni %1 secondo(i) - attendi prima di postare di nuovo", "too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondo(i) finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di postare nuovamente", - "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": "Come nuovo utente, puoi postare solo una volta ogni %1 minuto(i) finché non avrai guadagnato %2 reputazioni - attendi prima di postare di nuovo", "already-posting": "Stai già postando", "tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.", "tag-too-long": "Per favore inserisci un tag più corto. I tags non dovrebbero essere più lunghi di %1 caratteri", diff --git a/public/language/it/modules.json b/public/language/it/modules.json index 0ca700cedc..dd35cfa319 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -68,7 +68,7 @@ "chat.in-room": "In questa stanza", "chat.kick": "Butta fuori", "chat.show-ip": "Mostra indirizzo IP", - "chat.copy-link": "Copy link", + "chat.copy-link": "Copia link", "chat.owner": "Propietario stanza", "chat.grant-rescind-ownership": "Concedi/Revoca Proprietà", "chat.system.user-join": "%1 si è unito alla stanza ", diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 273e660bc8..d5564eb93e 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -27,7 +27,7 @@ "move": "Muovi", "change-owner": "Cambia proprietario", "fork": "Dividi", - "link": "Collegamento", + "link": "Link", "share": "Condividi", "tools": "Strumenti", "locked": "Bloccato", @@ -130,7 +130,7 @@ "bookmark": "Favorito", "bookmarks": "Favoriti", "bookmarks.has-no-bookmarks": "Non hai ancora aggiunto alcun post ai segnalibri.", - "copy-permalink": "Copia collegamento permanente", + "copy-permalink": "Copia link permanente", "loading-more-posts": "Caricamento altri post", "move-topic": "Sposta Discussione", "move-topics": "Sposta Discussioni", From b4297cd8f065a94e7de5a3ad4b992e61c9234ca0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 1 Nov 2023 10:54:41 -0400 Subject: [PATCH 042/201] fix: update ajaxifyTimer logic to only drop the request if the URL is the same as the one it's already processing re: #12133 --- public/src/ajaxify.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index bff7a330f2..8423f27fd8 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -17,6 +17,7 @@ ajaxify.widgets = { render: render }; ajaxify.count = 0; ajaxify.currentPage = null; + ajaxify.requestedPage = null; // disables scroll to top when back button is clicked // https://developer.chrome.com/blog/history-api-scroll-restoration/ if ('scrollRestoration' in history) { @@ -38,10 +39,11 @@ ajaxify.widgets = { render: render }; } // Abort subsequent requests if clicked multiple times within a short window of time - if (ajaxifyTimer && (Date.now() - ajaxifyTimer) < 500) { + if (ajaxify.requestedPage === url && ajaxifyTimer && (Date.now() - ajaxifyTimer) < 500) { return true; } ajaxifyTimer = Date.now(); + ajaxify.requestedPage = url; if (ajaxify.handleRedirects(url)) { return true; @@ -134,6 +136,7 @@ ajaxify.widgets = { render: render }; ajaxify.updateHistory = function (url, quiet) { ajaxify.currentPage = url.split(/[?#]/)[0]; + ajaxify.requestedPage = null; if (window.history && window.history.pushState) { window.history[!quiet ? 'pushState' : 'replaceState']({ url: url, From 4601a6f7fe1cc47f7c32aeb463ac1fdf49d12daf Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 1 Nov 2023 12:16:45 -0400 Subject: [PATCH 043/201] fix: #12133 dropdown menus on mobile stay open during ajaxify --- public/src/ajaxify.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index 8423f27fd8..995bab017c 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -472,6 +472,20 @@ ajaxify.widgets = { render: render }; hooks.fire('action:ajaxify.cleanup', { url, tpl_url }); }; + ajaxify.handleTransientElements = () => { + // todo: modals? + + const elements = ['[component="notifications"]', '[component="chat/dropdown"]', '[component="sidebar/drafts"]', '[component="header/avatar"]'] + .map(el => document.querySelector(`${el} .dropdown-menu.show`) || document.querySelector(`${el} + .dropdown-menu.show`)) + .filter(Boolean); + + if (elements.length) { + elements.forEach((el) => { + el.classList.remove('show'); + }); + } + }; + translator.translate('[[error:no-connection]]'); translator.translate('[[error:socket-reconnect-failed]]'); translator.translate(`[[global:reconnecting-message, ${config.siteTitle}]]`); @@ -482,15 +496,14 @@ ajaxify.widgets = { render: render }; }()); $(document).ready(function () { - $(window).on('popstate', function (ev) { - ev = ev.originalEvent; - + window.addEventListener('popstate', (ev) => { if (ev !== null && ev.state) { if (ev.state.url === null && ev.state.returnPath !== undefined) { window.history.replaceState({ url: ev.state.returnPath, }, ev.state.returnPath, config.relative_path + '/' + ev.state.returnPath); } else if (ev.state.url !== undefined) { + ajaxify.handleTransientElements(); ajaxify.go(ev.state.url, function () { hooks.fire('action:popstate', { url: ev.state.url }); }, true); From 4e23d0d12e4220b3b86041b4f62e9ac9dd12348c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 19:23:22 -0400 Subject: [PATCH 044/201] fix(deps): update dependency connect-pg-simple to v9.0.1 (#12144) 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 7354135e4d..5ccd41102b 100644 --- a/install/package.json +++ b/install/package.json @@ -55,7 +55,7 @@ "connect-flash": "0.1.1", "connect-mongo": "5.1.0", "connect-multiparty": "2.2.0", - "connect-pg-simple": "9.0.0", + "connect-pg-simple": "9.0.1", "connect-redis": "7.1.0", "cookie-parser": "1.4.6", "cron": "3.1.6", From 6c54f36feb7f648957345c0b2ebfa9516ea39754 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 2 Nov 2023 09:18:39 +0000 Subject: [PATCH 045/201] Latest translations and fallbacks --- public/language/he/user.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/he/user.json b/public/language/he/user.json index 1f177fba17..e9525c84ef 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -65,7 +65,7 @@ "email-updated": "האימייל עודכן", "email-same-as-password": "הכנס את הסיסמא הנוכחית שלך על מנת להמשיך – כתבת את כתובת המייל החדשה במקום.", "edit": "ערוך", - "edit-profile": "ערוך פרופיל", + "edit-profile": "עריכת פרופיל", "default-picture": "אייקון ברירת מחדל", "uploaded-picture": "התמונה הועלתה", "upload-new-picture": "העלה תמונה חדשה", From a5e3754b903fa6d7c1455a91f3f8ab50897b5834 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 2 Nov 2023 15:04:24 -0400 Subject: [PATCH 046/201] fix: bump harmony --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 5ccd41102b..07129e3b78 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.92", + "nodebb-theme-harmony": "1.1.93", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.24", "nodebb-theme-persona": "13.2.43", @@ -195,4 +195,4 @@ "url": "https://github.com/barisusakli" } ] -} \ No newline at end of file +} From 1b8dcbc2d7c9d5db2fa87c2c415d3409d582d632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 2 Nov 2023 15:44:45 -0400 Subject: [PATCH 047/201] 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 07129e3b78..a5d572d4f8 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.93", + "nodebb-theme-harmony": "1.1.94", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.24", "nodebb-theme-persona": "13.2.43", From f8cc8548bb69367d0ac5190222ebaffb816fbd90 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 3 Nov 2023 10:41:46 -0400 Subject: [PATCH 048/201] docs: fix improper verbiage in category watch schema --- public/openapi/write/categories/cid/watch.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/openapi/write/categories/cid/watch.yaml b/public/openapi/write/categories/cid/watch.yaml index 06fc399dbe..8ed5a10d1e 100644 --- a/public/openapi/write/categories/cid/watch.yaml +++ b/public/openapi/write/categories/cid/watch.yaml @@ -14,14 +14,14 @@ put: This API call does not pertain to notifications for new topics in categories. That behaviour is handled by a third-party plugin — nodebb-plugin-category-notifications - N.B. When a category's watch state is updated, all of that category's children also have their watch states updated. + Additionally, when a category's watch state is updated, all of that category's children also have their watch states updated. parameters: - in: path name: cid schema: type: string required: true - description: a valid category id, `0` for global privileges, `admin` for admin privileges + description: a valid category id example: 1 requestBody: required: true @@ -69,7 +69,7 @@ delete: schema: type: string required: true - description: a valid category id, `0` for global privileges, `admin` for admin privileges + description: a valid category id example: 1 requestBody: required: true From 84fed97b41be09461ec5f6ffb0850ee2e70407d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 3 Nov 2023 12:49:17 -0400 Subject: [PATCH 049/201] feat: add tracking categories and make watching send notifications (#12147) * feat: add tracking categories and make watching send notifications upgrade script to change the defaults * add missing spec * test: one more spec --- .../language/en-GB/admin/settings/user.json | 2 +- public/language/en-GB/category.json | 7 +++- public/language/en-GB/notifications.json | 4 ++ .../components/schemas/SettingsObj.yaml | 3 ++ public/openapi/read/category/category_id.yaml | 2 + .../read/user/userslug/categories.yaml | 2 + public/src/client/account/categories.js | 11 ++++- public/src/client/category.js | 5 ++- public/src/modules/categorySearch.js | 2 +- public/src/modules/topicList.js | 2 +- src/api/search.js | 2 +- src/categories/index.js | 1 + src/categories/topics.js | 40 +++++++++++++++++++ src/categories/watch.js | 3 +- src/controllers/accounts/categories.js | 3 +- src/controllers/accounts/notifications.js | 1 + src/notifications.js | 1 + src/topics/create.js | 1 + src/topics/unread.js | 16 +++++++- src/upgrades/3.6.0/category_tracking.js | 32 +++++++++++++++ src/user/categories.js | 6 +-- src/views/admin/settings/user.tpl | 2 +- 22 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 src/upgrades/3.6.0/category_tracking.js diff --git a/public/language/en-GB/admin/settings/user.json b/public/language/en-GB/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/en-GB/admin/settings/user.json +++ b/public/language/en-GB/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/en-GB/category.json b/public/language/en-GB/category.json index 968d3d218d..7e3c6630c5 100644 --- a/public/language/en-GB/category.json +++ b/public/language/en-GB/category.json @@ -13,13 +13,16 @@ "watch": "Watch", "ignore": "Ignore", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", diff --git a/public/language/en-GB/notifications.json b/public/language/en-GB/notifications.json index 8670874814..2782fdaff9 100644 --- a/public/language/en-GB/notifications.json +++ b/public/language/en-GB/notifications.json @@ -15,6 +15,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -61,6 +62,8 @@ "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-started-following-you": "%1 started following you.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -89,6 +92,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/openapi/components/schemas/SettingsObj.yaml b/public/openapi/components/schemas/SettingsObj.yaml index c9f990e5ac..2ccc8e161c 100644 --- a/public/openapi/components/schemas/SettingsObj.yaml +++ b/public/openapi/components/schemas/SettingsObj.yaml @@ -83,6 +83,9 @@ Settings: notificationType_new-topic-with-tag: type: string description: Notification type for new topics with followed tag + notificationType_new-topic-in-category: + type: string + description: Notification type for new topics in watched category notificationType_follow: type: string description: Notification type for another user following you diff --git a/public/openapi/read/category/category_id.yaml b/public/openapi/read/category/category_id.yaml index 2735ce5e90..c0d44fa413 100644 --- a/public/openapi/read/category/category_id.yaml +++ b/public/openapi/read/category/category_id.yaml @@ -52,6 +52,8 @@ get: type: number isWatched: type: boolean + isTracked: + type: boolean isNotWatched: type: boolean isIgnored: diff --git a/public/openapi/read/user/userslug/categories.yaml b/public/openapi/read/user/userslug/categories.yaml index 20cc798400..f25a168f91 100644 --- a/public/openapi/read/user/userslug/categories.yaml +++ b/public/openapi/read/user/userslug/categories.yaml @@ -52,6 +52,8 @@ get: type: boolean isWatched: type: boolean + isTracked: + type: boolean isNotWatched: type: boolean imageClass: diff --git a/public/src/client/account/categories.js b/public/src/client/account/categories.js index bb6849b166..19ada5d8dd 100644 --- a/public/src/client/account/categories.js +++ b/public/src/client/account/categories.js @@ -11,7 +11,9 @@ define('forum/account/categories', ['forum/account/header', 'alerts', 'api'], fu handleIgnoreWatch(category.cid); }); - $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => { + $('[component="category/watch/all"]').find( + '[component="category/watching"], [component="category/tracking"], [component="category/ignoring"], [component="category/notwatching"]' + ).on('click', async (e) => { const cids = []; const state = e.currentTarget.getAttribute('data-state'); const { uid } = ajaxify.data; @@ -30,7 +32,9 @@ define('forum/account/categories', ['forum/account/header', 'alerts', 'api'], fu function handleIgnoreWatch(cid) { const category = $('[data-cid="' + cid + '"]'); - category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => { + category.find( + '[component="category/watching"], [component="category/tracking"], [component="category/ignoring"], [component="category/notwatching"]' + ).on('click', async (e) => { const state = e.currentTarget.getAttribute('data-state'); const { uid } = ajaxify.data; @@ -46,6 +50,9 @@ define('forum/account/categories', ['forum/account/header', 'alerts', 'api'], fu category.find('[component="category/watching/menu"]').toggleClass('hidden', state !== 'watching'); category.find('[component="category/watching/check"]').toggleClass('fa-check', state === 'watching'); + category.find('[component="category/tracking/menu"]').toggleClass('hidden', state !== 'tracking'); + category.find('[component="category/tracking/check"]').toggleClass('fa-check', state === 'tracking'); + category.find('[component="category/notwatching/menu"]').toggleClass('hidden', state !== 'notwatching'); category.find('[component="category/notwatching/check"]').toggleClass('fa-check', state === 'notwatching'); diff --git a/public/src/client/category.js b/public/src/client/category.js index e1ab97431f..c452273761 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -65,7 +65,7 @@ define('forum/category', [ } function handleIgnoreWatch(cid) { - $('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { + $('[component="category/watching"], [component="category/tracking"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { const $this = $(this); const state = $this.attr('data-state'); @@ -77,6 +77,9 @@ define('forum/category', [ $('[component="category/watching/menu"]').toggleClass('hidden', state !== 'watching'); $('[component="category/watching/check"]').toggleClass('fa-check', state === 'watching'); + $('[component="category/tracking/menu"]').toggleClass('hidden', state !== 'tracking'); + $('[component="category/tracking/check"]').toggleClass('fa-check', state === 'tracking'); + $('[component="category/notwatching/menu"]').toggleClass('hidden', state !== 'notwatching'); $('[component="category/notwatching/check"]').toggleClass('fa-check', state === 'notwatching'); diff --git a/public/src/modules/categorySearch.js b/public/src/modules/categorySearch.js index ed9788ca3e..8c7461dcdf 100644 --- a/public/src/modules/categorySearch.js +++ b/public/src/modules/categorySearch.js @@ -7,7 +7,7 @@ define('categorySearch', ['alerts', 'bootstrap', 'api'], function (alerts, boots let categoriesList = null; options = options || {}; options.privilege = options.privilege || 'topics:read'; - options.states = options.states || ['watching', 'notwatching', 'ignoring']; + options.states = options.states || ['watching', 'tracking', 'notwatching', 'ignoring']; options.cacheList = options.hasOwnProperty('cacheList') ? options.cacheList : true; let localCategories = []; diff --git a/public/src/modules/topicList.js b/public/src/modules/topicList.js index f771eca47c..6396bb9e8e 100644 --- a/public/src/modules/topicList.js +++ b/public/src/modules/topicList.js @@ -34,7 +34,7 @@ define('topicList', [ categoryTools.init(); TopicList.watchForNewPosts(); - const states = ['watching']; + const states = ['watching', 'tracking']; if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'watched') { states.push('notwatching', 'ignoring'); } else if (template !== 'unread') { diff --git a/src/api/search.js b/src/api/search.js index 18bd9fa160..ac77347c26 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -17,7 +17,7 @@ searchApi.categories = async (caller, data) => { let cids = []; let matchedCids = []; const privilege = data.privilege || 'topics:read'; - data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map( + data.states = (data.states || ['watching', 'tracking', 'notwatching', 'ignoring']).map( state => categories.watchStates[state] ); data.parentCid = parseInt(data.parentCid || 0, 10); diff --git a/src/categories/index.js b/src/categories/index.js index 3fe5d9d457..6b8db37edd 100644 --- a/src/categories/index.js +++ b/src/categories/index.js @@ -53,6 +53,7 @@ Categories.getCategoryById = async function (data) { category.nextStart = topics.nextStart; category.topic_count = topicCount; category.isWatched = watchState[0] === Categories.watchStates.watching; + category.isTracked = watchState[0] === Categories.watchStates.tracking; category.isNotWatched = watchState[0] === Categories.watchStates.notwatching; category.isIgnored = watchState[0] === Categories.watchStates.ignoring; category.parent = parent; diff --git a/src/categories/topics.js b/src/categories/topics.js index 2abb272475..2bc9aa58cf 100644 --- a/src/categories/topics.js +++ b/src/categories/topics.js @@ -6,6 +6,9 @@ const plugins = require('../plugins'); const meta = require('../meta'); const privileges = require('../privileges'); const user = require('../user'); +const notifications = require('../notifications'); +const translator = require('../translator'); +const batch = require('../batch'); module.exports = function (Categories) { Categories.getCategoryTopics = async function (data) { @@ -203,4 +206,41 @@ module.exports = function (Categories) { const now = Date.now(); return tids.filter((tid, index) => tid && (!scores[index] || scores[index] <= now)); } + + Categories.notifyCategoryFollowers = async (postData, exceptUid) => { + const { cid } = postData.topic; + const followers = []; + await batch.processSortedSet(`cid:${cid}:uid:watch:state`, async (uids) => { + followers.push( + ...await privileges.categories.filterUids('topics:read', cid, uids) + ); + }, { + batch: 500, + min: Categories.watchStates.watching, + max: Categories.watchStates.watching, + }); + + if (!followers.length) { + return; + } + + const { displayname } = postData.user; + const categoryName = await Categories.getCategoryField(cid, 'name'); + const notifBase = 'notifications:user-posted-topic-in-category'; + + const bodyShort = translator.compile(notifBase, displayname, categoryName); + + const notification = await notifications.create({ + type: 'new-topic-in-category', + nid: `new_topic:tid:${postData.topic.tid}:uid:${exceptUid}`, + subject: bodyShort, + bodyShort: bodyShort, + bodyLong: postData.content, + pid: postData.pid, + path: `/post/${postData.pid}`, + tid: postData.topic.tid, + from: exceptUid, + }); + notifications.push(notification, followers); + }; }; diff --git a/src/categories/watch.js b/src/categories/watch.js index 4a64fa9ec8..f80d0bf15d 100644 --- a/src/categories/watch.js +++ b/src/categories/watch.js @@ -7,7 +7,8 @@ module.exports = function (Categories) { Categories.watchStates = { ignoring: 1, notwatching: 2, - watching: 3, + tracking: 3, + watching: 4, }; Categories.isIgnored = async function (cids, uid) { diff --git a/src/controllers/accounts/categories.js b/src/controllers/accounts/categories.js index 1e8ccab576..4dfaf95f31 100644 --- a/src/controllers/accounts/categories.js +++ b/src/controllers/accounts/categories.js @@ -24,9 +24,10 @@ categoriesController.get = async function (req, res) { categoriesData.forEach((category) => { if (category) { - category.isIgnored = states[category.cid] === categories.watchStates.ignoring; category.isWatched = states[category.cid] === categories.watchStates.watching; + category.isTracked = states[category.cid] === categories.watchStates.tracking; category.isNotWatched = states[category.cid] === categories.watchStates.notwatching; + category.isIgnored = states[category.cid] === categories.watchStates.ignoring; } }); diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index fd5ce2529c..301851ca36 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -13,6 +13,7 @@ notificationsController.get = async function (req, res, next) { { name: '[[global:topics]]', filter: 'new-topic' }, { name: '[[notifications:replies]]', filter: 'new-reply' }, { name: '[[notifications:tags]]', filter: 'new-topic-with-tag' }, + { name: '[[notifications:categories]]', filter: 'new-topic-in-category' }, { name: '[[notifications:chat]]', filter: 'new-chat' }, { name: '[[notifications:group-chat]]', filter: 'new-group-chat' }, { name: '[[notifications:public-chat]]', filter: 'new-public-chat' }, diff --git a/src/notifications.js b/src/notifications.js index 681c293067..612c67d95f 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -31,6 +31,7 @@ Notifications.baseTypes = [ 'notificationType_upvote', 'notificationType_new-topic', 'notificationType_new-topic-with-tag', + 'notificationType_new-topic-in-category', 'notificationType_new-reply', 'notificationType_post-edit', 'notificationType_follow', diff --git a/src/topics/create.js b/src/topics/create.js index e87b48327a..c8a098a9ae 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -153,6 +153,7 @@ module.exports = function (Topics) { if (parseInt(uid, 10) && !topicData.scheduled) { user.notifications.sendTopicNotificationToFollowers(uid, topicData, postData); Topics.notifyTagFollowers(postData, uid); + categories.notifyCategoryFollowers(postData, uid); } return { diff --git a/src/topics/unread.js b/src/topics/unread.js index 9c6d2c35c2..9c54445233 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -154,7 +154,8 @@ module.exports = function (Topics) { (!filterCids || filterCids.includes(topic.cid)) && (!filterTags || filterTags.every(tag => topic.tags.find(topicTag => topicTag.value === tag))) && !blockedUids.includes(topic.uid)) { - if (isTopicsFollowed[topic.tid] || userCidState[topic.cid] === categories.watchStates.watching) { + if (isTopicsFollowed[topic.tid] || + [categories.watchStates.watching, categories.watchStates.tracking].includes(userCidState[topic.cid])) { tidsByFilter[''].push(topic.tid); } @@ -192,11 +193,22 @@ module.exports = function (Topics) { if (params.filter === 'watched') { return []; } - const cids = params.cid || await user.getWatchedCategories(params.uid); + const cids = params.cid || await getWatchedTrackedCids(params.uid); const keys = cids.map(cid => `cid:${cid}:tids:lastposttime`); return await db.getSortedSetRevRangeByScoreWithScores(keys, 0, -1, '+inf', params.cutoff); } + async function getWatchedTrackedCids(uid) { + if (!(parseInt(uid, 10) > 0)) { + return []; + } + const cids = await user.getCategoriesByStates(uid, [ + categories.watchStates.watching, categories.watchStates.tracking, + ]); + const categoryData = await categories.getCategoriesFields(cids, ['disabled']); + return cids.filter((cid, index) => categoryData[index] && !categoryData[index].disabled); + } + async function getFollowedTids(params) { let tids = await db.getSortedSetMembers(`uid:${params.uid}:followed_tids`); const filterCids = params.cid && params.cid.map(cid => parseInt(cid, 10)); diff --git a/src/upgrades/3.6.0/category_tracking.js b/src/upgrades/3.6.0/category_tracking.js new file mode 100644 index 0000000000..a30be983b6 --- /dev/null +++ b/src/upgrades/3.6.0/category_tracking.js @@ -0,0 +1,32 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); +const user = require('../../user'); +const batch = require('../../batch'); + +module.exports = { + name: 'Add tracking category state', + timestamp: Date.UTC(2023, 10, 3), + method: async function () { + const { progress } = this; + + const current = await db.getObjectField('config', 'categoryWatchState'); + if (current === 'watching') { + await db.setObjectField('config', 'categoryWatchState', 'tracking'); + } + + await batch.processSortedSet(`users:joindate`, async (uids) => { + const userSettings = await user.getMultipleUserSettings(uids); + const change = userSettings.filter(s => s && s.categoryWatchState === 'watching'); + await db.setObjectBulk( + change.map(s => [`user:${s.uid}:settings`, { categoryWatchState: 'tracking' }]) + ); + progress.incr(uids.length); + }, { + batch: 500, + progress, + }); + }, +}; diff --git a/src/user/categories.js b/src/user/categories.js index 80839a7ea7..1bae181ef5 100644 --- a/src/user/categories.js +++ b/src/user/categories.js @@ -60,10 +60,10 @@ module.exports = function (User) { }; User.getCategoriesByStates = async function (uid, states) { - if (!(parseInt(uid, 10) > 0)) { - return await categories.getAllCidsFromSet('categories:cid'); - } const cids = await categories.getAllCidsFromSet('categories:cid'); + if (!(parseInt(uid, 10) > 0)) { + return cids; + } const userState = await categories.getWatchState(cids, uid); return cids.filter((cid, index) => states.includes(userState[index])); }; diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index e2aad039a1..60d80f781e 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -297,7 +297,7 @@
From 77b0baeace5bd8e53ddf40ae995f253b36e4a4eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:50:12 -0400 Subject: [PATCH 050/201] fix(deps): update dependency nodebb-theme-harmony to v1.1.95 (#12148) 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 a5d572d4f8..d8473d5f3a 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.94", + "nodebb-theme-harmony": "1.1.95", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.24", "nodebb-theme-persona": "13.2.43", From 29b3a4038fa41b0e8923709e5ee5794a16c68c67 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 3 Nov 2023 16:50:47 +0000 Subject: [PATCH 051/201] chore(i18n): fallback strings for new resources: nodebb.admin-settings-user, nodebb.category, nodebb.notifications --- public/language/ar/admin/settings/user.json | 2 +- public/language/ar/category.json | 7 +++++-- public/language/ar/notifications.json | 3 +++ public/language/bg/admin/settings/user.json | 2 +- public/language/bg/category.json | 7 +++++-- public/language/bg/notifications.json | 3 +++ public/language/bn/admin/settings/user.json | 2 +- public/language/bn/category.json | 7 +++++-- public/language/bn/notifications.json | 3 +++ public/language/cs/admin/settings/user.json | 2 +- public/language/cs/category.json | 7 +++++-- public/language/cs/notifications.json | 3 +++ public/language/da/admin/settings/user.json | 2 +- public/language/da/category.json | 7 +++++-- public/language/da/notifications.json | 3 +++ public/language/de/admin/settings/user.json | 2 +- public/language/de/category.json | 7 +++++-- public/language/de/notifications.json | 3 +++ public/language/el/admin/settings/user.json | 2 +- public/language/el/category.json | 7 +++++-- public/language/el/notifications.json | 3 +++ public/language/en-US/admin/settings/user.json | 2 +- public/language/en-US/category.json | 7 +++++-- public/language/en-US/notifications.json | 3 +++ public/language/en-x-pirate/admin/settings/user.json | 2 +- public/language/en-x-pirate/category.json | 7 +++++-- public/language/en-x-pirate/notifications.json | 3 +++ public/language/es/admin/settings/user.json | 2 +- public/language/es/category.json | 7 +++++-- public/language/es/notifications.json | 3 +++ public/language/et/admin/settings/user.json | 2 +- public/language/et/category.json | 7 +++++-- public/language/et/notifications.json | 3 +++ public/language/fa-IR/admin/settings/user.json | 2 +- public/language/fa-IR/category.json | 7 +++++-- public/language/fa-IR/notifications.json | 3 +++ public/language/fi/admin/settings/user.json | 2 +- public/language/fi/category.json | 7 +++++-- public/language/fi/notifications.json | 3 +++ public/language/fr/admin/settings/user.json | 2 +- public/language/fr/category.json | 7 +++++-- public/language/fr/notifications.json | 3 +++ public/language/gl/admin/settings/user.json | 2 +- public/language/gl/category.json | 7 +++++-- public/language/gl/notifications.json | 3 +++ public/language/he/admin/settings/user.json | 2 +- public/language/he/category.json | 7 +++++-- public/language/he/notifications.json | 3 +++ public/language/hr/admin/settings/user.json | 2 +- public/language/hr/category.json | 7 +++++-- public/language/hr/notifications.json | 3 +++ public/language/hu/admin/settings/user.json | 2 +- public/language/hu/category.json | 7 +++++-- public/language/hu/notifications.json | 3 +++ public/language/hy/admin/settings/user.json | 2 +- public/language/hy/category.json | 7 +++++-- public/language/hy/notifications.json | 3 +++ public/language/id/admin/settings/user.json | 2 +- public/language/id/category.json | 7 +++++-- public/language/id/notifications.json | 3 +++ public/language/it/admin/settings/user.json | 2 +- public/language/it/category.json | 7 +++++-- public/language/it/notifications.json | 3 +++ public/language/ja/admin/settings/user.json | 2 +- public/language/ja/category.json | 7 +++++-- public/language/ja/notifications.json | 3 +++ public/language/ko/admin/settings/user.json | 2 +- public/language/ko/category.json | 7 +++++-- public/language/ko/notifications.json | 3 +++ public/language/lt/admin/settings/user.json | 2 +- public/language/lt/category.json | 7 +++++-- public/language/lt/notifications.json | 3 +++ public/language/lv/admin/settings/user.json | 2 +- public/language/lv/category.json | 7 +++++-- public/language/lv/notifications.json | 3 +++ public/language/ms/admin/settings/user.json | 2 +- public/language/ms/category.json | 7 +++++-- public/language/ms/notifications.json | 3 +++ public/language/nb/admin/settings/user.json | 2 +- public/language/nb/category.json | 7 +++++-- public/language/nb/notifications.json | 3 +++ public/language/nl/admin/settings/user.json | 2 +- public/language/nl/category.json | 7 +++++-- public/language/nl/notifications.json | 3 +++ public/language/pl/admin/settings/user.json | 2 +- public/language/pl/category.json | 7 +++++-- public/language/pl/notifications.json | 3 +++ public/language/pt-BR/admin/settings/user.json | 2 +- public/language/pt-BR/category.json | 7 +++++-- public/language/pt-BR/notifications.json | 3 +++ public/language/pt-PT/admin/settings/user.json | 2 +- public/language/pt-PT/category.json | 7 +++++-- public/language/pt-PT/notifications.json | 3 +++ public/language/ro/admin/settings/user.json | 2 +- public/language/ro/category.json | 7 +++++-- public/language/ro/notifications.json | 3 +++ public/language/ru/admin/settings/user.json | 2 +- public/language/ru/category.json | 7 +++++-- public/language/ru/notifications.json | 3 +++ public/language/rw/admin/settings/user.json | 2 +- public/language/rw/category.json | 7 +++++-- public/language/rw/notifications.json | 3 +++ public/language/sc/admin/settings/user.json | 2 +- public/language/sc/category.json | 7 +++++-- public/language/sc/notifications.json | 3 +++ public/language/sk/admin/settings/user.json | 2 +- public/language/sk/category.json | 7 +++++-- public/language/sk/notifications.json | 3 +++ public/language/sl/admin/settings/user.json | 2 +- public/language/sl/category.json | 7 +++++-- public/language/sl/notifications.json | 3 +++ public/language/sq-AL/admin/settings/user.json | 2 +- public/language/sq-AL/category.json | 7 +++++-- public/language/sq-AL/notifications.json | 3 +++ public/language/sr/admin/settings/user.json | 2 +- public/language/sr/category.json | 7 +++++-- public/language/sr/notifications.json | 3 +++ public/language/sv/admin/settings/user.json | 2 +- public/language/sv/category.json | 7 +++++-- public/language/sv/notifications.json | 3 +++ public/language/th/admin/settings/user.json | 2 +- public/language/th/category.json | 7 +++++-- public/language/th/notifications.json | 3 +++ public/language/tr/admin/settings/user.json | 2 +- public/language/tr/category.json | 7 +++++-- public/language/tr/notifications.json | 3 +++ public/language/uk/admin/settings/user.json | 2 +- public/language/uk/category.json | 7 +++++-- public/language/uk/notifications.json | 3 +++ public/language/vi/admin/settings/user.json | 2 +- public/language/vi/category.json | 7 +++++-- public/language/vi/notifications.json | 3 +++ public/language/zh-CN/admin/settings/user.json | 2 +- public/language/zh-CN/category.json | 7 +++++-- public/language/zh-CN/notifications.json | 3 +++ public/language/zh-TW/admin/settings/user.json | 2 +- public/language/zh-TW/category.json | 7 +++++-- public/language/zh-TW/notifications.json | 3 +++ 138 files changed, 414 insertions(+), 138 deletions(-) diff --git a/public/language/ar/admin/settings/user.json b/public/language/ar/admin/settings/user.json index c4a57b64ce..6bded6c2b8 100644 --- a/public/language/ar/admin/settings/user.json +++ b/public/language/ar/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/ar/category.json b/public/language/ar/category.json index 0a7345879d..24a54025a1 100644 --- a/public/language/ar/category.json +++ b/public/language/ar/category.json @@ -10,12 +10,15 @@ "watch": "تابع", "ignore": "تجاهل", "watching": "متابع", + "tracking": "Tracking", "not-watching": "لست متابع", "ignoring": "متجاهل", - "watching.description": "أظهر المواضيع في الغير مقروء و الحديث", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "الأقسام المُتابعة", diff --git a/public/language/ar/notifications.json b/public/language/ar/notifications.json index 4e5512f94b..2dea48c500 100644 --- a/public/language/ar/notifications.json +++ b/public/language/ar/notifications.json @@ -13,6 +13,7 @@ "all": "الكل", "topics": "مواضيع", "tags": "Tags", + "categories": "Categories", "replies": "ردود", "chat": "محادثات", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 صار يتابعك.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "عندما يوافقك احدهم على منشورك", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/bg/admin/settings/user.json b/public/language/bg/admin/settings/user.json index 1cf5236a6c..79c5741418 100644 --- a/public/language/bg/admin/settings/user.json +++ b/public/language/bg/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Следване на темите, на които отговаряте", "default-notification-settings": "Настройки по подразбиране за известията", "categoryWatchState": "Състояние по подразбиране за следенето на категории", - "categoryWatchState.watching": "Да се следят", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Да не се следят", "categoryWatchState.ignoring": "Да се пренебрегват" } diff --git a/public/language/bg/category.json b/public/language/bg/category.json index a3a26eb40d..c2b15210f8 100644 --- a/public/language/bg/category.json +++ b/public/language/bg/category.json @@ -10,12 +10,15 @@ "watch": "Следене", "ignore": "Пренебрегване", "watching": "Следите", + "tracking": "Tracking", "not-watching": "Не следите", "ignoring": "Пренебрегвате", - "watching.description": "Темите да се показват в непрочетените и скорошните", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Темите да не се показват в непрочетените, а само в скорошните", - "ignoring.description": "Темите да не се показват нито в непрочетените, нито в скорошните", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Вече следите новите неща в категорията и подкатегориите ѝ", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Вече не следите новите неща в категорията и подкатегориите ѝ", "ignoring.message": "Вече пренебрегвате новите неща в тази категория и всички нейни подкатегории", "watched-categories": "Следени категории", diff --git a/public/language/bg/notifications.json b/public/language/bg/notifications.json index 2ffa11d10b..53b86760b2 100644 --- a/public/language/bg/notifications.json +++ b/public/language/bg/notifications.json @@ -13,6 +13,7 @@ "all": "Всички", "topics": "Теми", "tags": "Етикети", + "categories": "Categories", "replies": "Отговори", "chat": "Разговори", "group-chat": "Групови разговори", @@ -55,6 +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-started-following-you": "%1 започна да Ви следва.", "user-started-following-you-dual": "%1 и %2 започнаха да Ви следват.", "user-started-following-you-triple": "%1, %2 и %3 започнаха да Ви следват.", @@ -81,6 +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-reply": "Когато бъде публикуван нов отговор в тема, която следвате", "notificationType-post-edit": "Когато бъде редактирана публикация в тема, която следите", "notificationType-follow": "Когато някой започне да Ви следва", diff --git a/public/language/bn/admin/settings/user.json b/public/language/bn/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/bn/admin/settings/user.json +++ b/public/language/bn/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/bn/category.json b/public/language/bn/category.json index 67b4f3c5d6..259bf0e7d6 100644 --- a/public/language/bn/category.json +++ b/public/language/bn/category.json @@ -10,12 +10,15 @@ "watch": "নজর রাখুন", "ignore": "উপেক্ষা করুন", "watching": "দৃশ্যমান", + "tracking": "Tracking", "not-watching": "দেখা হচ্ছে না", "ignoring": "উপেক্ষারত", - "watching.description": "অপঠিত এবং সাম্প্রতিক বিষয়গুলো দেখাও", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "প্রেক্ষিত বিভাগসমূহ", diff --git a/public/language/bn/notifications.json b/public/language/bn/notifications.json index 5921660630..c40359bca7 100644 --- a/public/language/bn/notifications.json +++ b/public/language/bn/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 আপনাকে অনুসরন করা শুরু করেছেন।", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/cs/admin/settings/user.json b/public/language/cs/admin/settings/user.json index a35dddd470..81c4e2f23f 100644 --- a/public/language/cs/admin/settings/user.json +++ b/public/language/cs/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Sledovat témata, na které jste odpověděl", "default-notification-settings": "Nastavení výchozího oznámení", "categoryWatchState": "Stav sledování výchozí kategorie", - "categoryWatchState.watching": "Sledování", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nesleduji", "categoryWatchState.ignoring": "Ignorace" } diff --git a/public/language/cs/category.json b/public/language/cs/category.json index c4837b6795..eb98a2b514 100644 --- a/public/language/cs/category.json +++ b/public/language/cs/category.json @@ -10,12 +10,15 @@ "watch": "Sledovat", "ignore": "Ignorovat", "watching": "Sledováno", + "tracking": "Tracking", "not-watching": "Nesledováno", "ignoring": "Ignorováno", - "watching.description": "Zobrazit témata v nepřečtených a posledních", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Nezobrazovat témata v nepřečtených, zobrazit poslední", - "ignoring.description": "Nezobrazovat témata v nepřečtených a posledních", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Nyní sledujete aktualizace pro tuto kategorii a všech podkategorii", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Nyní nesledujete aktualizace z této kategorie a všech podkategorií", "ignoring.message": "Nyní ignorujete aktualizace této kategorie a všech jejich kategorii", "watched-categories": "Sledované kategorie", diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json index da8fc30c1a..dc1f11eaf7 100644 --- a/public/language/cs/notifications.json +++ b/public/language/cs/notifications.json @@ -13,6 +13,7 @@ "all": "Vše", "topics": "Témata", "tags": "Tags", + "categories": "Categories", "replies": "Odpovědi", "chat": "Konverzace", "group-chat": "Skupinová konverzace", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 vás začal sledovat.", "user-started-following-you-dual": "%1 a %2 vás začali sledovat.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Jakmile někdo vyjádří souhlas s vaším příspěvkem", "notificationType-new-topic": "Jakmile někdo koho sledujete vytvoří nové téma", "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-reply": "Jakmile je přidán nový příspěvek v tématu, které sledujete", "notificationType-post-edit": "Jakmile je upraven příspěvek v tématu, které sledujete", "notificationType-follow": "Jakmile vás někdo začne sledovat", diff --git a/public/language/da/admin/settings/user.json b/public/language/da/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/da/admin/settings/user.json +++ b/public/language/da/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/da/category.json b/public/language/da/category.json index 7af45d9980..3c8ed6132d 100644 --- a/public/language/da/category.json +++ b/public/language/da/category.json @@ -10,12 +10,15 @@ "watch": "Overvåg", "ignore": "Ignorer", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Fulgte kategorier", diff --git a/public/language/da/notifications.json b/public/language/da/notifications.json index 1efb8708d2..68a591ebdd 100644 --- a/public/language/da/notifications.json +++ b/public/language/da/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 har valgt at følge dig.", "user-started-following-you-dual": "%1 og %2 har valgt at følge dig.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/de/admin/settings/user.json b/public/language/de/admin/settings/user.json index 7610a312eb..7372f39fb7 100644 --- a/public/language/de/admin/settings/user.json +++ b/public/language/de/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Themen folgen, auf die du antwortest", "default-notification-settings": "Standardbenachrichtigungseinstellungen", "categoryWatchState": "Standardmäßige Beobachtung", - "categoryWatchState.watching": "Beobachtet", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nicht beobachtet", "categoryWatchState.ignoring": "Ignoriert" } diff --git a/public/language/de/category.json b/public/language/de/category.json index 9ac3360370..da7d6d33c7 100644 --- a/public/language/de/category.json +++ b/public/language/de/category.json @@ -10,12 +10,15 @@ "watch": "Beobachten", "ignore": "Ignorieren", "watching": "Beobachte", + "tracking": "Tracking", "not-watching": "Nicht beobachtet", "ignoring": "Ignoriert", - "watching.description": "Zeige Themen in Ungelesen und Aktuell", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Zeige keine Themen in Ungelesen, zeige sie in Aktuell", - "ignoring.description": "Zeige keine Themen in Ungelesen und Aktuell", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Du beobachtest jetzt Aktualisierungen aus dieser Kategorie und allen Unterkategorien", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Du beobachtest jetzt keine Aktualisierungen aus dieser Kategorie und allen Unterkategorien", "ignoring.message": "Du ignorierst jetzt Aktualisierungen aus dieser Kategorie und allen Unterkategorien", "watched-categories": "Beobachtete Kategorien", diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json index 0fe3b1354d..2e4dfcab7a 100644 --- a/public/language/de/notifications.json +++ b/public/language/de/notifications.json @@ -13,6 +13,7 @@ "all": "Alle", "topics": "Themen", "tags": "Tags", + "categories": "Categories", "replies": "Antworten", "chat": "Chats", "group-chat": "Gruppenchats", @@ -55,6 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 hat ein neues Thema mit den Tags %2 and %3 erstellt", "user-posted-topic-with-tag-triple": "%1 hat ein neues Thema mit den Tags %2, %3 und %4 erstellt", "user-posted-topic-with-tag-multiple": "%1 hat ein neues Thema mit den Tags %2 erstellt", + "user-posted-topic-in-category": "%1 has posted a new topic in %2", "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.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Wenn jemand deinen beitrag positiv bewertet", "notificationType-new-topic": "Wenn jemand, dem du folgst, einen Beitrag erstellt", "notificationType-new-topic-with-tag": "Wenn ein Thema mit einem Tag gepostet wird, dem du folgst", + "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", "notificationType-new-reply": "Wenn es eine neue Antwort auf ein Thema das du beobachtest gibt", "notificationType-post-edit": "Wenn ein Post bearbeitet wurde, in einem Thema welches du beobachtest", "notificationType-follow": "Wenn dir jemand neues folgt", diff --git a/public/language/el/admin/settings/user.json b/public/language/el/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/el/admin/settings/user.json +++ b/public/language/el/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/el/category.json b/public/language/el/category.json index eacdb65d4c..52cb9e2f8a 100644 --- a/public/language/el/category.json +++ b/public/language/el/category.json @@ -10,12 +10,15 @@ "watch": "Παρακολουθήστε", "ignore": "Αγνόηση", "watching": "Παρακολουθώ", + "tracking": "Tracking", "not-watching": "Δεν παρακολουθώ", "ignoring": "Αγνόησε", - "watching.description": "Εμφάνιση θεμάτων σε μη αναγνωσμένα και πρόσφατα", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Να μην εμφανίζονται θέματα σε μη αναγνωσμένα, να εμφανίζονται σε πρόσφατα", - "ignoring.description": "Να μην εμφανίζονται θέματα σε μη αναγνωσμένα και πρόσφατα", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Παρακολουθείτε τώρα ενημερώσεις από αυτήν την κατηγορία και όλες τις υποκατηγορίες", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Δεν παρακολουθείτε ενημερώσεις από αυτήν την κατηγορία και όλες τις υποκατηγορίες", "ignoring.message": "Τώρα αγνοείτε ενημερώσεις από αυτήν την κατηγορία και όλες τις υποκατηγορίες", "watched-categories": "Κατηγορίες υπό παρακολούθηση", diff --git a/public/language/el/notifications.json b/public/language/el/notifications.json index e2b5a4f89a..8043fa57da 100644 --- a/public/language/el/notifications.json +++ b/public/language/el/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 started following you.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/en-US/admin/settings/user.json b/public/language/en-US/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/en-US/admin/settings/user.json +++ b/public/language/en-US/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/en-US/category.json b/public/language/en-US/category.json index 37480c3507..8b1e83e8b3 100644 --- a/public/language/en-US/category.json +++ b/public/language/en-US/category.json @@ -10,12 +10,15 @@ "watch": "Watch", "ignore": "Ignore", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories", diff --git a/public/language/en-US/notifications.json b/public/language/en-US/notifications.json index 04414a883a..02125c4009 100644 --- a/public/language/en-US/notifications.json +++ b/public/language/en-US/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 started following you.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/en-x-pirate/admin/settings/user.json b/public/language/en-x-pirate/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/en-x-pirate/admin/settings/user.json +++ b/public/language/en-x-pirate/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/en-x-pirate/category.json b/public/language/en-x-pirate/category.json index 0d41a99de2..3ee2e842f9 100644 --- a/public/language/en-x-pirate/category.json +++ b/public/language/en-x-pirate/category.json @@ -10,12 +10,15 @@ "watch": "Be watchin'", "ignore": "Be ignorin'", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Categories ye be watchin'", diff --git a/public/language/en-x-pirate/notifications.json b/public/language/en-x-pirate/notifications.json index 9e636088a6..975f837cd7 100644 --- a/public/language/en-x-pirate/notifications.json +++ b/public/language/en-x-pirate/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 started following you.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/es/admin/settings/user.json b/public/language/es/admin/settings/user.json index 1c23a2102c..483a8e942e 100644 --- a/public/language/es/admin/settings/user.json +++ b/public/language/es/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Seguir los temas a los que contestas", "default-notification-settings": "Configuración de notificación por defecto", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Vigilando ", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignorando" } diff --git a/public/language/es/category.json b/public/language/es/category.json index d2ef1e57a4..8ef18c632f 100644 --- a/public/language/es/category.json +++ b/public/language/es/category.json @@ -10,12 +10,15 @@ "watch": "Seguir", "ignore": "Ignorar", "watching": "Siguiendo", + "tracking": "Tracking", "not-watching": "No siguiendo", "ignoring": "Ignorando", - "watching.description": "Mostrar temas en no leídos y recientes", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "No mostrar temas en no leído, mostrar en reciente", - "ignoring.description": "No mostrar temas en no leídos y recientes.", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Ahora estás viendo las actualizaciones de esta categoría y todas las subcategorías", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "No estás viendo las actualizaciones de esta categoría y todas las subcategorías.", "ignoring.message": "Ahora estás ignorando las actualizaciones de esta categoría y todas las subcategorías", "watched-categories": "Categorías seguidas", diff --git a/public/language/es/notifications.json b/public/language/es/notifications.json index 23badc8831..16d514cb9c 100644 --- a/public/language/es/notifications.json +++ b/public/language/es/notifications.json @@ -13,6 +13,7 @@ "all": "Todo", "topics": "Temas", "tags": "Tags", + "categories": "Categories", "replies": "Respuestas", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 comenzó a seguirte.", "user-started-following-you-dual": "%1 y %2 comenzaron a seguirte.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Cuando alguien vota positivamente en tu entrada", "notificationType-new-topic": "Cuando alguien a quien sigues comenta en un tema", "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-reply": "Cuando hay una respuesta nueva en un tema que estás viendo", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "Cuando alguien comienza a seguirte", diff --git a/public/language/et/admin/settings/user.json b/public/language/et/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/et/admin/settings/user.json +++ b/public/language/et/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/et/category.json b/public/language/et/category.json index 66b46ad60d..c7ca3a30d0 100644 --- a/public/language/et/category.json +++ b/public/language/et/category.json @@ -10,12 +10,15 @@ "watch": "Vaata", "ignore": "Ignoreeri", "watching": "Vaatab", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoreerib", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Jälgitavad kategooriad", diff --git a/public/language/et/notifications.json b/public/language/et/notifications.json index e75f9507c3..a4dd123e07 100644 --- a/public/language/et/notifications.json +++ b/public/language/et/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 hakkas sind jälgima.", "user-started-following-you-dual": "%1 ja %2 hakkasid sind jälgima.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/fa-IR/admin/settings/user.json b/public/language/fa-IR/admin/settings/user.json index def56feb2e..b589f0575f 100644 --- a/public/language/fa-IR/admin/settings/user.json +++ b/public/language/fa-IR/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "دنبال‌ کردن تاپیک‌هایی که شما در آن پست گذاشتین ", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/fa-IR/category.json b/public/language/fa-IR/category.json index da2638fb2f..e6d045ecb1 100644 --- a/public/language/fa-IR/category.json +++ b/public/language/fa-IR/category.json @@ -10,12 +10,15 @@ "watch": "پیگیری", "ignore": "نادیده گرفتن", "watching": "درحال پیگیری", + "tracking": "Tracking", "not-watching": "درحال پیگیری نیستید", "ignoring": "در حال نادیده گرفتن", - "watching.description": "موضوع ها را در بخش نخوانده ها و تازه ها نشان بده", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "موضوع ها را در بخش نخوانده ها نمایش نده و در بخش تازه ها نشان بده", - "ignoring.description": "موضوع ها را در بخش نخوانده ها و تازه ها نمایش نده", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "اکنون شما به‌روزرسانی‌های این دسته و تمام زیر دسته‌هایش را پیگیری می‌کنید", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "اکنون شما به‌روزرسانی‌های این دسته و همه‌ی زیر دسته‌هایش را پیگیری نمی کنید", "ignoring.message": "اکنون شما به‌روزرسانی‌های این دسته و همه‌ی زیر دسته‌هایش را نادیده گرفته‌اید", "watched-categories": "دسته بندی های پیگیری شده", diff --git a/public/language/fa-IR/notifications.json b/public/language/fa-IR/notifications.json index 43402f0703..012fd6cbd2 100644 --- a/public/language/fa-IR/notifications.json +++ b/public/language/fa-IR/notifications.json @@ -13,6 +13,7 @@ "all": "همه", "topics": "موضوع ها", "tags": "Tags", + "categories": "Categories", "replies": "پاسخ ها", "chat": "گفتگو ها", "group-chat": "چت‌های گروهی", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 شروع به دنبال کردن شما کرده", "user-started-following-you-dual": "%1 و %2 شروع به دنبال کردن شما کرده.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "هنگامی که شخصی به پست شما رای مثبت می دهد", "notificationType-new-topic": "هنگامی که شخصی که شما دنبال می کنید موضوعی ایجاد نماید", "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-reply": "هنگامی که پاسخ جدید در موضوعی که شما پیگیری می کنید فرستاده می شود", "notificationType-post-edit": "وقتی در موضوعی که شما پیگیری می کنید پستی ویرایش می شود", "notificationType-follow": "هنگامی که کسی شما را دنبال می کند", diff --git a/public/language/fi/admin/settings/user.json b/public/language/fi/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/fi/admin/settings/user.json +++ b/public/language/fi/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/fi/category.json b/public/language/fi/category.json index 243574e35f..26bd42b07e 100644 --- a/public/language/fi/category.json +++ b/public/language/fi/category.json @@ -10,12 +10,15 @@ "watch": "Seuraa", "ignore": "Sivuuta", "watching": "Seurataan", + "tracking": "Tracking", "not-watching": "Älä seuraa", "ignoring": "Ohita", - "watching.description": "Näytä aiheet lukemattomissa ja viimeisimmissä", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Älä näytä aiheita lukemattomissa, näytä viimeisimmissä", - "ignoring.description": "Älä näytä aiheita lukemattomissa ja viimeisimmissä", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Seuraat nyt päivityksiä tästä kategoriasta ja kaikista alikategorioista", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Et seuraa päivityksiää tästä kategoriasta tai alikategorioista", "ignoring.message": "Ohitat kaikki päivitykset tästä kategoriasta ja kaikista alikategorioista", "watched-categories": "Seuratut kategoriat", diff --git a/public/language/fi/notifications.json b/public/language/fi/notifications.json index 44879cb6c4..44df935d3b 100644 --- a/public/language/fi/notifications.json +++ b/public/language/fi/notifications.json @@ -13,6 +13,7 @@ "all": "Kaikki", "topics": "Aiheet", "tags": "Tags", + "categories": "Categories", "replies": "Vastaukset", "chat": "Keskustelut", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 alkoi seurata sinua.", "user-started-following-you-dual": "%1 ja %2 alkoivat seurata sinua", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Kun joku tykkää viestistäsi", "notificationType-new-topic": "Kun joku seuraa viestejäsi aiheessa", "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-reply": "Kun uusi vastaus on lähetetty aiheeseen, jota seuraat", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "Kun joku alkaa seurata sinua", diff --git a/public/language/fr/admin/settings/user.json b/public/language/fr/admin/settings/user.json index b49f53701e..fc105213e5 100644 --- a/public/language/fr/admin/settings/user.json +++ b/public/language/fr/admin/settings/user.json @@ -79,7 +79,7 @@ "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.watching": "Abonné", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Non abonné", "categoryWatchState.ignoring": "Ignoré" } diff --git a/public/language/fr/category.json b/public/language/fr/category.json index 8dcf7f570d..679de3c00d 100644 --- a/public/language/fr/category.json +++ b/public/language/fr/category.json @@ -10,12 +10,15 @@ "watch": "S'abonner", "ignore": "Ne plus surveiller", "watching": "Suivi", + "tracking": "Tracking", "not-watching": "Ne plus suivre", "ignoring": "Ignoré", - "watching.description": "Afficher les sujets non lus et récents", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Ne pas afficher les sujets non lus, afficher les récents", - "ignoring.description": "Ne pas afficher les sujets non lus et récents", + "ignoring.description": "Do not show topics in unread & recent", "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", "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/notifications.json b/public/language/fr/notifications.json index 086f5be176..7eca6d7a16 100644 --- a/public/language/fr/notifications.json +++ b/public/language/fr/notifications.json @@ -13,6 +13,7 @@ "all": "Tout", "topics": "Sujets", "tags": "Mots-clés", + "categories": "Categories", "replies": "Réponses", "chat": "Discussions", "group-chat": "Groupe de discussions", @@ -55,6 +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-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.", @@ -81,6 +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-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/gl/admin/settings/user.json b/public/language/gl/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/gl/admin/settings/user.json +++ b/public/language/gl/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/gl/category.json b/public/language/gl/category.json index 962fcac276..ed21644e65 100644 --- a/public/language/gl/category.json +++ b/public/language/gl/category.json @@ -10,12 +10,15 @@ "watch": "Vixiar", "ignore": "Ignorar", "watching": "Seguindo", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignorando", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Categorías vixiadas", diff --git a/public/language/gl/notifications.json b/public/language/gl/notifications.json index d9400e534f..a4511bc9b9 100644 --- a/public/language/gl/notifications.json +++ b/public/language/gl/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 comezou a seguirte.", "user-started-following-you-dual": "%1 e %2 comezaron a seguirte.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/he/admin/settings/user.json b/public/language/he/admin/settings/user.json index 10dfebbca3..3f5eb95978 100644 --- a/public/language/he/admin/settings/user.json +++ b/public/language/he/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "עקוב אחר נושאים שהגבת עליהם", "default-notification-settings": "הגדרות התראות ברירת מחדל", "categoryWatchState": "מצב מעקב על קטגוריה בברירת מחדל", - "categoryWatchState.watching": "עוקב", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "לא עוקב", "categoryWatchState.ignoring": "מתעלם" } diff --git a/public/language/he/category.json b/public/language/he/category.json index 93209bc7c7..30108704ea 100644 --- a/public/language/he/category.json +++ b/public/language/he/category.json @@ -10,12 +10,15 @@ "watch": "עקוב", "ignore": "התעלם", "watching": "עוקב", + "tracking": "Tracking", "not-watching": "לא עוקב", "ignoring": "מתעלם", - "watching.description": "הצג נושאים בנושאים שלא נקראו ובנושאים אחרונים", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "הסתר בנושאים שלא נקראו, הצג בנושאים אחרונים", - "ignoring.description": "הסתר נושאים בנושאים שלא נקראו ובנושאים אחרונים", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "בחרת לעקוב אחר עדכונים בקטגוריה זו וכל תת-הקטגוריות", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "בחרת לא לעקוב אחר עדכונים בקטגוריה זו וכל תת-הקטגוריות", "ignoring.message": "בחרת להתעלם מעדכונים בקטגוריה זו וכל תת-הקטגוריות", "watched-categories": "קטגוריות במעקב", diff --git a/public/language/he/notifications.json b/public/language/he/notifications.json index 189bbf9c7e..bc99828297 100644 --- a/public/language/he/notifications.json +++ b/public/language/he/notifications.json @@ -13,6 +13,7 @@ "all": "הכל", "topics": "נושאים", "tags": "תגיות", + "categories": "Categories", "replies": "תגובות", "chat": "צ'אטים", "group-chat": "צ'אט קבוצתי", @@ -55,6 +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-started-following-you": "%1 התחיל לעקוב אחריך.", "user-started-following-you-dual": "%1 ו-%2 התחילו לעקוב אחריך.", "user-started-following-you-triple": "%1, %2 ו3% התחילו לעקוב אחריך.", @@ -81,6 +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-reply": "כשתגובה חדשה מפורסמת בנושא שאתה עוקב אחריו", "notificationType-post-edit": "כשפוסט נערך בנושא שאתה עוקב אחריו", "notificationType-follow": "כשמישהו מתחיל לעקוב אחריך", diff --git a/public/language/hr/admin/settings/user.json b/public/language/hr/admin/settings/user.json index 46e92a6fb5..2875386274 100644 --- a/public/language/hr/admin/settings/user.json +++ b/public/language/hr/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Prati teme na koje odgovorim", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/hr/category.json b/public/language/hr/category.json index c82ef101b4..73726a84c7 100644 --- a/public/language/hr/category.json +++ b/public/language/hr/category.json @@ -10,12 +10,15 @@ "watch": "Prati", "ignore": "Ignoriraj", "watching": "Pratim", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoriram", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Praćene Kategorije", diff --git a/public/language/hr/notifications.json b/public/language/hr/notifications.json index fbe22b2ece..8433cb789b 100644 --- a/public/language/hr/notifications.json +++ b/public/language/hr/notifications.json @@ -13,6 +13,7 @@ "all": "Sve", "topics": "Teme", "tags": "Tags", + "categories": "Categories", "replies": "Odgovori", "chat": "Razgovori", "group-chat": "Grupni Chat", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 Vas sada prati.", "user-started-following-you-dual": "%1 i %2 vas sada prate.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Kada netko ocijeni vašu objavi", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/hu/admin/settings/user.json b/public/language/hu/admin/settings/user.json index 405f4ca0d6..48ddff6652 100644 --- a/public/language/hu/admin/settings/user.json +++ b/public/language/hu/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Minden témakör követése, amire válaszoltál", "default-notification-settings": "Alapértelmezett értesítési beállítások", "categoryWatchState": "Alapértelmezett kategóriafigyelés", - "categoryWatchState.watching": "Figyelés", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nem megfigyelt", "categoryWatchState.ignoring": "Mellőzés" } diff --git a/public/language/hu/category.json b/public/language/hu/category.json index ed02189dda..e58125b3f1 100644 --- a/public/language/hu/category.json +++ b/public/language/hu/category.json @@ -10,12 +10,15 @@ "watch": "Figyelés", "ignore": "Mellőzés", "watching": "Figyelés", + "tracking": "Tracking", "not-watching": "Nem megfigyelt", "ignoring": "Mellőzés", - "watching.description": "Témakörök mutatása a friss és olvasatlanok között", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Olvasatlan témakörök elrejtése, csak a friss témák mutatása", - "ignoring.description": "Olvasatlan és friss témakörök elrejtése", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Most már figyeled ennek a kategóriának és az alkategóriáinak a frissítéseit", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Nem figyeled ennek a kategóriának és alkategóriáinak frissítéseit", "ignoring.message": "Nem kapsz most már frissítéseket erről a kategóriáról és az alkategóriáiról", "watched-categories": "Figyelt kategóriák", diff --git a/public/language/hu/notifications.json b/public/language/hu/notifications.json index f85fd35c5d..43eeda6b5e 100644 --- a/public/language/hu/notifications.json +++ b/public/language/hu/notifications.json @@ -13,6 +13,7 @@ "all": "Mind", "topics": "Témakör", "tags": "Tags", + "categories": "Categories", "replies": "Válasz", "chat": "Chat", "group-chat": "Csoport Csevegők", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 elkezdett követni téged.", "user-started-following-you-dual": "%1 és%2 elkezdett követni téged.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Mikor valaki kedveli a hozzászólásod", "notificationType-new-topic": "Mikor egy követett felhasználód hozzászól", "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-reply": "Mikor egy általad figyelt témakörre válasz érkezik", "notificationType-post-edit": "Mikor egy a megfigyelt témakörön belül módosítanak egy bejegyzést", "notificationType-follow": "Mikor valaki elkezd követni téged", diff --git a/public/language/hy/admin/settings/user.json b/public/language/hy/admin/settings/user.json index aab6156aa3..302e9c06b8 100644 --- a/public/language/hy/admin/settings/user.json +++ b/public/language/hy/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Հետևեք այն թեմաներին, որոնց պատասխանում եք", "default-notification-settings": "Հիմնական ծանուցման կարգավորումներ", "categoryWatchState": "Հիմնական կատեգորիայի դիտման վիճակը", - "categoryWatchState.watching": "Դիտում ", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Չեն դիտում ", "categoryWatchState.ignoring": "Անտեսել " } diff --git a/public/language/hy/category.json b/public/language/hy/category.json index 25be87feea..e63f73702e 100644 --- a/public/language/hy/category.json +++ b/public/language/hy/category.json @@ -10,12 +10,15 @@ "watch": "Դիտել", "ignore": "Անտեսել", "watching": "Դիտում", + "tracking": "Tracking", "not-watching": "Չեն դիտում", "ignoring": "Անտեսել", - "watching.description": "Ցույց տալ թեմաները չկարդացված և վերջին բաժնում", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Չընթերցված թեմաները չցուցադրել, ցուցադրել վերջինները", - "ignoring.description": "Մի ցուցադրեք թեմաները չընթերցված և վերջին բաժնում", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Դուք այժմ դիտում եք թարմացումներ այս կատեգորիայից և բոլոր ենթակատեգորիաներից", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Դուք չեք դիտում այս կատեգորիայի և բոլոր ենթակատեգորիաների թարմացումները", "ignoring.message": "Դուք այժմ անտեսում եք այս կատեգորիայի և բոլոր ենթակատեգորիաների թարմացումները", "watched-categories": "Դիտված կատեգորիաներ", diff --git a/public/language/hy/notifications.json b/public/language/hy/notifications.json index 4cd7a6b7a4..104e391cfc 100644 --- a/public/language/hy/notifications.json +++ b/public/language/hy/notifications.json @@ -13,6 +13,7 @@ "all": "Բոլորը", "topics": "Թեմաներ", "tags": "Պիտակներ", + "categories": "Categories", "replies": "Պատասխաններ", "chat": "Զրույցներ", "group-chat": "Խմբային զրույցներ", @@ -55,6 +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-started-following-you": "%1 սկսեց հետևել ձեզ", "user-started-following-you-dual": "%1 և %2 սկսեցին հետևել ձեզ:", "user-started-following-you-triple": "%1, %2 և %3 սկսել են հետևել Ձեզ։", @@ -81,6 +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-reply": "Երբ ձեր դիտած թեմայում տեղադրվում է նոր պատասխան", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "Երբ ինչ-որ մեկը սկսում է հետևել քեզ", diff --git a/public/language/id/admin/settings/user.json b/public/language/id/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/id/admin/settings/user.json +++ b/public/language/id/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/id/category.json b/public/language/id/category.json index 6f861ec405..3acd7c111e 100644 --- a/public/language/id/category.json +++ b/public/language/id/category.json @@ -10,12 +10,15 @@ "watch": "mengamati", "ignore": "Abaikan", "watching": "mengamati", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Abaikan", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Kategori yang diamati", diff --git a/public/language/id/notifications.json b/public/language/id/notifications.json index 7c5955b077..4b6259caba 100644 --- a/public/language/id/notifications.json +++ b/public/language/id/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 mulai mengikutimu.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/it/admin/settings/user.json b/public/language/it/admin/settings/user.json index 007b2120ac..769e9a712b 100644 --- a/public/language/it/admin/settings/user.json +++ b/public/language/it/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Segui discussioni a cui rispondi tu", "default-notification-settings": "Impostazioni di notifica predefinite", "categoryWatchState": "Stato predefinito della categoria di controllo", - "categoryWatchState.watching": "Seguito", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Non seguito", "categoryWatchState.ignoring": "Ignorato" } diff --git a/public/language/it/category.json b/public/language/it/category.json index c141fa95dc..973e1632a0 100644 --- a/public/language/it/category.json +++ b/public/language/it/category.json @@ -10,12 +10,15 @@ "watch": "Segui", "ignore": "Ignora", "watching": "Seguito", + "tracking": "Tracking", "not-watching": "Non seguito", "ignoring": "Ignorato", - "watching.description": "Mostra discussioni in non letti e recenti", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Non mostrare discussioni in non letti, mostra in recenti", - "ignoring.description": "Non mostrare discussioni in non letti e recenti", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Ora stai seguendo gli aggiornamenti di questa categoria e di tutte le sottocategorie", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Ora non stai seguendo gli aggiornamenti di questa categoria e di tutte le sottocategorie", "ignoring.message": "Ora stai ignorando gli aggiornamenti di questa categoria e di tutte le sottocategorie", "watched-categories": "Categorie seguite", diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index 927212a623..d84967a764 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -13,6 +13,7 @@ "all": "Tutte", "topics": "Discussioni", "tags": "Tag", + "categories": "Categories", "replies": "Risposte", "chat": "Chat", "group-chat": "Chat di gruppo", @@ -55,6 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 ha postato una nuova discussione con i tag %2 e %3", "user-posted-topic-with-tag-triple": "%1 ha postato una nuova discussione con e tag %2, %3 e %4", "user-posted-topic-with-tag-multiple": "%1 ha postato una nuova discussione con i tag %2", + "user-posted-topic-in-category": "%1 has posted a new topic in %2", "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.", @@ -81,6 +83,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": "When a topic is posted in a category you are watching", "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", diff --git a/public/language/ja/admin/settings/user.json b/public/language/ja/admin/settings/user.json index f42f0212e9..4df1099cff 100644 --- a/public/language/ja/admin/settings/user.json +++ b/public/language/ja/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "返信したスレッドをフォローします", "default-notification-settings": "デフォルトの通知設定", "categoryWatchState": "デフォルトのカテゴリウォッチ状態", - "categoryWatchState.watching": "ウォッチ中", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未ウォッチ", "categoryWatchState.ignoring": "無視中" } diff --git a/public/language/ja/category.json b/public/language/ja/category.json index a7b4a6ed57..320ff70d34 100644 --- a/public/language/ja/category.json +++ b/public/language/ja/category.json @@ -10,12 +10,15 @@ "watch": "ウォッチする", "ignore": "無視する", "watching": "ウォッチ中", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "無視中", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "ウォッチ中のカテゴリ", diff --git a/public/language/ja/notifications.json b/public/language/ja/notifications.json index c618a066cc..ab09acafd9 100644 --- a/public/language/ja/notifications.json +++ b/public/language/ja/notifications.json @@ -13,6 +13,7 @@ "all": "全て", "topics": "スレッド", "tags": "Tags", + "categories": "Categories", "replies": "返信", "chat": "チャット", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1があなたをフォローしました。", "user-started-following-you-dual": "%1%2 があなたをフォローしました。", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "誰かがあなたの投稿を評価したとき", "notificationType-new-topic": "フォロワーがスレッドを投稿したとき", "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-reply": "あなたが見ているトピックに新しい返信が投稿されたとき", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "誰かがあなたをフォローしたとき", diff --git a/public/language/ko/admin/settings/user.json b/public/language/ko/admin/settings/user.json index a9b451a842..f4d4d12789 100644 --- a/public/language/ko/admin/settings/user.json +++ b/public/language/ko/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "내가 답글을 작성한 화제 팔로우", "default-notification-settings": "기본 알림 설정", "categoryWatchState": "기본 카테고리 관심 상태", - "categoryWatchState.watching": "관심", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "관심 해제", "categoryWatchState.ignoring": "무시" } diff --git a/public/language/ko/category.json b/public/language/ko/category.json index 883a141435..3db57f8689 100644 --- a/public/language/ko/category.json +++ b/public/language/ko/category.json @@ -10,12 +10,15 @@ "watch": "관심 화제", "ignore": "관심 해제", "watching": "관심 카테고리", + "tracking": "Tracking", "not-watching": "관심 해제 카테고리", "ignoring": "카테고리 무시", - "watching.description": "읽지 않음과 최근 목록에 화제 표시", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "읽지 않음 제외, 최근 목록에만 화제 표시", - "ignoring.description": "읽지 않음과 최근 목록에서 화제 제외", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "이 카테고리 및 모든 하위 카테고리를 관심 등록했습니다.", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "이 카테고리 및 모든 하위 카테고리를 관심 해제했습니다.", "ignoring.message": "이 카테고리 및 모든 하위 카테고리를 무시하고 있습니다", "watched-categories": "관심 카테고리", diff --git a/public/language/ko/notifications.json b/public/language/ko/notifications.json index 5f93becf89..747c086d3c 100644 --- a/public/language/ko/notifications.json +++ b/public/language/ko/notifications.json @@ -13,6 +13,7 @@ "all": "모든 알림", "topics": "화제", "tags": "Tags", + "categories": "Categories", "replies": "답글", "chat": "채팅", "group-chat": "그룹 채팅", @@ -55,6 +56,7 @@ "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-started-following-you": "%1님이 나를 팔로우 합니다.", "user-started-following-you-dual": "%1님과 %2님이 나를 팔로우 합니다.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "누군가 내 글을 추천", "notificationType-new-topic": "팔로우 하는 사람이 새로운 화제 작성", "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-reply": "관심 화제에 새로운 답글", "notificationType-post-edit": "관심 화제의 포스트 수정", "notificationType-follow": "누군가 나를 팔로우", diff --git a/public/language/lt/admin/settings/user.json b/public/language/lt/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/lt/admin/settings/user.json +++ b/public/language/lt/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/lt/category.json b/public/language/lt/category.json index ed507c2b9e..2955e922c4 100644 --- a/public/language/lt/category.json +++ b/public/language/lt/category.json @@ -10,12 +10,15 @@ "watch": "Stebėti", "ignore": "Ignoruoti", "watching": "Stebima", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoruojama", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Stebimos kategorijos", diff --git a/public/language/lt/notifications.json b/public/language/lt/notifications.json index 442e88ec23..c212e1b3de 100644 --- a/public/language/lt/notifications.json +++ b/public/language/lt/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 pradėjo sekti tave", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/lv/admin/settings/user.json b/public/language/lv/admin/settings/user.json index 620ab2855c..53e4d0c401 100644 --- a/public/language/lv/admin/settings/user.json +++ b/public/language/lv/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Sekot tematiem, kuros esi rakstījis(-jusi)", "default-notification-settings": "Noklusējuma ziņojumu iestatījumi", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/lv/category.json b/public/language/lv/category.json index 7af1d6a127..c13c0a2508 100644 --- a/public/language/lv/category.json +++ b/public/language/lv/category.json @@ -10,12 +10,15 @@ "watch": "Novērošana", "ignore": "Ignorēt", "watching": "Novērots", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignorēts", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Novērotās kategorijas", diff --git a/public/language/lv/notifications.json b/public/language/lv/notifications.json index 23d3d6816c..ef3573784a 100644 --- a/public/language/lv/notifications.json +++ b/public/language/lv/notifications.json @@ -13,6 +13,7 @@ "all": "Visi", "topics": "Par tematiem", "tags": "Tags", + "categories": "Categories", "replies": "Par atbildēm", "chat": "Par sarunām", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 sāka Tev sekot.", "user-started-following-you-dual": "%1 un %2 sāka Tev sekot.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Kad kāds balso \"par\" Tavu rakstu", "notificationType-new-topic": "Kad kāds, kuru Tu seko, publicē rakstu", "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-reply": "Kad jauna atbilde tiek pievienota tematam, kuru novēro", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "Kad kāds sāk Tev sekot", diff --git a/public/language/ms/admin/settings/user.json b/public/language/ms/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/ms/admin/settings/user.json +++ b/public/language/ms/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/ms/category.json b/public/language/ms/category.json index 1ea5a03723..d967bca72c 100644 --- a/public/language/ms/category.json +++ b/public/language/ms/category.json @@ -10,12 +10,15 @@ "watch": "Melihat", "ignore": "Abai", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Kategori Dilihat", diff --git a/public/language/ms/notifications.json b/public/language/ms/notifications.json index f6b5ea0ad0..a852e75ac5 100644 --- a/public/language/ms/notifications.json +++ b/public/language/ms/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 mula mengikut anda.", "user-started-following-you-dual": "%1 dan %2 mula mengikuti anda.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/nb/admin/settings/user.json b/public/language/nb/admin/settings/user.json index ed62c14a59..a2e72f61d0 100644 --- a/public/language/nb/admin/settings/user.json +++ b/public/language/nb/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Følg tråder du svarer på", "default-notification-settings": "Standard varslingsinnstillinger", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Overvåker", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Overvåker ikke", "categoryWatchState.ignoring": "Ignorerer" } diff --git a/public/language/nb/category.json b/public/language/nb/category.json index 9d86f3c228..a55e20d5ce 100644 --- a/public/language/nb/category.json +++ b/public/language/nb/category.json @@ -10,12 +10,15 @@ "watch": "Overvåk", "ignore": "Ignorer", "watching": "Følger", + "tracking": "Tracking", "not-watching": "Følger ikke", "ignoring": "Ignorerer", - "watching.description": "Vis tråder blandt uleste og nylige", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Ikke vis emner i ulest, vis nylig", - "ignoring.description": "Ikke vis tråder blandt uleste og nylige", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Du ser nå på oppdateringer fra denne kategorien og alle underkategorier", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Du ser ikke på oppdateringer fra denne kategorien og alle underkategorier", "ignoring.message": "Du ignorerer nå oppdateringer fra denne kategorien og alle underkategorier", "watched-categories": "Overvåkede kategorier", diff --git a/public/language/nb/notifications.json b/public/language/nb/notifications.json index f0c64087a2..cccf2fcb23 100644 --- a/public/language/nb/notifications.json +++ b/public/language/nb/notifications.json @@ -13,6 +13,7 @@ "all": "Alle", "topics": "Emner", "tags": "Tags", + "categories": "Categories", "replies": "Svar", "chat": "Samtaler", "group-chat": "Gruppesamtaler", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 begynte å følge deg.", "user-started-following-you-dual": "%1 og 2% har begynt å følge deg.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Når noen stemmer opp innlegget ditt", "notificationType-new-topic": "Når noen du følger følger legger ut et emne", "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-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-follow": "Når noen starter å følge deg", diff --git a/public/language/nl/admin/settings/user.json b/public/language/nl/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/nl/admin/settings/user.json +++ b/public/language/nl/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/nl/category.json b/public/language/nl/category.json index 21db91c403..4efaa05ad3 100644 --- a/public/language/nl/category.json +++ b/public/language/nl/category.json @@ -10,12 +10,15 @@ "watch": "Volgen", "ignore": "Negeren", "watching": "Volgend", + "tracking": "Tracking", "not-watching": "Niet gevolgd", "ignoring": "Negerend", - "watching.description": "Toon ongelezen en recente onderwerpen", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Toon geen ongelezen onderwerpen, toon wel recente onderwerpen", - "ignoring.description": "Toon geen onderwerpen onder ongelezen en recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Van deze categorie en alle sub-categorieën worden nu meldingen ontvangen", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Deze categorie en alle sub-categorieën worden niet gevolgd", "ignoring.message": "Er worden geen meldingen van deze categorie en alle sub-categorieën ontvangen", "watched-categories": "Categorieën die bekeken zijn.", diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json index ccf671dfb4..5a103b7489 100644 --- a/public/language/nl/notifications.json +++ b/public/language/nl/notifications.json @@ -13,6 +13,7 @@ "all": "Alles", "topics": "Onderwerpen", "tags": "Tags", + "categories": "Categories", "replies": "Antwoorden", "chat": "Chats", "group-chat": "Groepsgesprekken", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 volgt jou nu.", "user-started-following-you-dual": "%1 en %2 volgen jou nu.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Als iemand positief stemt voor je bericht", "notificationType-new-topic": "Wanneer iemand die jij volgt een onderwerp post", "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-reply": "Als een nieuwe reactie komt op een onderwerp dat je volgt", "notificationType-post-edit": "Als een bericht wordt aangepast in een onderwerp dat je volgt", "notificationType-follow": "Als iemand begint met jou te volgen", diff --git a/public/language/pl/admin/settings/user.json b/public/language/pl/admin/settings/user.json index 010c356602..b66265b053 100644 --- a/public/language/pl/admin/settings/user.json +++ b/public/language/pl/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Obserwuj tematy, w których się wypowiedziałeś ", "default-notification-settings": "Domyślne ustawienia powiadomień", "categoryWatchState": "Domyślny stan oglądania kategorii", - "categoryWatchState.watching": "Obserwowane", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nie obserwowane", "categoryWatchState.ignoring": "Ignorowane" } diff --git a/public/language/pl/category.json b/public/language/pl/category.json index a8b578a7c9..eb8d6372b0 100644 --- a/public/language/pl/category.json +++ b/public/language/pl/category.json @@ -10,12 +10,15 @@ "watch": "Obserwuj", "ignore": "Ignoruj", "watching": "Obserwowane", + "tracking": "Tracking", "not-watching": "Nie obserwowane", "ignoring": "Ignorowane", - "watching.description": "Pokaż tematy w nieprzeczytanych i najnowszych", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Nie pokazuj tematów w nieprzeczytanym, pokaż je w ostatnim czasie", - "ignoring.description": "Nie pokazuj tematów w nieprzeczytanych i najnowszych", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Obserwujesz teraz aktualizacje tej kategorii i wszystkie podkategorie", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Obserwujesz teraz aktualizacje tej kategorii i wszystkie podkategorie", "ignoring.message": "Obserwujesz teraz aktualizacje tej kategorii i wszystkie podkategorie", "watched-categories": "Obserwowane kategorie", diff --git a/public/language/pl/notifications.json b/public/language/pl/notifications.json index fce3d1b0b1..bfeb97f4f8 100644 --- a/public/language/pl/notifications.json +++ b/public/language/pl/notifications.json @@ -13,6 +13,7 @@ "all": "Wszystko", "topics": "Tematy", "tags": "Tags", + "categories": "Categories", "replies": "Odpowiedzi", "chat": "Czaty", "group-chat": "Rozmowy grupowe", @@ -55,6 +56,7 @@ "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-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 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Kiedy ktoś zagłosuje na Twój post", "notificationType-new-topic": "Kiedy ktoś, kogo obserwujesz, utworzy temat", "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-reply": "Kiedy ktoś doda nową odpowiedź w temacie, który obserwujesz", "notificationType-post-edit": "Kiedy post jest edytowany w temacie, który obserwujesz", "notificationType-follow": "Kiedy ktoś zacznie Cię obserwować", diff --git a/public/language/pt-BR/admin/settings/user.json b/public/language/pt-BR/admin/settings/user.json index 2127d1a844..44fac00f21 100644 --- a/public/language/pt-BR/admin/settings/user.json +++ b/public/language/pt-BR/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Seguir os tópicos que você responder", "default-notification-settings": "Configurações Padrão de Notificações", "categoryWatchState": "Configuração padrão em relação a acompanhar as novidades das categorias", - "categoryWatchState.watching": "Acompanhando", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Não Acompanhar", "categoryWatchState.ignoring": "Ignorar" } diff --git a/public/language/pt-BR/category.json b/public/language/pt-BR/category.json index 8c6563791c..f2badb6332 100644 --- a/public/language/pt-BR/category.json +++ b/public/language/pt-BR/category.json @@ -10,12 +10,15 @@ "watch": "Acompanhar", "ignore": "Ignorar", "watching": "Acompanhar", + "tracking": "Tracking", "not-watching": "Não Acompanhar", "ignoring": "Ignorar", - "watching.description": "Mostrar tópicos em não-lidos e recentes", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Não mostrar tópicos em não-lidos, mostrar em recentes", - "ignoring.description": "Não mostrar tópicos em não-lidos e nem em recentes", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Agora, você está acompanhando as novidades desta categoria e todas as suas subcategorias", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Agora, você não está acompanhando as novidades nem desta categoria e nem de suas subcategorias", "ignoring.message": "Agora, você está ignorando as novidades desta categorias e de todas as suas subcategorias", "watched-categories": "Categorias acompanhadas", diff --git a/public/language/pt-BR/notifications.json b/public/language/pt-BR/notifications.json index 2a1ecdc7cf..b310b45bce 100644 --- a/public/language/pt-BR/notifications.json +++ b/public/language/pt-BR/notifications.json @@ -13,6 +13,7 @@ "all": "Tudo", "topics": "Tópicos", "tags": "Tags", + "categories": "Categories", "replies": "Respostas", "chat": "Conversas", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 começou a seguir você.", "user-started-following-you-dual": "%1 e %2 começaram a lhe acompanhar.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Quando alguém dá um voto positivo em seu post", "notificationType-new-topic": "Quando alguém que você segue posta um tópico", "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-reply": "Quando uma nova resposta é postada em um tópico que você está acompanhando", "notificationType-post-edit": "Quando uma postagem é editada em um tópico que você está assistindo", "notificationType-follow": "Quando alguém começar a seguir você", diff --git a/public/language/pt-PT/admin/settings/user.json b/public/language/pt-PT/admin/settings/user.json index 355545de5b..21536017af 100644 --- a/public/language/pt-PT/admin/settings/user.json +++ b/public/language/pt-PT/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Definições predefinidas das notificações", "categoryWatchState": "Estado predefinido da subscrição de categorias", - "categoryWatchState.watching": "A seguir", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "A não seguir", "categoryWatchState.ignoring": "A ignorar" } diff --git a/public/language/pt-PT/category.json b/public/language/pt-PT/category.json index 457df7c296..ccbb674cf4 100644 --- a/public/language/pt-PT/category.json +++ b/public/language/pt-PT/category.json @@ -10,12 +10,15 @@ "watch": "Subscrever", "ignore": "Ignorar", "watching": "A seguir", + "tracking": "Tracking", "not-watching": "Não seguir", "ignoring": "A ignorar", - "watching.description": "Mostrar tópicos em \"não lidos\" e \"recentes\"", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Não mostrar tópicos em \"não lidos\", mostrar em \"recentes\"", - "ignoring.description": "Não mostrar tópicos em \"não lidos\" e \"recentes\"", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Estás agora a seguir todas as atualizações desta categoria e de todas as suas subcategorias", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Não estás a seguir atualizações desta categoria e de todas as suas subcategorias", "ignoring.message": "Estás agora a ignorar todas as atualizações desta categoria e de todas as suas subcategorias", "watched-categories": "Categorias subscritas", diff --git a/public/language/pt-PT/notifications.json b/public/language/pt-PT/notifications.json index 1d44cad1f9..8f4e18c6f2 100644 --- a/public/language/pt-PT/notifications.json +++ b/public/language/pt-PT/notifications.json @@ -13,6 +13,7 @@ "all": "Tudo", "topics": "Tópicos", "tags": "Tags", + "categories": "Categories", "replies": "Respostas", "chat": "Chat", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 começou a seguir-te.", "user-started-following-you-dual": "%1 e %2 começaram a seguir-te.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Quando alguém vota positivamente numa publicação tua", "notificationType-new-topic": "Quando alguém que tu segues publica um tópico", "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-reply": "Quando uma nova resposta é publicada num tópico que tu estás a seguir", "notificationType-post-edit": "Quando uma publicação é editada num tópico que estás a seguir", "notificationType-follow": "Quando alguém começa a seguir-te", diff --git a/public/language/ro/admin/settings/user.json b/public/language/ro/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/ro/admin/settings/user.json +++ b/public/language/ro/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/ro/category.json b/public/language/ro/category.json index 2be03c90ca..a2868a09bb 100644 --- a/public/language/ro/category.json +++ b/public/language/ro/category.json @@ -10,12 +10,15 @@ "watch": "Urmărește", "ignore": "Ignoră", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Categorii urmărite", diff --git a/public/language/ro/notifications.json b/public/language/ro/notifications.json index 2f4cd05785..daa30b04d8 100644 --- a/public/language/ro/notifications.json +++ b/public/language/ro/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 a început să te urmărească.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/ru/admin/settings/user.json b/public/language/ru/admin/settings/user.json index 6afbb5e046..ed1d5078da 100644 --- a/public/language/ru/admin/settings/user.json +++ b/public/language/ru/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Включать отслеживание во всех темах, в которых вы отвечаете", "default-notification-settings": "Стандартные настройки уведомлений", "categoryWatchState": "Стандартные настройки отслеживания категорий", - "categoryWatchState.watching": "Отслеживается", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Не отслеживается", "categoryWatchState.ignoring": "Игнорируется" } diff --git a/public/language/ru/category.json b/public/language/ru/category.json index f0a8c22466..0c868965f4 100644 --- a/public/language/ru/category.json +++ b/public/language/ru/category.json @@ -10,12 +10,15 @@ "watch": "Отслеживать", "ignore": "Игнорировать", "watching": "Отслеживается", + "tracking": "Tracking", "not-watching": "Не отслеживается", "ignoring": "Игнорируется", - "watching.description": "Показывать темы из этой категории в списках непрочитанных и недавних", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Не показывать темы из этой категории в непрочитанных, но оставить в списке недавних", - "ignoring.description": "Не показывать темы из этой категории ни в списке непрочитанных, ни в недавних", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Вы отслеживаете обновления этой категории, включая все подкатегории", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Вы более не отслеживаете обновления этой категории, включая все подкатегории", "ignoring.message": "Вы игнорируете обновления этой категории, включая все подкатегории", "watched-categories": "Отслеживаемые категории", diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json index 77b3ee7652..c24ff5f7ff 100644 --- a/public/language/ru/notifications.json +++ b/public/language/ru/notifications.json @@ -13,6 +13,7 @@ "all": "Все", "topics": "Темы", "tags": "Tags", + "categories": "Categories", "replies": "Ответы", "chat": "Чаты", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "Пользователь %1 подписался на вас.", "user-started-following-you-dual": "Пользователи %1 и %2 подписались на вас.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Когда кто-то проголосовал за ваше сообщение", "notificationType-new-topic": "Когда кто-то, на кого вы подписаны, создаёт новую тему", "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-reply": "Когда в теме, за которой вы следите, появляется новое сообщение", "notificationType-post-edit": "Когда сообщение было отредактировано в теме, на которую вы подписаны", "notificationType-follow": "Когда кто-то подписался на вас", diff --git a/public/language/rw/admin/settings/user.json b/public/language/rw/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/rw/admin/settings/user.json +++ b/public/language/rw/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/rw/category.json b/public/language/rw/category.json index a5c106572d..b71de51d7c 100644 --- a/public/language/rw/category.json +++ b/public/language/rw/category.json @@ -10,12 +10,15 @@ "watch": "Kurikirana", "ignore": "Ihorere", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Ibyiciro Bikurikirwa", diff --git a/public/language/rw/notifications.json b/public/language/rw/notifications.json index 703bcc1d73..94aef14349 100644 --- a/public/language/rw/notifications.json +++ b/public/language/rw/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 yatangiye kugukurikira.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/sc/admin/settings/user.json b/public/language/sc/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/sc/admin/settings/user.json +++ b/public/language/sc/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/sc/category.json b/public/language/sc/category.json index 938949b6af..7032bfb8c7 100644 --- a/public/language/sc/category.json +++ b/public/language/sc/category.json @@ -10,12 +10,15 @@ "watch": "Watch", "ignore": "Ignore", "watching": "Watching", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignoring", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Watched categories", diff --git a/public/language/sc/notifications.json b/public/language/sc/notifications.json index 083c230ca5..a1d860076a 100644 --- a/public/language/sc/notifications.json +++ b/public/language/sc/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 started following you.", "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/sk/admin/settings/user.json b/public/language/sk/admin/settings/user.json index ad7273b4b6..5275896c0e 100644 --- a/public/language/sk/admin/settings/user.json +++ b/public/language/sk/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Sledovať témy, na ktoré ste odpovedal", "default-notification-settings": "Predvolené nastavenia oznámení", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/sk/category.json b/public/language/sk/category.json index 5ec2a13bcf..4501fb419d 100644 --- a/public/language/sk/category.json +++ b/public/language/sk/category.json @@ -10,12 +10,15 @@ "watch": "Sledovať", "ignore": "Ignorovať", "watching": "Sledované", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "Ignorovať", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Sledované kategórie", diff --git a/public/language/sk/notifications.json b/public/language/sk/notifications.json index 8993f6096f..de66fd4959 100644 --- a/public/language/sk/notifications.json +++ b/public/language/sk/notifications.json @@ -13,6 +13,7 @@ "all": "Všetko", "topics": "Témy", "tags": "Tags", + "categories": "Categories", "replies": "Odpovede", "chat": "Konverzácie", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 Vás začal sledovať.", "user-started-following-you-dual": "%1 a %2 Vás začali sledovať.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Ak niekto vyjadri súhlas s vaším príspevkom", "notificationType-new-topic": "Ak začne niekto sledovať príspevky a témy", "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-reply": "Ak bude pridaný nový príspevok v téme, ktorú sledujete", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "Ak Vás začne niekto sledovať", diff --git a/public/language/sl/admin/settings/user.json b/public/language/sl/admin/settings/user.json index 7653db13a2..4277a8db20 100644 --- a/public/language/sl/admin/settings/user.json +++ b/public/language/sl/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Spremljanj teme, na katere si odgovoril", "default-notification-settings": "Privzete nastavitve obveščanja", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Spremljano", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Ni spremljano", "categoryWatchState.ignoring": "Prezrto" } diff --git a/public/language/sl/category.json b/public/language/sl/category.json index 6884a31b53..10ac321844 100644 --- a/public/language/sl/category.json +++ b/public/language/sl/category.json @@ -10,12 +10,15 @@ "watch": "Spremljaj.", "ignore": "Prezri.", "watching": "Spremljano", + "tracking": "Tracking", "not-watching": "Ni spremljano", "ignoring": "Prezrto", - "watching.description": "Prikaži teme v nedavno in nazadnje", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "Spremljane kategorije", diff --git a/public/language/sl/notifications.json b/public/language/sl/notifications.json index 9e09bc1a07..2f8f6337f3 100644 --- a/public/language/sl/notifications.json +++ b/public/language/sl/notifications.json @@ -13,6 +13,7 @@ "all": "All", "topics": "Topics", "tags": "Tags", + "categories": "Categories", "replies": "Replies", "chat": "Chats", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 te je začel/-a spremljati.", "user-started-following-you-dual": "%1 in %2 sta te začela/-i spremljati.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "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-reply": "When a new reply is posted in a topic you are watching", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "When someone starts following you", diff --git a/public/language/sq-AL/admin/settings/user.json b/public/language/sq-AL/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/sq-AL/admin/settings/user.json +++ b/public/language/sq-AL/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/sq-AL/category.json b/public/language/sq-AL/category.json index 94930bb93c..94f45c4810 100644 --- a/public/language/sq-AL/category.json +++ b/public/language/sq-AL/category.json @@ -10,12 +10,15 @@ "watch": "Shiko", "ignore": "Injoro", "watching": "Ndiq temën", + "tracking": "Tracking", "not-watching": "Mos e ndiq temën", "ignoring": "Injoro", - "watching.description": "Shfaq temat e fundit dhe të palexuara", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Mos shfaq temat e palexuara, shfaq vetem temat më të fundit", - "ignoring.description": "Mos shfaq temat e fundit dhe të palexuara", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Tani je duke ndjekur përditësimet nga kjo kategori dhe të gjitha nënkategoritë e saj.", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Tani nuk je duke ndjekur përditësimet nga kjo kategori dhe të gjitha nënkategoritë e saj.", "ignoring.message": "Tani je duke injoruar përditësimet nga kjo kategori dhe të gjitha nënkategoritë e saj.", "watched-categories": "Kategoritë që keni ndjekur", diff --git a/public/language/sq-AL/notifications.json b/public/language/sq-AL/notifications.json index 95bc874011..14fe76838b 100644 --- a/public/language/sq-AL/notifications.json +++ b/public/language/sq-AL/notifications.json @@ -13,6 +13,7 @@ "all": "Të gjitha", "topics": "Temat", "tags": "Tags", + "categories": "Categories", "replies": "Përgjigjet", "chat": "Bisedat", "group-chat": "Bisedat në Grup", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 filloi t'ju ndjekë.", "user-started-following-you-dual": "% 1 dhe %2 filluan t'ju ndjekin.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Kur dikush voton pro për postimin tuaj", "notificationType-new-topic": "Kur dikush që ndiqni poston një temë", "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-reply": "Kur një përgjigje e re postohet në një temë që po shikoni", "notificationType-post-edit": "Kur një postim redaktohet në një temë që po shikoni", "notificationType-follow": "Kur dikush fillon të të ndjekë", diff --git a/public/language/sr/admin/settings/user.json b/public/language/sr/admin/settings/user.json index e9944ba22b..aa03462c75 100644 --- a/public/language/sr/admin/settings/user.json +++ b/public/language/sr/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Prati teme na koje si ti odgovorio", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/sr/category.json b/public/language/sr/category.json index 69b36e5400..e43cf7d4db 100644 --- a/public/language/sr/category.json +++ b/public/language/sr/category.json @@ -10,12 +10,15 @@ "watch": "Надгледај", "ignore": "Игнориши", "watching": "Надгледај", + "tracking": "Tracking", "not-watching": "Не надгледај", "ignoring": "Игнориши", - "watching.description": "Прикажи теме у непрочитаним и недавним", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Не приказуј теме у непрочитаним, прикажи у недавним", - "ignoring.description": "Не приказуј теме у непрочитаним и недавним", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Сада надгледате ажурирања из ове категорије и свих поткатегорија", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Не надгледате ажурирања из ове категорије и свих поткатегорија", "ignoring.message": "Сада игноришете ажурирања из ове категорије и свих поткатегорија", "watched-categories": "Надгледане категорије", diff --git a/public/language/sr/notifications.json b/public/language/sr/notifications.json index 8d02dbd660..ba93a6346c 100644 --- a/public/language/sr/notifications.json +++ b/public/language/sr/notifications.json @@ -13,6 +13,7 @@ "all": "Све", "topics": "Теме", "tags": "Ознаке", + "categories": "Categories", "replies": "Одговори", "chat": "Ћаскања", "group-chat": "Групна ћаскања", @@ -55,6 +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-started-following-you": "%1 је почео да вас прати.", "user-started-following-you-dual": "%1 и %2 су почели да вас прате.", "user-started-following-you-triple": "%1, %2 и %3 су вас запратили.", @@ -81,6 +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-reply": "Када је објављен нови одговор у теми коју надгледате", "notificationType-post-edit": "Када је порука уређена у теми коју надгледате", "notificationType-follow": "Када неко почне да вас прати", diff --git a/public/language/sv/admin/settings/user.json b/public/language/sv/admin/settings/user.json index 816bdfc150..88f14c76cd 100644 --- a/public/language/sv/admin/settings/user.json +++ b/public/language/sv/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/sv/category.json b/public/language/sv/category.json index 858b7ba5a9..8fb311c91a 100644 --- a/public/language/sv/category.json +++ b/public/language/sv/category.json @@ -10,12 +10,15 @@ "watch": "Bevaka", "ignore": "Ignorera", "watching": "Bevakar", + "tracking": "Tracking", "not-watching": "Följer inte", "ignoring": "Ignorerar", - "watching.description": "Visa ämnen i olästa och senaste", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Visa inte ämnen i olästa, visa i senaste", - "ignoring.description": "Visa inte ämnen i olästa och senaste", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Nu får du uppdateringar från den här kategorin och alla underkategorier", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Du får inga uppdateringar från den här kategorin eller alla underkategorier", "ignoring.message": "Nu ignorerar du alla uppdateringar från den här kategorin och alla underkategorier.", "watched-categories": "Bevakade kategorier", diff --git a/public/language/sv/notifications.json b/public/language/sv/notifications.json index 22fdace483..9ca90fa226 100644 --- a/public/language/sv/notifications.json +++ b/public/language/sv/notifications.json @@ -13,6 +13,7 @@ "all": "Alla", "topics": "Ämnen", "tags": "Tags", + "categories": "Categories", "replies": "Svar", "chat": "Chattar", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 började följa dig.", "user-started-following-you-dual": "%1 och %2 började följa dig.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "När någon röstar upp ditt inlägg", "notificationType-new-topic": "När någon du följer skapar ett ämne", "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-reply": "När ett nytt svar skrivs inom ett ämne du följer", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "När någon börjar följa dig", diff --git a/public/language/th/admin/settings/user.json b/public/language/th/admin/settings/user.json index 65cd91d437..5e48bf8700 100644 --- a/public/language/th/admin/settings/user.json +++ b/public/language/th/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Follow topics that you reply to", "default-notification-settings": "Default notification settings", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/th/category.json b/public/language/th/category.json index 0a20f0c192..81de3410b5 100644 --- a/public/language/th/category.json +++ b/public/language/th/category.json @@ -10,12 +10,15 @@ "watch": "ตามดู", "ignore": "ไม่ต้องสนใจอีก", "watching": "กำลังตามดู", + "tracking": "Tracking", "not-watching": "Not Watching", "ignoring": "เมินเฉย", - "watching.description": "Show topics in unread and recent", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "You are now watching updates from this category and all subcategories", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", "watched-categories": "หมวดหมู่ที่ดูแล้ว", diff --git a/public/language/th/notifications.json b/public/language/th/notifications.json index 0a08e7ca02..e984c973cf 100644 --- a/public/language/th/notifications.json +++ b/public/language/th/notifications.json @@ -13,6 +13,7 @@ "all": "ทั้งหมด", "topics": "กระทู้", "tags": "Tags", + "categories": "Categories", "replies": "คำตอบ", "chat": "แชท", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 ได้เริ่มติดตามคุณ", "user-started-following-you-dual": "%1และ%2ได้เริ่มติดตามคุณ", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "เมื่อมีคนโหวตอัพให้โพสต์คุณ", "notificationType-new-topic": "เมื่อมีคนติดตามโพสต์คุณ", "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-reply": "เมื่อมีการตอบกลับในโพสต์ที่คุณกำลังติดตาม", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "เมื่อมีคนติดตามคุณ", diff --git a/public/language/tr/admin/settings/user.json b/public/language/tr/admin/settings/user.json index f8154d4566..664c82a7de 100644 --- a/public/language/tr/admin/settings/user.json +++ b/public/language/tr/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Cevap verdiğim konuları takip et", "default-notification-settings": "Varsayılan bildirim ayarları", "categoryWatchState": "Varsayılan kategori izlenme durumu", - "categoryWatchState.watching": "Takip ediliyor", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Takip edilmiyor", "categoryWatchState.ignoring": "Yok sayılıyor" } diff --git a/public/language/tr/category.json b/public/language/tr/category.json index 9be758a5d3..1473237eea 100644 --- a/public/language/tr/category.json +++ b/public/language/tr/category.json @@ -10,12 +10,15 @@ "watch": "Takip et", "ignore": "Yok say", "watching": "Takip ediliyor", + "tracking": "Tracking", "not-watching": "Takip edilmiyor", "ignoring": "Yok sayılıyor", - "watching.description": "Bu kategorideki konuları, okunmamış ve güncel konular arasında göster", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Bu kategorideki konuları, okunmamış konular arasında gösterme; ama güncel konular arasında göster", - "ignoring.description": "Bu kategorideki konuları, okunmamış ve güncel konular arasında gösterme", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Bu kategori ve alt kategorilerindeki güncellemeleri artık takip ediyorsunuz", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Bu kategori ve alt kategorilerindeki güncellemeleri artık takip etmiyorsunuz", "ignoring.message": "Bu kategori ve alt kategorilerindeki güncellemeleri artık yok sayıyorsunuz", "watched-categories": "Takip edilen kategoriler", diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json index 99991881c2..7feb6acdf3 100644 --- a/public/language/tr/notifications.json +++ b/public/language/tr/notifications.json @@ -13,6 +13,7 @@ "all": "Hepsi", "topics": "Konular", "tags": "Tags", + "categories": "Categories", "replies": "Yanıtlar", "chat": "Sohbetler", "group-chat": "Grup Sohbetleri", @@ -55,6 +56,7 @@ "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-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.", @@ -81,6 +83,7 @@ "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-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", diff --git a/public/language/uk/admin/settings/user.json b/public/language/uk/admin/settings/user.json index 8f2ea1da36..154ce7ffde 100644 --- a/public/language/uk/admin/settings/user.json +++ b/public/language/uk/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Стежити за темами в котрих ви відповідаєте", "default-notification-settings": "Стандартні налаштування сповіщень", "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring" } diff --git a/public/language/uk/category.json b/public/language/uk/category.json index e18fa3d3e2..fa48efea41 100644 --- a/public/language/uk/category.json +++ b/public/language/uk/category.json @@ -10,12 +10,15 @@ "watch": "Стежити", "ignore": "Ігнорувати", "watching": "Відстежується", + "tracking": "Tracking", "not-watching": "Не спостерігається", "ignoring": "Ігнорувати", - "watching.description": "Показати теми в непрочитаних та останніх", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Не показувати теми в непрочитаних, показувати в останніх", - "ignoring.description": "Не показувати теми в непрочитаних і останніх", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Ви зараз спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Зараз ви не спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій", "ignoring.message": "Зараз ви ігноруєте оновлення з цієї категорії та всіх її підкатегорій", "watched-categories": "Переглянуті категорії", diff --git a/public/language/uk/notifications.json b/public/language/uk/notifications.json index 959453de69..74aeed642d 100644 --- a/public/language/uk/notifications.json +++ b/public/language/uk/notifications.json @@ -13,6 +13,7 @@ "all": "Всі", "topics": "Теми", "tags": "Tags", + "categories": "Categories", "replies": "Відповіді", "chat": "Чати", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1 почав стежити за вами.", "user-started-following-you-dual": "%1 та %2 почали стежити за вами.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Коли хтось голосує за ваш пост", "notificationType-new-topic": "Коли хтось, кого ви читаєте, публікує тему", "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-reply": "Коли з'являється нова відповідь у темі, за якою ви слідкуєте", "notificationType-post-edit": "When a post is edited in a topic you are watching", "notificationType-follow": "Коли хтось починає слідкувати за вами", diff --git a/public/language/vi/admin/settings/user.json b/public/language/vi/admin/settings/user.json index 8f45876418..9b895f041a 100644 --- a/public/language/vi/admin/settings/user.json +++ b/public/language/vi/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Theo dõi các chủ đề mà bạn trả lời", "default-notification-settings": "Cài đặt thông báo mặc định", "categoryWatchState": "Trạng thái xem chuyên mục mặc định", - "categoryWatchState.watching": "Đang Xem", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Không Xem", "categoryWatchState.ignoring": "Bỏ Qua" } diff --git a/public/language/vi/category.json b/public/language/vi/category.json index 0796168e77..1edff4bc14 100644 --- a/public/language/vi/category.json +++ b/public/language/vi/category.json @@ -10,12 +10,15 @@ "watch": "Xem", "ignore": "Bỏ qua", "watching": "Đang xem", + "tracking": "Tracking", "not-watching": "Không xem", "ignoring": "Bỏ qua", - "watching.description": "Hiển thị chủ đề chưa đọc và gần đây", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "Không hiển thị chủ đề trong chưa đọc, hiển thị gần đây", - "ignoring.description": "Không hiển thị chủ đề trong chưa đọc và gần đây", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "Bây giờ bạn đang xem cập nhật từ danh mục này và tất cả các danh mục phụ", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "Bạn không xem cập nhật từ danh mục này và tất cả các danh mục phụ", "ignoring.message": "Bây giờ bạn đang bỏ qua các cập nhật từ danh mục này và tất cả các danh mục phụ", "watched-categories": "Chuyên mục đã xem", diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json index 823a5f695c..d02537243a 100644 --- a/public/language/vi/notifications.json +++ b/public/language/vi/notifications.json @@ -13,6 +13,7 @@ "all": "Tất cả", "topics": "Chủ đề", "tags": "Thẻ", + "categories": "Categories", "replies": "Phản hồi", "chat": "Trò Chuyện", "group-chat": "Trò Chuyện Nhóm", @@ -55,6 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 đã đăng một chủ đề mới với thẻ %2%3", "user-posted-topic-with-tag-triple": "%1 đã đăng một chủ đề mới với thẻ %2, %3%4", "user-posted-topic-with-tag-multiple": "%1 đã đăng một chủ đề mới với thẻ %2", + "user-posted-topic-in-category": "%1 has posted a new topic in %2", "user-started-following-you": "%1 đã theo dõi bạn.", "user-started-following-you-dual": "%1%2 đã bắt đầu theo dõi bạn.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "Khi ai đó ủng hộ bài viết của bạn", "notificationType-new-topic": "Khi người bạn theo dõi đăng một chủ đề", "notificationType-new-topic-with-tag": "Khi một chủ đề được đăng với một thẻ bạn theo dõi", + "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", "notificationType-new-reply": "Khi một câu trả lời mới được đăng trong một chủ đề bạn đang xem", "notificationType-post-edit": "Khi bài viết được chỉnh sửa trong chủ đề bạn đang xem", "notificationType-follow": "Khi ai đó bắt đầu theo dõi bạn", diff --git a/public/language/zh-CN/admin/settings/user.json b/public/language/zh-CN/admin/settings/user.json index a3e3217682..95182457d3 100644 --- a/public/language/zh-CN/admin/settings/user.json +++ b/public/language/zh-CN/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "关注您回复的主题", "default-notification-settings": "默认通知设置", "categoryWatchState": "默认版块关注状态", - "categoryWatchState.watching": "已关注", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未关注", "categoryWatchState.ignoring": "已忽略" } diff --git a/public/language/zh-CN/category.json b/public/language/zh-CN/category.json index fefabc4b83..64fd876938 100644 --- a/public/language/zh-CN/category.json +++ b/public/language/zh-CN/category.json @@ -10,12 +10,15 @@ "watch": "关注", "ignore": "忽略", "watching": "已关注", + "tracking": "Tracking", "not-watching": "未关注", "ignoring": "已忽略", - "watching.description": "显示未读和最近的主题", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "不显示未读主题,显示最近主题", - "ignoring.description": "不显示未读和最近的主题", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "您关注了此版块和全部子版块的动态。", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "您未关注了此版块和全部子版块的动态。", "ignoring.message": "您未关注此版块和全部子版块的动态。", "watched-categories": "已关注的版块", diff --git a/public/language/zh-CN/notifications.json b/public/language/zh-CN/notifications.json index d7b5d8518c..5356a0be4c 100644 --- a/public/language/zh-CN/notifications.json +++ b/public/language/zh-CN/notifications.json @@ -13,6 +13,7 @@ "all": "所有", "topics": "主题", "tags": "Tags", + "categories": "Categories", "replies": "回复", "chat": "聊天", "group-chat": "群聊", @@ -55,6 +56,7 @@ "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-started-following-you": "%1关注了您。", "user-started-following-you-dual": "%1%2 关注了您。", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "当有人顶了我的帖子时", "notificationType-new-topic": "当您关注的人发布了主题时", "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-reply": "当您正在查看的主题中有新回复时", "notificationType-post-edit": "当您关注的主题有帖子被编辑时", "notificationType-follow": "当有人关注您时", diff --git a/public/language/zh-TW/admin/settings/user.json b/public/language/zh-TW/admin/settings/user.json index 28dc7ac2dd..f8be0f39e4 100644 --- a/public/language/zh-TW/admin/settings/user.json +++ b/public/language/zh-TW/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "關注您回覆的主題", "default-notification-settings": "預設通知設定", "categoryWatchState": "預設版面關注狀態", - "categoryWatchState.watching": "已關注", + "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未關注", "categoryWatchState.ignoring": "已忽略" } diff --git a/public/language/zh-TW/category.json b/public/language/zh-TW/category.json index ac9682d2d1..f9c6b075ac 100644 --- a/public/language/zh-TW/category.json +++ b/public/language/zh-TW/category.json @@ -10,12 +10,15 @@ "watch": "關注", "ignore": "忽略", "watching": "已關注", + "tracking": "Tracking", "not-watching": "未關注", "ignoring": "已忽略", - "watching.description": "顯示未讀和最近的主題", + "watching.description": "Notify me of new topics.
Show topics in unread & recent", + "tracking.description": "Shows topics in unread & recent", "not-watching.description": "不顯示未讀主題,顯示最近主題", - "ignoring.description": "不顯示未讀和最近的主題", + "ignoring.description": "Do not show topics in unread & recent", "watching.message": "您關注了此版面和全部子版面的動態。", + "tracking.message": "You are now tracking updates from this category and all subcategories", "notwatching.message": "您未關注了此版面和全部子版面的動態。", "ignoring.message": "您忽略了此版面和全部子版面的動態。", "watched-categories": "已關注的版面", diff --git a/public/language/zh-TW/notifications.json b/public/language/zh-TW/notifications.json index 2c9a89f1db..a136577f77 100644 --- a/public/language/zh-TW/notifications.json +++ b/public/language/zh-TW/notifications.json @@ -13,6 +13,7 @@ "all": "所有", "topics": "主題", "tags": "Tags", + "categories": "Categories", "replies": "回覆", "chat": "聊天", "group-chat": "Group Chats", @@ -55,6 +56,7 @@ "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-started-following-you": "%1追隨了您。", "user-started-following-you-dual": "%1%2 追隨了您。", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -81,6 +83,7 @@ "notificationType-upvote": "當有人點贊了我的貼文時", "notificationType-new-topic": "當有人回覆我的貼文時", "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-reply": "當您正在查看的主題中有新回覆時", "notificationType-post-edit": "當您關注中的主題有貼文被編輯時", "notificationType-follow": "當有人追隨您時", From 36c5025e4b2bebe346f7fe10e5fa143c3bc0f1be Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 4 Nov 2023 09:18:20 +0000 Subject: [PATCH 052/201] Latest translations and fallbacks --- public/language/de/admin/settings/user.json | 2 +- public/language/de/category.json | 10 +++++----- public/language/de/notifications.json | 6 +++--- public/language/it/admin/settings/post.json | 2 +- public/language/it/admin/settings/user.json | 2 +- public/language/it/category.json | 10 +++++----- public/language/it/notifications.json | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/public/language/de/admin/settings/user.json b/public/language/de/admin/settings/user.json index 7372f39fb7..b18f5a5889 100644 --- a/public/language/de/admin/settings/user.json +++ b/public/language/de/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Themen folgen, auf die du antwortest", "default-notification-settings": "Standardbenachrichtigungseinstellungen", "categoryWatchState": "Standardmäßige Beobachtung", - "categoryWatchState.tracking": "Tracking", + "categoryWatchState.tracking": "Verfolgung", "categoryWatchState.notwatching": "Nicht beobachtet", "categoryWatchState.ignoring": "Ignoriert" } diff --git a/public/language/de/category.json b/public/language/de/category.json index da7d6d33c7..1078ba6e54 100644 --- a/public/language/de/category.json +++ b/public/language/de/category.json @@ -10,15 +10,15 @@ "watch": "Beobachten", "ignore": "Ignorieren", "watching": "Beobachte", - "tracking": "Tracking", + "tracking": "Verfolgung", "not-watching": "Nicht beobachtet", "ignoring": "Ignoriert", - "watching.description": "Notify me of new topics.
Show topics in unread & recent", - "tracking.description": "Shows topics in unread & recent", + "watching.description": "Über neue Themen benachrichtigen.
Themen in Ungelesen & Aktuell anzeigen", + "tracking.description": "Zeigt Themen in Ungelesen und Aktuell", "not-watching.description": "Zeige keine Themen in Ungelesen, zeige sie in Aktuell", - "ignoring.description": "Do not show topics in unread & recent", + "ignoring.description": "Zeige keine Themen in Ungelesen & Neu", "watching.message": "Du beobachtest jetzt Aktualisierungen aus dieser Kategorie und allen Unterkategorien", - "tracking.message": "You are now tracking updates from this category and all subcategories", + "tracking.message": "Du verfolgst jetzt Aktualisierungen von dieser Kategorie und allen Unterkategorien", "notwatching.message": "Du beobachtest jetzt keine Aktualisierungen aus dieser Kategorie und allen Unterkategorien", "ignoring.message": "Du ignorierst jetzt Aktualisierungen aus dieser Kategorie und allen Unterkategorien", "watched-categories": "Beobachtete Kategorien", diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json index 2e4dfcab7a..6c26f60728 100644 --- a/public/language/de/notifications.json +++ b/public/language/de/notifications.json @@ -13,7 +13,7 @@ "all": "Alle", "topics": "Themen", "tags": "Tags", - "categories": "Categories", + "categories": "Kategorien", "replies": "Antworten", "chat": "Chats", "group-chat": "Gruppenchats", @@ -56,7 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 hat ein neues Thema mit den Tags %2 and %3 erstellt", "user-posted-topic-with-tag-triple": "%1 hat ein neues Thema mit den Tags %2, %3 und %4 erstellt", "user-posted-topic-with-tag-multiple": "%1 hat ein neues Thema mit den Tags %2 erstellt", - "user-posted-topic-in-category": "%1 has posted a new topic in %2", + "user-posted-topic-in-category": "%1 hat ein neues Thema in %2 erstellt", "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.", @@ -83,7 +83,7 @@ "notificationType-upvote": "Wenn jemand deinen beitrag positiv bewertet", "notificationType-new-topic": "Wenn jemand, dem du folgst, einen Beitrag erstellt", "notificationType-new-topic-with-tag": "Wenn ein Thema mit einem Tag gepostet wird, dem du folgst", - "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", + "notificationType-new-topic-in-category": "Wenn ein Thema in einer Kategorie veröffentlicht wird, die du beobachtest", "notificationType-new-reply": "Wenn es eine neue Antwort auf ein Thema das du beobachtest gibt", "notificationType-post-edit": "Wenn ein Post bearbeitet wurde, in einem Thema welches du beobachtest", "notificationType-follow": "Wenn dir jemand neues folgt", diff --git a/public/language/it/admin/settings/post.json b/public/language/it/admin/settings/post.json index 6a46886fe3..e593b1eeed 100644 --- a/public/language/it/admin/settings/post.json +++ b/public/language/it/admin/settings/post.json @@ -59,7 +59,7 @@ "backlinks": "Backlink", "backlinks.enabled": "Abilita backlink discussione", "backlinks.help": "Se un post fa riferimento ad un altra discussione, un link al post sarà inserito nella discussione di riferimento in quel momento.", - "ip-tracking": "Monitoraggio IP", + "ip-tracking": "Tracciamento IP", "ip-tracking.each-post": "Traccia l'indirizzo IP per ogni post", "enable-post-history": "Abilita cronologia post" } \ No newline at end of file diff --git a/public/language/it/admin/settings/user.json b/public/language/it/admin/settings/user.json index 769e9a712b..9788dd5ab0 100644 --- a/public/language/it/admin/settings/user.json +++ b/public/language/it/admin/settings/user.json @@ -79,7 +79,7 @@ "follow-replied-topics": "Segui discussioni a cui rispondi tu", "default-notification-settings": "Impostazioni di notifica predefinite", "categoryWatchState": "Stato predefinito della categoria di controllo", - "categoryWatchState.tracking": "Tracking", + "categoryWatchState.tracking": "Tracciamento", "categoryWatchState.notwatching": "Non seguito", "categoryWatchState.ignoring": "Ignorato" } diff --git a/public/language/it/category.json b/public/language/it/category.json index 973e1632a0..0cb69d20c9 100644 --- a/public/language/it/category.json +++ b/public/language/it/category.json @@ -10,15 +10,15 @@ "watch": "Segui", "ignore": "Ignora", "watching": "Seguito", - "tracking": "Tracking", + "tracking": "Tracciamento", "not-watching": "Non seguito", "ignoring": "Ignorato", - "watching.description": "Notify me of new topics.
Show topics in unread & recent", - "tracking.description": "Shows topics in unread & recent", + "watching.description": "Notificami nuove discussioni.
Mostra le discussioni non lette e recenti", + "tracking.description": "Mostra le discussioni non lette e recenti", "not-watching.description": "Non mostrare discussioni in non letti, mostra in recenti", - "ignoring.description": "Do not show topics in unread & recent", + "ignoring.description": "Non mostrare le discussioni non lette e recenti", "watching.message": "Ora stai seguendo gli aggiornamenti di questa categoria e di tutte le sottocategorie", - "tracking.message": "You are now tracking updates from this category and all subcategories", + "tracking.message": "Ora stai monitorando gli aggiornamenti di questa categoria e di tutte le sottocategorie", "notwatching.message": "Ora non stai seguendo gli aggiornamenti di questa categoria e di tutte le sottocategorie", "ignoring.message": "Ora stai ignorando gli aggiornamenti di questa categoria e di tutte le sottocategorie", "watched-categories": "Categorie seguite", diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index d84967a764..e88ed398b0 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -13,7 +13,7 @@ "all": "Tutte", "topics": "Discussioni", "tags": "Tag", - "categories": "Categories", + "categories": "Categorie", "replies": "Risposte", "chat": "Chat", "group-chat": "Chat di gruppo", @@ -56,7 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 ha postato una nuova discussione con i tag %2 e %3", "user-posted-topic-with-tag-triple": "%1 ha postato una nuova discussione con e tag %2, %3 e %4", "user-posted-topic-with-tag-multiple": "%1 ha postato una nuova discussione con i tag %2", - "user-posted-topic-in-category": "%1 has posted a new topic in %2", + "user-posted-topic-in-category": "%1 ha postato una nuova discussione in %2", "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.", @@ -83,7 +83,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": "When a topic is posted in a category you are watching", + "notificationType-new-topic-in-category": "Quando una discussione viene pubblicata 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", From 519bc67066c74f9e6e9b8914647f1d0395ded3fa Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 7 Nov 2023 09:18:45 +0000 Subject: [PATCH 053/201] Latest translations and fallbacks --- public/language/bg/admin/settings/user.json | 6 ++--- public/language/bg/category.json | 22 +++++++++---------- public/language/bg/notifications.json | 10 ++++----- public/language/bg/pages.json | 6 ++--- public/language/bg/search.json | 2 +- public/language/bg/tags.json | 4 ++-- public/language/bg/topic.json | 10 ++++----- public/language/bg/unread.json | 2 +- public/language/bg/user.json | 14 ++++++------ public/language/vi/admin/admin.json | 2 +- .../language/vi/admin/advanced/database.json | 2 +- public/language/vi/admin/extend/widgets.json | 4 ++-- .../language/vi/admin/manage/categories.json | 2 +- .../language/vi/admin/manage/privileges.json | 2 +- .../vi/admin/manage/registration.json | 4 ++-- public/language/vi/admin/menu.json | 4 ++-- .../vi/admin/settings/notifications.json | 2 +- public/language/vi/admin/settings/post.json | 10 ++++----- public/language/vi/admin/settings/user.json | 4 ++-- public/language/vi/category.json | 12 +++++----- public/language/vi/error.json | 4 ++-- public/language/vi/groups.json | 2 +- public/language/vi/login.json | 2 +- public/language/vi/modules.json | 4 ++-- public/language/vi/notifications.json | 16 +++++++------- public/language/vi/pages.json | 4 ++-- public/language/vi/post-queue.json | 6 ++--- public/language/vi/register.json | 2 +- public/language/vi/search.json | 16 +++++++------- public/language/vi/success.json | 2 +- public/language/vi/topic.json | 2 +- public/language/vi/unread.json | 4 ++-- public/language/vi/user.json | 4 ++-- 33 files changed, 96 insertions(+), 96 deletions(-) diff --git a/public/language/bg/admin/settings/user.json b/public/language/bg/admin/settings/user.json index 79c5741418..9c401ae75d 100644 --- a/public/language/bg/admin/settings/user.json +++ b/public/language/bg/admin/settings/user.json @@ -78,8 +78,8 @@ "follow-created-topics": "Следване на темите, които създавате", "follow-replied-topics": "Следване на темите, на които отговаряте", "default-notification-settings": "Настройки по подразбиране за известията", - "categoryWatchState": "Състояние по подразбиране за следенето на категории", - "categoryWatchState.tracking": "Tracking", - "categoryWatchState.notwatching": "Да не се следят", + "categoryWatchState": "Състояние по подразбиране за наблюдаването на категории", + "categoryWatchState.tracking": "Да се следят", + "categoryWatchState.notwatching": "Да не се наблюдават", "categoryWatchState.ignoring": "Да се пренебрегват" } diff --git a/public/language/bg/category.json b/public/language/bg/category.json index c2b15210f8..2d28ec9299 100644 --- a/public/language/bg/category.json +++ b/public/language/bg/category.json @@ -7,20 +7,20 @@ "browsing": "разглежда", "no-replies": "Няма отговори", "no-new-posts": "Няма нови публикации.", - "watch": "Следене", + "watch": "Наблюдаване", "ignore": "Пренебрегване", - "watching": "Следите", - "tracking": "Tracking", - "not-watching": "Не следите", + "watching": "Наблюдавате", + "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", - "watching.message": "Вече следите новите неща в категорията и подкатегориите ѝ", - "tracking.message": "You are now tracking updates from this category and all subcategories", - "notwatching.message": "Вече не следите новите неща в категорията и подкатегориите ѝ", + "ignoring.description": "Темите да не се показват нито в непрочетените, нито в скорошните", + "watching.message": "Вече наблюдавате новите неща в категорията и подкатегориите ѝ", + "tracking.message": "Вече следите новите неща в категорията и подкатегориите ѝ", + "notwatching.message": "Вече не наблюдавате новите неща в категорията и подкатегориите ѝ", "ignoring.message": "Вече пренебрегвате новите неща в тази категория и всички нейни подкатегории", - "watched-categories": "Следени категории", + "watched-categories": "Наблюдавани категории", "x-more-categories": "Още %1 категории" } \ No newline at end of file diff --git a/public/language/bg/notifications.json b/public/language/bg/notifications.json index 53b86760b2..213d72f9d4 100644 --- a/public/language/bg/notifications.json +++ b/public/language/bg/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 публикува нова тема с етикети %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,9 +83,9 @@ "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-reply": "Когато бъде публикуван нов отговор в тема, която следвате", - "notificationType-post-edit": "Когато бъде редактирана публикация в тема, която следите", + "notificationType-new-topic-in-category": "Когато бъде публикувана нова тема в категория, която наблюдавате", + "notificationType-new-reply": "Когато бъде публикуван нов отговор в тема, която наблюдавате", + "notificationType-post-edit": "Когато бъде редактирана публикация в тема, която наблюдавате", "notificationType-follow": "Когато някой започне да Ви следва", "notificationType-new-chat": "Когато получите съобщение в разговор", "notificationType-new-group-chat": "Когато получите съобщение в групов разговор", diff --git a/public/language/bg/pages.json b/public/language/bg/pages.json index 8a0d1c7d10..b6dc4db256 100644 --- a/public/language/bg/pages.json +++ b/public/language/bg/pages.json @@ -47,12 +47,12 @@ "account/latest-posts": "Последни публикации от %1", "account/topics": "Теми, създадени от %1", "account/groups": "Групите на %1", - "account/watched-categories": "Следените категории на %1", - "account/watched-tags": "Следените етикети на %1", + "account/watched-categories": "Наблюдаваните категории на %1", + "account/watched-tags": "Наблюдаваните етикети на %1", "account/bookmarks": "Отметнатите публикации на %1", "account/settings": "Потребителски настройки", "account/settings-of": "Променят се настройките на %1", - "account/watched": "Теми, следени от %1", + "account/watched": "Теми, наблюдавани от %1", "account/ignored": "Теми, пренебрегвани от %1", "account/upvoted": "Публикации, получили положителен глас от %1", "account/downvoted": "Публикации, получили отрицателен глас от %1", diff --git a/public/language/bg/search.json b/public/language/bg/search.json index 85b4201bb9..ccd5a1f0ca 100644 --- a/public/language/bg/search.json +++ b/public/language/bg/search.json @@ -13,7 +13,7 @@ "categories": "Категории", "all-categories": "Всички категории", "categories-x": "Категории: %1", - "categories-watched-categories": "Категории: Следени категории", + "categories-watched-categories": "Категории: Наблюдавани категории", "type-a-category": "Въведете категория", "tags": "Етикети", "tags-x": "Етикети: %1", diff --git a/public/language/bg/tags.json b/public/language/bg/tags.json index 0ef3141a68..675e33280b 100644 --- a/public/language/bg/tags.json +++ b/public/language/bg/tags.json @@ -8,8 +8,8 @@ "no-tags": "Все още няма етикети.", "select-tags": "Изберете етикети", "tag-whitelist": "Списък от разрешени етикети", - "watching": "Следите", - "not-watching": "Не следите", + "watching": "Наблюдавате", + "not-watching": "Не наблюдавате", "watching.description": "Искам да получавам известия за нови теми.", "not-watching.description": "Не искам да получавам известия за нови теми.", "following-tag.message": "Вече ще получавате известия, когато някой публикува тема с този етикет.", diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json index 37addfe20f..5d4a19c877 100644 --- a/public/language/bg/topic.json +++ b/public/language/bg/topic.json @@ -80,13 +80,13 @@ "markAsUnreadForAll.success": "Темата е отбелязана като непрочетена за всички.", "mark-unread": "Отбелязване като непрочетена", "mark-unread.success": "Темата е отбелязана като непрочетена.", - "watch": "Следене", - "unwatch": "Спиране на следенето", + "watch": "Наблюдаване", + "unwatch": "Спиране на наблюдаването", "watch.title": "Получавайте известия за новите отговори в тази тема", - "unwatch.title": "Спрете да следите тази тема", + "unwatch.title": "Спрете да наблюдавате тази тема", "share-this-post": "Споделете тази публикация", - "watching": "Следите", - "not-watching": "Не следите", + "watching": "Наблюдавате", + "not-watching": "Не наблюдавате", "ignoring": "Пренебрегвате", "watching.description": "Искам да получавам известия за новите отговори.
Искам темата да се показва в списъка с непрочетени.", "not-watching.description": "Не искам да получавам известия за новите отговори.
Темата да се показва в списъка с непрочетени, само ако категорията не се пренебрегва.", diff --git a/public/language/bg/unread.json b/public/language/bg/unread.json index 8bb274ddec..3bdc4d2954 100644 --- a/public/language/bg/unread.json +++ b/public/language/bg/unread.json @@ -9,7 +9,7 @@ "topics-marked-as-read.success": "Темите бяха отбелязани като прочетени!", "all-topics": "Всички теми", "new-topics": "Нови теми", - "watched-topics": "Следени теми", + "watched-topics": "Наблюдавани теми", "unreplied-topics": "Теми без отговор", "multiple-categories-selected": "Избрани са няколко" } \ No newline at end of file diff --git a/public/language/bg/user.json b/public/language/bg/user.json index 503aa429ec..1453254f71 100644 --- a/public/language/bg/user.json +++ b/public/language/bg/user.json @@ -35,12 +35,12 @@ "profile-views": "Преглеждания на профила", "reputation": "Репутация", "bookmarks": "Отметки", - "watched-categories": "Следени категории", - "watched-tags": "Следени етикети", + "watched-categories": "Наблюдавани категории", + "watched-tags": "Наблюдавани етикети", "change-all": "Промяна на всички", - "watched": "Следени", + "watched": "Наблюдавани", "ignored": "Пренебрегвани", - "default-category-watch-state": "Състояние по подразбиране за следенето на категории", + "default-category-watch-state": "Състояние по подразбиране за наблюдаването на категории", "followers": "Последователи", "following": "Следва", "blocks": "Блокира", @@ -108,7 +108,7 @@ "has-no-posts": "Този потребител не е публикувал нищо досега.", "has-no-best-posts": "Този потребител не е получавал положителни гласове за публикациите си досега.", "has-no-topics": "Този потребител не е създавал теми досега.", - "has-no-watched-topics": "Този потребител не е следил нито една тема досега.", + "has-no-watched-topics": "Този потребител не е наблюдавал нито една тема досега.", "has-no-ignored-topics": "Този потребител не е пренебрегнал нито една тема досега.", "has-no-upvoted-posts": "Този потребител не е гласувал положително досега.", "has-no-downvoted-posts": "Този потребител не е гласувал отрицателно досега.", @@ -137,8 +137,8 @@ "topic-search-help": "Ако е включено, търсенето в темата ще замени стандартното поведение на браузъра при търсене в страницата и ще Ви позволи да претърсвате цялата тема, а не само това, което се вижда на екрана", "update-url-with-post-index": "Обновяване на адресната лента с номера на публикацията по време на разглеждане на темите", "scroll-to-my-post": "След публикуване на отговор, да се показва новата публикация", - "follow-topics-you-reply-to": "Следене на темите, в които отговаряте", - "follow-topics-you-create": "Следене на темите, които създавате", + "follow-topics-you-reply-to": "Наблюдаване на темите, в които отговаряте", + "follow-topics-you-create": "Наблюдаване на темите, които създавате", "grouptitle": "Заглавие на групата", "group-order-help": "Изберете група и използвайте стрелките, за да пренаредите заглавията", "no-group-title": "Няма заглавие на група", diff --git a/public/language/vi/admin/admin.json b/public/language/vi/admin/admin.json index 66d88dc622..3089b0067e 100644 --- a/public/language/vi/admin/admin.json +++ b/public/language/vi/admin/admin.json @@ -2,7 +2,7 @@ "alert.confirm-rebuild-and-restart": "Bạn có chắc chắn muốn xây lại và khởi động lại NodeBB?", "alert.confirm-restart": "Bạn có chắc muốn khởi động lại NodeBB", - "acp-title": "%1 | Bảng Điểu Khiển Quản Trị Viên NodeBB", + "acp-title": "%1 | Bảng Điểu Khiển Quản Trị NodeBB", "settings-header-contents": "Nội dung", "changes-saved": "Changes Saved", "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", diff --git a/public/language/vi/admin/advanced/database.json b/public/language/vi/admin/advanced/database.json index 1befc744af..a1ae4cd261 100644 --- a/public/language/vi/admin/advanced/database.json +++ b/public/language/vi/admin/advanced/database.json @@ -17,7 +17,7 @@ "mongo.file-size": "Kích cỡ tệp", "mongo.resident-memory": "Bộ Nhớ Thường Trú", "mongo.virtual-memory": "Bộ Nhớ Ảo", - "mongo.mapped-memory": "Mapped Memory", + "mongo.mapped-memory": "Bộ nhớ được ánh xạ", "mongo.bytes-in": "Byte trong", "mongo.bytes-out": "Byte ngoài", "mongo.num-requests": "Số lượng yêu cầu", diff --git a/public/language/vi/admin/extend/widgets.json b/public/language/vi/admin/extend/widgets.json index 1f01555b49..271ee1eede 100644 --- a/public/language/vi/admin/extend/widgets.json +++ b/public/language/vi/admin/extend/widgets.json @@ -30,6 +30,6 @@ "start-date": "Ngày bắt đầu", "end-date": "Ngày kết thúc", "hide-on-mobile": "Ẩn trên thiết bị di động", - "hide-drafts": "Hide drafts", - "show-drafts": "Show drafts" + "hide-drafts": "Ẩn bản nháp", + "show-drafts": "Hiện bản nháp" } \ No newline at end of file diff --git a/public/language/vi/admin/manage/categories.json b/public/language/vi/admin/manage/categories.json index 433508a0bc..d12779ddff 100644 --- a/public/language/vi/admin/manage/categories.json +++ b/public/language/vi/admin/manage/categories.json @@ -16,7 +16,7 @@ "ext-link": "Liên Kết Bên Ngoài", "subcategories-per-page": "Danh mục phụ mỗi trang", "is-section": "Coi danh mục này như một phần", - "post-queue": "Hàng đợi bài đăng", + "post-queue": "Xếp hàng bài đăng", "tag-whitelist": "Danh Sách Trắng Gắn Thẻ ", "upload-image": "Tải Lên Ảnh", "upload": "Tải lên", diff --git a/public/language/vi/admin/manage/privileges.json b/public/language/vi/admin/manage/privileges.json index 3cc3c25e73..fbe4434e79 100644 --- a/public/language/vi/admin/manage/privileges.json +++ b/public/language/vi/admin/manage/privileges.json @@ -41,7 +41,7 @@ "purge": "Loại Bỏ", "moderate": "Điều hành", "admin-dashboard": "Bảng Điều Khiển", - "admin-categories": "Chuyên mục", + "admin-categories": "Danh mục", "admin-privileges": "Đặc Quyền", "admin-users": "Người dùng", "admin-admins-mods": "Quản Trị Viên & Người Điều Hành", diff --git a/public/language/vi/admin/manage/registration.json b/public/language/vi/admin/manage/registration.json index c923fd69bd..adc7ff9d32 100644 --- a/public/language/vi/admin/manage/registration.json +++ b/public/language/vi/admin/manage/registration.json @@ -1,6 +1,6 @@ { - "queue": "Hàng đợi", - "description": "Không có ai xếp hàng đợi đăng ký.
Để bật tính năng này, truy cập Cài đặt → Người dùng → Người dùng đăng ký và đặt Kiểu Đăng Ký là \"Phê Duyệt Của Quản Trị Viên\".", + "queue": "Xếp hàng", + "description": "Không có ai xếp hàng đăng ký.
Để bật tính năng này, vào Cài đặt → Người dùng → Người dùng đăng ký và đặt Kiểu Đăng Ký là \"Phê Duyệt Của Quản Trị Viên\".", "list.name": "Tên", "list.email": "Thư điện tử", diff --git a/public/language/vi/admin/menu.json b/public/language/vi/admin/menu.json index 185e8cfeb7..857588ff72 100644 --- a/public/language/vi/admin/menu.json +++ b/public/language/vi/admin/menu.json @@ -13,9 +13,9 @@ "manage/tags": "Thẻ", "manage/users": "Người dùng", "manage/admins-mods": "Quản trị viên & Người điều hành", - "manage/registration": "Hàng Đợi Đăng Ký", + "manage/registration": "Xếp Hàng Đăng Ký", "manage/flagged-content": "Flagged Content", - "manage/post-queue": "Hàng Đợi Bài Viết", + "manage/post-queue": "Xếp Hàng Bài Đăng", "manage/groups": "Nhóm", "manage/ip-blacklist": "Danh sách đen IP", "manage/uploads": "Tải lên", diff --git a/public/language/vi/admin/settings/notifications.json b/public/language/vi/admin/settings/notifications.json index c7055b3c21..f153ecfe8c 100644 --- a/public/language/vi/admin/settings/notifications.json +++ b/public/language/vi/admin/settings/notifications.json @@ -3,5 +3,5 @@ "welcome-notification": "Thông Báo Chào Mừng", "welcome-notification-link": "Liên Kết Thông Báo Chào Mừng", "welcome-notification-uid": "Thông Báo Chào Mừng Người Dùng (UID)", - "post-queue-notification-uid": "Hàng Đợi Người Dùng Đăng Bài (UID)" + "post-queue-notification-uid": "Người Dùng Đợi Đăng (UID)" } \ No newline at end of file diff --git a/public/language/vi/admin/settings/post.json b/public/language/vi/admin/settings/post.json index fec778cb83..ac0bfad6b8 100644 --- a/public/language/vi/admin/settings/post.json +++ b/public/language/vi/admin/settings/post.json @@ -8,14 +8,14 @@ "sorting.most-posts": "Nhiều Bài Đăng", "sorting.topic-default": "Sắp Xếp Chủ Đề Mặc Định", "length": "Độ Dài Bài Đăng", - "post-queue": "Hàng Đợi Bài Đăng", + "post-queue": "Xếp Hàng Bài Đăng", "restrictions": "Hạn Chế Đăng Bài", "restrictions-new": "Giới Hạn Người Dùng Mới", - "restrictions.post-queue": "Bật Hàng Đợi Bài Đăng", - "restrictions.post-queue-rep-threshold": "Cần có đủ uy tín để vượt qua hàng đợi bài đăng", - "restrictions.groups-exempt-from-post-queue": "Chọn các nhóm được miễn khỏi hàng đợi bài đăng", + "restrictions.post-queue": "Bật Xếp Hàng Bài Đăng", + "restrictions.post-queue-rep-threshold": "Danh tiếng cần thiết để vượt qua đợi đăng bài", + "restrictions.groups-exempt-from-post-queue": "Chọn các nhóm được miễn khỏi đợi đăng bài", "restrictions-new.post-queue": "Bật hạn chế người dùng mới", - "restrictions.post-queue-help": "Bật hàng đợi bài đăng sẽ đưa các bài đăng của người dùng mới vào hàng đợi phê duyệt", + "restrictions.post-queue-help": "Bật xếp hàng đăng bài sẽ đưa các bài của người dùng mới vào xếp hàng phê duyệt", "restrictions-new.post-queue-help": "Bật hạn chế người dùng mới sẽ đặt hạn chế đối với bài đăng do người dùng mới tạo", "restrictions.seconds-between": "Số giây giữa các bài đăng", "restrictions.seconds-between-new": "Số giây giữa các bài đăng cho người dùng mới", diff --git a/public/language/vi/admin/settings/user.json b/public/language/vi/admin/settings/user.json index 9b895f041a..cf0d134b36 100644 --- a/public/language/vi/admin/settings/user.json +++ b/public/language/vi/admin/settings/user.json @@ -43,7 +43,7 @@ "registration-type.admin-invite-only": "Chỉ Quản Trị Viên Mời", "registration-type.disabled": "Không có đăng ký", "registration-type.help": "Bình thường - Người dùng có thể đăng ký từ trang /register.
\nChỉ mời - Người dùng có thể mời những người khác từ trang người dùng.
\nChỉ Quản Trị Viên mời - Chỉ quản trị viên mới có thể mời người khác từ trang người dùngadmin/manage/users.
\nKhông đăng ký - Không đăng ký người dùng.
", - "registration-approval-type.help": "Bình thường - Người dùng được đăng ký ngay lập tức.
\nPhê duyệt của quản trị viên - Đăng ký người dùng được đặt trong một hàng đợi phê duyệt cho quản trị viên.
\nPhê duyệt của quản trị viên cho các IP - Bình thường cho người dùng mới, Phê duyệt quản trị cho các địa chỉ IP đã có tài khoản.
", + "registration-approval-type.help": "Bình thường - Người dùng được đăng ký ngay lập tức.
\nPhê duyệt của quản trị viên - Đăng ký người dùng được đặt trong xếp hàng phê duyệt cho quản trị viên.
\nPhê duyệt của quản trị viên cho các IP - Bình thường cho người dùng mới, Phê duyệt quản trị cho các địa chỉ IP đã có tài khoản.
", "registration-queue-auto-approve-time": "Thời Gian Xét Duyệt Tự Động", "registration-queue-auto-approve-time-help": "Giờ trước khi người dùng được xét duyệt tự động. 0 để tắt.", "registration-queue-show-average-time": "Hiện thời gian xét duyệt cho người dùng mới biết", @@ -79,7 +79,7 @@ "follow-replied-topics": "Theo dõi các chủ đề mà bạn trả lời", "default-notification-settings": "Cài đặt thông báo mặc định", "categoryWatchState": "Trạng thái xem chuyên mục mặc định", - "categoryWatchState.tracking": "Tracking", + "categoryWatchState.tracking": "Theo dõi", "categoryWatchState.notwatching": "Không Xem", "categoryWatchState.ignoring": "Bỏ Qua" } diff --git a/public/language/vi/category.json b/public/language/vi/category.json index 1edff4bc14..3a7edad00e 100644 --- a/public/language/vi/category.json +++ b/public/language/vi/category.json @@ -1,5 +1,5 @@ { - "category": "Chuyên mục", + "category": "Danh mục", "subcategories": "Danh mục phụ", "new-topic-button": "Chủ Đề Mới", "guest-login-post": "Đăng nhập để đăng bài", @@ -10,15 +10,15 @@ "watch": "Xem", "ignore": "Bỏ qua", "watching": "Đang xem", - "tracking": "Tracking", + "tracking": "Theo dõi", "not-watching": "Không xem", "ignoring": "Bỏ qua", - "watching.description": "Notify me of new topics.
Show topics in unread & recent", - "tracking.description": "Shows topics in unread & recent", + "watching.description": "Thông báo tôi về chủ đề mới.
Hiển thị chủ đề chưa đọc và gần đây", + "tracking.description": "Hiển thị chủ đề chưa đọc và gần đây", "not-watching.description": "Không hiển thị chủ đề trong chưa đọc, hiển thị gần đây", - "ignoring.description": "Do not show topics in unread & recent", + "ignoring.description": "Không hiển thị chủ đề chưa đọc và gần đây", "watching.message": "Bây giờ bạn đang xem cập nhật từ danh mục này và tất cả các danh mục phụ", - "tracking.message": "You are now tracking updates from this category and all subcategories", + "tracking.message": "Bạn hiện đang theo dõi thông tin cập nhật từ danh mục này và tất cả các danh mục phụ", "notwatching.message": "Bạn không xem cập nhật từ danh mục này và tất cả các danh mục phụ", "ignoring.message": "Bây giờ bạn đang bỏ qua các cập nhật từ danh mục này và tất cả các danh mục phụ", "watched-categories": "Chuyên mục đã xem", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index ec394ade91..aa354aa01b 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -91,7 +91,7 @@ "category-not-selected": "Danh mục không được chọn.", "too-many-posts": "Bạn chỉ có đăng bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.", "too-many-posts-newbie": "Là người dùng mới, bạn chỉ có thể đăng %1 giây một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại", - "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": "Là người dùng mới, bạn chỉ có thể đăng bài %1 phút một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại", "already-posting": "You are already posting", "tag-too-short": "Vui lòng nhập tag dài hơn. Tag phải có tối thiểu %1 ký tự.", "tag-too-long": "Vui lòng nhập tag ngắn hơn. Tag chỉ có thể có tối đa %1 ký tự.", @@ -213,7 +213,7 @@ "cant-move-to-same-topic": "Bạn không thể di chuyển bài viết vào cùng chủ đề hiện tại!", "cant-move-topic-to-same-category": "Không thể di chuyển chủ đề sang cùng chuyên mục!", "cannot-block-self": "Bạn không thể tự khóa tài khoản của bạn!", - "cannot-block-privileged": "Bạn không thể khóa quản trị viên hoặc là người quản lý chung.", + "cannot-block-privileged": "Bạn không thể khóa quản trị viên hay người quản lý chung.", "cannot-block-guest": "Khách không thể chặn người dùng khác", "already-blocked": "Người dùng này đã bị chặn", "already-unblocked": "Người dùng này đã được bỏ chặn", diff --git a/public/language/vi/groups.json b/public/language/vi/groups.json index 05dde2a4da..cbd704ca1b 100644 --- a/public/language/vi/groups.json +++ b/public/language/vi/groups.json @@ -32,7 +32,7 @@ "details.kick": "Đá 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", + "details.owner-options": "Quản Trị Nhóm", "details.group-name": "Tên nhóm", "details.member-count": "Số thành viên", "details.creation-date": "Ngày Thành Lập", diff --git a/public/language/vi/login.json b/public/language/vi/login.json index 02fd0d75b9..2925e769f2 100644 --- a/public/language/vi/login.json +++ b/public/language/vi/login.json @@ -7,6 +7,6 @@ "failed-login-attempt": "Đăng Nhập Thất Bại", "login-successful": "Bạn đã đăng nhập thành công!", "dont-have-account": "Chưa có tài khoản?", - "logged-out-due-to-inactivity": "Đã đăng xuất bạn khỏi Bảng Điều Khiển Quản Trị Viên do không hoạt động quá lâu", + "logged-out-due-to-inactivity": "Bạn đã đăng xuất khỏi Bảng Điều Khiển Quản Trị Viên do không hoạt động quá lâu", "caps-lock-enabled": "Caps Lock được bật" } \ No newline at end of file diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index de9ad3c35f..93cd6e8b04 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -68,7 +68,7 @@ "chat.in-room": "Trong phòng này", "chat.kick": "Loại ra", "chat.show-ip": "Hiện IP", - "chat.copy-link": "Copy link", + "chat.copy-link": "Sao chép liên kết", "chat.owner": "Chủ Phòng", "chat.grant-rescind-ownership": "Cấp/Hủy bỏ Quyền sở hữu", "chat.system.user-join": "%1 đã tham gia phòng ", @@ -103,7 +103,7 @@ "composer.zen-mode": "Chế Độ Zen", "composer.select-category": "Chọn một chuyên mục", "composer.textarea.placeholder": "Nhập nội dung bài đăng của bạn vào đây, kéo và thả hình ảnh", - "composer.post-queue-alert": "Hello👋!
This forum uses a post queue system, since you are a new user your post will be hidden until it is approved by our moderation team.", + "composer.post-queue-alert": "Chào👋!
Diễn đàn này dùng hệ thống xếp hàng bài đăng, vì bạn là người dùng mới nên bài đăng của bạn bị ẩn cho đến khi nhóm kiểm duyệt chấp thuận.", "composer.schedule-for": "Lên lịch chủ đề cho", "composer.schedule-date": "Ngày", "composer.schedule-time": "Thời gian", diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json index d02537243a..b60e410d8b 100644 --- a/public/language/vi/notifications.json +++ b/public/language/vi/notifications.json @@ -13,7 +13,7 @@ "all": "Tất cả", "topics": "Chủ đề", "tags": "Thẻ", - "categories": "Categories", + "categories": "Danh mục", "replies": "Phản hồi", "chat": "Trò Chuyện", "group-chat": "Trò Chuyện Nhóm", @@ -56,7 +56,7 @@ "user-posted-topic-with-tag-dual": "%1 đã đăng một chủ đề mới với thẻ %2%3", "user-posted-topic-with-tag-triple": "%1 đã đăng một chủ đề mới với thẻ %2, %3%4", "user-posted-topic-with-tag-multiple": "%1 đã đăng một chủ đề mới với thẻ %2", - "user-posted-topic-in-category": "%1 has posted a new topic in %2", + "user-posted-topic-in-category": "%1 đã đăng chủ đề mới trong %2", "user-started-following-you": "%1 đã theo dõi bạn.", "user-started-following-you-dual": "%1%2 đã bắt đầu theo dõi bạn.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", @@ -69,9 +69,9 @@ "posts-exported": "%1 đã xuất bài viết, nhấn tải xuống", "uploads-exported": "%1 đã xuất tải lên, nhấn tải xuống", "users-csv-exported": "Đã xuất csv người dùng, nhấp để tải xuống", - "post-queue-accepted": "Bài đăng trong hàng đợi của bạn đã được chấp nhận. Nhấn vào đây để xem bài viết của bạn.", - "post-queue-rejected": "Bài đăng trong hàng đợi của bạn đã bị từ chối", - "post-queue-notify": "Bài đã xếp hàng đã nhận được thông báo:
\"%1\"", + "post-queue-accepted": "Bài đăng đã xếp hàng của bạn được chấp nhận. Bấm vào đây để xem bài của bạn.", + "post-queue-rejected": "Bài đăng đã xếp hàng của bạn đã bị từ chối", + "post-queue-notify": "Bài đăng đã xếp hàng nhận được thông báo:
\"%1\"", "email-confirmed": "Đã Xác Nhận Email", "email-confirmed-message": "Cảm ơn bạn đã xác nhận địa chỉ email của bạn. Tài khoản của bạn đã được kích hoạt đầy đủ.", "email-confirm-error-message": "Đã có lỗi khi xác nhận địa chỉ email. Có thể đoạn mã không đúng hoặc đã hết hạn.", @@ -83,7 +83,7 @@ "notificationType-upvote": "Khi ai đó ủng hộ bài viết của bạn", "notificationType-new-topic": "Khi người bạn theo dõi đăng một chủ đề", "notificationType-new-topic-with-tag": "Khi một chủ đề được đăng với một thẻ bạn theo dõi", - "notificationType-new-topic-in-category": "When a topic is posted in a category you are watching", + "notificationType-new-topic-in-category": "Khi một chủ đề được đăng trong danh mục bạn đang xem", "notificationType-new-reply": "Khi một câu trả lời mới được đăng trong một chủ đề bạn đang xem", "notificationType-post-edit": "Khi bài viết được chỉnh sửa trong chủ đề bạn đang xem", "notificationType-follow": "Khi ai đó bắt đầu theo dõi bạn", @@ -93,8 +93,8 @@ "notificationType-group-invite": "Khi bạn nhận một lời mời nhóm", "notificationType-group-leave": "Khi người dùng rời khỏi nhóm của bạn", "notificationType-group-request-membership": "Khi ai đó yêu cầu tham gia một nhóm bạn sở hữu", - "notificationType-new-register": "Khi ai đó được thêm vào hàng đợi đăng ký", - "notificationType-post-queue": "Khi bài đăng được thêm vào lượt chờ", + "notificationType-new-register": "Khi ai đó được thêm vào xếp hàng đợi đăng ký", + "notificationType-post-queue": "Khi một bài đăng mới xếp hàng đợi", "notificationType-new-post-flag": "Khi bài đăng bị gắn cờ cảnh báo", "notificationType-new-user-flag": "Khi người dùng bị gắn cờ cảnh báo", "notificationType-new-reward": "Khi bạn kiếm được phần thưởng mới" diff --git a/public/language/vi/pages.json b/public/language/vi/pages.json index 0c283f1b36..ce064852f2 100644 --- a/public/language/vi/pages.json +++ b/public/language/vi/pages.json @@ -13,8 +13,8 @@ "moderator-tools": "Công Cụ Điều Hành", "flagged-content": "Nội Dung Bị Gắn Cờ", "ip-blacklist": "Danh sách đen IP", - "post-queue": "Hàng Đợi Bài Viết", - "registration-queue": "Hàng Đợi Đăng Ký", + "post-queue": "Xếp Hàng Bài Viết", + "registration-queue": "Xếp Hàng Đăng Ký", "users/online": "Thành viên trực tuyến", "users/latest": "Thành viên mới nhất", "users/sort-posts": "Thành viên có nhiều bài đăng", diff --git a/public/language/vi/post-queue.json b/public/language/vi/post-queue.json index 826a01287e..31beec5aaf 100644 --- a/public/language/vi/post-queue.json +++ b/public/language/vi/post-queue.json @@ -1,8 +1,8 @@ { - "post-queue": "Hàng Đợi Bài Đăng", - "no-queued-posts": "Không có bài viết trong hàng đợi bài viết.", - "no-single-post": "Chủ đề hoặc bài đăng bạn đang tìm kiếm không còn trong hàng đợi. Nó có thể đã được phê duyệt hoặc xóa rồi.", + "post-queue": "Xếp Hàng Bài Đăng", + "no-queued-posts": "Không có bài trong xếp hàng đăng bài", + "no-single-post": "Chủ đề hoặc bài đăng bạn đang tìm kiếm không còn xếp hàng. Có thể đã phê duyệt hoặc xóa rồi.", "enabling-help": "Để bật tính năng này, hãy đi tới Cài Đặt → Bài Đăng → Hàng Đợi Đăng và bật Hàng Đợi Đăng.", "back-to-list": "Trở về Hàng Đợi Đăng", "user": "Người dùng", diff --git a/public/language/vi/register.json b/public/language/vi/register.json index 7fdbce8f59..0018ecbecc 100644 --- a/public/language/vi/register.json +++ b/public/language/vi/register.json @@ -18,7 +18,7 @@ "terms-of-use": "Điều khoản sử dụng", "agree-to-terms-of-use": "Tôi đồng ý với các điều khoản sử dụng", "terms-of-use-error": "Bạn phải đồng ý với Điều Khoản Sử Dụng", - "registration-added-to-queue": "Đăng ký của bạn đã được thêm vào hàng đợi phê duyệt. Bạn sẽ nhận được email khi quản trị viên chấp nhận yêu cầu.", + "registration-added-to-queue": "Đăng ký của bạn đã được xếp hàng đợi phê duyệt. Bạn sẽ nhận được email khi quản trị viên chấp nhận yêu cầu.", "registration-queue-average-time": "Thời gian chúng tôi phê duyệt tư cách thành viên là %1 giờ %2 phút.", "registration-queue-auto-approve-time": "Tư cách thành viên của bạn sẽ được kích hoạt đầy đủ trong tối đa %1 giờ.", "interstitial.intro": "Chúng tôi muốn một số thông tin bổ sung để cập nhật tài khoản của bạn…", diff --git a/public/language/vi/search.json b/public/language/vi/search.json index 95f9e7218d..59c6f24984 100644 --- a/public/language/vi/search.json +++ b/public/language/vi/search.json @@ -4,16 +4,16 @@ "no-matches": "Không tìm thấy kết quả phù hợp", "advanced-search": "Tìm kiếm nâng cao", "in": "Trong", - "in-titles": "In titles", - "in-titles-posts": "In titles and posts", - "in-posts": "In posts", - "in-categories": "In categories", - "in-users": "In users", - "in-tags": "In tags", + "in-titles": "Trong tiêu đề", + "in-titles-posts": "Trong tiêu đề và bài đăng", + "in-posts": "Trong bài đăng", + "in-categories": "Trong danh mục", + "in-users": "Trong người dùng", + "in-tags": "Trong thẻ", "categories": "Danh mục", "all-categories": "Tất cả danh mục", "categories-x": "Danh mục: %1", - "categories-watched-categories": "Categories: Watched categories", + "categories-watched-categories": "Danh mục: Danh mục đã xem", "type-a-category": "Nhập một danh mục", "tags": "Thẻ", "tags-x": "Tags: %1", @@ -71,7 +71,7 @@ "number-of-views": "Số lượt xem", "topic-start-date": "Ngày bắt đầu chủ đề", "username": "Tên đăng nhập", - "category": "Chuyên mục", + "category": "Danh mục", "descending": "Theo thứ tự giảm dần", "ascending": "Theo thứ tự tăng dần", "sort-by-relevance-desc": "Sắp xếp theo: Mức độ liên quan giảm dần", diff --git a/public/language/vi/success.json b/public/language/vi/success.json index f4034365f8..7324015d79 100644 --- a/public/language/vi/success.json +++ b/public/language/vi/success.json @@ -1,7 +1,7 @@ { "success": "Thành công", "topic-post": "Bạn đã đăng bài thành công", - "post-queued": "Bài đăng của bạn được xếp hàng để phê duyệt. Bạn sẽ nhận được thông báo khi nó được chấp nhận hoặc bị từ chối.", + "post-queued": "Bài đăng của bạn đã xếp hàng phê duyệt. Bạn sẽ được thông báo khi chấp nhận hoặc bị từ chối.", "authentication-successful": "Xác thực thành công", "settings-saved": "Đã lưu thiết lập" } \ No newline at end of file diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json index 7cb5f8eacd..bb4ee0a8f7 100644 --- a/public/language/vi/topic.json +++ b/public/language/vi/topic.json @@ -58,7 +58,7 @@ "user-restored-topic-on": "%1 đã khôi phục chủ đề này trên %2", "user-moved-topic-from-ago": "%1 đã chuyển chủ đề này từ %2 %3", "user-moved-topic-from-on": "%1 đã chuyển chủ đề này từ %2 trên %3", - "user-queued-post-ago": "%1 đã xếp hàng bài đăng để được phê duyệt %3", + "user-queued-post-ago": "%1 đã xếp hàng bài đăng để phê duyệt %3", "user-queued-post-on": "%1 queued post for approval on %3", "user-referenced-topic-ago": "%1 referenced this topic %3", "user-referenced-topic-on": "%1 referenced this topic on %3", diff --git a/public/language/vi/unread.json b/public/language/vi/unread.json index 4bf06365f3..a0abb036eb 100644 --- a/public/language/vi/unread.json +++ b/public/language/vi/unread.json @@ -5,11 +5,11 @@ "mark-as-read": "Đánh dấu đã đọc", "selected": "Đã chọn", "all": "Tất cả", - "all-categories": "Tất cả chuyên mục", + "all-categories": "Tất cả danh mục", "topics-marked-as-read.success": "Chủ đề được đánh dấu đã đọc", "all-topics": "Tất Cả Chủ Đề", "new-topics": "Chủ đề mới", "watched-topics": "Chủ đề đã xem", "unreplied-topics": "Chủ Đề Chưa Có Trả Lời", - "multiple-categories-selected": "Chọn nhiều cùng lúc" + "multiple-categories-selected": "Chọn Nhiều" } \ No newline at end of file diff --git a/public/language/vi/user.json b/public/language/vi/user.json index 1c8863896d..d26ab3009d 100644 --- a/public/language/vi/user.json +++ b/public/language/vi/user.json @@ -122,7 +122,7 @@ "category-topic-sort": "Sắp xếp chủ đề danh mục", "topic-post-sort": "Sắp xếp bài đăng chủ đề", "max-items-per-page": "Tối đa %1", - "acp-language": "Ngôn ngữ trang quản trị", + "acp-language": "Ngôn Ngữ Trang Quản Trị", "notifications": "Thông báo", "upvote-notif-freq": "Tần Suất Thông Báo Ủng Hộ", "upvote-notif-freq.all": "Tất Cả Số Ủng Hộ", @@ -185,7 +185,7 @@ "consent.digest-frequency": "Trừ khi thay đổi rõ ràng trong cài đặt người dùng của bạn, cộng đồng này cung cấp thông báo email mỗi %1.", "consent.digest-off": "Trừ khi thay đổi trong cài đặt người dùng của bạn, cộng đồng này sẽ không gửi thông báo qua email thông báo", "consent.received": "Bạn đã đồng ý cho trang web này để thu thập và xử lý thông tin của bạn. Không có hành động bổ sung được yêu cầu.", - "consent.not-received": "Bạn đã không đồng ý cung cấp cho thu thập và xử lý dữ liệu. Bất cứ lúc nào quản trị trang web này'có thể chọn xóa tài khoản của bạn để tuân thủ Quy định bảo vệ dữ liệu chung.", + "consent.not-received": "Bạn đã không đồng ý cung cấp cho thu thập và xử lý dữ liệu. Bất cứ lúc nào, ban quản trị trang web này có thể chọn xóa tài khoản của bạn để tuân thủ Quy định chung về bảo vệ dữ liệu.", "consent.give": "Cho phép", "consent.right-of-access": "Bạn có quyền truy cập", "consent.right-of-access-description": "Bạn có quyền truy cập bất kỳ dữ liệu trang web này thu thập. Bạn có thể lấy một bản sao của dữ liệu này bằng cách nhấp vào nút thích hợp bên dưới.", From 3b91e8e27ff5de070d5bd397b4a81c8348301db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Nov 2023 10:49:06 -0500 Subject: [PATCH 054/201] fix: add missing tracking --- public/src/modules/categorySelector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/modules/categorySelector.js b/public/src/modules/categorySelector.js index 4911682624..f402d128cf 100644 --- a/public/src/modules/categorySelector.js +++ b/public/src/modules/categorySelector.js @@ -12,7 +12,7 @@ define('categorySelector', [ options = options || {}; const onSelect = options.onSelect || function () {}; - options.states = options.states || ['watching', 'notwatching', 'ignoring']; + options.states = options.states || ['watching', 'tracking', 'notwatching', 'ignoring']; options.template = options.template || 'partials/category/selector-dropdown-left'; hooks.fire('action:category.selector.options', { el: el, options: options }); From f4bbc5bd3ffecee225894c8162c857816a5e3302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Nov 2023 10:49:42 -0500 Subject: [PATCH 055/201] fix: another missing state --- public/src/modules/categoryFilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/modules/categoryFilter.js b/public/src/modules/categoryFilter.js index fcbdc35e6a..21dcd46b96 100644 --- a/public/src/modules/categoryFilter.js +++ b/public/src/modules/categoryFilter.js @@ -8,7 +8,7 @@ define('categoryFilter', ['categorySearch', 'api', 'hooks'], function (categoryS return; } options = options || {}; - options.states = options.states || ['watching', 'notwatching', 'ignoring']; + options.states = options.states || ['watching', 'tracking', 'notwatching', 'ignoring']; options.template = options.template || 'partials/category/filter-dropdown-left'; hooks.fire('action:category.filter.options', { el: el, options: options }); From 3d505c5c9d6859159bff2d3642e14f377ca22745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Nov 2023 11:32:51 -0500 Subject: [PATCH 056/201] feat: move new user restrrictions --- .../language/en-GB/admin/settings/chat.json | 1 - .../language/en-GB/admin/settings/post.json | 4 --- .../language/en-GB/admin/settings/user.json | 8 ++++- src/views/admin/settings/chat.tpl | 14 +++----- src/views/admin/settings/post.tpl | 27 ---------------- src/views/admin/settings/user.tpl | 32 +++++++++++++++++++ 6 files changed, 43 insertions(+), 43 deletions(-) diff --git a/public/language/en-GB/admin/settings/chat.json b/public/language/en-GB/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/en-GB/admin/settings/chat.json +++ b/public/language/en-GB/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/en-GB/admin/settings/post.json b/public/language/en-GB/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/en-GB/admin/settings/post.json +++ b/public/language/en-GB/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/en-GB/admin/settings/user.json b/public/language/en-GB/admin/settings/user.json index 88f14c76cd..7c55d69ee5 100644 --- a/public/language/en-GB/admin/settings/user.json +++ b/public/language/en-GB/admin/settings/user.json @@ -81,5 +81,11 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" + "categoryWatchState.ignoring": "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)" } diff --git a/src/views/admin/settings/chat.tpl b/src/views/admin/settings/chat.tpl index cbd138d0c8..bc37c83757 100644 --- a/src/views/admin/settings/chat.tpl +++ b/src/views/admin/settings/chat.tpl @@ -49,16 +49,10 @@
-
-
- - -
-
- - -
-
+
+ + +
diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index 816dfabede..bd99f75a4a 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -120,33 +120,6 @@
- -
-
[[admin/settings/post:restrictions-new]]
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
-
[[admin/settings/post:post-queue]]
diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 60d80f781e..4a2bcc19b7 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -213,6 +213,38 @@
+ +
+
[[admin/settings/user:restrictions-new]]
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
[[admin/settings/guest:guest-settings]]
From 119c3362ff590ea570cdba9eb3be44a40b5b3bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Nov 2023 11:37:21 -0500 Subject: [PATCH 057/201] feat: update moved keys --- public/language/ar/admin/settings/post.json | 4 ---- public/language/ar/admin/settings/user.json | 8 ++++++-- public/language/bg/admin/settings/post.json | 4 ---- public/language/bg/admin/settings/user.json | 8 ++++++-- public/language/bn/admin/settings/post.json | 4 ---- public/language/bn/admin/settings/user.json | 8 ++++++-- public/language/cs/admin/settings/post.json | 4 ---- public/language/cs/admin/settings/user.json | 8 ++++++-- public/language/da/admin/settings/post.json | 4 ---- public/language/da/admin/settings/user.json | 8 ++++++-- public/language/de/admin/settings/post.json | 4 ---- public/language/de/admin/settings/user.json | 8 ++++++-- public/language/el/admin/settings/post.json | 4 ---- public/language/el/admin/settings/user.json | 8 ++++++-- public/language/en-US/admin/settings/post.json | 4 ---- public/language/en-US/admin/settings/user.json | 8 ++++++-- public/language/en-x-pirate/admin/settings/post.json | 4 ---- public/language/en-x-pirate/admin/settings/user.json | 8 ++++++-- public/language/es/admin/settings/post.json | 4 ---- public/language/es/admin/settings/user.json | 8 ++++++-- public/language/et/admin/settings/post.json | 4 ---- public/language/et/admin/settings/user.json | 8 ++++++-- public/language/fa-IR/admin/settings/post.json | 4 ---- public/language/fa-IR/admin/settings/user.json | 8 ++++++-- public/language/fi/admin/settings/post.json | 4 ---- public/language/fi/admin/settings/user.json | 8 ++++++-- public/language/fr/admin/settings/post.json | 4 ---- public/language/fr/admin/settings/user.json | 8 ++++++-- public/language/gl/admin/settings/post.json | 4 ---- public/language/gl/admin/settings/user.json | 8 ++++++-- public/language/he/admin/settings/post.json | 4 ---- public/language/he/admin/settings/user.json | 8 ++++++-- public/language/hr/admin/settings/post.json | 4 ---- public/language/hr/admin/settings/user.json | 8 ++++++-- public/language/hu/admin/settings/post.json | 4 ---- public/language/hu/admin/settings/user.json | 8 ++++++-- public/language/hy/admin/settings/post.json | 4 ---- public/language/hy/admin/settings/user.json | 8 ++++++-- public/language/id/admin/settings/post.json | 4 ---- public/language/id/admin/settings/user.json | 8 ++++++-- public/language/it/admin/settings/post.json | 4 ---- public/language/it/admin/settings/user.json | 8 ++++++-- public/language/ja/admin/settings/post.json | 4 ---- public/language/ja/admin/settings/user.json | 8 ++++++-- public/language/ko/admin/settings/post.json | 4 ---- public/language/ko/admin/settings/user.json | 8 ++++++-- public/language/lt/admin/settings/post.json | 4 ---- public/language/lt/admin/settings/user.json | 8 ++++++-- public/language/lv/admin/settings/post.json | 4 ---- public/language/lv/admin/settings/user.json | 8 ++++++-- public/language/ms/admin/settings/post.json | 4 ---- public/language/ms/admin/settings/user.json | 8 ++++++-- public/language/nb/admin/settings/post.json | 4 ---- public/language/nb/admin/settings/user.json | 8 ++++++-- public/language/nl/admin/settings/post.json | 4 ---- public/language/nl/admin/settings/user.json | 8 ++++++-- public/language/pl/admin/settings/post.json | 4 ---- public/language/pl/admin/settings/user.json | 8 ++++++-- public/language/pt-BR/admin/settings/post.json | 4 ---- public/language/pt-BR/admin/settings/user.json | 8 ++++++-- public/language/pt-PT/admin/settings/post.json | 4 ---- public/language/pt-PT/admin/settings/user.json | 8 ++++++-- public/language/ro/admin/settings/post.json | 4 ---- public/language/ro/admin/settings/user.json | 8 ++++++-- public/language/ru/admin/settings/post.json | 4 ---- public/language/ru/admin/settings/user.json | 8 ++++++-- public/language/rw/admin/settings/post.json | 4 ---- public/language/rw/admin/settings/user.json | 8 ++++++-- public/language/sc/admin/settings/post.json | 4 ---- public/language/sc/admin/settings/user.json | 8 ++++++-- public/language/sk/admin/settings/post.json | 4 ---- public/language/sk/admin/settings/user.json | 8 ++++++-- public/language/sl/admin/settings/post.json | 4 ---- public/language/sl/admin/settings/user.json | 8 ++++++-- public/language/sq-AL/admin/settings/post.json | 4 ---- public/language/sq-AL/admin/settings/user.json | 8 ++++++-- public/language/sr/admin/settings/post.json | 4 ---- public/language/sr/admin/settings/user.json | 8 ++++++-- public/language/sv/admin/settings/post.json | 4 ---- public/language/sv/admin/settings/user.json | 8 ++++++-- public/language/th/admin/settings/post.json | 4 ---- public/language/th/admin/settings/user.json | 8 ++++++-- public/language/tr/admin/settings/post.json | 4 ---- public/language/tr/admin/settings/user.json | 8 ++++++-- public/language/uk/admin/settings/post.json | 4 ---- public/language/uk/admin/settings/user.json | 8 ++++++-- public/language/vi/admin/settings/post.json | 4 ---- public/language/vi/admin/settings/user.json | 8 ++++++-- public/language/zh-CN/admin/settings/post.json | 4 ---- public/language/zh-CN/admin/settings/user.json | 8 ++++++-- public/language/zh-TW/admin/settings/post.json | 4 ---- public/language/zh-TW/admin/settings/user.json | 8 ++++++-- 92 files changed, 276 insertions(+), 276 deletions(-) diff --git a/public/language/ar/admin/settings/post.json b/public/language/ar/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/ar/admin/settings/post.json +++ b/public/language/ar/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/ar/admin/settings/user.json b/public/language/ar/admin/settings/user.json index 6bded6c2b8..2ffa3c39f9 100644 --- a/public/language/ar/admin/settings/user.json +++ b/public/language/ar/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ 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 1e12db43b9..7f7781c1cf 100644 --- a/public/language/bg/admin/settings/post.json +++ b/public/language/bg/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Дължина на публикациите", "post-queue": "Опашка за публикации", "restrictions": "Ограничения за публикуването", - "restrictions-new": "Ограничения за новите потребители", "restrictions.post-queue": "Включване на опашката за публикации", "restrictions.post-queue-rep-threshold": "Нужна репутация за пропускане на опашката за публикации", "restrictions.groups-exempt-from-post-queue": "Избиране на групи, които да пропускат опашката за публикации", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Ако опашката за публикации е включена, публикациите на новите потребители ще бъдат добавяни в опашка за одобрение", "restrictions-new.post-queue-help": "Ако ограниченията за новите потребители са включени, това ще зададе някои ограничения за публикациите създадени от новите потребители", "restrictions.seconds-between": "Брой секунди между публикациите", - "restrictions.seconds-between-new": "Брой секунди между публикациите за нови потребители", - "restrictions.rep-threshold": "Необходима репутация за премахване на това ограничение", - "restrictions.seconds-before-new": "Брой секунди преди новите потребители да могат да публикуват за пръв път", "restrictions.seconds-edit-after": "Брой секунди, през които публикациите могат да бъдат редактирани. (0 = изключено)", "restrictions.seconds-delete-after": "Брой секунди, през които публикациите могат да бъдат изтрити. (0 = изключено)", "restrictions.replies-no-delete": "Брой отговори, след които потребителите вече не могат да изтриват собствените си теми. (0 = изключено)", diff --git a/public/language/bg/admin/settings/user.json b/public/language/bg/admin/settings/user.json index 9c401ae75d..f5728c4c6e 100644 --- a/public/language/bg/admin/settings/user.json +++ b/public/language/bg/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Състояние по подразбиране за наблюдаването на категории", "categoryWatchState.tracking": "Да се следят", "categoryWatchState.notwatching": "Да не се наблюдават", - "categoryWatchState.ignoring": "Да се пренебрегват" -} + "categoryWatchState.ignoring": "Да се пренебрегват", + "restrictions-new": "Ограничения за новите потребители", + "restrictions.seconds-between-new": "Брой секунди между публикациите за нови потребители", + "restrictions.rep-threshold": "Необходима репутация за премахване на това ограничение", + "restrictions.seconds-before-new": "Брой секунди преди новите потребители да могат да публикуват за пръв път" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/post.json b/public/language/bn/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/bn/admin/settings/post.json +++ b/public/language/bn/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/bn/admin/settings/user.json b/public/language/bn/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/bn/admin/settings/user.json +++ b/public/language/bn/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/post.json b/public/language/cs/admin/settings/post.json index 02d7a148bb..7e0611c5f4 100644 --- a/public/language/cs/admin/settings/post.json +++ b/public/language/cs/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Délka příspěvku", "post-queue": "Příspěvky ve frontě", "restrictions": "Omezení příspěvků", - "restrictions-new": "Omezení nového uživatele", "restrictions.post-queue": "Povolit frontu pro příspěvky", "restrictions.post-queue-rep-threshold": "Vyžadovaná reputace pro přeskočení fronty příspěvků", "restrictions.groups-exempt-from-post-queue": "Vyberte skupinu, která by měla být vyloučena z fronty příspěvků", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Povolení fronty příspěvků bude mít za následek vložení příspěvků nových uživatelů do fronty pro schválení.", "restrictions-new.post-queue-help": "Povolení omezení nových uživatelů bude mít za následek omezení příspěvků vytvořených novými uživateli", "restrictions.seconds-between": "Počet sekund mezi novými příspěvky", - "restrictions.seconds-between-new": "Sekund mezi příspěvky pro nové uživatele", - "restrictions.rep-threshold": "Ohraničení reputace než začnou platit tato omezení", - "restrictions.seconds-before-new": "Počet sekund, než může nový uživatel vytvořit první příspěvek", "restrictions.seconds-edit-after": "Počet sekund, kdy příspěvek může být ještě upraven (pro zakázání - 0)", "restrictions.seconds-delete-after": "Počet sekund, kdy příspěvek může být ještě odstraněn (pro zakázání - 0)", "restrictions.replies-no-delete": "Počet odpovědí, kdy je již uživateli zakázáno odstranit založená témata (pro zakázání - 0)", diff --git a/public/language/cs/admin/settings/user.json b/public/language/cs/admin/settings/user.json index 81c4e2f23f..22a89b704c 100644 --- a/public/language/cs/admin/settings/user.json +++ b/public/language/cs/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Stav sledování výchozí kategorie", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nesleduji", - "categoryWatchState.ignoring": "Ignorace" -} + "categoryWatchState.ignoring": "Ignorace", + "restrictions-new": "Omezení nového uživatele", + "restrictions.seconds-between-new": "Sekund mezi příspěvky pro nové uživatele", + "restrictions.rep-threshold": "Ohraničení reputace než začnou platit tato omezení", + "restrictions.seconds-before-new": "Počet sekund, než může nový uživatel vytvořit první příspěvek" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/post.json b/public/language/da/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/da/admin/settings/post.json +++ b/public/language/da/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/da/admin/settings/user.json b/public/language/da/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/da/admin/settings/user.json +++ b/public/language/da/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/post.json b/public/language/de/admin/settings/post.json index ad7975d46e..99609e8e57 100644 --- a/public/language/de/admin/settings/post.json +++ b/public/language/de/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Beitragslänge", "post-queue": "Beitragswarteschlange", "restrictions": "Posting beschränkungen", - "restrictions-new": "Beschränkungen für neue Benutzer", "restrictions.post-queue": "Beitragswarteschlange verwenden", "restrictions.post-queue-rep-threshold": "Benötigte Reputation um Beiträge ohne Warteschlange zu verfassen", "restrictions.groups-exempt-from-post-queue": "Gruppen auswählen, die von der Beitragswarteschlange ausgenommen sind", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Aktivierte Beitragswarteschlange sorgt dafür, dass Posts von neuen Benutzern vor dem Veröffentlichen genehmigt werden müssen.", "restrictions-new.post-queue-help": "Durch das Aktivieren von Einschränkungen für neue Benutzer werden Einschränkungen für Beiträge festgelegt, die von neuen Benutzern erstellt wurden", "restrictions.seconds-between": "Anzahl der Sekunden zwischen Posts", - "restrictions.seconds-between-new": "Sekunden zwischen Beiträgen für neue Benutzer", - "restrictions.rep-threshold": "Mindesreputation bevor die Beschränkungen aufgehoben werden", - "restrictions.seconds-before-new": "Sekunden, bevor ein neuer Benutzer seinen ersten Beitrag schreiben kann", "restrictions.seconds-edit-after": "Anzahl der Sekunden, die ein Beitrag bearbeitet werden kann (zum Deaktivieren auf 0 setzen)", "restrictions.seconds-delete-after": "Anzahl der Sekunden, die ein Beitrag löschbar bleibt (zum Deaktivieren auf 0 setzen)", "restrictions.replies-no-delete": "Anzahl der Antworten, nachdem Benutzern das Löschen ihrer eigenen Themen verweigert wurde (zum Deaktivieren auf 0 setzen)", diff --git a/public/language/de/admin/settings/user.json b/public/language/de/admin/settings/user.json index b18f5a5889..862a3bd5c3 100644 --- a/public/language/de/admin/settings/user.json +++ b/public/language/de/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Standardmäßige Beobachtung", "categoryWatchState.tracking": "Verfolgung", "categoryWatchState.notwatching": "Nicht beobachtet", - "categoryWatchState.ignoring": "Ignoriert" -} + "categoryWatchState.ignoring": "Ignoriert", + "restrictions-new": "Beschränkungen für neue Benutzer", + "restrictions.seconds-between-new": "Sekunden zwischen Beiträgen für neue Benutzer", + "restrictions.rep-threshold": "Mindesreputation bevor die Beschränkungen aufgehoben werden", + "restrictions.seconds-before-new": "Sekunden, bevor ein neuer Benutzer seinen ersten Beitrag schreiben kann" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/post.json b/public/language/el/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/el/admin/settings/post.json +++ b/public/language/el/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/el/admin/settings/user.json b/public/language/el/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/el/admin/settings/user.json +++ b/public/language/el/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/post.json b/public/language/en-US/admin/settings/post.json index d7a782babb..e4d42e7c39 100644 --- a/public/language/en-US/admin/settings/post.json +++ b/public/language/en-US/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/en-US/admin/settings/user.json b/public/language/en-US/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/en-US/admin/settings/user.json +++ b/public/language/en-US/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/post.json b/public/language/en-x-pirate/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/en-x-pirate/admin/settings/post.json +++ b/public/language/en-x-pirate/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/en-x-pirate/admin/settings/user.json b/public/language/en-x-pirate/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/en-x-pirate/admin/settings/user.json +++ b/public/language/en-x-pirate/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/post.json b/public/language/es/admin/settings/post.json index 322eb8ef19..88d1a965f0 100644 --- a/public/language/es/admin/settings/post.json +++ b/public/language/es/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Longitud de la entrada", "post-queue": "Post Queue", "restrictions": "Restricciones a las Respuestas", - "restrictions-new": "Restriciones a Nuevos Usuarios", "restrictions.post-queue": "Permitir cola de respuestas", "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", @@ -18,9 +17,6 @@ "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-between-new": "Segundos entre respuestas para nuevos usuarios", - "restrictions.rep-threshold": "Límite de reputación antes de que estas restricciones sean eliminadas", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/es/admin/settings/user.json b/public/language/es/admin/settings/user.json index 483a8e942e..cbe1f6f422 100644 --- a/public/language/es/admin/settings/user.json +++ b/public/language/es/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignorando" -} + "categoryWatchState.ignoring": "Ignorando", + "restrictions-new": "Restriciones a Nuevos Usuarios", + "restrictions.seconds-between-new": "Segundos entre respuestas para nuevos usuarios", + "restrictions.rep-threshold": "Límite de reputación antes de que estas restricciones sean eliminadas", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/post.json b/public/language/et/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/et/admin/settings/post.json +++ b/public/language/et/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/et/admin/settings/user.json b/public/language/et/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/et/admin/settings/user.json +++ b/public/language/et/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/post.json b/public/language/fa-IR/admin/settings/post.json index d6b516463a..318ba8cade 100644 --- a/public/language/fa-IR/admin/settings/post.json +++ b/public/language/fa-IR/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/fa-IR/admin/settings/user.json b/public/language/fa-IR/admin/settings/user.json index b589f0575f..7f8600b457 100644 --- a/public/language/fa-IR/admin/settings/user.json +++ b/public/language/fa-IR/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/post.json b/public/language/fi/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/fi/admin/settings/post.json +++ b/public/language/fi/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/fi/admin/settings/user.json b/public/language/fi/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/fi/admin/settings/user.json +++ b/public/language/fi/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/post.json b/public/language/fr/admin/settings/post.json index 56e77f6d81..bb49cb1b25 100644 --- a/public/language/fr/admin/settings/post.json +++ b/public/language/fr/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Longueur de message", "post-queue": "File d'attente", "restrictions": "Restrictions d'envoi", - "restrictions-new": "Restrictions des nouveaux utilisateurs", "restrictions.post-queue": "Activer la file d'attente des messages", "restrictions.post-queue-rep-threshold": "Réputation requise pour contourner la file d'attente de publication", "restrictions.groups-exempt-from-post-queue": "Sélectionnez les groupes qui devraient être exemptés de la file d'attente de publication", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "L'activation de la file d'attente de publication placera les messages des nouveaux utilisateurs dans une file d'attente pour approbation.", "restrictions-new.post-queue-help": "L'activation de restrictions aux nouveaux utilisateurs définira des restrictions sur les messages créés par les nouveaux utilisateurs.", "restrictions.seconds-between": "Nombre de secondes entre les messages", - "restrictions.seconds-between-new": "Secondes entre les messages pour les nouveaux utilisateurs", - "restrictions.rep-threshold": "Seuil de réputation avant que ces restrictions ne soient levées", - "restrictions.seconds-before-new": "Secondes avant qu'un nouvel utilisateur puisse publier son premier message", "restrictions.seconds-edit-after": "Nombre de secondes pendant lesquelles une publication reste modifiable (définissez la valeur sur 0 pour la désactiver)", "restrictions.seconds-delete-after": "Nombre de secondes pendant lesquelles un message reste supprimable (définissez la valeur sur 0 pour désactiver)", "restrictions.replies-no-delete": "Nombre de réponses avant que les utilisateurs soient interdits de supprimer leurs propres sujets (définissez la valeur sur 0 pour désactiver)", diff --git a/public/language/fr/admin/settings/user.json b/public/language/fr/admin/settings/user.json index fc105213e5..0a67fc5922 100644 --- a/public/language/fr/admin/settings/user.json +++ b/public/language/fr/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Abonnement par défaut", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Non abonné", - "categoryWatchState.ignoring": "Ignoré" -} + "categoryWatchState.ignoring": "Ignoré", + "restrictions-new": "Restrictions des nouveaux utilisateurs", + "restrictions.seconds-between-new": "Secondes entre les messages pour les nouveaux utilisateurs", + "restrictions.rep-threshold": "Seuil de réputation avant que ces restrictions ne soient levées", + "restrictions.seconds-before-new": "Secondes avant qu'un nouvel utilisateur puisse publier son premier message" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/post.json b/public/language/gl/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/gl/admin/settings/post.json +++ b/public/language/gl/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/gl/admin/settings/user.json b/public/language/gl/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/gl/admin/settings/user.json +++ b/public/language/gl/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/post.json b/public/language/he/admin/settings/post.json index e4dee08403..d2b7abe9c6 100644 --- a/public/language/he/admin/settings/post.json +++ b/public/language/he/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "אורך פוסט", "post-queue": "תור פוסטים", "restrictions": "הגבלות רישום", - "restrictions-new": "הגבלות משתמש חדש", "restrictions.post-queue": "הפוך תור פוסטים לזמין", "restrictions.post-queue-rep-threshold": "מוניטין נדרש כדי לעקוף תור פוסט", "restrictions.groups-exempt-from-post-queue": "בחר קבוצות פטורות מתור פוסטים", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "הפעלת תור פוסטים תכניס את ההודעות של משתמשים חדשים לתור לאישור", "restrictions-new.post-queue-help": "הפעלת הגבלות משתמשים חדשים תגדיר הגבלות על פוסטים שנוצרו על-ידי משתמשים חדשים", "restrictions.seconds-between": "מספר השניות בין פוסטים", - "restrictions.seconds-between-new": "שניות בין פוסטים עבור משתמשים חדשים", - "restrictions.rep-threshold": "סף המוניטין לפני ביטול המגבלות הללו", - "restrictions.seconds-before-new": "שניות לפני שמשתמש חדש יוכל לפרסם את הפוסט הראשון שלו", "restrictions.seconds-edit-after": "מספר השניות בה ניתן לערוך פוסט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", "restrictions.seconds-delete-after": "מספר השניות בה ניתן למחוק פוסט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", "restrictions.replies-no-delete": "מספר תשובות בנושא שלאחריו לא יוכל מפרסם הנושא למחקו (כתבו 0 להפוך ללא זמין)", diff --git a/public/language/he/admin/settings/user.json b/public/language/he/admin/settings/user.json index 3f5eb95978..04dc34fc70 100644 --- a/public/language/he/admin/settings/user.json +++ b/public/language/he/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "מצב מעקב על קטגוריה בברירת מחדל", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "לא עוקב", - "categoryWatchState.ignoring": "מתעלם" -} + "categoryWatchState.ignoring": "מתעלם", + "restrictions-new": "הגבלות משתמש חדש", + "restrictions.seconds-between-new": "שניות בין פוסטים עבור משתמשים חדשים", + "restrictions.rep-threshold": "סף המוניטין לפני ביטול המגבלות הללו", + "restrictions.seconds-before-new": "שניות לפני שמשתמש חדש יוכל לפרסם את הפוסט הראשון שלו" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/post.json b/public/language/hr/admin/settings/post.json index c0edcb6157..243b4f4f9b 100644 --- a/public/language/hr/admin/settings/post.json +++ b/public/language/hr/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Restrikcije objave", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/hr/admin/settings/user.json b/public/language/hr/admin/settings/user.json index 2875386274..de4ad7f54b 100644 --- a/public/language/hr/admin/settings/user.json +++ b/public/language/hr/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/post.json b/public/language/hu/admin/settings/post.json index e888c1243d..d4cb8f57ab 100644 --- a/public/language/hu/admin/settings/post.json +++ b/public/language/hu/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Hozzászólás hossza", "post-queue": "Hozzászólás várólista", "restrictions": "Hozzászólás korlátozás", - "restrictions-new": "Új felhasználók korlátozása", "restrictions.post-queue": "Hozzászólás várólista engedélyezése", "restrictions.post-queue-rep-threshold": "Milyen hírnév szint szükséges a hozzászólás várólista megkerüléséhez", "restrictions.groups-exempt-from-post-queue": "Válaszd ki a csoportokat, amiket nem korlátoz a hozzászólás várólista", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "A hozzászólás várólista engedélyezése az új felhasználók hozzászólásait várólistára teszi és engedélyezés szükséges a megjelenítése előtt", "restrictions-new.post-queue-help": "Az új felhasználók korlátozásának engedélyezése korlátokat ad meg az új felhasználók által létrehozott hozzászólásokra", "restrictions.seconds-between": "Hozzászólások közötti kötelező szünet (másodpercben)", - "restrictions.seconds-between-new": "Hozzászólások közötti kötelező szünet új felhasználóknak (másodpercben)", - "restrictions.rep-threshold": "Szükséges hírnév szint ezen korlátozások feloldásához", - "restrictions.seconds-before-new": "Szükséges eltelt idő, mielőtt egy új felhasználó hozzászólást írhat (másodpercben)", "restrictions.seconds-edit-after": "Hány másodpercig maradjanak a hozzászlóások szerkeszthetőek (0: nincs korlátozás)", "restrictions.seconds-delete-after": "Hány másodpercig maradjanak a hozzászólások törölhetőek (0: nincs korlátozás)", "restrictions.replies-no-delete": "Hány hozzászólás után ne törölhessék a felhasználók a saját témaköreiket (0: nincs korlátozás)", diff --git a/public/language/hu/admin/settings/user.json b/public/language/hu/admin/settings/user.json index 48ddff6652..ec0e3344dd 100644 --- a/public/language/hu/admin/settings/user.json +++ b/public/language/hu/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Alapértelmezett kategóriafigyelés", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nem megfigyelt", - "categoryWatchState.ignoring": "Mellőzés" -} + "categoryWatchState.ignoring": "Mellőzés", + "restrictions-new": "Új felhasználók korlátozása", + "restrictions.seconds-between-new": "Hozzászólások közötti kötelező szünet új felhasználóknak (másodpercben)", + "restrictions.rep-threshold": "Szükséges hírnév szint ezen korlátozások feloldásához", + "restrictions.seconds-before-new": "Szükséges eltelt idő, mielőtt egy új felhasználó hozzászólást írhat (másodpercben)" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/post.json b/public/language/hy/admin/settings/post.json index 9f6bf5ced2..c30a06218a 100644 --- a/public/language/hy/admin/settings/post.json +++ b/public/language/hy/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Գրառման երկարությունը", "post-queue": "Գրառման հերթ", "restrictions": "Posting Restrictions", - "restrictions-new": "Նոր Օգտատիրոջ սահմանափակումներ", "restrictions.post-queue": "Միացնել գրառումների հերթը", "restrictions.post-queue-rep-threshold": "Փոստի հերթը շրջանցելու համար պահանջվում է վարկանիշ", "restrictions.groups-exempt-from-post-queue": "Ընտրեք խմբեր, որոնք պետք է ազատվեն հաղորդագրությունների հերթից", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Գրառումների հերթի ակտիվացումը նոր օգտատերերի գրառումները հերթում կդնի հաստատման համար", "restrictions-new.post-queue-help": "Օգտատերերի նոր սահմանափակումների ակտիվացումը սահմանափակումներ կսահմանի նոր օգտատերերի կողմից ստեղծված գրառումների վրա", "restrictions.seconds-between": "Գրառումների միջև ընկած վայրկյանների քանակը", - "restrictions.seconds-between-new": "Նոր օգտատերերի համար գրառումների միջև ընկած վայրկյաններ", - "restrictions.rep-threshold": "Վարկանիշի շեմը՝ մինչև այս սահմանափակումների վերացումը", - "restrictions.seconds-before-new": "Վայրկյաններ առաջ, երբ նոր օգտատերը կարող է կատարել իր առաջին գրառումը", "restrictions.seconds-edit-after": "Գրառման վայրկյանների քանակը մնում է խմբագրելի (անջատելու համար սահմանել 0)", "restrictions.seconds-delete-after": "Գրառման համար ջնջելի մնալու վայրկյանների քանակը (անջատելու համար դրված է 0)", "restrictions.replies-no-delete": "Պատասխանների քանակը այն բանից հետո, երբ օգտատերերին թույլ չեն տվել ջնջել իրենց սեփական թեմաները (անջատելու համար սահմանվել է 0)", diff --git a/public/language/hy/admin/settings/user.json b/public/language/hy/admin/settings/user.json index 302e9c06b8..79d082a829 100644 --- a/public/language/hy/admin/settings/user.json +++ b/public/language/hy/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Հիմնական կատեգորիայի դիտման վիճակը", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Չեն դիտում ", - "categoryWatchState.ignoring": "Անտեսել " -} + "categoryWatchState.ignoring": "Անտեսել ", + "restrictions-new": "Նոր Օգտատիրոջ սահմանափակումներ", + "restrictions.seconds-between-new": "Նոր օգտատերերի համար գրառումների միջև ընկած վայրկյաններ", + "restrictions.rep-threshold": "Վարկանիշի շեմը՝ մինչև այս սահմանափակումների վերացումը", + "restrictions.seconds-before-new": "Վայրկյաններ առաջ, երբ նոր օգտատերը կարող է կատարել իր առաջին գրառումը" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/post.json b/public/language/id/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/id/admin/settings/post.json +++ b/public/language/id/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/id/admin/settings/user.json b/public/language/id/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/id/admin/settings/user.json +++ b/public/language/id/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/post.json b/public/language/it/admin/settings/post.json index e593b1eeed..a588c7fe30 100644 --- a/public/language/it/admin/settings/post.json +++ b/public/language/it/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Lunghezza post", "post-queue": "Coda post", "restrictions": "Restrizioni pubblicazione", - "restrictions-new": "Restrizioni Nuovo Utente", "restrictions.post-queue": "Abilita coda post", "restrictions.post-queue-rep-threshold": "Reputazione necessaria a superare la coda dei post", "restrictions.groups-exempt-from-post-queue": "Seleziona i gruppi che dovrebbero essere esclusi dalla coda dei post", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Abilitando la coda dei post, i post dei nuovi utenti saranno messi in coda per l'approvazione", "restrictions-new.post-queue-help": "Abilitando le restrizioni per i nuovi utenti verranno impostate le restrizioni sui post dei nuovi utenti", "restrictions.seconds-between": "Numero di secondi tra i post", - "restrictions.seconds-between-new": "Numero di secondi tra i post per i nuovi utenti", - "restrictions.rep-threshold": "Soglia di reputazione dopo la quale queste restrizioni vengono rimosse", - "restrictions.seconds-before-new": "Secondi dopo i quali un nuovo utente può creare il suo primo post", "restrictions.seconds-edit-after": "Numero di secondi per i quali il post rimane modificabile (imposta a 0 per disabilitare)", "restrictions.seconds-delete-after": "Numero di secondi per i quali il post rimane cancellabile (imposta a 0 per disabilitare)", "restrictions.replies-no-delete": "Numero di risposte dopo le quali l'utente non può più cancellare le proprie discussioni (imposta a 0 per disabilitare)", diff --git a/public/language/it/admin/settings/user.json b/public/language/it/admin/settings/user.json index 9788dd5ab0..f962604705 100644 --- a/public/language/it/admin/settings/user.json +++ b/public/language/it/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Stato predefinito della categoria di controllo", "categoryWatchState.tracking": "Tracciamento", "categoryWatchState.notwatching": "Non seguito", - "categoryWatchState.ignoring": "Ignorato" -} + "categoryWatchState.ignoring": "Ignorato", + "restrictions-new": "Restrizioni Nuovo Utente", + "restrictions.seconds-between-new": "Numero di secondi tra i post per i nuovi utenti", + "restrictions.rep-threshold": "Soglia di reputazione dopo la quale queste restrizioni vengono rimosse", + "restrictions.seconds-before-new": "Secondi dopo i quali un nuovo utente può creare il suo primo post" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/post.json b/public/language/ja/admin/settings/post.json index 7a73abec1f..4ac8aa58eb 100644 --- a/public/language/ja/admin/settings/post.json +++ b/public/language/ja/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "投稿の長さ", "post-queue": "Post Queue", "restrictions": "転記の制限", - "restrictions-new": "新しいユーザー制限", "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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/ja/admin/settings/user.json b/public/language/ja/admin/settings/user.json index 4df1099cff..9354d12e7a 100644 --- a/public/language/ja/admin/settings/user.json +++ b/public/language/ja/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "デフォルトのカテゴリウォッチ状態", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未ウォッチ", - "categoryWatchState.ignoring": "無視中" -} + "categoryWatchState.ignoring": "無視中", + "restrictions-new": "新しいユーザー制限", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/post.json b/public/language/ko/admin/settings/post.json index f3f233f868..ef0fae35b7 100644 --- a/public/language/ko/admin/settings/post.json +++ b/public/language/ko/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "포스트 길이", "post-queue": "게시 대기열", "restrictions": "글 작성 제한", - "restrictions-new": "신규 사용자 제한", "restrictions.post-queue": "게시 대기열 활성화", "restrictions.post-queue-rep-threshold": "게시 대기 대상에서 제외되는 최소 인지도", "restrictions.groups-exempt-from-post-queue": "선택한 그룹은 게시 대기 대상에서 제외됩니다.", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "게시 대기열을 활성화하면 신규 사용자들이 포스트를 작성할 때 게시 대기열에서 승인이 필요합니다.", "restrictions-new.post-queue-help": "신규 사용자 제한을 활성화할 경우 신규 사용자들의 포스트 생성이 제한됩니다.", "restrictions.seconds-between": "포스트 작성 지연(단위: 초)", - "restrictions.seconds-between-new": "신규 사용자 포스트 작성 지연(단위: 초)", - "restrictions.rep-threshold": "제한이 해제되는 최소 인지도", - "restrictions.seconds-before-new": "신규 사용자 첫 포스트 작성 대기 시간(단위: 초)", "restrictions.seconds-edit-after": "포스트 수정 가능 시간(단위: 초, 0일 경우 비활성화)", "restrictions.seconds-delete-after": "포스트 삭제 가능 시간(단위: 초, 0일 경우 비활성화)", "restrictions.replies-no-delete": "화제 삭제 금지 답글 수(0일 경우 비활성화)", diff --git a/public/language/ko/admin/settings/user.json b/public/language/ko/admin/settings/user.json index f4d4d12789..99b785083d 100644 --- a/public/language/ko/admin/settings/user.json +++ b/public/language/ko/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "기본 카테고리 관심 상태", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "관심 해제", - "categoryWatchState.ignoring": "무시" -} + "categoryWatchState.ignoring": "무시", + "restrictions-new": "신규 사용자 제한", + "restrictions.seconds-between-new": "신규 사용자 포스트 작성 지연(단위: 초)", + "restrictions.rep-threshold": "제한이 해제되는 최소 인지도", + "restrictions.seconds-before-new": "신규 사용자 첫 포스트 작성 대기 시간(단위: 초)" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/post.json b/public/language/lt/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/lt/admin/settings/post.json +++ b/public/language/lt/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/lt/admin/settings/user.json b/public/language/lt/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/lt/admin/settings/user.json +++ b/public/language/lt/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/post.json b/public/language/lv/admin/settings/post.json index ddc2d8e78e..b9731c6f80 100644 --- a/public/language/lv/admin/settings/post.json +++ b/public/language/lv/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Raksta garums", "post-queue": "Post Queue", "restrictions": "Publicēšanas ierobežojumi", - "restrictions-new": "Jauno lietotāju ierobežojumi", "restrictions.post-queue": "Iespējot rakstu apstiprināšanas rindu", "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", @@ -18,9 +17,6 @@ "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-between-new": "Jauniem lietotājiem sekundes starp rakstiem", - "restrictions.rep-threshold": "Reputācijas slieksnis pirms ierobežojumu atcelšanas", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/lv/admin/settings/user.json b/public/language/lv/admin/settings/user.json index 53e4d0c401..b9b9de6413 100644 --- a/public/language/lv/admin/settings/user.json +++ b/public/language/lv/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "Jauno lietotāju ierobežojumi", + "restrictions.seconds-between-new": "Jauniem lietotājiem sekundes starp rakstiem", + "restrictions.rep-threshold": "Reputācijas slieksnis pirms ierobežojumu atcelšanas", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/post.json b/public/language/ms/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/ms/admin/settings/post.json +++ b/public/language/ms/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/ms/admin/settings/user.json b/public/language/ms/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/ms/admin/settings/user.json +++ b/public/language/ms/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/post.json b/public/language/nb/admin/settings/post.json index 0cdf3efe1d..93140ebb23 100644 --- a/public/language/nb/admin/settings/post.json +++ b/public/language/nb/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Omdømme-terskel før disse restriksjonene fjernes", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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": "Antall svar i tråd før bruker ikke lenger får lov til å slette egen tråd (sett til 0 for å deaktivere)", diff --git a/public/language/nb/admin/settings/user.json b/public/language/nb/admin/settings/user.json index a2e72f61d0..1a9b8d7fb4 100644 --- a/public/language/nb/admin/settings/user.json +++ b/public/language/nb/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Overvåker ikke", - "categoryWatchState.ignoring": "Ignorerer" -} + "categoryWatchState.ignoring": "Ignorerer", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Omdømme-terskel før disse restriksjonene fjernes", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/post.json b/public/language/nl/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/nl/admin/settings/post.json +++ b/public/language/nl/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/nl/admin/settings/user.json b/public/language/nl/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/nl/admin/settings/user.json +++ b/public/language/nl/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/post.json b/public/language/pl/admin/settings/post.json index 4a27a1c0e2..8e3da24cb1 100644 --- a/public/language/pl/admin/settings/post.json +++ b/public/language/pl/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Długość postu", "post-queue": "Kolejka postów", "restrictions": "Restrykcje postowania", - "restrictions-new": "Restrykcje dla nowych użytkowników", "restrictions.post-queue": "Włącz kolejkę postów", "restrictions.post-queue-rep-threshold": "Reputacja wymagana do ominięcia kolejki postów", "restrictions.groups-exempt-from-post-queue": "Wybierz grupy, które powinny być zwolnione z kolejki postów", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Włączenie kolejki postów spowoduje umieszczenie postów nowych użytkowników w kolejce do zatwierdzenia", "restrictions-new.post-queue-help": "Włączenie restrykcji dla nowych użytkowników ustawi restrykcje na ich wpisy.", "restrictions.seconds-between": "Liczba sekund pomiędzy wpisami", - "restrictions.seconds-between-new": "Liczba sekund pomiędzy wpisami dla nowych użytkowników", - "restrictions.rep-threshold": "Próg reputacji wymagany do zdjęcia restrykcji", - "restrictions.seconds-before-new": "Sekundy, zanim nowy użytkownik może wykonać swój pierwszy post", "restrictions.seconds-edit-after": "Liczba sekund, przez które wpisy mogą zostać edytowane. (0 wyłączone)", "restrictions.seconds-delete-after": "Liczba sekund, przez które wpisy mogą zostać usunięte. (0 wyłączone)", "restrictions.replies-no-delete": "Liczba odpowiedzi, po których użytkownicy nie mogą edytować własnych tematów (0 wyłącza)", diff --git a/public/language/pl/admin/settings/user.json b/public/language/pl/admin/settings/user.json index b66265b053..6bbb4acc6a 100644 --- a/public/language/pl/admin/settings/user.json +++ b/public/language/pl/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Domyślny stan oglądania kategorii", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nie obserwowane", - "categoryWatchState.ignoring": "Ignorowane" -} + "categoryWatchState.ignoring": "Ignorowane", + "restrictions-new": "Restrykcje dla nowych użytkowników", + "restrictions.seconds-between-new": "Liczba sekund pomiędzy wpisami dla nowych użytkowników", + "restrictions.rep-threshold": "Próg reputacji wymagany do zdjęcia restrykcji", + "restrictions.seconds-before-new": "Sekundy, zanim nowy użytkownik może wykonać swój pierwszy post" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/post.json b/public/language/pt-BR/admin/settings/post.json index fac4a6ab28..b295b0f9ad 100644 --- a/public/language/pt-BR/admin/settings/post.json +++ b/public/language/pt-BR/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Tamanho do Post", "post-queue": "Fila de Posts", "restrictions": "Restições de Postagem", - "restrictions-new": "Restrições a Novos Usuários", "restrictions.post-queue": "Ativar enfileiramento de posts", "restrictions.post-queue-rep-threshold": "Reputação exigida para evitar a fila de posts", "restrictions.groups-exempt-from-post-queue": "Selecionar grupos que devem ficar isentos da fila de postagem", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Habilitar a fila de postagens colocará as postagens de novos usuários na fila para aprovação", "restrictions-new.post-queue-help": "Habilitar restrições a novos usuários irá estabelecer restrições em postagens criadas por novos usuários", "restrictions.seconds-between": "Segundos entre postagens", - "restrictions.seconds-between-new": "Tempo em segundos entre postagens para novos usuários", - "restrictions.rep-threshold": "Reputação mínima para que essas restrições sejam desativadas", - "restrictions.seconds-before-new": "Segundos necessários antes de um novo usuário poder realizar sua primeira postagem", "restrictions.seconds-edit-after": "Tempo, em segundos, que uma postagem permanece editável, após postada (coloque 0 para desabilitar)", "restrictions.seconds-delete-after": "Tempo, em segundos, que uma postagem pode ser deletada, após postada (coloque 0 para desabilitar)", "restrictions.replies-no-delete": "Após este número de respostas em uma postagem, o usuário não poderá deletar sua postagem (coloque 0 para desabilitar)", diff --git a/public/language/pt-BR/admin/settings/user.json b/public/language/pt-BR/admin/settings/user.json index 44fac00f21..8e6a54c684 100644 --- a/public/language/pt-BR/admin/settings/user.json +++ b/public/language/pt-BR/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Configuração padrão em relação a acompanhar as novidades das categorias", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Não Acompanhar", - "categoryWatchState.ignoring": "Ignorar" -} + "categoryWatchState.ignoring": "Ignorar", + "restrictions-new": "Restrições a Novos Usuários", + "restrictions.seconds-between-new": "Tempo em segundos entre postagens para novos usuários", + "restrictions.rep-threshold": "Reputação mínima para que essas restrições sejam desativadas", + "restrictions.seconds-before-new": "Segundos necessários antes de um novo usuário poder realizar sua primeira postagem" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/post.json b/public/language/pt-PT/admin/settings/post.json index 29ca6376c0..9c8ed3da74 100644 --- a/public/language/pt-PT/admin/settings/post.json +++ b/public/language/pt-PT/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Comprimento da Publicação", "post-queue": "Fila de Espera para Publicações", "restrictions": "Restrições de Publicações", - "restrictions-new": "Restrições para Novos Utilizadores", "restrictions.post-queue": "Ativar publicações em fila de espera", "restrictions.post-queue-rep-threshold": "Reputação necessária para ignorar a fila de espera para publicações", "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/pt-PT/admin/settings/user.json b/public/language/pt-PT/admin/settings/user.json index 21536017af..940ea40508 100644 --- a/public/language/pt-PT/admin/settings/user.json +++ b/public/language/pt-PT/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Estado predefinido da subscrição de categorias", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "A não seguir", - "categoryWatchState.ignoring": "A ignorar" -} + "categoryWatchState.ignoring": "A ignorar", + "restrictions-new": "Restrições para Novos Utilizadores", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/post.json b/public/language/ro/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/ro/admin/settings/post.json +++ b/public/language/ro/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/ro/admin/settings/user.json b/public/language/ro/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/ro/admin/settings/user.json +++ b/public/language/ro/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/post.json b/public/language/ru/admin/settings/post.json index bb4b722a35..3f08482a79 100644 --- a/public/language/ru/admin/settings/post.json +++ b/public/language/ru/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Длина сообщения", "post-queue": "Очередь на публикацию", "restrictions": "Ограничения на публикацию", - "restrictions-new": "Ограничения для новых пользователей", "restrictions.post-queue": "Включить очередь на публикацию", "restrictions.post-queue-rep-threshold": "Минимум репутации для публикации без проверки", "restrictions.groups-exempt-from-post-queue": "Выберите группы, участники которых смогут публиковать сообщения без предварительной проверки", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Сообщения от новых пользователей будут опубликованы только после проверки модератором", "restrictions-new.post-queue-help": "Включение ограничений для новых пользователей будет устанавливать ограничения для сообщений, создаваемых новыми пользователями.", "restrictions.seconds-between": "Пауза между сообщениями (в секундах)", - "restrictions.seconds-between-new": "Пауза между сообщениями новых пользователей (в секундах)", - "restrictions.rep-threshold": "Минимум репутации, чтобы снять это ограничение", - "restrictions.seconds-before-new": "Пауза перед тем, как новый пользователь сможет написать первое сообщение (в секундах)", "restrictions.seconds-edit-after": "Через сколько секунд после отправки сообщение нельзя будет отредактировать (0 — время не ограничено)", "restrictions.seconds-delete-after": "Через сколько секунд после отправки сообщение нельзя будет удалить (0 — время не ограничено)", "restrictions.replies-no-delete": "Кол-во ответов, после которого пользователям будет запрещено удалять тему (0 — ограничения нет)", diff --git a/public/language/ru/admin/settings/user.json b/public/language/ru/admin/settings/user.json index ed1d5078da..159580f40c 100644 --- a/public/language/ru/admin/settings/user.json +++ b/public/language/ru/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Стандартные настройки отслеживания категорий", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Не отслеживается", - "categoryWatchState.ignoring": "Игнорируется" -} + "categoryWatchState.ignoring": "Игнорируется", + "restrictions-new": "Ограничения для новых пользователей", + "restrictions.seconds-between-new": "Пауза между сообщениями новых пользователей (в секундах)", + "restrictions.rep-threshold": "Минимум репутации, чтобы снять это ограничение", + "restrictions.seconds-before-new": "Пауза перед тем, как новый пользователь сможет написать первое сообщение (в секундах)" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/post.json b/public/language/rw/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/rw/admin/settings/post.json +++ b/public/language/rw/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/rw/admin/settings/user.json b/public/language/rw/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/rw/admin/settings/user.json +++ b/public/language/rw/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/post.json b/public/language/sc/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/sc/admin/settings/post.json +++ b/public/language/sc/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/sc/admin/settings/user.json b/public/language/sc/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/sc/admin/settings/user.json +++ b/public/language/sc/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/post.json b/public/language/sk/admin/settings/post.json index 8573b4d096..a9d0b790a0 100644 --- a/public/language/sk/admin/settings/post.json +++ b/public/language/sk/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Dĺžka príspevku", "post-queue": "Post Queue", "restrictions": "Obmedzenie príspevkov", - "restrictions-new": "Obmedzenia nového používateľa", "restrictions.post-queue": "Povoliť frontu pre príspevky", "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", @@ -18,9 +17,6 @@ "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-between-new": "Sekúnd medzi príspevky pre nových používateľov", - "restrictions.rep-threshold": "Ohraničenie reputácie pred zrušením týchto obmedzení", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/sk/admin/settings/user.json b/public/language/sk/admin/settings/user.json index 5275896c0e..6aad526670 100644 --- a/public/language/sk/admin/settings/user.json +++ b/public/language/sk/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "Obmedzenia nového používateľa", + "restrictions.seconds-between-new": "Sekúnd medzi príspevky pre nových používateľov", + "restrictions.rep-threshold": "Ohraničenie reputácie pred zrušením týchto obmedzení", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/post.json b/public/language/sl/admin/settings/post.json index c7d0d69382..22fe7287c5 100644 --- a/public/language/sl/admin/settings/post.json +++ b/public/language/sl/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Dolžina objave", "post-queue": "Čakalna vrsta objav", "restrictions": "Omejitve objavljanja", - "restrictions-new": "Omejitve novega uporabnika", "restrictions.post-queue": "Omogoči čakalno vrsto objav", "restrictions.post-queue-rep-threshold": "Da se izogne ​​čakalni vrsti objav je potreben ugled", "restrictions.groups-exempt-from-post-queue": "Izberite skupine, ki bi morale biti izvzete iz čakalne vrste objav", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Če omogočite čakalno vrsto objav, bodo objave novih uporabnikov postavljene v čakalno vrsto za odobritev", "restrictions-new.post-queue-help": "Omogočanje omejitev za nove uporabnike bo postavilo omejitve za objave novih uporabnikov", "restrictions.seconds-between": "Število sekund med objavami", - "restrictions.seconds-between-new": "Sekunde med objavami za novega uporabnika", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Sekunde, preden lahko nov uporabnik objavi svojo prvo objavo", "restrictions.seconds-edit-after": "Število sekund, ko je objavo še mogoče urejati (nastavite na 0, da onemogočite)", "restrictions.seconds-delete-after": "Število sekund, ko je objavo še mogoče izbrisati (nastavite na 0, da onemogočite)", "restrictions.replies-no-delete": "Število odgovorov, ko uporabnikom ni dovoljeno izbrisati lastnih tem (nastavite na 0, da onemogočite)", diff --git a/public/language/sl/admin/settings/user.json b/public/language/sl/admin/settings/user.json index 4277a8db20..4001aaec5f 100644 --- a/public/language/sl/admin/settings/user.json +++ b/public/language/sl/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Ni spremljano", - "categoryWatchState.ignoring": "Prezrto" -} + "categoryWatchState.ignoring": "Prezrto", + "restrictions-new": "Omejitve novega uporabnika", + "restrictions.seconds-between-new": "Sekunde med objavami za novega uporabnika", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Sekunde, preden lahko nov uporabnik objavi svojo prvo objavo" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/post.json b/public/language/sq-AL/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/sq-AL/admin/settings/post.json +++ b/public/language/sq-AL/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/sq-AL/admin/settings/user.json b/public/language/sq-AL/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/sq-AL/admin/settings/user.json +++ b/public/language/sq-AL/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/post.json b/public/language/sr/admin/settings/post.json index 75758f787a..ae1433efe6 100644 --- a/public/language/sr/admin/settings/post.json +++ b/public/language/sr/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Restrikcije postavljanja", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/sr/admin/settings/user.json b/public/language/sr/admin/settings/user.json index aa03462c75..440add2174 100644 --- a/public/language/sr/admin/settings/user.json +++ b/public/language/sr/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/post.json b/public/language/sv/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/sv/admin/settings/post.json +++ b/public/language/sv/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/sv/admin/settings/user.json b/public/language/sv/admin/settings/user.json index 88f14c76cd..165a97014b 100644 --- a/public/language/sv/admin/settings/user.json +++ b/public/language/sv/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/post.json b/public/language/th/admin/settings/post.json index 9de7dc76fa..c93c901455 100644 --- a/public/language/th/admin/settings/post.json +++ b/public/language/th/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Post Length", "post-queue": "Post Queue", "restrictions": "Posting Restrictions", - "restrictions-new": "New User Restrictions", "restrictions.post-queue": "Enable 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", @@ -18,9 +17,6 @@ "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-between-new": "Seconds between posts for new users", - "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/th/admin/settings/user.json b/public/language/th/admin/settings/user.json index 5e48bf8700..59d0336b5b 100644 --- a/public/language/th/admin/settings/user.json +++ b/public/language/th/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "New User Restrictions", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/post.json b/public/language/tr/admin/settings/post.json index 2aaadbbebf..09cc14c3f1 100644 --- a/public/language/tr/admin/settings/post.json +++ b/public/language/tr/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "İleti Uzunluğu", "post-queue": "İleti Kuyruğu", "restrictions": "İleti Kısıtlamaları", - "restrictions-new": "Yeni Kullanıcı Kısıtlamaları", "restrictions.post-queue": "İleti kuyruğunu etkinleştir", "restrictions.post-queue-rep-threshold": "İleti kuyruğuna girmemek için gereken saygınlık sayısı", "restrictions.groups-exempt-from-post-queue": "İleti kuyruğuna girmeyecek grupları seçiniz", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "İleti kuyruğunu etkinleştirirseniz yeni kullanıcıların iletilerinin foruma aktarılmadan önce onayı gerekecek", "restrictions-new.post-queue-help": "Yeni kullanıcı kısıtlamalarını etkinleştirmek yeni kullanıcılar tarafından oluşturulan iletilere sınırlama getirecek", "restrictions.seconds-between": "Her ileti gönderimi arasındaki saniye cinsinden süre", - "restrictions.seconds-between-new": "Yeni kullanıcılar için gönderimler arasındaki saniye cinsinden süre", - "restrictions.rep-threshold": "Bu kısıtlamalardan önceki itibar eşiği kaldırıldı", - "restrictions.seconds-before-new": "Yeni kullanıcılar kaç saniye sonra ilk iletiyi gönderebilir", "restrictions.seconds-edit-after": "Bir ileti kaç saniye boyunca değiştirilebilir (Etkinsizleştirmek için 0 yazınız)", "restrictions.seconds-delete-after": "Bir ileti kaç saniye boyunca silinebilir (Etkinsizleştirmek için 0 yazınız)", "restrictions.replies-no-delete": "Bir başlığa kaç ileti yazıldıktan sonra o başlık silinemez (Etkinsizleştirmek için 0 yazınız)", diff --git a/public/language/tr/admin/settings/user.json b/public/language/tr/admin/settings/user.json index 664c82a7de..f5b64dd88f 100644 --- a/public/language/tr/admin/settings/user.json +++ b/public/language/tr/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Varsayılan kategori izlenme durumu", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Takip edilmiyor", - "categoryWatchState.ignoring": "Yok sayılıyor" -} + "categoryWatchState.ignoring": "Yok sayılıyor", + "restrictions-new": "Yeni Kullanıcı Kısıtlamaları", + "restrictions.seconds-between-new": "Yeni kullanıcılar için gönderimler arasındaki saniye cinsinden süre", + "restrictions.rep-threshold": "Bu kısıtlamalardan önceki itibar eşiği kaldırıldı", + "restrictions.seconds-before-new": "Yeni kullanıcılar kaç saniye sonra ilk iletiyi gönderebilir" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/post.json b/public/language/uk/admin/settings/post.json index 3dec93a499..b255b3183f 100644 --- a/public/language/uk/admin/settings/post.json +++ b/public/language/uk/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Довжина посту", "post-queue": "Post Queue", "restrictions": "Обмеження постингу", - "restrictions-new": "Нові обмеження користувачів", "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", @@ -18,9 +17,6 @@ "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-between-new": "Секунд між постами для нових користувачів", - "restrictions.rep-threshold": "Рівень репутації до того, як ці обмеження скасовуються", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post", "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)", diff --git a/public/language/uk/admin/settings/user.json b/public/language/uk/admin/settings/user.json index 154ce7ffde..5ac6e15579 100644 --- a/public/language/uk/admin/settings/user.json +++ b/public/language/uk/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Default category watch state", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", - "categoryWatchState.ignoring": "Ignoring" -} + "categoryWatchState.ignoring": "Ignoring", + "restrictions-new": "Нові обмеження користувачів", + "restrictions.seconds-between-new": "Секунд між постами для нових користувачів", + "restrictions.rep-threshold": "Рівень репутації до того, як ці обмеження скасовуються", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/post.json b/public/language/vi/admin/settings/post.json index ac0bfad6b8..2eb0403bd9 100644 --- a/public/language/vi/admin/settings/post.json +++ b/public/language/vi/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "Độ Dài Bài Đăng", "post-queue": "Xếp Hàng Bài Đăng", "restrictions": "Hạn Chế Đăng Bài", - "restrictions-new": "Giới Hạn Người Dùng Mới", "restrictions.post-queue": "Bật Xếp Hàng Bài Đăng", "restrictions.post-queue-rep-threshold": "Danh tiếng cần thiết để vượt qua đợi đăng bài", "restrictions.groups-exempt-from-post-queue": "Chọn các nhóm được miễn khỏi đợi đăng bài", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "Bật xếp hàng đăng bài sẽ đưa các bài của người dùng mới vào xếp hàng phê duyệt", "restrictions-new.post-queue-help": "Bật hạn chế người dùng mới sẽ đặt hạn chế đối với bài đăng do người dùng mới tạo", "restrictions.seconds-between": "Số giây giữa các bài đăng", - "restrictions.seconds-between-new": "Số giây giữa các bài đăng cho người dùng mới", - "restrictions.rep-threshold": "Ngưỡng uy tín trước khi những hạn chế này được dỡ bỏ", - "restrictions.seconds-before-new": "Vài giây trước khi người dùng mới có thể đăng bài đầu tiên của họ", "restrictions.seconds-edit-after": "Số giây bài đăng vẫn có thể chỉnh sửa được (đặt thành 0 để tắt)", "restrictions.seconds-delete-after": "Số giây một bài đăng vẫn có thể xóa được (đặt thành 0 để tắt)", "restrictions.replies-no-delete": "Số câu trả lời sau khi người dùng không được phép xóa chủ đề của chính họ (đặt thành 0 để tắt)", diff --git a/public/language/vi/admin/settings/user.json b/public/language/vi/admin/settings/user.json index cf0d134b36..d7e01814a5 100644 --- a/public/language/vi/admin/settings/user.json +++ b/public/language/vi/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "Trạng thái xem chuyên mục mặc định", "categoryWatchState.tracking": "Theo dõi", "categoryWatchState.notwatching": "Không Xem", - "categoryWatchState.ignoring": "Bỏ Qua" -} + "categoryWatchState.ignoring": "Bỏ Qua", + "restrictions-new": "Giới Hạn Người Dùng Mới", + "restrictions.seconds-between-new": "Số giây giữa các bài đăng cho người dùng mới", + "restrictions.rep-threshold": "Ngưỡng uy tín trước khi những hạn chế này được dỡ bỏ", + "restrictions.seconds-before-new": "Vài giây trước khi người dùng mới có thể đăng bài đầu tiên của họ" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/post.json b/public/language/zh-CN/admin/settings/post.json index 74ebb69f83..d70b1a78fc 100644 --- a/public/language/zh-CN/admin/settings/post.json +++ b/public/language/zh-CN/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "帖子字数", "post-queue": "发帖队列", "restrictions": "发帖限制", - "restrictions-new": "新用户限制", "restrictions.post-queue": "启用发帖队列", "restrictions.post-queue-rep-threshold": "忽略发帖队列的威望值", "restrictions.groups-exempt-from-post-queue": "选择会被从提交队列豁免的分组", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "启用发帖队列会将新用户的帖子放入一个队列用于审核", "restrictions-new.post-queue-help": "启用新用户限制将对新用户创建的帖子设置限制", "restrictions.seconds-between": "发帖间隔的秒数", - "restrictions.seconds-between-new": "新用户发布帖子间隔的秒数", - "restrictions.rep-threshold": "取消发帖间隔限制所需的声望值", - "restrictions.seconds-before-new": "新用户可以在第一次发布之前的秒数", "restrictions.seconds-edit-after": "帖子保持可编辑的秒数(设置为 0 表示禁用)", "restrictions.seconds-delete-after": "帖子保持可删除的秒数(设置为 0 表示禁用)", "restrictions.replies-no-delete": "在用户被禁止删除自己的主题后的回复数。 (0为禁用) ", diff --git a/public/language/zh-CN/admin/settings/user.json b/public/language/zh-CN/admin/settings/user.json index 95182457d3..5e873fe3fe 100644 --- a/public/language/zh-CN/admin/settings/user.json +++ b/public/language/zh-CN/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "默认版块关注状态", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未关注", - "categoryWatchState.ignoring": "已忽略" -} + "categoryWatchState.ignoring": "已忽略", + "restrictions-new": "新用户限制", + "restrictions.seconds-between-new": "新用户发布帖子间隔的秒数", + "restrictions.rep-threshold": "取消发帖间隔限制所需的声望值", + "restrictions.seconds-before-new": "新用户可以在第一次发布之前的秒数" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/post.json b/public/language/zh-TW/admin/settings/post.json index 707f24bf03..244d966e3b 100644 --- a/public/language/zh-TW/admin/settings/post.json +++ b/public/language/zh-TW/admin/settings/post.json @@ -10,7 +10,6 @@ "length": "貼文字數", "post-queue": "貼文隊列", "restrictions": "貼文限制", - "restrictions-new": "新使用者限制", "restrictions.post-queue": "啟用貼文隊列", "restrictions.post-queue-rep-threshold": "忽略貼文隊列的聲望值", "restrictions.groups-exempt-from-post-queue": "選擇豁免貼文隊列的群組", @@ -18,9 +17,6 @@ "restrictions.post-queue-help": "啟用貼文審查會將新使用者的貼文放入審查佇列", "restrictions-new.post-queue-help": "啟用新使用者限制將對新使用者張貼的文章設定限制", "restrictions.seconds-between": "貼文間隔時間(秒)", - "restrictions.seconds-between-new": "新使用者貼文間隔時間(秒)", - "restrictions.rep-threshold": "取消貼文限制所需的聲望值", - "restrictions.seconds-before-new": "新使用者可以在第一次發佈之前的秒數", "restrictions.seconds-edit-after": "貼文保持可編輯的秒數(設定為0表示禁用)", "restrictions.seconds-delete-after": "貼文保持可刪除的秒數(設定為0表示禁用)", "restrictions.replies-no-delete": "在使用者被禁止刪除自己的主題後的回覆數。 (0為禁用) ", diff --git a/public/language/zh-TW/admin/settings/user.json b/public/language/zh-TW/admin/settings/user.json index f8be0f39e4..94b782e755 100644 --- a/public/language/zh-TW/admin/settings/user.json +++ b/public/language/zh-TW/admin/settings/user.json @@ -81,5 +81,9 @@ "categoryWatchState": "預設版面關注狀態", "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未關注", - "categoryWatchState.ignoring": "已忽略" -} + "categoryWatchState.ignoring": "已忽略", + "restrictions-new": "新使用者限制", + "restrictions.seconds-between-new": "新使用者貼文間隔時間(秒)", + "restrictions.rep-threshold": "取消貼文限制所需的聲望值", + "restrictions.seconds-before-new": "新使用者可以在第一次發佈之前的秒數" +} \ No newline at end of file From fdff165e2b6d9e21cb3c0f8467b8d6b9ab8bf919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 7 Nov 2023 12:36:40 -0500 Subject: [PATCH 058/201] feat: closes #12154, add exempt groups for new user restrictions --- install/data/defaults.json | 3 ++- .../language/en-GB/admin/settings/user.json | 3 ++- public/openapi/read/admin/settings/user.yaml | 4 ++++ src/api/chats.js | 2 +- src/controllers/admin/settings.js | 6 ++++- src/groups/update.js | 24 ++++++++++--------- src/privileges/posts.js | 2 +- src/upgrades/3.6.0/rename_newbie_config.js | 15 ++++++++++++ src/user/posts.js | 11 +++++---- src/views/admin/settings/user.tpl | 14 +++++++++-- test/user.js | 2 +- 11 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 src/upgrades/3.6.0/rename_newbie_config.js diff --git a/install/data/defaults.json b/install/data/defaults.json index 92bac0b2b7..d60984e99e 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -23,10 +23,11 @@ "chatMessageDelay": 2000, "newbieChatMessageDelay": 120000, "notificationSendDelay": 60, - "newbiePostDelayThreshold": 3, + "newbieReputationThreshold": 3, "postQueue": 0, "postQueueReputationThreshold": 0, "groupsExemptFromPostQueue": ["administrators", "Global Moderators"], + "groupsExemptFromNewUserRestrictions": ["administrators", "Global Moderators"], "groupsExemptFromMaintenanceMode": ["administrators", "Global Moderators"], "minimumPostLength": 8, "maximumPostLength": 32767, diff --git a/public/language/en-GB/admin/settings/user.json b/public/language/en-GB/admin/settings/user.json index 7c55d69ee5..528debebaa 100644 --- a/public/language/en-GB/admin/settings/user.json +++ b/public/language/en-GB/admin/settings/user.json @@ -87,5 +87,6 @@ "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.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" } diff --git a/public/openapi/read/admin/settings/user.yaml b/public/openapi/read/admin/settings/user.yaml index 89b0b1067a..fe89bae2a6 100644 --- a/public/openapi/read/admin/settings/user.yaml +++ b/public/openapi/read/admin/settings/user.yaml @@ -13,6 +13,10 @@ get: properties: title: type: string + groupsExemptFromNewUserRestrictions: + type: array + items: + $ref: ../../../components/schemas/GroupObject.yaml#/GroupDataObject notificationSettings: type: array items: diff --git a/src/api/chats.js b/src/api/chats.js index d3788ea9e1..2ea2b26760 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -20,7 +20,7 @@ async function rateLimitExceeded(caller, field) { user.isPrivileged(caller.uid), user.getUserField(caller.uid, 'reputation'), ]); - const newbie = !isPrivileged && meta.config.newbiePostDelayThreshold > reputation; + const newbie = !isPrivileged && meta.config.newbieReputationThreshold > reputation; const delay = newbie ? meta.config.newbieChatMessageDelay : meta.config.chatMessageDelay; session[field] = session[field] || 0; diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index 61ccd6a091..61e8a6fb2d 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -46,7 +46,10 @@ settingsController.email = async (req, res) => { }; settingsController.user = async (req, res) => { - const notificationTypes = await notifications.getAllNotificationTypes(); + const [notificationTypes, groupData] = await Promise.all([ + notifications.getAllNotificationTypes(), + groups.getNonPrivilegeGroups('groups:createtime', 0, -1), + ]); const notificationSettings = notificationTypes.map(type => ({ name: type, label: `[[notifications:${type.replace(/_/g, '-')}]]`, @@ -54,6 +57,7 @@ settingsController.user = async (req, res) => { res.render('admin/settings/user', { title: '[[admin/menu:settings/user]]', notificationSettings: notificationSettings, + groupsExemptFromNewUserRestrictions: groupData, }); }; diff --git a/src/groups/update.js b/src/groups/update.js index ced53ef04e..9da76c9180 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -274,17 +274,19 @@ module.exports = function (Groups) { } async function updateConfig(oldName, newName) { - if (meta.config.groupsExemptFromPostQueue.includes(oldName)) { - meta.config.groupsExemptFromPostQueue.splice( - meta.config.groupsExemptFromPostQueue.indexOf(oldName), 1, newName - ); - await meta.configs.set('groupsExemptFromPostQueue', meta.config.groupsExemptFromPostQueue); - } - if (meta.config.groupsExemptFromMaintenanceMode.includes(oldName)) { - meta.config.groupsExemptFromMaintenanceMode.splice( - meta.config.groupsExemptFromMaintenanceMode.indexOf(oldName), 1, newName - ); - await meta.configs.set('groupsExemptFromMaintenanceMode', meta.config.groupsExemptFromMaintenanceMode); + const configKeys = [ + 'groupsExemptFromPostQueue', + 'groupsExemptFromNewUserRestrictions', + 'groupsExemptFromMaintenanceMode', + ]; + + for (const key of configKeys) { + if (meta.config[key] && meta.config[key].includes(oldName)) { + meta.config[key].splice( + meta.config[key].indexOf(oldName), 1, newName + ); + await meta.configs.set(key, meta.config[key]); + } } } diff --git a/src/privileges/posts.js b/src/privileges/posts.js index 8b32065df6..fbd6858282 100644 --- a/src/privileges/posts.js +++ b/src/privileges/posts.js @@ -139,7 +139,7 @@ privsPosts.canEdit = async function (pid, uid) { if ( !results.isMod && meta.config.newbiePostEditDuration > 0 && - meta.config.newbiePostDelayThreshold > results.userData.reputation && + meta.config.newbieReputationThreshold > results.userData.reputation && Date.now() - results.postData.timestamp > meta.config.newbiePostEditDuration * 1000 ) { return { flag: false, message: `[[error:post-edit-duration-expired, ${meta.config.newbiePostEditDuration}]]` }; diff --git a/src/upgrades/3.6.0/rename_newbie_config.js b/src/upgrades/3.6.0/rename_newbie_config.js new file mode 100644 index 0000000000..6b496d970c --- /dev/null +++ b/src/upgrades/3.6.0/rename_newbie_config.js @@ -0,0 +1,15 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Rename newbiePostDelayThreshold to newbieReputationThreshold', + timestamp: Date.UTC(2023, 10, 7), + method: async function () { + const current = await db.getObjectField('config', 'newbiePostDelayThreshold'); + await db.setObjectField('config', 'newbieReputationThreshold', current); + await db.deleteObjectField('config', 'newbiePostDelayThreshold'); + }, +}; diff --git a/src/user/posts.js b/src/user/posts.js index b2e2b57be2..5ee7f5e8aa 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -3,6 +3,7 @@ const db = require('../database'); const meta = require('../meta'); const privileges = require('../privileges'); +const groups = require('../groups'); module.exports = function (User) { User.isReadyToPost = async function (uid, cid) { @@ -31,9 +32,10 @@ module.exports = function (User) { if (parseInt(uid, 10) === 0) { return; } - const [userData, isAdminOrMod] = await Promise.all([ + const [userData, isAdminOrMod, isMemberOfExempt] = await Promise.all([ User.getUserFields(uid, ['uid', 'mutedUntil', 'joindate', 'email', 'reputation'].concat([field])), privileges.categories.isAdminOrMod(cid, uid), + groups.isMemberOfAny(uid, meta.config.groupsExemptFromNewUserRestrictions), ]); if (!userData.uid) { @@ -54,14 +56,15 @@ module.exports = function (User) { const lasttime = userData[field] || 0; if ( + !isMemberOfExempt && meta.config.newbiePostDelay > 0 && - meta.config.newbiePostDelayThreshold > userData.reputation && + meta.config.newbieReputationThreshold > userData.reputation && now - lasttime < meta.config.newbiePostDelay * 1000 ) { if (meta.config.newbiewPostDelay % 60 === 0) { - throw new Error(`[[error:too-many-posts-newbie-minutes, ${Math.floor(meta.config.newbiePostDelay / 60)}, ${meta.config.newbiePostDelayThreshold}]]`); + throw new Error(`[[error:too-many-posts-newbie-minutes, ${Math.floor(meta.config.newbiePostDelay / 60)}, ${meta.config.newbieReputationThreshold}]]`); } else { - throw new Error(`[[error:too-many-posts-newbie, ${meta.config.newbiePostDelay}, ${meta.config.newbiePostDelayThreshold}]]`); + throw new Error(`[[error:too-many-posts-newbie, ${meta.config.newbiePostDelay}, ${meta.config.newbieReputationThreshold}]]`); } } else if (now - lasttime < meta.config.postDelay * 1000) { throw new Error(`[[error:too-many-posts, ${meta.config.postDelay}]]`); diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 4a2bcc19b7..6a5c703a2d 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -218,8 +218,8 @@
[[admin/settings/user:restrictions-new]]
- - + +
@@ -241,6 +241,16 @@
+ +
+ + +
+

diff --git a/test/user.js b/test/user.js index 45916cbaf9..6133f14f82 100644 --- a/test/user.js +++ b/test/user.js @@ -317,7 +317,7 @@ describe('User', () => { it('should error when a new user posts if the last post time is 10 < 30 seconds', (done) => { meta.config.newbiePostDelay = 30; - meta.config.newbiePostDelayThreshold = 3; + meta.config.newbieReputationThreshold = 3; User.setUserField(testUid, 'lastposttime', +new Date() - (20 * 1000), () => { Topics.post({ From 94777927ffee6d692882e6c2b733038022ad9c0c Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 7 Nov 2023 17:37:34 +0000 Subject: [PATCH 059/201] chore(i18n): fallback strings for new resources: nodebb.admin-settings-chat, nodebb.admin-settings-post, nodebb.admin-settings-user --- public/language/ar/admin/settings/chat.json | 1 - public/language/ar/admin/settings/user.json | 9 ++++++--- public/language/bg/admin/settings/chat.json | 1 - public/language/bg/admin/settings/user.json | 13 ++++++++----- public/language/bn/admin/settings/chat.json | 1 - public/language/bn/admin/settings/user.json | 9 ++++++--- public/language/cs/admin/settings/chat.json | 1 - public/language/cs/admin/settings/user.json | 13 ++++++++----- public/language/da/admin/settings/chat.json | 1 - public/language/da/admin/settings/user.json | 9 ++++++--- public/language/de/admin/settings/chat.json | 1 - public/language/de/admin/settings/user.json | 13 ++++++++----- public/language/el/admin/settings/chat.json | 1 - public/language/el/admin/settings/user.json | 9 ++++++--- public/language/en-US/admin/settings/chat.json | 1 - public/language/en-US/admin/settings/user.json | 9 ++++++--- .../language/en-x-pirate/admin/settings/chat.json | 1 - .../language/en-x-pirate/admin/settings/user.json | 9 ++++++--- public/language/es/admin/settings/chat.json | 1 - public/language/es/admin/settings/user.json | 13 ++++++++----- public/language/et/admin/settings/chat.json | 1 - public/language/et/admin/settings/user.json | 9 ++++++--- public/language/fa-IR/admin/settings/chat.json | 1 - public/language/fa-IR/admin/settings/user.json | 9 ++++++--- public/language/fi/admin/settings/chat.json | 1 - public/language/fi/admin/settings/user.json | 9 ++++++--- public/language/fr/admin/settings/chat.json | 1 - public/language/fr/admin/settings/user.json | 13 ++++++++----- public/language/gl/admin/settings/chat.json | 1 - public/language/gl/admin/settings/user.json | 9 ++++++--- public/language/he/admin/settings/chat.json | 1 - public/language/he/admin/settings/user.json | 13 ++++++++----- public/language/hr/admin/settings/chat.json | 1 - public/language/hr/admin/settings/user.json | 9 ++++++--- public/language/hu/admin/settings/chat.json | 1 - public/language/hu/admin/settings/user.json | 13 ++++++++----- public/language/hy/admin/settings/chat.json | 1 - public/language/hy/admin/settings/user.json | 13 ++++++++----- public/language/id/admin/settings/chat.json | 1 - public/language/id/admin/settings/user.json | 9 ++++++--- public/language/it/admin/settings/chat.json | 1 - public/language/it/admin/settings/user.json | 13 ++++++++----- public/language/ja/admin/settings/chat.json | 1 - public/language/ja/admin/settings/user.json | 11 +++++++---- public/language/ko/admin/settings/chat.json | 1 - public/language/ko/admin/settings/user.json | 13 ++++++++----- public/language/lt/admin/settings/chat.json | 1 - public/language/lt/admin/settings/user.json | 9 ++++++--- public/language/lv/admin/settings/chat.json | 1 - public/language/lv/admin/settings/user.json | 13 ++++++++----- public/language/ms/admin/settings/chat.json | 1 - public/language/ms/admin/settings/user.json | 9 ++++++--- public/language/nb/admin/settings/chat.json | 1 - public/language/nb/admin/settings/user.json | 9 ++++++--- public/language/nl/admin/settings/chat.json | 1 - public/language/nl/admin/settings/user.json | 9 ++++++--- public/language/pl/admin/settings/chat.json | 1 - public/language/pl/admin/settings/user.json | 13 ++++++++----- public/language/pt-BR/admin/settings/chat.json | 1 - public/language/pt-BR/admin/settings/user.json | 13 ++++++++----- public/language/pt-PT/admin/settings/chat.json | 1 - public/language/pt-PT/admin/settings/user.json | 11 +++++++---- public/language/ro/admin/settings/chat.json | 1 - public/language/ro/admin/settings/user.json | 9 ++++++--- public/language/ru/admin/settings/chat.json | 1 - public/language/ru/admin/settings/user.json | 13 ++++++++----- public/language/rw/admin/settings/chat.json | 1 - public/language/rw/admin/settings/user.json | 9 ++++++--- public/language/sc/admin/settings/chat.json | 1 - public/language/sc/admin/settings/user.json | 9 ++++++--- public/language/sk/admin/settings/chat.json | 1 - public/language/sk/admin/settings/user.json | 13 ++++++++----- public/language/sl/admin/settings/chat.json | 1 - public/language/sl/admin/settings/user.json | 11 +++++++---- public/language/sq-AL/admin/settings/chat.json | 1 - public/language/sq-AL/admin/settings/user.json | 9 ++++++--- public/language/sr/admin/settings/chat.json | 1 - public/language/sr/admin/settings/user.json | 9 ++++++--- public/language/sv/admin/settings/chat.json | 1 - public/language/sv/admin/settings/user.json | 9 ++++++--- public/language/th/admin/settings/chat.json | 1 - public/language/th/admin/settings/user.json | 9 ++++++--- public/language/tr/admin/settings/chat.json | 1 - public/language/tr/admin/settings/user.json | 13 ++++++++----- public/language/uk/admin/settings/chat.json | 1 - public/language/uk/admin/settings/user.json | 13 ++++++++----- public/language/vi/admin/settings/chat.json | 1 - public/language/vi/admin/settings/user.json | 13 ++++++++----- public/language/zh-CN/admin/settings/chat.json | 1 - public/language/zh-CN/admin/settings/user.json | 13 ++++++++----- public/language/zh-TW/admin/settings/chat.json | 1 - public/language/zh-TW/admin/settings/user.json | 13 ++++++++----- 92 files changed, 319 insertions(+), 227 deletions(-) diff --git a/public/language/ar/admin/settings/chat.json b/public/language/ar/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/ar/admin/settings/chat.json +++ b/public/language/ar/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/ar/admin/settings/user.json b/public/language/ar/admin/settings/user.json index 2ffa3c39f9..905ef1a240 100644 --- a/public/language/ar/admin/settings/user.json +++ b/public/language/ar/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/bg/admin/settings/chat.json b/public/language/bg/admin/settings/chat.json index 0b653316df..d56749ba48 100644 --- a/public/language/bg/admin/settings/chat.json +++ b/public/language/bg/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Максимална дължина на имената на стаи за разговори", "max-room-size": "Максимален брой потребители в стая за разговор", "delay": "Време между съобщенията в разговорите (мсек)", - "newbieDelay": "…за нови потребители (мсек)", "notification-delay": "Забавяне преди известяване за съобщения в разговорите", "notification-delay-help": "Допълнителните съобщения, изпратени в рамките на това време, се комбинират, и потребителят получава по едно известие за всеки такъв период на забавяне. Задайте стойност 0, за да изключите забавянето.", "restrictions.seconds-edit-after": "Брой секунди, през които съобщенията в разговор могат да бъдат редактирани.", diff --git a/public/language/bg/admin/settings/user.json b/public/language/bg/admin/settings/user.json index f5728c4c6e..0aa33c80e6 100644 --- a/public/language/bg/admin/settings/user.json +++ b/public/language/bg/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Да се следят", "categoryWatchState.notwatching": "Да не се наблюдават", "categoryWatchState.ignoring": "Да се пренебрегват", - "restrictions-new": "Ограничения за новите потребители", - "restrictions.seconds-between-new": "Брой секунди между публикациите за нови потребители", - "restrictions.rep-threshold": "Необходима репутация за премахване на това ограничение", - "restrictions.seconds-before-new": "Брой секунди преди новите потребители да могат да публикуват за пръв път" -} \ No newline at end of file + "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" +} diff --git a/public/language/bn/admin/settings/chat.json b/public/language/bn/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/bn/admin/settings/chat.json +++ b/public/language/bn/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/bn/admin/settings/user.json b/public/language/bn/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/bn/admin/settings/user.json +++ b/public/language/bn/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/cs/admin/settings/chat.json b/public/language/cs/admin/settings/chat.json index 7238e340ee..dbdd040d95 100644 --- a/public/language/cs/admin/settings/chat.json +++ b/public/language/cs/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximální počet uživatelů v konverzační místnosti", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/cs/admin/settings/user.json b/public/language/cs/admin/settings/user.json index 22a89b704c..b867c55056 100644 --- a/public/language/cs/admin/settings/user.json +++ b/public/language/cs/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nesleduji", "categoryWatchState.ignoring": "Ignorace", - "restrictions-new": "Omezení nového uživatele", - "restrictions.seconds-between-new": "Sekund mezi příspěvky pro nové uživatele", - "restrictions.rep-threshold": "Ohraničení reputace než začnou platit tato omezení", - "restrictions.seconds-before-new": "Počet sekund, než může nový uživatel vytvořit první příspěvek" -} \ No newline at end of file + "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" +} diff --git a/public/language/da/admin/settings/chat.json b/public/language/da/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/da/admin/settings/chat.json +++ b/public/language/da/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/da/admin/settings/user.json b/public/language/da/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/da/admin/settings/user.json +++ b/public/language/da/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/de/admin/settings/chat.json b/public/language/de/admin/settings/chat.json index 7e89c35603..5379998ae2 100644 --- a/public/language/de/admin/settings/chat.json +++ b/public/language/de/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximale Länge der Namen von Chaträumen", "max-room-size": "Maximale Anzahl von Benutzern in Chatrooms", "delay": "Zeit zwischen Chat-Nachrichten (ms)", - "newbieDelay": "... für neue Benutzer (ms)", "notification-delay": "Benachrichtigungsverzögerung für Chat-Nachrichten", "notification-delay-help": "Zusätzliche Nachrichten, die zwischen dieser Zeit gesendet werden, werden gesammelt, und der Benutzer wird einmal pro Verzögerungszeitraum benachrichtigt. Setze diesen Wert auf 0, um die Verzögerung zu deaktivieren.", "restrictions.seconds-edit-after": "Anzahl der Sekunden, die eine Chat-Nachricht bearbeitbar bleibt.", diff --git a/public/language/de/admin/settings/user.json b/public/language/de/admin/settings/user.json index 862a3bd5c3..a6404a925c 100644 --- a/public/language/de/admin/settings/user.json +++ b/public/language/de/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Verfolgung", "categoryWatchState.notwatching": "Nicht beobachtet", "categoryWatchState.ignoring": "Ignoriert", - "restrictions-new": "Beschränkungen für neue Benutzer", - "restrictions.seconds-between-new": "Sekunden zwischen Beiträgen für neue Benutzer", - "restrictions.rep-threshold": "Mindesreputation bevor die Beschränkungen aufgehoben werden", - "restrictions.seconds-before-new": "Sekunden, bevor ein neuer Benutzer seinen ersten Beitrag schreiben kann" -} \ No newline at end of file + "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" +} diff --git a/public/language/el/admin/settings/chat.json b/public/language/el/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/el/admin/settings/chat.json +++ b/public/language/el/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/el/admin/settings/user.json b/public/language/el/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/el/admin/settings/user.json +++ b/public/language/el/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/en-US/admin/settings/chat.json b/public/language/en-US/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/en-US/admin/settings/chat.json +++ b/public/language/en-US/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/en-US/admin/settings/user.json b/public/language/en-US/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/en-US/admin/settings/user.json +++ b/public/language/en-US/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/en-x-pirate/admin/settings/chat.json b/public/language/en-x-pirate/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/en-x-pirate/admin/settings/chat.json +++ b/public/language/en-x-pirate/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/en-x-pirate/admin/settings/user.json b/public/language/en-x-pirate/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/en-x-pirate/admin/settings/user.json +++ b/public/language/en-x-pirate/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/es/admin/settings/chat.json b/public/language/es/admin/settings/chat.json index 048be4c445..d64107e39a 100644 --- a/public/language/es/admin/settings/chat.json +++ b/public/language/es/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Máximo numero de usuarios en las salas de chat", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/es/admin/settings/user.json b/public/language/es/admin/settings/user.json index cbe1f6f422..5696e19163 100644 --- a/public/language/es/admin/settings/user.json +++ b/public/language/es/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignorando", - "restrictions-new": "Restriciones a Nuevos Usuarios", - "restrictions.seconds-between-new": "Segundos entre respuestas para nuevos usuarios", - "restrictions.rep-threshold": "Límite de reputación antes de que estas restricciones sean eliminadas", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/et/admin/settings/chat.json b/public/language/et/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/et/admin/settings/chat.json +++ b/public/language/et/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/et/admin/settings/user.json b/public/language/et/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/et/admin/settings/user.json +++ b/public/language/et/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/fa-IR/admin/settings/chat.json b/public/language/fa-IR/admin/settings/chat.json index a150d404fe..3935045670 100644 --- a/public/language/fa-IR/admin/settings/chat.json +++ b/public/language/fa-IR/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "بیشترین تعداد کاربران در چت‌روم ", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/fa-IR/admin/settings/user.json b/public/language/fa-IR/admin/settings/user.json index 7f8600b457..0256e00ae6 100644 --- a/public/language/fa-IR/admin/settings/user.json +++ b/public/language/fa-IR/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/fi/admin/settings/chat.json b/public/language/fi/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/fi/admin/settings/chat.json +++ b/public/language/fi/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/fi/admin/settings/user.json b/public/language/fi/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/fi/admin/settings/user.json +++ b/public/language/fi/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/fr/admin/settings/chat.json b/public/language/fr/admin/settings/chat.json index 56f57672b0..3634d2118c 100644 --- a/public/language/fr/admin/settings/chat.json +++ b/public/language/fr/admin/settings/chat.json @@ -8,7 +8,6 @@ "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)", - "newbieDelay": "... pour les nouveaux utilisateurs (ms)", "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 0a67fc5922..df7c33e54f 100644 --- a/public/language/fr/admin/settings/user.json +++ b/public/language/fr/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Non abonné", "categoryWatchState.ignoring": "Ignoré", - "restrictions-new": "Restrictions des nouveaux utilisateurs", - "restrictions.seconds-between-new": "Secondes entre les messages pour les nouveaux utilisateurs", - "restrictions.rep-threshold": "Seuil de réputation avant que ces restrictions ne soient levées", - "restrictions.seconds-before-new": "Secondes avant qu'un nouvel utilisateur puisse publier son premier message" -} \ No newline at end of file + "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" +} diff --git a/public/language/gl/admin/settings/chat.json b/public/language/gl/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/gl/admin/settings/chat.json +++ b/public/language/gl/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/gl/admin/settings/user.json b/public/language/gl/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/gl/admin/settings/user.json +++ b/public/language/gl/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/he/admin/settings/chat.json b/public/language/he/admin/settings/chat.json index 0767ca5cf7..55ac62c854 100644 --- a/public/language/he/admin/settings/chat.json +++ b/public/language/he/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "אורך מקסימלי של שם חדר צ'אט", "max-room-size": "מספר המשתמשים המרבי בחדרי צ'אט", "delay": "זמן בין הודעות צ'אט (ms)", - "newbieDelay": "...עבור משתמשים חדשים (במילישניות)", "notification-delay": "עיכוב התראה עבור הודעות צ'אט", "notification-delay-help": "הודעות נוספות שנשלחות בין הזמן הזה נאספות, והמשתמש מקבל הודעה פעם אחת בכל תקופת עיכוב. הגדר ל-0 כדי לבטל את ההשהיה.", "restrictions.seconds-edit-after": "מספר השניות שהודעת צ'אט תישאר ניתנת לעריכה.", diff --git a/public/language/he/admin/settings/user.json b/public/language/he/admin/settings/user.json index 04dc34fc70..79fac8cbb5 100644 --- a/public/language/he/admin/settings/user.json +++ b/public/language/he/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "לא עוקב", "categoryWatchState.ignoring": "מתעלם", - "restrictions-new": "הגבלות משתמש חדש", - "restrictions.seconds-between-new": "שניות בין פוסטים עבור משתמשים חדשים", - "restrictions.rep-threshold": "סף המוניטין לפני ביטול המגבלות הללו", - "restrictions.seconds-before-new": "שניות לפני שמשתמש חדש יוכל לפרסם את הפוסט הראשון שלו" -} \ No newline at end of file + "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" +} diff --git a/public/language/hr/admin/settings/chat.json b/public/language/hr/admin/settings/chat.json index 9a54a27c29..4d4fada209 100644 --- a/public/language/hr/admin/settings/chat.json +++ b/public/language/hr/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maksimalan broj korisnika u sobama za razgovor", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/hr/admin/settings/user.json b/public/language/hr/admin/settings/user.json index de4ad7f54b..abca425479 100644 --- a/public/language/hr/admin/settings/user.json +++ b/public/language/hr/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/hu/admin/settings/chat.json b/public/language/hu/admin/settings/chat.json index 94e9fae5c8..ec3ff375f1 100644 --- a/public/language/hu/admin/settings/chat.json +++ b/public/language/hu/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "A csevegési szobákban lévő felhasználók maximális száma", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/hu/admin/settings/user.json b/public/language/hu/admin/settings/user.json index ec0e3344dd..5ebb8b5bee 100644 --- a/public/language/hu/admin/settings/user.json +++ b/public/language/hu/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nem megfigyelt", "categoryWatchState.ignoring": "Mellőzés", - "restrictions-new": "Új felhasználók korlátozása", - "restrictions.seconds-between-new": "Hozzászólások közötti kötelező szünet új felhasználóknak (másodpercben)", - "restrictions.rep-threshold": "Szükséges hírnév szint ezen korlátozások feloldásához", - "restrictions.seconds-before-new": "Szükséges eltelt idő, mielőtt egy új felhasználó hozzászólást írhat (másodpercben)" -} \ No newline at end of file + "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" +} diff --git a/public/language/hy/admin/settings/chat.json b/public/language/hy/admin/settings/chat.json index c2ae499c3b..f55bfa7492 100644 --- a/public/language/hy/admin/settings/chat.json +++ b/public/language/hy/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Զրույցի սենյակների անունների առավելագույն երկարությունը", "max-room-size": "Զրուցարաններում օգտատերերի առավելագույն քանակը", "delay": "Ժամանակը զրույցի հաղորդագրությունների միջև (մս)", - "newbieDelay": "... նոր օգտվողների համար (ms)", "notification-delay": "Զրույցի հաղորդագրությունների ծանուցման հետաձգում", "notification-delay-help": "Այս ժամանակահատվածում ուղարկված հավելյալ հաղորդագրությունները հավաքվում են, և օգտատերը ծանուցվում է մեկ անգամ ուշացման ժամանակահատվածում: Սահմանեք սա 0՝ ուշացումն անջատելու համար:", "restrictions.seconds-edit-after": "Վայրկյանների քանակը զրույցի հաղորդագրությունը կմնա խմբագրելի:", diff --git a/public/language/hy/admin/settings/user.json b/public/language/hy/admin/settings/user.json index 79d082a829..ab653e838e 100644 --- a/public/language/hy/admin/settings/user.json +++ b/public/language/hy/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Չեն դիտում ", "categoryWatchState.ignoring": "Անտեսել ", - "restrictions-new": "Նոր Օգտատիրոջ սահմանափակումներ", - "restrictions.seconds-between-new": "Նոր օգտատերերի համար գրառումների միջև ընկած վայրկյաններ", - "restrictions.rep-threshold": "Վարկանիշի շեմը՝ մինչև այս սահմանափակումների վերացումը", - "restrictions.seconds-before-new": "Վայրկյաններ առաջ, երբ նոր օգտատերը կարող է կատարել իր առաջին գրառումը" -} \ No newline at end of file + "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" +} diff --git a/public/language/id/admin/settings/chat.json b/public/language/id/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/id/admin/settings/chat.json +++ b/public/language/id/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/id/admin/settings/user.json b/public/language/id/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/id/admin/settings/user.json +++ b/public/language/id/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/it/admin/settings/chat.json b/public/language/it/admin/settings/chat.json index 457f54a3e9..ecb9f525f1 100644 --- a/public/language/it/admin/settings/chat.json +++ b/public/language/it/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Lunghezza massima dei nomi delle stanze chat", "max-room-size": "Numero massimo di utenti nelle stanza chat", "delay": "Tempo tra i messaggi di chat (ms)", - "newbieDelay": "... per i nuovi utenti (ms)", "notification-delay": "Ritardo nella notifica dei messaggi di chat", "notification-delay-help": "I messaggi aggiuntivi inviati in questo intervallo di tempo vengono raccolti e l'utente riceve una notifica per ogni periodo di ritardo. Impostalo su 0 per disabilitare il ritardo.", "restrictions.seconds-edit-after": "Numero di secondi in cui un messaggio di chat rimane modificabile.", diff --git a/public/language/it/admin/settings/user.json b/public/language/it/admin/settings/user.json index f962604705..467f3b4ab3 100644 --- a/public/language/it/admin/settings/user.json +++ b/public/language/it/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracciamento", "categoryWatchState.notwatching": "Non seguito", "categoryWatchState.ignoring": "Ignorato", - "restrictions-new": "Restrizioni Nuovo Utente", - "restrictions.seconds-between-new": "Numero di secondi tra i post per i nuovi utenti", - "restrictions.rep-threshold": "Soglia di reputazione dopo la quale queste restrizioni vengono rimosse", - "restrictions.seconds-before-new": "Secondi dopo i quali un nuovo utente può creare il suo primo post" -} \ No newline at end of file + "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" +} diff --git a/public/language/ja/admin/settings/chat.json b/public/language/ja/admin/settings/chat.json index 146c304185..8d10f16716 100644 --- a/public/language/ja/admin/settings/chat.json +++ b/public/language/ja/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "チャットルームの最大ユーザー数", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/ja/admin/settings/user.json b/public/language/ja/admin/settings/user.json index 9354d12e7a..89d24d7c6d 100644 --- a/public/language/ja/admin/settings/user.json +++ b/public/language/ja/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未ウォッチ", "categoryWatchState.ignoring": "無視中", - "restrictions-new": "新しいユーザー制限", - "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions-new": "New User Restrictions", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/ko/admin/settings/chat.json b/public/language/ko/admin/settings/chat.json index 8c0e315af2..fe366d703d 100644 --- a/public/language/ko/admin/settings/chat.json +++ b/public/language/ko/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "채팅방 최대 인원", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/ko/admin/settings/user.json b/public/language/ko/admin/settings/user.json index 99b785083d..dc438c1297 100644 --- a/public/language/ko/admin/settings/user.json +++ b/public/language/ko/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "관심 해제", "categoryWatchState.ignoring": "무시", - "restrictions-new": "신규 사용자 제한", - "restrictions.seconds-between-new": "신규 사용자 포스트 작성 지연(단위: 초)", - "restrictions.rep-threshold": "제한이 해제되는 최소 인지도", - "restrictions.seconds-before-new": "신규 사용자 첫 포스트 작성 대기 시간(단위: 초)" -} \ No newline at end of file + "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" +} diff --git a/public/language/lt/admin/settings/chat.json b/public/language/lt/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/lt/admin/settings/chat.json +++ b/public/language/lt/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/lt/admin/settings/user.json b/public/language/lt/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/lt/admin/settings/user.json +++ b/public/language/lt/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/lv/admin/settings/chat.json b/public/language/lv/admin/settings/chat.json index 3fb3ef434c..db69e3a8d8 100644 --- a/public/language/lv/admin/settings/chat.json +++ b/public/language/lv/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maksimālais lietotāju skaits tērzētavā", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/lv/admin/settings/user.json b/public/language/lv/admin/settings/user.json index b9b9de6413..acc0890c61 100644 --- a/public/language/lv/admin/settings/user.json +++ b/public/language/lv/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", - "restrictions-new": "Jauno lietotāju ierobežojumi", - "restrictions.seconds-between-new": "Jauniem lietotājiem sekundes starp rakstiem", - "restrictions.rep-threshold": "Reputācijas slieksnis pirms ierobežojumu atcelšanas", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/ms/admin/settings/chat.json b/public/language/ms/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/ms/admin/settings/chat.json +++ b/public/language/ms/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/ms/admin/settings/user.json b/public/language/ms/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/ms/admin/settings/user.json +++ b/public/language/ms/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/nb/admin/settings/chat.json b/public/language/nb/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/nb/admin/settings/chat.json +++ b/public/language/nb/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/nb/admin/settings/user.json b/public/language/nb/admin/settings/user.json index 1a9b8d7fb4..b0902f0465 100644 --- a/public/language/nb/admin/settings/user.json +++ b/public/language/nb/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Overvåker ikke", "categoryWatchState.ignoring": "Ignorerer", "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.rep-threshold": "Omdømme-terskel før disse restriksjonene fjernes", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/nl/admin/settings/chat.json b/public/language/nl/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/nl/admin/settings/chat.json +++ b/public/language/nl/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/nl/admin/settings/user.json b/public/language/nl/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/nl/admin/settings/user.json +++ b/public/language/nl/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/pl/admin/settings/chat.json b/public/language/pl/admin/settings/chat.json index 8b2c7bb20b..a4ffb8e2cf 100644 --- a/public/language/pl/admin/settings/chat.json +++ b/public/language/pl/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maksymalna liczba użytkowników w pokojach czatu", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/pl/admin/settings/user.json b/public/language/pl/admin/settings/user.json index 6bbb4acc6a..1d765a1415 100644 --- a/public/language/pl/admin/settings/user.json +++ b/public/language/pl/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Nie obserwowane", "categoryWatchState.ignoring": "Ignorowane", - "restrictions-new": "Restrykcje dla nowych użytkowników", - "restrictions.seconds-between-new": "Liczba sekund pomiędzy wpisami dla nowych użytkowników", - "restrictions.rep-threshold": "Próg reputacji wymagany do zdjęcia restrykcji", - "restrictions.seconds-before-new": "Sekundy, zanim nowy użytkownik może wykonać swój pierwszy post" -} \ No newline at end of file + "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" +} diff --git a/public/language/pt-BR/admin/settings/chat.json b/public/language/pt-BR/admin/settings/chat.json index 596d8a4216..f252a89236 100644 --- a/public/language/pt-BR/admin/settings/chat.json +++ b/public/language/pt-BR/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Número máximo de usuários nas salas de chat", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/pt-BR/admin/settings/user.json b/public/language/pt-BR/admin/settings/user.json index 8e6a54c684..dde1f90374 100644 --- a/public/language/pt-BR/admin/settings/user.json +++ b/public/language/pt-BR/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Não Acompanhar", "categoryWatchState.ignoring": "Ignorar", - "restrictions-new": "Restrições a Novos Usuários", - "restrictions.seconds-between-new": "Tempo em segundos entre postagens para novos usuários", - "restrictions.rep-threshold": "Reputação mínima para que essas restrições sejam desativadas", - "restrictions.seconds-before-new": "Segundos necessários antes de um novo usuário poder realizar sua primeira postagem" -} \ No newline at end of file + "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" +} diff --git a/public/language/pt-PT/admin/settings/chat.json b/public/language/pt-PT/admin/settings/chat.json index 200ce2ce17..d81a28dc35 100644 --- a/public/language/pt-PT/admin/settings/chat.json +++ b/public/language/pt-PT/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Número máximo de utilizadores nas salas de conversa", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/pt-PT/admin/settings/user.json b/public/language/pt-PT/admin/settings/user.json index 940ea40508..5ca3ec7d51 100644 --- a/public/language/pt-PT/admin/settings/user.json +++ b/public/language/pt-PT/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "A não seguir", "categoryWatchState.ignoring": "A ignorar", - "restrictions-new": "Restrições para Novos Utilizadores", - "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions-new": "New User Restrictions", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/ro/admin/settings/chat.json b/public/language/ro/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/ro/admin/settings/chat.json +++ b/public/language/ro/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/ro/admin/settings/user.json b/public/language/ro/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/ro/admin/settings/user.json +++ b/public/language/ro/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/ru/admin/settings/chat.json b/public/language/ru/admin/settings/chat.json index b7b4ba2270..d9f91971dd 100644 --- a/public/language/ru/admin/settings/chat.json +++ b/public/language/ru/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Максимальное кол-во пользователей в чат-комнатах", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/ru/admin/settings/user.json b/public/language/ru/admin/settings/user.json index 159580f40c..0ab605742b 100644 --- a/public/language/ru/admin/settings/user.json +++ b/public/language/ru/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Не отслеживается", "categoryWatchState.ignoring": "Игнорируется", - "restrictions-new": "Ограничения для новых пользователей", - "restrictions.seconds-between-new": "Пауза между сообщениями новых пользователей (в секундах)", - "restrictions.rep-threshold": "Минимум репутации, чтобы снять это ограничение", - "restrictions.seconds-before-new": "Пауза перед тем, как новый пользователь сможет написать первое сообщение (в секундах)" -} \ No newline at end of file + "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" +} diff --git a/public/language/rw/admin/settings/chat.json b/public/language/rw/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/rw/admin/settings/chat.json +++ b/public/language/rw/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/rw/admin/settings/user.json b/public/language/rw/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/rw/admin/settings/user.json +++ b/public/language/rw/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/sc/admin/settings/chat.json b/public/language/sc/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/sc/admin/settings/chat.json +++ b/public/language/sc/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/sc/admin/settings/user.json b/public/language/sc/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/sc/admin/settings/user.json +++ b/public/language/sc/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/sk/admin/settings/chat.json b/public/language/sk/admin/settings/chat.json index 6dbe11a643..3082ed2222 100644 --- a/public/language/sk/admin/settings/chat.json +++ b/public/language/sk/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximálny počet používateľov v konverzačnej miestnosti", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/sk/admin/settings/user.json b/public/language/sk/admin/settings/user.json index 6aad526670..4c1bcd69a2 100644 --- a/public/language/sk/admin/settings/user.json +++ b/public/language/sk/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", - "restrictions-new": "Obmedzenia nového používateľa", - "restrictions.seconds-between-new": "Sekúnd medzi príspevky pre nových používateľov", - "restrictions.rep-threshold": "Ohraničenie reputácie pred zrušením týchto obmedzení", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/sl/admin/settings/chat.json b/public/language/sl/admin/settings/chat.json index d58183cfdb..74ed1941bf 100644 --- a/public/language/sl/admin/settings/chat.json +++ b/public/language/sl/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/sl/admin/settings/user.json b/public/language/sl/admin/settings/user.json index 4001aaec5f..93c94e9e65 100644 --- a/public/language/sl/admin/settings/user.json +++ b/public/language/sl/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Ni spremljano", "categoryWatchState.ignoring": "Prezrto", - "restrictions-new": "Omejitve novega uporabnika", - "restrictions.seconds-between-new": "Sekunde med objavami za novega uporabnika", + "restrictions-new": "New User Restrictions", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Sekunde, preden lahko nov uporabnik objavi svojo prvo objavo" -} \ No newline at end of file + "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" +} diff --git a/public/language/sq-AL/admin/settings/chat.json b/public/language/sq-AL/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/sq-AL/admin/settings/chat.json +++ b/public/language/sq-AL/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/sq-AL/admin/settings/user.json b/public/language/sq-AL/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/sq-AL/admin/settings/user.json +++ b/public/language/sq-AL/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/sr/admin/settings/chat.json b/public/language/sr/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/sr/admin/settings/chat.json +++ b/public/language/sr/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/sr/admin/settings/user.json b/public/language/sr/admin/settings/user.json index 440add2174..13ac98d8db 100644 --- a/public/language/sr/admin/settings/user.json +++ b/public/language/sr/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/sv/admin/settings/chat.json b/public/language/sv/admin/settings/chat.json index 86360a34ee..a5412d1095 100644 --- a/public/language/sv/admin/settings/chat.json +++ b/public/language/sv/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Maximum number of users in chat rooms", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/sv/admin/settings/user.json b/public/language/sv/admin/settings/user.json index 165a97014b..528debebaa 100644 --- a/public/language/sv/admin/settings/user.json +++ b/public/language/sv/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/th/admin/settings/chat.json b/public/language/th/admin/settings/chat.json index 5b989f012c..3e5d9df9fb 100644 --- a/public/language/th/admin/settings/chat.json +++ b/public/language/th/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "จำนวนผู้ใช้ในห้องแชทมากที่สุด", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/th/admin/settings/user.json b/public/language/th/admin/settings/user.json index 59d0336b5b..e04950954d 100644 --- a/public/language/th/admin/settings/user.json +++ b/public/language/th/admin/settings/user.json @@ -83,7 +83,10 @@ "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", "restrictions-new": "New User Restrictions", - "restrictions.seconds-between-new": "Seconds between posts for new users", "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/tr/admin/settings/chat.json b/public/language/tr/admin/settings/chat.json index 61599fcf81..848cfbe643 100644 --- a/public/language/tr/admin/settings/chat.json +++ b/public/language/tr/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maksimum sohbet oda adı uzunluğu", "max-room-size": "Sohbet odalarındaki maksimum kullanıcı sayısı", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Geciken sohbet mesajları bildirimi", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/tr/admin/settings/user.json b/public/language/tr/admin/settings/user.json index f5b64dd88f..3ecc5ba35f 100644 --- a/public/language/tr/admin/settings/user.json +++ b/public/language/tr/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Takip edilmiyor", "categoryWatchState.ignoring": "Yok sayılıyor", - "restrictions-new": "Yeni Kullanıcı Kısıtlamaları", - "restrictions.seconds-between-new": "Yeni kullanıcılar için gönderimler arasındaki saniye cinsinden süre", - "restrictions.rep-threshold": "Bu kısıtlamalardan önceki itibar eşiği kaldırıldı", - "restrictions.seconds-before-new": "Yeni kullanıcılar kaç saniye sonra ilk iletiyi gönderebilir" -} \ No newline at end of file + "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" +} diff --git a/public/language/uk/admin/settings/chat.json b/public/language/uk/admin/settings/chat.json index 7dc74d468b..f5795a6c5c 100644 --- a/public/language/uk/admin/settings/chat.json +++ b/public/language/uk/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "Максимальна кількість людей у кімнаті", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/uk/admin/settings/user.json b/public/language/uk/admin/settings/user.json index 5ac6e15579..4c0ea21d0a 100644 --- a/public/language/uk/admin/settings/user.json +++ b/public/language/uk/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Not Watching", "categoryWatchState.ignoring": "Ignoring", - "restrictions-new": "Нові обмеження користувачів", - "restrictions.seconds-between-new": "Секунд між постами для нових користувачів", - "restrictions.rep-threshold": "Рівень репутації до того, як ці обмеження скасовуються", - "restrictions.seconds-before-new": "Seconds before a new user can make their first post" -} \ No newline at end of file + "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" +} diff --git a/public/language/vi/admin/settings/chat.json b/public/language/vi/admin/settings/chat.json index c172529361..8fdf0a4339 100644 --- a/public/language/vi/admin/settings/chat.json +++ b/public/language/vi/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Độ dài tối đa tên phòng trò chuyện", "max-room-size": "Số lượng người dùng tối đa trong phòng trò chuyện", "delay": "Thời gian giữa các tin nhắn trò chuyện (ms)", - "newbieDelay": "... cho người dùng mới (ms)", "notification-delay": "Độ trễ thông báo cho tin nhắn trò chuyện", "notification-delay-help": "Các tin nhắn bổ sung được gửi trong khoảng thời gian này sẽ được đối chiếu và người dùng sẽ được thông báo một lần trong mỗi khoảng thời gian trì hoãn. Đặt giá trị này thành 0 để tắt độ trễ.", "restrictions.seconds-edit-after": "Số giây mà một tin nhắn trò chuyện sẽ vẫn có thể chỉnh sửa được.", diff --git a/public/language/vi/admin/settings/user.json b/public/language/vi/admin/settings/user.json index d7e01814a5..a6c88151fc 100644 --- a/public/language/vi/admin/settings/user.json +++ b/public/language/vi/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Theo dõi", "categoryWatchState.notwatching": "Không Xem", "categoryWatchState.ignoring": "Bỏ Qua", - "restrictions-new": "Giới Hạn Người Dùng Mới", - "restrictions.seconds-between-new": "Số giây giữa các bài đăng cho người dùng mới", - "restrictions.rep-threshold": "Ngưỡng uy tín trước khi những hạn chế này được dỡ bỏ", - "restrictions.seconds-before-new": "Vài giây trước khi người dùng mới có thể đăng bài đầu tiên của họ" -} \ No newline at end of file + "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" +} diff --git a/public/language/zh-CN/admin/settings/chat.json b/public/language/zh-CN/admin/settings/chat.json index bd36981d98..73db01fd1e 100644 --- a/public/language/zh-CN/admin/settings/chat.json +++ b/public/language/zh-CN/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "聊天室名称最大长度", "max-room-size": "聊天室的最多用户数", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/zh-CN/admin/settings/user.json b/public/language/zh-CN/admin/settings/user.json index 5e873fe3fe..925f9ff00c 100644 --- a/public/language/zh-CN/admin/settings/user.json +++ b/public/language/zh-CN/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未关注", "categoryWatchState.ignoring": "已忽略", - "restrictions-new": "新用户限制", - "restrictions.seconds-between-new": "新用户发布帖子间隔的秒数", - "restrictions.rep-threshold": "取消发帖间隔限制所需的声望值", - "restrictions.seconds-before-new": "新用户可以在第一次发布之前的秒数" -} \ No newline at end of file + "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" +} diff --git a/public/language/zh-TW/admin/settings/chat.json b/public/language/zh-TW/admin/settings/chat.json index 550078c36f..65e3556292 100644 --- a/public/language/zh-TW/admin/settings/chat.json +++ b/public/language/zh-TW/admin/settings/chat.json @@ -8,7 +8,6 @@ "max-chat-room-name-length": "Maximum length of chat room names", "max-room-size": "聊天室的最多使用者數", "delay": "Time between chat messages (ms)", - "newbieDelay": "... for new users (ms)", "notification-delay": "Notification delay for chat messages", "notification-delay-help": "Additional messages sent between this time are collated, and the user is notified once per delay period. Set this to 0 to disable the delay.", "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable.", diff --git a/public/language/zh-TW/admin/settings/user.json b/public/language/zh-TW/admin/settings/user.json index 94b782e755..dd7a3695ae 100644 --- a/public/language/zh-TW/admin/settings/user.json +++ b/public/language/zh-TW/admin/settings/user.json @@ -82,8 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "未關注", "categoryWatchState.ignoring": "已忽略", - "restrictions-new": "新使用者限制", - "restrictions.seconds-between-new": "新使用者貼文間隔時間(秒)", - "restrictions.rep-threshold": "取消貼文限制所需的聲望值", - "restrictions.seconds-before-new": "新使用者可以在第一次發佈之前的秒數" -} \ No newline at end of file + "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" +} From b489af0662ea4b6cc11d52bc2506b7f3dddc70ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:51:57 -0500 Subject: [PATCH 060/201] fix(deps): update dependency nodebb-theme-persona to v13.2.44 (#12149) 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 d8473d5f3a..01a2d87761 100644 --- a/install/package.json +++ b/install/package.json @@ -105,7 +105,7 @@ "nodebb-theme-harmony": "1.1.95", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.24", - "nodebb-theme-persona": "13.2.43", + "nodebb-theme-persona": "13.2.44", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", From 0e9eafa191d8f8d56bf748384e5bdeff94e511df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:52:19 -0500 Subject: [PATCH 061/201] fix(deps): update dependency async to v3.2.5 (#12150) 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 01a2d87761..5f3bbe9cbb 100644 --- a/install/package.json +++ b/install/package.json @@ -36,7 +36,7 @@ "@popperjs/core": "2.11.8", "ace-builds": "1.31.1", "archiver": "6.0.1", - "async": "3.2.4", + "async": "3.2.5", "autoprefixer": "10.4.16", "bcryptjs": "2.4.3", "benchpressjs": "2.5.1", From 2eff691299623e0d6e4dccf4ef2e7d760932878b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:52:48 -0500 Subject: [PATCH 062/201] chore(deps): update redis docker tag to v7.2.3 (#12152) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4a03c0d5fd..0733c4d02f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: - 5432:5432 redis: - image: 'redis:7.2.2' + image: 'redis:7.2.3' # Set health checks to wait until redis has started options: >- --health-cmd "redis-cli ping" From 50aa1a7c5a9a9340c3bbb4174c0eaee66169d5ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:53:02 -0500 Subject: [PATCH 063/201] fix(deps): update dependency helmet to v7.1.0 (#12155) 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 5f3bbe9cbb..07cebd2519 100644 --- a/install/package.json +++ b/install/package.json @@ -70,7 +70,7 @@ "file-loader": "6.2.0", "fs-extra": "11.1.1", "graceful-fs": "4.2.11", - "helmet": "7.0.0", + "helmet": "7.1.0", "html-to-text": "9.0.5", "imagesloaded": "5.0.0", "ipaddr.js": "2.1.0", From ab05cc1c74f5ea19b4cf00d815a2df0ffc1ad2a5 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 8 Nov 2023 09:18:43 +0000 Subject: [PATCH 064/201] Latest translations and fallbacks --- public/language/bg/admin/settings/user.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/language/bg/admin/settings/user.json b/public/language/bg/admin/settings/user.json index 0aa33c80e6..727bf61aca 100644 --- a/public/language/bg/admin/settings/user.json +++ b/public/language/bg/admin/settings/user.json @@ -82,11 +82,11 @@ "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": "Време между съобщенията в разговорите за нови потребители (мсек)", + "restrictions.groups-exempt-from-new-user-restrictions": "Избиране на групи, за които да не важат ограниченията за нови потребители" } From d1a7ba35a7e6187c3505f10bf512ec5c4a36c1fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:50:35 -0500 Subject: [PATCH 065/201] chore(deps): update dependency eslint to v8.53.0 (#12151) 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 07cebd2519..ce741a938c 100644 --- a/install/package.json +++ b/install/package.json @@ -157,7 +157,7 @@ "@commitlint/cli": "18.2.0", "@commitlint/config-angular": "18.1.0", "coveralls": "3.1.1", - "eslint": "8.52.0", + "eslint": "8.53.0", "eslint-config-nodebb": "0.2.1", "eslint-plugin-import": "2.29.0", "grunt": "1.6.1", From 9d77857233722832c6403e1e1874d0cca529757d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 9 Nov 2023 09:18:51 +0000 Subject: [PATCH 066/201] Latest translations and fallbacks --- public/language/it/admin/settings/user.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/language/it/admin/settings/user.json b/public/language/it/admin/settings/user.json index 467f3b4ab3..5a23248242 100644 --- a/public/language/it/admin/settings/user.json +++ b/public/language/it/admin/settings/user.json @@ -82,11 +82,11 @@ "categoryWatchState.tracking": "Tracciamento", "categoryWatchState.notwatching": "Non seguito", "categoryWatchState.ignoring": "Ignorato", - "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": "Restrizioni per i nuovi utenti", + "restrictions.rep-threshold": "Soglia di reputazione prima dell'abolizione delle restrizioni", + "restrictions.seconds-between-new": "Secondi tra i post per i nuovi utenti", + "restrictions.seconds-before-new": "Secondi prima che un nuovo utente possa pubblicare il suo primo post", + "restrictions.seconds-edit-after-new": "Numero di secondi per cui un post rimane modificabile per i nuovi utenti (impostare a 0 per disabilitarlo)", + "restrictions.milliseconds-between-messages": "Tempo tra i messaggi di chat per i nuovi utenti (ms)", + "restrictions.groups-exempt-from-new-user-restrictions": "Seleziona i gruppi che devono essere esenti dalle restrizioni dei nuovi utente" } From 54a08087cbae4a19a6487c349620afa8034f98f8 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 1 Nov 2023 14:19:36 -0400 Subject: [PATCH 067/201] chore: add in note at top of files ready for deletion in v4 --- src/socket.io/categories.js | 4 ++++ src/socket.io/categories/search.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 934defaeac..016985935c 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -1,5 +1,9 @@ 'use strict'; +/** + * v4 note — all socket.io methods here have been deprecated, and can be removed for v4 + */ + const categories = require('../categories'); const user = require('../user'); const topics = require('../topics'); diff --git a/src/socket.io/categories/search.js b/src/socket.io/categories/search.js index dbb355ce89..ee60d2b089 100644 --- a/src/socket.io/categories/search.js +++ b/src/socket.io/categories/search.js @@ -1,5 +1,9 @@ 'use strict'; +/** + * v4 note — all socket.io methods here have been deprecated, and can be removed for v4 + */ + const sockets = require('..'); const api = require('../../api'); From b61e814787142a462b90b4948df5e9b67fd318a6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 1 Nov 2023 14:43:47 -0400 Subject: [PATCH 068/201] refactor(socket.io): deprecate socketGroups.loadMore in favour of api.groups.list --- public/openapi/write/groups.yaml | 75 ++++++++++++++++++++++++++++++++ src/api/groups.js | 9 ++++ src/controllers/write/groups.js | 4 ++ src/routes/write/groups.js | 1 + src/socket.io/groups.js | 12 ++--- 5 files changed, 96 insertions(+), 5 deletions(-) diff --git a/public/openapi/write/groups.yaml b/public/openapi/write/groups.yaml index 8d325c758e..432e474f8a 100644 --- a/public/openapi/write/groups.yaml +++ b/public/openapi/write/groups.yaml @@ -1,3 +1,78 @@ +get: + tags: + - groups + summary: list groups + description: This operation returns a list of user groups. The number of groups returned is hardcoded to 10. + parameters: + - in: query + name: 'after' + schema: + type: number + required: false + description: An offset used to display a different subset of groups. + example: '0' + - in: query + name: 'sort' + schema: + type: string + enum: ['date', 'count'] + required: false + description: Changes how the returned groups are sorted. By default, will return groups in alphanumeric order. + example: 'date' + responses: + '200': + description: user groups successfully listed + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../components/schemas/Status.yaml#/Status + response: + type: object + properties: + groups: + type: array + items: + allOf: + - $ref: ../components/schemas/GroupObject.yaml#/GroupDataObject + - type: object + properties: + members: + type: array + items: + type: object + properties: + uid: + type: number + description: A user identifier + example: 1 + username: + type: string + description: A friendly name for a given user account + example: Dragon Fruit + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.) + example: dragon-fruit + displayname: + type: string + description: This is either username or fullname depending on forum and user settings + example: Dragon Fruit + '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 + example: D + '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: '#9c27b0' + truncated: + type: boolean + description: Whether this returned member list is a subset of the total membership + nextStart: + type: number post: tags: - groups diff --git a/src/api/groups.js b/src/api/groups.js index 9bd08b5afc..3b0ecb7b10 100644 --- a/src/api/groups.js +++ b/src/api/groups.js @@ -12,6 +12,15 @@ const slugify = require('../slugify'); const groupsAPI = module.exports; +groupsAPI.list = async (caller, data) => { + const groupsPerPage = 10; + const start = parseInt(data.after || 0, 10); + const stop = start + groupsPerPage - 1; + const groupData = await groups.getGroupsBySort(data.sort, start, stop); + + return { groups: groupData, nextStart: stop + 1 }; +}; + groupsAPI.create = async function (caller, data) { if (!caller.uid) { throw new Error('[[error:no-privileges]]'); diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js index a8abd56404..8e18450139 100644 --- a/src/controllers/write/groups.js +++ b/src/controllers/write/groups.js @@ -6,6 +6,10 @@ const helpers = require('../helpers'); const Groups = module.exports; +Groups.list = async (req, res) => { + helpers.formatApiResponse(200, res, await api.groups.list(req, { ...req.query })); +}; + Groups.exists = async (req, res) => { helpers.formatApiResponse(200, res); }; diff --git a/src/routes/write/groups.js b/src/routes/write/groups.js index f2ee86eab6..d6f78217c9 100644 --- a/src/routes/write/groups.js +++ b/src/routes/write/groups.js @@ -10,6 +10,7 @@ const { setupApiRoute } = routeHelpers; module.exports = function () { const middlewares = [middleware.ensureLoggedIn]; + setupApiRoute(router, 'get', '/', [], controllers.write.groups.list); setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.groups.create); setupApiRoute(router, 'head', '/:slug', [middleware.assert.group], controllers.write.groups.exists); setupApiRoute(router, 'put', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.update); diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index d34eec366e..2f3d45dc7a 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -4,6 +4,9 @@ const groups = require('../groups'); const user = require('../user'); const utils = require('../utils'); const privileges = require('../privileges'); +const api = require('../api'); + +const sockets = require('.'); const SocketGroups = module.exports; @@ -26,15 +29,14 @@ SocketGroups.search = async (socket, data) => { }; SocketGroups.loadMore = async (socket, data) => { + sockets.warnDeprecated(socket, 'GET /api/v3/groups'); + + // These restrictions were left behind for websocket specific calls, the API is more flexible and requires no params if (!data.sort || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { throw new Error('[[error:invalid-data]]'); } - const groupsPerPage = 10; - const start = parseInt(data.after, 10); - const stop = start + groupsPerPage - 1; - const groupData = await groups.getGroupsBySort(data.sort, start, stop); - return { groups: groupData, nextStart: stop + 1 }; + return api.groups.list(socket, data); }; SocketGroups.searchMembers = async (socket, data) => { From d2f3333af017e810f0a532497b612be011d6f3de Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 1 Nov 2023 15:06:35 -0400 Subject: [PATCH 069/201] refactor(socket.io): deprecate socketGroups.searchMembers in favour of api.groups.listMembers --- public/openapi/write.yaml | 2 + public/openapi/write/groups/slug/members.yaml | 46 +++++++++++++++++++ src/api/groups.js | 29 ++++++++++++ src/controllers/write/groups.js | 5 ++ src/groups/search.js | 4 +- src/routes/write/groups.js | 2 + src/socket.io/groups.js | 16 +++---- 7 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 public/openapi/write/groups/slug/members.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 1c4dfd44a4..70462881aa 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -96,6 +96,8 @@ paths: $ref: 'write/groups.yaml' /groups/{slug}: $ref: 'write/groups/slug.yaml' + /groups/{slug}/members: + $ref: 'write/groups/slug/members.yaml' /groups/{slug}/membership/{uid}: $ref: 'write/groups/slug/membership/uid.yaml' /groups/{slug}/ownership/{uid}: diff --git a/public/openapi/write/groups/slug/members.yaml b/public/openapi/write/groups/slug/members.yaml new file mode 100644 index 0000000000..706efceecf --- /dev/null +++ b/public/openapi/write/groups/slug/members.yaml @@ -0,0 +1,46 @@ +get: + tags: + - groups + summary: list group members + description: This operation returns a list of members in a user groups. Group owners (if any) are floated to the top of the returned users. + parameters: + - in: path + name: slug + schema: + type: string + required: true + description: group slug (that also acts as its identifier) to check + example: administrators + - in: query + name: 'query' + schema: + type: string + required: false + description: A keyword search query + example: 'a' + responses: + '200': + description: matching user group members successfully listed + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + users: + type: array + items: + allOf: + - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectSlim + - type: object + properties: + isOwner: + type: boolean + matchCount: + type: number + timing: + type: string \ No newline at end of file diff --git a/src/api/groups.js b/src/api/groups.js index 3b0ecb7b10..7c0186ac31 100644 --- a/src/api/groups.js +++ b/src/api/groups.js @@ -73,6 +73,35 @@ groupsAPI.delete = async function (caller, data) { }); }; +groupsAPI.listMembers = async (caller, data) => { + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + + await canSearchMembers(caller.uid, groupName); + if (!await privileges.global.can('search:users', caller.uid)) { + throw new Error('[[error:no-privileges]]'); + } + + return await groups.searchMembers({ + uid: caller.uid, + query: data.query, + groupName, + }); +}; + +async function canSearchMembers(uid, groupName) { + const [isHidden, isMember, hasAdminPrivilege, isGlobalMod, viewGroups] = await Promise.all([ + groups.isHidden(groupName), + groups.isMember(uid, groupName), + privileges.admin.can('admin:groups', uid), + user.isGlobalModerator(uid), + privileges.global.can('view:groups', uid), + ]); + + if (!viewGroups || (isHidden && !isMember && !hasAdminPrivilege && !isGlobalMod)) { + throw new Error('[[error:no-privileges]]'); + } +} + groupsAPI.join = async function (caller, data) { if (!data) { throw new Error('[[error:invalid-data]]'); diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js index 8e18450139..8e261c1b95 100644 --- a/src/controllers/write/groups.js +++ b/src/controllers/write/groups.js @@ -32,6 +32,11 @@ Groups.delete = async (req, res) => { helpers.formatApiResponse(200, res); }; +Groups.listMembers = async (req, res) => { + const { slug } = req.params; + helpers.formatApiResponse(200, res, await api.groups.listMembers(req, { ...req.query, slug })); +}; + Groups.join = async (req, res) => { await api.groups.join(req, req.params); helpers.formatApiResponse(200, res); diff --git a/src/groups/search.js b/src/groups/search.js index 9b6b15d5a4..d1fd8892be 100644 --- a/src/groups/search.js +++ b/src/groups/search.js @@ -53,7 +53,9 @@ module.exports = function (Groups) { Groups.searchMembers = async function (data) { if (!data.query) { const users = await Groups.getOwnersAndMembers(data.groupName, data.uid, 0, 19); - return { users: users }; + const matchCount = users.length; + const timing = '0.00'; + return { users, matchCount, timing }; } const results = await user.search({ diff --git a/src/routes/write/groups.js b/src/routes/write/groups.js index d6f78217c9..6452e9c4cb 100644 --- a/src/routes/write/groups.js +++ b/src/routes/write/groups.js @@ -16,6 +16,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.update); setupApiRoute(router, 'delete', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.delete); + setupApiRoute(router, 'get', '/:slug/members', [...middlewares, middleware.assert.group], controllers.write.groups.listMembers); + setupApiRoute(router, 'put', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.join); setupApiRoute(router, 'delete', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.leave); diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 2f3d45dc7a..508078d4d3 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -5,6 +5,7 @@ const user = require('../user'); const utils = require('../utils'); const privileges = require('../privileges'); const api = require('../api'); +const slugify = require('../slugify'); const sockets = require('.'); @@ -40,18 +41,15 @@ SocketGroups.loadMore = async (socket, data) => { }; SocketGroups.searchMembers = async (socket, data) => { + sockets.warnDeprecated(socket, 'GET /api/v3/groups/:groupName/members'); + if (!data.groupName) { throw new Error('[[error:invalid-data]]'); } - await canSearchMembers(socket.uid, data.groupName); - if (!await privileges.global.can('search:users', socket.uid)) { - throw new Error('[[error:no-privileges]]'); - } - return await groups.searchMembers({ - uid: socket.uid, - query: data.query, - groupName: data.groupName, - }); + data.slug = slugify(data.groupName); + delete data.groupName; + + return api.groups.listMembers(socket, data); }; SocketGroups.loadMoreMembers = async (socket, data) => { From 807d778c7758812696b99b5d90dc6f6191945cd2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 2 Nov 2023 12:04:53 -0400 Subject: [PATCH 070/201] refactor(socket.io): deprecate socketGroups.loadMoreMembers in favour of api.groups.listMembers --- public/openapi/write/groups/slug/members.yaml | 26 +++++++++++++++--- public/src/client/groups/list.js | 2 +- public/src/client/groups/memberlist.js | 8 ++---- src/api/groups.js | 26 ++++++++++++++---- src/socket.io/groups.js | 27 +++++-------------- 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/public/openapi/write/groups/slug/members.yaml b/public/openapi/write/groups/slug/members.yaml index 706efceecf..324738b5fa 100644 --- a/public/openapi/write/groups/slug/members.yaml +++ b/public/openapi/write/groups/slug/members.yaml @@ -9,18 +9,31 @@ get: schema: type: string required: true - description: group slug (that also acts as its identifier) to check + description: a group slug (that also acts as its identifier) example: administrators - in: query name: 'query' schema: type: string required: false - description: A keyword search query + description: > + A keyword search query. + + This parameter conflicts with `after`. If both are present, `after` is ignored. example: 'a' + - in: query + name: 'after' + schema: + type: string + required: false + description: > + Offset returned results. + + This parameter conflicts with `query`. If both are present, this parameter is ignored. + example: '0' responses: '200': - description: matching user group members successfully listed + description: user group members successfully listed content: application/json: schema: @@ -42,5 +55,10 @@ get: type: boolean matchCount: type: number + nullable: true timing: - type: string \ No newline at end of file + type: string + nullable: true + nextStart: + type: number + nullable: true \ No newline at end of file diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js index cef4dfc660..aa270bf7f4 100644 --- a/public/src/client/groups/list.js +++ b/public/src/client/groups/list.js @@ -36,7 +36,7 @@ define('forum/groups/list', [ return; } - infinitescroll.loadMore('groups.loadMore', { + infinitescroll.loadMore('/groups', { sort: $('#search-sort').val(), after: $('[component="groups/container"]').attr('data-nextstart'), }, function (data, done) { diff --git a/public/src/client/groups/memberlist.js b/public/src/client/groups/memberlist.js index 986257bf9a..3a2839cab3 100644 --- a/public/src/client/groups/memberlist.js +++ b/public/src/client/groups/memberlist.js @@ -92,10 +92,7 @@ define('forum/groups/memberlist', ['api', 'bootbox', 'alerts'], function (api, b const searchEl = $('[component="groups/members/search"]'); searchEl.on('keyup', utils.debounce(function () { const query = searchEl.val(); - socket.emit('groups.searchMembers', { - groupName: groupName, - query: query, - }, function (err, results) { + api.get(`/groups/${groupName}/members`, { query }, function (err, results) { if (err) { return alerts.error(err); } @@ -125,8 +122,7 @@ define('forum/groups/memberlist', ['api', 'bootbox', 'alerts'], function (api, b } members.attr('loading', 1); - socket.emit('groups.loadMoreMembers', { - groupName: groupName, + api.get(`/groups/${groupName}/members`, { after: members.attr('data-nextstart'), }, function (err, data) { if (err) { diff --git a/src/api/groups.js b/src/api/groups.js index 7c0186ac31..5372f386be 100644 --- a/src/api/groups.js +++ b/src/api/groups.js @@ -74,6 +74,7 @@ groupsAPI.delete = async function (caller, data) { }; groupsAPI.listMembers = async (caller, data) => { + // v4 wishlist — search should paginate (with lru caching I guess) to match index listing behaviour const groupName = await groups.getGroupNameByGroupSlug(data.slug); await canSearchMembers(caller.uid, groupName); @@ -81,11 +82,26 @@ groupsAPI.listMembers = async (caller, data) => { throw new Error('[[error:no-privileges]]'); } - return await groups.searchMembers({ - uid: caller.uid, - query: data.query, - groupName, - }); + const { query } = data; + const after = parseInt(data.after || 0, 10); + let response; + if (query) { + response = await groups.searchMembers({ + uid: caller.uid, + query, + groupName, + }); + response.nextStart = null; + } else { + response = { + users: await groups.getOwnersAndMembers(groupName, caller.uid, after, after + 19), + nextStart: after + 20, + matchCount: null, + timing: null, + }; + } + + return response; }; async function canSearchMembers(uid, groupName) { diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 508078d4d3..760c6e7e14 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -53,16 +53,15 @@ SocketGroups.searchMembers = async (socket, data) => { }; SocketGroups.loadMoreMembers = async (socket, data) => { + sockets.warnDeprecated(socket, 'GET /api/v3/groups/:groupName/members'); + if (!data.groupName || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { throw new Error('[[error:invalid-data]]'); } - await canSearchMembers(socket.uid, data.groupName); - data.after = parseInt(data.after, 10); - const users = await groups.getOwnersAndMembers(data.groupName, socket.uid, data.after, data.after + 9); - return { - users: users, - nextStart: data.after + 10, - }; + data.slug = slugify(data.groupName); + delete data.groupName; + + return api.groups.listMembers(socket, data); }; SocketGroups.getChatGroups = async (socket) => { @@ -76,20 +75,6 @@ SocketGroups.getChatGroups = async (socket) => { return groupsList.map(g => ({ name: g.name, displayName: g.displayName })); }; -async function canSearchMembers(uid, groupName) { - const [isHidden, isMember, hasAdminPrivilege, isGlobalMod, viewGroups] = await Promise.all([ - groups.isHidden(groupName), - groups.isMember(uid, groupName), - privileges.admin.can('admin:groups', uid), - user.isGlobalModerator(uid), - privileges.global.can('view:groups', uid), - ]); - - if (!viewGroups || (isHidden && !isMember && !hasAdminPrivilege && !isGlobalMod)) { - throw new Error('[[error:no-privileges]]'); - } -} - SocketGroups.cover = {}; SocketGroups.cover.update = async (socket, data) => { From 181a93996c71b3ff810e4f00c46ec4212405fc2d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 8 Nov 2023 14:03:56 -0500 Subject: [PATCH 071/201] chore: minor re-order of routes/controllers in admin api v3 router/controller --- src/controllers/write/admin.js | 38 +++++++++++++++++----------------- src/routes/write/admin.js | 4 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/controllers/write/admin.js b/src/controllers/write/admin.js index 4ffeb4df54..0e5669c2e6 100644 --- a/src/controllers/write/admin.js +++ b/src/controllers/write/admin.js @@ -31,25 +31,6 @@ Admin.getAnalyticsData = async (req, res) => { })); }; -Admin.chats = {}; - -Admin.chats.deleteRoom = async (req, res) => { - const roomData = await messaging.getRoomData(req.params.roomId); - if (!roomData) { - throw new Error('[[error:no-room]]'); - } - await messaging.deleteRooms([req.params.roomId]); - - events.log({ - type: 'chat-room-deleted', - roomId: req.params.roomId, - roomName: roomData.roomName ? roomData.roomName : `No room name`, - uid: req.uid, - ip: req.ip, - }); - helpers.formatApiResponse(200, res); -}; - Admin.generateToken = async (req, res) => { const { uid, description } = req.body; const token = await api.utils.tokens.generate({ uid, description }); @@ -78,3 +59,22 @@ Admin.deleteToken = async (req, res) => { const { token } = req.params; helpers.formatApiResponse(200, res, await api.utils.tokens.delete(token)); }; + +Admin.chats = {}; + +Admin.chats.deleteRoom = async (req, res) => { + const roomData = await messaging.getRoomData(req.params.roomId); + if (!roomData) { + throw new Error('[[error:no-room]]'); + } + await messaging.deleteRooms([req.params.roomId]); + + events.log({ + type: 'chat-room-deleted', + roomId: req.params.roomId, + roomName: roomData.roomName ? roomData.roomName : `No room name`, + uid: req.uid, + ip: req.ip, + }); + helpers.formatApiResponse(200, res); +}; diff --git a/src/routes/write/admin.js b/src/routes/write/admin.js index 593e9ce123..8aaf27fc5d 100644 --- a/src/routes/write/admin.js +++ b/src/routes/write/admin.js @@ -15,13 +15,13 @@ module.exports = function () { setupApiRoute(router, 'get', '/analytics', [...middlewares], controllers.write.admin.getAnalyticsKeys); setupApiRoute(router, 'get', '/analytics/:set', [...middlewares], controllers.write.admin.getAnalyticsData); - setupApiRoute(router, 'delete', '/chats/:roomId', [...middlewares, middleware.assert.room], controllers.write.admin.chats.deleteRoom); - setupApiRoute(router, 'post', '/tokens', [...middlewares], controllers.write.admin.generateToken); setupApiRoute(router, 'get', '/tokens/:token', [...middlewares], controllers.write.admin.getToken); setupApiRoute(router, 'put', '/tokens/:token', [...middlewares], controllers.write.admin.updateToken); setupApiRoute(router, 'delete', '/tokens/:token', [...middlewares], controllers.write.admin.deleteToken); setupApiRoute(router, 'post', '/tokens/:token/roll', [...middlewares], controllers.write.admin.rollToken); + setupApiRoute(router, 'delete', '/chats/:roomId', [...middlewares, middleware.assert.room], controllers.write.admin.chats.deleteRoom); + return router; }; From dc4cc74fbd28026d63410990f265e41b2072aa18 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 8 Nov 2023 15:10:44 -0500 Subject: [PATCH 072/201] refator(socket.io): deprecate socketGroups.getChatGroups in favour of api.admin.listGroups --- public/openapi/write.yaml | 6 ++++-- public/openapi/write/admin/groups.yaml | 22 ++++++++++++++++++++++ public/src/client/chats/create.js | 8 ++++++-- public/src/client/chats/manage.js | 7 ++++++- src/api/admin.js | 9 +++++++++ src/controllers/write/admin.js | 4 ++++ src/groups/index.js | 14 ++++++++++++-- src/routes/write/admin.js | 2 ++ src/socket.io/groups.js | 12 ++++++++---- 9 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 public/openapi/write/admin/groups.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 70462881aa..91a736cd0f 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -218,14 +218,16 @@ paths: $ref: 'write/admin/analytics.yaml' /admin/analytics/{set}: $ref: 'write/admin/analytics/set.yaml' - /admin/chats/{roomId}: - $ref: 'write/admin/chats/roomId.yaml' /admin/tokens: $ref: 'write/admin/tokens.yaml' /admin/tokens/{token}: $ref: 'write/admin/tokens/token.yaml' /admin/tokens/{token}/roll: $ref: 'write/admin/tokens/token/roll.yaml' + /admin/chats/{roomId}: + $ref: 'write/admin/chats/roomId.yaml' + /admin/groups: + $ref: 'write/admin/groups.yaml' /files/: $ref: 'write/files.yaml' /files/folder: diff --git a/public/openapi/write/admin/groups.yaml b/public/openapi/write/admin/groups.yaml new file mode 100644 index 0000000000..fa2e3a2474 --- /dev/null +++ b/public/openapi/write/admin/groups.yaml @@ -0,0 +1,22 @@ +get: + tags: + - admin + summary: list all groups + description: This operation returns a full list of user groups, including hidden groups. + responses: + '200': + description: user groups successfully listed + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + groups: + type: array + items: + $ref: ../../components/schemas/GroupObject.yaml#/GroupDataObject \ No newline at end of file diff --git a/public/src/client/chats/create.js b/public/src/client/chats/create.js index f9e48c2f13..ce70fa8bab 100644 --- a/public/src/client/chats/create.js +++ b/public/src/client/chats/create.js @@ -12,11 +12,15 @@ define('forum/chats/create', [ async function handleCreate() { let groups = []; if (app.user.isAdmin) { - groups = await socket.emit('groups.getChatGroups', {}); + ({ groups } = await api.get('/admin/groups')); + groups.sort((a, b) => b.system - a.system).map((g) => { + const { name, displayName } = g; + return { name, displayName }; + }); } const html = await app.parseAndTranslate('modals/create-room', { user: app.user, - groups: groups, + groups, }); const modal = bootbox.dialog({ diff --git a/public/src/client/chats/manage.js b/public/src/client/chats/manage.js index 73f33c53d3..a69cafc4f4 100644 --- a/public/src/client/chats/manage.js +++ b/public/src/client/chats/manage.js @@ -12,7 +12,12 @@ define('forum/chats/manage', [ buttonEl.on('click', async function () { let groups = []; if (app.user.isAdmin) { - groups = await socket.emit('groups.getChatGroups', {}); + ({ groups } = await api.get('/admin/groups')); + groups.sort((a, b) => b.system - a.system).map((g) => { + const { name, displayName } = g; + return { name, displayName }; + }); + if (Array.isArray(ajaxify.data.groups)) { groups.forEach((g) => { g.selected = ajaxify.data.groups.includes(g.name); diff --git a/src/api/admin.js b/src/api/admin.js index e6b6994597..1b52ccd1ba 100644 --- a/src/api/admin.js +++ b/src/api/admin.js @@ -3,6 +3,7 @@ const meta = require('../meta'); const analytics = require('../analytics'); const privileges = require('../privileges'); +const groups = require('../groups'); const adminApi = module.exports; @@ -34,3 +35,11 @@ adminApi.getAnalyticsData = async (caller, { set, until, amount, units }) => { const getStats = units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; return await getStats(`analytics:${set}`, parseInt(until, 10) || Date.now(), amount); }; + +adminApi.listGroups = async () => { + // N.B. Returns all groups, even hidden. Beware of leakage. + // Access control handled at controller level + + const payload = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1, { ephemeral: false }); + return { groups: payload }; +}; diff --git a/src/controllers/write/admin.js b/src/controllers/write/admin.js index 0e5669c2e6..c4c8e29c8c 100644 --- a/src/controllers/write/admin.js +++ b/src/controllers/write/admin.js @@ -78,3 +78,7 @@ Admin.chats.deleteRoom = async (req, res) => { }); helpers.formatApiResponse(200, res); }; + +Admin.listGroups = async (req, res) => { + helpers.formatApiResponse(200, res, await api.admin.listGroups()); +}; diff --git a/src/groups/index.js b/src/groups/index.js index d9549ba805..ec92f05fb1 100644 --- a/src/groups/index.js +++ b/src/groups/index.js @@ -84,9 +84,19 @@ Groups.getGroupsBySort = async function (sort, start, stop) { return await Groups.getGroupsFromSet(set, start, stop); }; -Groups.getNonPrivilegeGroups = async function (set, start, stop) { +Groups.getNonPrivilegeGroups = async function (set, start, stop, flags) { + if (!flags) { + flags = { + ephemeral: true, + }; + } + let groupNames = await db.getSortedSetRevRange(set, start, stop); - groupNames = groupNames.concat(Groups.ephemeralGroups).filter(groupName => !Groups.isPrivilegeGroup(groupName)); + groupNames = groupNames.filter(groupName => !Groups.isPrivilegeGroup(groupName)); + if (flags.ephemeral) { + groupNames = groupNames.concat(Groups.ephemeralGroups); + } + const groupsData = await Groups.getGroupsData(groupNames); return groupsData.filter(Boolean); }; diff --git a/src/routes/write/admin.js b/src/routes/write/admin.js index 8aaf27fc5d..4a70e48022 100644 --- a/src/routes/write/admin.js +++ b/src/routes/write/admin.js @@ -23,5 +23,7 @@ module.exports = function () { setupApiRoute(router, 'delete', '/chats/:roomId', [...middlewares, middleware.assert.room], controllers.write.admin.chats.deleteRoom); + setupApiRoute(router, 'get', '/groups', [...middlewares], controllers.write.admin.listGroups); + return router; }; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 760c6e7e14..bf07173e0b 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -65,14 +65,18 @@ SocketGroups.loadMoreMembers = async (socket, data) => { }; SocketGroups.getChatGroups = async (socket) => { + sockets.warnDeprecated(socket, 'GET /api/v3/admin/groups'); + const isAdmin = await user.isAdministrator(socket.uid); if (!isAdmin) { throw new Error('[[error:no-privileges]]'); } - const allGroups = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); - const groupsList = allGroups.filter(g => !groups.ephemeralGroups.includes(g.name)); - groupsList.sort((a, b) => b.system - a.system); - return groupsList.map(g => ({ name: g.name, displayName: g.displayName })); + + const { groups } = await api.admin.listGroups(socket); + + // Float system groups to top and return only name/displayName + groups.sort((a, b) => b.system - a.system); + return groups.map(g => ({ name: g.name, displayName: g.displayName })); }; SocketGroups.cover = {}; From f4c36b842bea0d0d44f6dffc4f312d2c2bcf7eb4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 9 Nov 2023 11:39:26 -0500 Subject: [PATCH 073/201] chore: v4 note for deprecations --- src/socket.io/groups.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index bf07173e0b..789ac9941a 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -1,5 +1,10 @@ 'use strict'; +/** + * v4 note — all socket methods here have been deprecated and can be removed for v4 + * EXCEPT socketGroups.cover.* + */ + const groups = require('../groups'); const user = require('../user'); const utils = require('../utils'); From 4b2491becdfbee867d2e03952bd34ddd33b36ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 9 Nov 2023 12:46:31 -0500 Subject: [PATCH 074/201] feat: add copy text --- public/language/en-GB/modules.json | 3 ++- public/src/client/chats.js | 25 +++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 3307b1c213..36731fe37f 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 0ad676dc12..bf16015d53 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -106,7 +106,7 @@ define('forum/chats', [ Chats.addTextareaResizeHandler(mainWrapper); Chats.addTypingHandler(mainWrapper, roomId); Chats.addIPHandler(mainWrapper); - Chats.addCopyLinkHandler(mainWrapper); + Chats.addCopyTextLinkHandler(mainWrapper); Chats.createAutoComplete(roomId, $('[component="chat/input"]')); Chats.addUploadHandler({ dragDropAreaEl: $('.chats-full'), @@ -168,7 +168,7 @@ define('forum/chats', [ }); containerEl.tooltip({ - selector: '[component="chat/message/controls"] button', + selector: '[component="chat/message/controls"] > .btn-group > button', placement: 'top', container: '#content', animation: false, @@ -241,15 +241,28 @@ define('forum/chats', [ }); }; - Chats.addCopyLinkHandler = function (container) { + Chats.addCopyTextLinkHandler = function (container) { + function doCopy(copyEl, text) { + navigator.clipboard.writeText(text); + copyEl.find('i').addClass('fa-check').removeClass('fa-link'); + setTimeout(() => copyEl.find('i').removeClass('fa-check').addClass('fa-link'), 2000); + } + container.off('click', '[data-action="copy-link"]') .on('click', '[data-action="copy-link"]', function () { const copyEl = $(this); const mid = copyEl.attr('data-mid'); if (mid) { - navigator.clipboard.writeText(`${window.location.origin}/message/${mid}`); - copyEl.find('i').addClass('fa-check').removeClass('fa-link'); - setTimeout(() => copyEl.find('i').removeClass('fa-check').addClass('fa-link'), 2000); + doCopy(copyEl, `${window.location.origin}/message/${mid}`); + } + }); + + container.off('click', '[data-action="copy-text"]') + .on('click', '[data-action="copy-text"]', function () { + const copyEl = $(this); + const messageEl = copyEl.parents('[data-mid]'); + if (messageEl.length) { + doCopy(copyEl, messageEl.find('[component="chat/message/body"]').text().trim()); } }); }; From 4fe84ae897365f4d8dbc4e64983db82b7694c387 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 9 Nov 2023 17:46:55 +0000 Subject: [PATCH 075/201] chore(i18n): fallback strings for new resources: nodebb.modules --- public/language/ar/modules.json | 3 ++- public/language/bg/modules.json | 3 ++- public/language/bn/modules.json | 3 ++- public/language/cs/modules.json | 3 ++- public/language/da/modules.json | 3 ++- public/language/de/modules.json | 3 ++- public/language/el/modules.json | 3 ++- public/language/en-US/modules.json | 3 ++- public/language/en-x-pirate/modules.json | 3 ++- public/language/es/modules.json | 3 ++- public/language/et/modules.json | 3 ++- public/language/fa-IR/modules.json | 3 ++- public/language/fi/modules.json | 3 ++- public/language/fr/modules.json | 3 ++- public/language/gl/modules.json | 3 ++- public/language/he/modules.json | 3 ++- public/language/hr/modules.json | 3 ++- public/language/hu/modules.json | 3 ++- public/language/hy/modules.json | 3 ++- public/language/id/modules.json | 3 ++- public/language/it/modules.json | 3 ++- public/language/ja/modules.json | 3 ++- public/language/ko/modules.json | 3 ++- public/language/lt/modules.json | 3 ++- public/language/lv/modules.json | 3 ++- public/language/ms/modules.json | 3 ++- public/language/nb/modules.json | 3 ++- public/language/nl/modules.json | 3 ++- public/language/pl/modules.json | 3 ++- public/language/pt-BR/modules.json | 3 ++- public/language/pt-PT/modules.json | 3 ++- public/language/ro/modules.json | 3 ++- public/language/ru/modules.json | 3 ++- public/language/rw/modules.json | 3 ++- public/language/sc/modules.json | 3 ++- public/language/sk/modules.json | 3 ++- public/language/sl/modules.json | 3 ++- public/language/sq-AL/modules.json | 3 ++- public/language/sr/modules.json | 3 ++- public/language/sv/modules.json | 3 ++- public/language/th/modules.json | 3 ++- public/language/tr/modules.json | 3 ++- public/language/uk/modules.json | 3 ++- public/language/vi/modules.json | 3 ++- public/language/zh-CN/modules.json | 3 ++- public/language/zh-TW/modules.json | 3 ++- 46 files changed, 92 insertions(+), 46 deletions(-) diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json index ddaace29f8..051ccb5798 100644 --- a/public/language/ar/modules.json +++ b/public/language/ar/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index bc1c329b62..93d964f172 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "В тази стая", "chat.kick": "Изгонване", "chat.show-ip": "Показване на IP адреса", - "chat.copy-link": "Копиране на връзката", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Собственик на стаята", "chat.grant-rescind-ownership": "Даване/отнемане на собственост", "chat.system.user-join": "%1 се присъедини към стаята ", diff --git a/public/language/bn/modules.json b/public/language/bn/modules.json index f75974dc76..68ba8f6743 100644 --- a/public/language/bn/modules.json +++ b/public/language/bn/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json index 2956e3de31..8449a1a15e 100644 --- a/public/language/cs/modules.json +++ b/public/language/cs/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "V této místnosti", "chat.kick": "Vykopnout", "chat.show-ip": "Zobrazit IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Majitel místnosti", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/da/modules.json b/public/language/da/modules.json index ccc0e8f08d..d8fc415351 100644 --- a/public/language/da/modules.json +++ b/public/language/da/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/de/modules.json b/public/language/de/modules.json index d01efdb91b..995cda6a3e 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In diesem Chat-Room", "chat.kick": "Rauswerfen", "chat.show-ip": "IP anzeigen", - "chat.copy-link": "Link kopieren", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Raumbesitzer", "chat.grant-rescind-ownership": "Erteilung/Aufhebung des Eigentums", "chat.system.user-join": "%1 hat den Raum betreten ", diff --git a/public/language/el/modules.json b/public/language/el/modules.json index 0bd634c214..3bc454ffdc 100644 --- a/public/language/el/modules.json +++ b/public/language/el/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/en-US/modules.json b/public/language/en-US/modules.json index 0bd634c214..3bc454ffdc 100644 --- a/public/language/en-US/modules.json +++ b/public/language/en-US/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/en-x-pirate/modules.json b/public/language/en-x-pirate/modules.json index 96bdb6b14e..a9cf4d3ed8 100644 --- a/public/language/en-x-pirate/modules.json +++ b/public/language/en-x-pirate/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/es/modules.json b/public/language/es/modules.json index 119aab5b91..3d907c74ed 100644 --- a/public/language/es/modules.json +++ b/public/language/es/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "En esta sala", "chat.kick": "Expulsar", "chat.show-ip": "Mostrar IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/et/modules.json b/public/language/et/modules.json index f8eed1d1d0..539eecc169 100644 --- a/public/language/et/modules.json +++ b/public/language/et/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/fa-IR/modules.json b/public/language/fa-IR/modules.json index ce76938e40..0086b8f2f0 100644 --- a/public/language/fa-IR/modules.json +++ b/public/language/fa-IR/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "در این چت روم", "chat.kick": "اخراج", "chat.show-ip": "نشان دادن IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "مدیر چت روم", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/fi/modules.json b/public/language/fi/modules.json index c6beaaa3cd..717152a3b3 100644 --- a/public/language/fi/modules.json +++ b/public/language/fi/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json index 5e7ae37ef4..1004b3cfc2 100644 --- a/public/language/fr/modules.json +++ b/public/language/fr/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Dans cet espace de discussion", "chat.kick": "Exclure", "chat.show-ip": "Voir IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "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/gl/modules.json b/public/language/gl/modules.json index 478d2e227d..3d33b96021 100644 --- a/public/language/gl/modules.json +++ b/public/language/gl/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/he/modules.json b/public/language/he/modules.json index cc63b93c9d..977c3fd015 100644 --- a/public/language/he/modules.json +++ b/public/language/he/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "בתוך חדר זה", "chat.kick": "הוצא", "chat.show-ip": "הצג IP", - "chat.copy-link": "העתק לינק", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "מנהלי החדר", "chat.grant-rescind-ownership": "הענק/בטל בעלות", "chat.system.user-join": "%1 הצטרף לחדר ", diff --git a/public/language/hr/modules.json b/public/language/hr/modules.json index 535ca6816b..6750e12f2a 100644 --- a/public/language/hr/modules.json +++ b/public/language/hr/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/hu/modules.json b/public/language/hu/modules.json index 6a7692cbc9..966ae38ef4 100644 --- a/public/language/hu/modules.json +++ b/public/language/hu/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Ebben a szobában", "chat.kick": "Kirúgás", "chat.show-ip": "IP cím mutatása", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Szoba tulajdonos", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/hy/modules.json b/public/language/hy/modules.json index f9706f24c5..4ffec84f27 100644 --- a/public/language/hy/modules.json +++ b/public/language/hy/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Այս սենյակում", "chat.kick": "Kick", "chat.show-ip": "Ցույց տալ IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Սենյակի սեփականատեր", "chat.grant-rescind-ownership": "Տրամադրել/վերացնել սեփականության իրավունքը", "chat.system.user-join": "%1-ը միացել է սենյակին ", diff --git a/public/language/id/modules.json b/public/language/id/modules.json index 97849e4b85..991e7bfeb8 100644 --- a/public/language/id/modules.json +++ b/public/language/id/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/it/modules.json b/public/language/it/modules.json index dd35cfa319..1e9c09ce3b 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In questa stanza", "chat.kick": "Butta fuori", "chat.show-ip": "Mostra indirizzo IP", - "chat.copy-link": "Copia link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Propietario stanza", "chat.grant-rescind-ownership": "Concedi/Revoca Proprietà", "chat.system.user-join": "%1 si è unito alla stanza ", diff --git a/public/language/ja/modules.json b/public/language/ja/modules.json index b44d722a68..221c7c04c9 100644 --- a/public/language/ja/modules.json +++ b/public/language/ja/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "この部屋内", "chat.kick": "キック", "chat.show-ip": "IP表示", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "部屋の管理者", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json index ae568ff649..580df13416 100644 --- a/public/language/ko/modules.json +++ b/public/language/ko/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "채팅 참여자", "chat.kick": "추방", "chat.show-ip": "IP 보이기", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "채팅 관리자", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json index 11180da6f0..8d8f34de03 100644 --- a/public/language/lt/modules.json +++ b/public/language/lt/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/lv/modules.json b/public/language/lv/modules.json index 66032799e1..41bd1996d3 100644 --- a/public/language/lv/modules.json +++ b/public/language/lv/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Šajā tērzētavā", "chat.kick": "Izslēgt", "chat.show-ip": "Rādīt IP adresi", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Tērzētavas īpašnieks", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ms/modules.json b/public/language/ms/modules.json index bc9e268850..2e1026ca70 100644 --- a/public/language/ms/modules.json +++ b/public/language/ms/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json index f565ef1348..b9b2dcf3af 100644 --- a/public/language/nb/modules.json +++ b/public/language/nb/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json index 4254d47faf..a252352d8b 100644 --- a/public/language/nl/modules.json +++ b/public/language/nl/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In deze chat room", "chat.kick": "Schop", "chat.show-ip": "Geef IP weer", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Chatroom-eigenaar", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json index 82429f519b..cc9a9619d3 100644 --- a/public/language/pl/modules.json +++ b/public/language/pl/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "W tym pokoju", "chat.kick": "Wyrzuć", "chat.show-ip": "Pokaż IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Właściciel pokoju", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/pt-BR/modules.json b/public/language/pt-BR/modules.json index 5a9547f678..3d803930c0 100644 --- a/public/language/pt-BR/modules.json +++ b/public/language/pt-BR/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Nesta sala", "chat.kick": "Expulsar", "chat.show-ip": "Mostrar IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Dono da Sala", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/pt-PT/modules.json b/public/language/pt-PT/modules.json index fd2ed13e64..72f2a10f89 100644 --- a/public/language/pt-PT/modules.json +++ b/public/language/pt-PT/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Participantes nesta sala", "chat.kick": "Expulsar", "chat.show-ip": "Mostrar IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Dono da Sala", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ro/modules.json b/public/language/ro/modules.json index 923a63764c..39a9da3cd2 100644 --- a/public/language/ro/modules.json +++ b/public/language/ro/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json index c4c30f03fb..0519451b3c 100644 --- a/public/language/ru/modules.json +++ b/public/language/ru/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "В этой комнате", "chat.kick": "Исключить", "chat.show-ip": "Показать IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Владелец комнаты", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/rw/modules.json b/public/language/rw/modules.json index 86a2ee9757..5f531d7b66 100644 --- a/public/language/rw/modules.json +++ b/public/language/rw/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sc/modules.json b/public/language/sc/modules.json index 622f68e00d..00e8a42142 100644 --- a/public/language/sc/modules.json +++ b/public/language/sc/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json index 525dbf2206..76abdaac69 100644 --- a/public/language/sk/modules.json +++ b/public/language/sk/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "V tejto miestnosti", "chat.kick": "Vykopnúť", "chat.show-ip": "Zobraziť IP adresu", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Majiteľ miestnosti", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sl/modules.json b/public/language/sl/modules.json index 33cb814c8b..a95028d95f 100644 --- a/public/language/sl/modules.json +++ b/public/language/sl/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "In this room", "chat.kick": "Kick", "chat.show-ip": "Pokaži IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sq-AL/modules.json b/public/language/sq-AL/modules.json index b8eec66eb8..8d6f61e825 100644 --- a/public/language/sq-AL/modules.json +++ b/public/language/sq-AL/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Në këtë dhomë", "chat.kick": "Largo", "chat.show-ip": "Shfaq IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Administratori i hapësirës", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/sr/modules.json b/public/language/sr/modules.json index e6de178576..717904742c 100644 --- a/public/language/sr/modules.json +++ b/public/language/sr/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "У овој соби", "chat.kick": "Избаци", "chat.show-ip": "Прикажи IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Власник собе", "chat.grant-rescind-ownership": "Додели/поништи власништво", "chat.system.user-join": "%1 се придружио соби ", diff --git a/public/language/sv/modules.json b/public/language/sv/modules.json index 3d498c4760..779b08ad3f 100644 --- a/public/language/sv/modules.json +++ b/public/language/sv/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "I detta rum", "chat.kick": "Sparka ut", "chat.show-ip": "Visa IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Rummets ägare", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/th/modules.json b/public/language/th/modules.json index 633a0efa03..4b680a2356 100644 --- a/public/language/th/modules.json +++ b/public/language/th/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "ในห้องนี้", "chat.kick": "Kick", "chat.show-ip": "Show IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Room Owner", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json index 7e5a72bf35..0012f90210 100644 --- a/public/language/tr/modules.json +++ b/public/language/tr/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Bu odada", "chat.kick": "Dışarı At", "chat.show-ip": "IP Göster", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Oda Sahibi", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 odaya katıldı ", diff --git a/public/language/uk/modules.json b/public/language/uk/modules.json index 87be11241a..7c61d6bea9 100644 --- a/public/language/uk/modules.json +++ b/public/language/uk/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "У цій кімнаті", "chat.kick": "Штурхнути", "chat.show-ip": "Показати IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Власник кімнати", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json index 93cd6e8b04..df9c0dfb86 100644 --- a/public/language/vi/modules.json +++ b/public/language/vi/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "Trong phòng này", "chat.kick": "Loại ra", "chat.show-ip": "Hiện IP", - "chat.copy-link": "Sao chép liên kết", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "Chủ Phòng", "chat.grant-rescind-ownership": "Cấp/Hủy bỏ Quyền sở hữu", "chat.system.user-join": "%1 đã tham gia phòng ", diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json index 49fbfbae6e..77ed19e482 100644 --- a/public/language/zh-CN/modules.json +++ b/public/language/zh-CN/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "在此房间", "chat.kick": "踢出", "chat.show-ip": "显示 IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "房间所有者", "chat.grant-rescind-ownership": "给予/撤销所有权", "chat.system.user-join": "%1 加入了房间", diff --git a/public/language/zh-TW/modules.json b/public/language/zh-TW/modules.json index 486c562316..a1ff97dea8 100644 --- a/public/language/zh-TW/modules.json +++ b/public/language/zh-TW/modules.json @@ -68,7 +68,8 @@ "chat.in-room": "在此房間", "chat.kick": "踢出", "chat.show-ip": "顯示 IP", - "chat.copy-link": "Copy link", + "chat.copy-text": "Copy Text", + "chat.copy-link": "Copy Link", "chat.owner": "房間所有者", "chat.grant-rescind-ownership": "Grant/Rescind Ownership", "chat.system.user-join": "%1 has joined the room ", From fcdd3737419b837b60fbf13ce7b37438b54cda5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 9 Nov 2023 13:08:09 -0500 Subject: [PATCH 076/201] 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 ce741a938c..4cc8e3bfc2 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.95", + "nodebb-theme-harmony": "1.1.96", "nodebb-theme-lavender": "7.1.5", - "nodebb-theme-peace": "2.1.24", - "nodebb-theme-persona": "13.2.44", + "nodebb-theme-peace": "2.1.25", + "nodebb-theme-persona": "13.2.45", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", From 7fba622e52934a3059d873876aa5798b15d53063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 9 Nov 2023 15:54:52 -0500 Subject: [PATCH 077/201] mobile widgets changes --- public/src/admin/extend/widgets.js | 4 ++-- src/views/admin/extend/widgets.tpl | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/src/admin/extend/widgets.js b/public/src/admin/extend/widgets.js index ab8c41a1ae..1238ee772b 100644 --- a/public/src/admin/extend/widgets.js +++ b/public/src/admin/extend/widgets.js @@ -36,13 +36,13 @@ define('admin/extend/widgets', [ $(this).addClass('hidden'); $('#show-drafts').removeClass('hidden'); $('[component="drafts-container"]').addClass('hidden'); - $('[component="widgets-container"]').addClass('col-12').removeClass('col-6'); + $('[component="widgets-container"]').addClass('col-md-12').removeClass('col-md-6'); }); $('#show-drafts').on('click', function () { $(this).addClass('hidden'); $('#hide-drafts').removeClass('hidden'); $('[component="drafts-container"]').removeClass('hidden'); - $('[component="widgets-container"]').addClass('col-6').removeClass('col-12'); + $('[component="widgets-container"]').addClass('col-md-6').removeClass('col-md-12'); }); }; diff --git a/src/views/admin/extend/widgets.tpl b/src/views/admin/extend/widgets.tpl index 680416aa74..fea0ea6dbe 100644 --- a/src/views/admin/extend/widgets.tpl +++ b/src/views/admin/extend/widgets.tpl @@ -8,7 +8,7 @@
-
+
-
+
{{{ each templates }}}
@@ -43,13 +43,13 @@ {{{ end }}}
-
+
-
+
[[admin/extend/widgets:available]]
From 0fa6a26a0bc64b2e5e2b6c5258bfe21f13d24662 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 10 Nov 2023 09:18:39 +0000 Subject: [PATCH 078/201] Latest translations and fallbacks --- public/language/it/modules.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/it/modules.json b/public/language/it/modules.json index 1e9c09ce3b..1d29c33648 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -68,8 +68,8 @@ "chat.in-room": "In questa stanza", "chat.kick": "Butta fuori", "chat.show-ip": "Mostra indirizzo IP", - "chat.copy-text": "Copy Text", - "chat.copy-link": "Copy Link", + "chat.copy-text": "Copia testo", + "chat.copy-link": "Copia link", "chat.owner": "Propietario stanza", "chat.grant-rescind-ownership": "Concedi/Revoca Proprietà", "chat.system.user-join": "%1 si è unito alla stanza ", From 5ea7dec939af50bb0327e9bf213d47846f2d17f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 10 Nov 2023 09:54:37 -0500 Subject: [PATCH 079/201] feat: closes #12158, add sortable rewards convert rewards:list to a sorted set --- public/src/admin/extend/rewards.js | 11 ++- src/rewards/admin.js | 19 ++--- src/upgrades/3.6.0/rewards_zsets.js | 22 ++++++ src/views/admin/extend/rewards.tpl | 105 +++++++++++++++------------- 4 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 src/upgrades/3.6.0/rewards_zsets.js diff --git a/public/src/admin/extend/rewards.js b/public/src/admin/extend/rewards.js index 109c84695a..805b217193 100644 --- a/public/src/admin/extend/rewards.js +++ b/public/src/admin/extend/rewards.js @@ -1,10 +1,12 @@ 'use strict'; -define('admin/extend/rewards', ['alerts'], function (alerts) { +define('admin/extend/rewards', [ + 'alerts', + 'jquery-ui/widgets/sortable', +], function (alerts) { const rewards = {}; - let available; let active; let conditions; @@ -45,6 +47,11 @@ define('admin/extend/rewards', ['alerts'], function (alerts) { btn.addClass('hidden'); // send disable api call return false; + }) + .sortable({ + handle: '[component="sort/handle"]', + axis: 'y', + zIndex: 9999, }); $('#new').on('click', newReward); diff --git a/src/rewards/admin.js b/src/rewards/admin.js index cddbec401b..1379c89e66 100644 --- a/src/rewards/admin.js +++ b/src/rewards/admin.js @@ -7,7 +7,7 @@ const utils = require('../utils'); const rewards = module.exports; rewards.save = async function (data) { - async function save(data) { + await Promise.all(data.map(async (data, index) => { if (!Object.keys(data.rewards).length) { return; } @@ -17,19 +17,17 @@ rewards.save = async function (data) { data.id = await db.incrObjectField('global', 'rewards:id'); } await rewards.delete(data); - await db.setAdd('rewards:list', data.id); + await db.sortedSetAdd('rewards:list', index, data.id); await db.setObject(`rewards:id:${data.id}`, data); await db.setObject(`rewards:id:${data.id}:rewards`, rewardsData); - } - - await Promise.all(data.map(data => save(data))); + })); await saveConditions(data); return data; }; rewards.delete = async function (data) { await Promise.all([ - db.setRemove('rewards:list', data.id), + db.sortedSetRemove('rewards:list', data.id), db.delete(`rewards:id:${data.id}`), db.delete(`rewards:id:${data.id}:rewards`), ]); @@ -61,7 +59,8 @@ async function saveConditions(data) { } async function getActiveRewards() { - async function load(id) { + const rewardsList = await db.getSortedSetRange('rewards:list', 0, -1); + const rewardData = await Promise.all(rewardsList.map(async (id) => { const [main, rewards] = await Promise.all([ db.getObject(`rewards:id:${id}`), db.getObject(`rewards:id:${id}:rewards`), @@ -71,11 +70,7 @@ async function getActiveRewards() { main.rewards = rewards; } return main; - } - - const rewardsList = await db.getSetMembers('rewards:list'); - rewardsList.sort((a, b) => a - b); - const rewardData = await Promise.all(rewardsList.map(id => load(id))); + })); return rewardData.filter(Boolean); } diff --git a/src/upgrades/3.6.0/rewards_zsets.js b/src/upgrades/3.6.0/rewards_zsets.js new file mode 100644 index 0000000000..026d5ec7fc --- /dev/null +++ b/src/upgrades/3.6.0/rewards_zsets.js @@ -0,0 +1,22 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Convert rewards:list to a sorted set', + timestamp: Date.UTC(2023, 10, 10), + method: async function () { + const rewards = await db.getSetMembers('rewards:list'); + if (rewards.length) { + rewards.sort((a, b) => a - b); + await db.delete('rewards:list'); + await db.sortedSetAdd( + 'rewards:list', + rewards.map((id, index) => index), + rewards.map(id => id) + ); + } + }, +}; diff --git a/src/views/admin/extend/rewards.tpl b/src/views/admin/extend/rewards.tpl index 1477430dec..fb56731912 100644 --- a/src/views/admin/extend/rewards.tpl +++ b/src/views/admin/extend/rewards.tpl @@ -16,59 +16,64 @@
    {{{ each active }}}
  • -
    -
    -
    - - -
    -
    - -
    - +
    + +
    +
    + +
    + + +
    +
    + +
    + - + +
    +
    +
    + + +
    + +
    +
    [[admin/extend/rewards:select-reward]]
    +
    +
    + +
    +
    +
    + +

    + [[admin/extend/rewards:zero-infinite]] +

    +
    + + +
    +
    + + + + +
    -
    - - -
    - -
    -
    [[admin/extend/rewards:select-reward]]
    -
    -
    - -
    -
    -
    - -

    - [[admin/extend/rewards:zero-infinite]] -

    -
    - - -
    -
    - - - - -
    From 128c24f29c2129f76d481de3fb746da39080936b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:57:46 -0500 Subject: [PATCH 080/201] chore(deps): update commitlint monorepo to v18.4.0 (#12159) 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 4cc8e3bfc2..4d4ba2f0f8 100644 --- a/install/package.json +++ b/install/package.json @@ -154,8 +154,8 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@commitlint/cli": "18.2.0", - "@commitlint/config-angular": "18.1.0", + "@commitlint/cli": "18.4.0", + "@commitlint/config-angular": "18.4.0", "coveralls": "3.1.1", "eslint": "8.53.0", "eslint-config-nodebb": "0.2.1", From 5aa4bf3310f31a05bd2072e18771fb3728d76ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 10 Nov 2023 16:29:33 -0500 Subject: [PATCH 081/201] dont show legends on error charts --- public/src/admin/advanced/errors.js | 31 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/public/src/admin/advanced/errors.js b/public/src/admin/advanced/errors.js index 5767b45dd4..15d4bc9c7f 100644 --- a/public/src/admin/advanced/errors.js +++ b/public/src/admin/advanced/errors.js @@ -92,30 +92,29 @@ function setupCharts() { ], }, }; + const chartOptions = { + responsive: true, + scales: { + y: { + beginAtZero: true, + }, + }, + plugins: { + legend: { + display: false, + }, + }, + }; new Chart(notFoundCanvas.getContext('2d'), { type: 'line', data: data['not-found'], - options: { - responsive: true, - scales: { - y: { - beginAtZero: true, - }, - }, - }, + options: chartOptions, }); new Chart(tooBusyCanvas.getContext('2d'), { type: 'line', data: data.toobusy, - options: { - responsive: true, - scales: { - y: { - beginAtZero: true, - }, - }, - }, + options: chartOptions, }); } From fdb6e9b0b19e9afa129e1db212e063644fdfa78f Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 11 Nov 2023 09:18:16 +0000 Subject: [PATCH 082/201] Latest translations and fallbacks --- public/language/bg/modules.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json index 93d964f172..b3a4e6d996 100644 --- a/public/language/bg/modules.json +++ b/public/language/bg/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 се присъедини към стаята ", From 22d7e92f8a29c4a7f03ec926a7fbb9954684240b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 11 Nov 2023 13:24:54 -0500 Subject: [PATCH 083/201] 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 4d4ba2f0f8..ceb777ce4e 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.25", + "nodebb-plugin-composer-default": "10.2.26", "nodebb-plugin-dbsearch": "6.2.2", "nodebb-plugin-emoji": "5.1.13", "nodebb-plugin-emoji-android": "4.0.0", From d563fa0708397363e1c8f08c8fa3236cbdf275ea Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 12 Nov 2023 09:18:18 +0000 Subject: [PATCH 084/201] Latest translations and fallbacks --- public/language/tr/admin/settings/user.json | 14 +++++------ public/language/tr/email.json | 26 ++++++++++----------- public/language/tr/rewards.json | 4 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/public/language/tr/admin/settings/user.json b/public/language/tr/admin/settings/user.json index 3ecc5ba35f..7061d9ad63 100644 --- a/public/language/tr/admin/settings/user.json +++ b/public/language/tr/admin/settings/user.json @@ -82,11 +82,11 @@ "categoryWatchState.tracking": "Tracking", "categoryWatchState.notwatching": "Takip edilmiyor", "categoryWatchState.ignoring": "Yok sayılıyor", - "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": "Yeni kullanıcı kısıtlamaları", + "restrictions.rep-threshold": "Yeni kullanıcı kısıtlamalarının kalkması için gereken minimum saygınlık sınırı", + "restrictions.seconds-between-new": "Yeni kullanıcılar için ileti gönderimleri arasındaki saniye miktarı", + "restrictions.seconds-before-new": "Yeni kullanıcıların ilk iletilerini göndermesi için geçmesi gereken saniye miktarı", + "restrictions.seconds-edit-after-new": "Yeni kullanıcıların iletileri kaç saniye boyunca düzenlenebilir (devre dışı bırakmak için 0 giriniz)", + "restrictions.milliseconds-between-messages": "Yeni kullanıcıların sohbet mesajları arasındaki süre (ms)", + "restrictions.groups-exempt-from-new-user-restrictions": "Yeni kullanıcı kısıtlamalarının uygulanmayacağı grupları seçiniz" } diff --git a/public/language/tr/email.json b/public/language/tr/email.json index cfcfec239d..3d5abb005f 100644 --- a/public/language/tr/email.json +++ b/public/language/tr/email.json @@ -7,7 +7,7 @@ "greeting-with-name": "Merhaba %1", "email.verify-your-email.subject": "Lütfen e-posta adresinizi doğrulayın", "email.verify.text1": "E-posta adresinizi değiştirmemizi veya onaylamamızı istediniz", - "email.verify.text2": "Güvenlik amacıyla, kayıtlı e-posta adresini yalnızca sahipliği e-posta yoluyla onaylandıktan sonra değiştirir veya onaylarız. Bunu talep etmediyseniz, herhangi bir işlem yapmanız gerekmez.", + "email.verify.text2": "Güvenlik amacıyla, kayıtlı e-posta adresini yalnızca sahipliği e-posta yoluyla onaylandıktan sonra değiştirir veya onaylarız. Bunu talep etmediyseniz, herhangi bir işlem yapmanız gerekmez.", "email.verify.text3": "Bu e-posta adresini onayladığınızda, mevcut e-posta adresinizi bununla (%1) değiştireceğiz.", "welcome.text1": "Kaydolduğunuz için teşekkürler!", "welcome.text2": "Hesabınızı aktif hale getirmek için, kaydolduğunuz e-posta adresinin size ait olduğunu onaylamamız gerekiyor.", @@ -20,14 +20,14 @@ "reset.text2": "Şifre değiştirme işlemine devam etmek için aşağıdaki bağlantıya tıklayın:", "reset.cta": "Şifrenizi değiştirmek için buraya tıklayın", "reset.notify.subject": "Şifre başarıyla değiştirildi", - "reset.notify.text1": "Şifrenizin %1 zamanında başarı ile değiştirildiğini bildirmek isteriz.", + "reset.notify.text1": "Şifrenizin %1 tarihinde başarı ile değiştirildiğini bildirmek isteriz.", "reset.notify.text2": "Bunu siz yetkilendirmediyseniz, lütfen hiç vakit kaybetmeden site yöneticisine bu durumu bildiriniz.", - "digest.unread-rooms": "Unread rooms", - "digest.room-name-unreadcount": "%1 (%2 unread)", - "digest.latest-topics": "'daki En Güncel Konular", - "digest.top-topics": "%1 tarafından zirve başlıklar", - "digest.popular-topics": "%1 tarafından popüler başlıklar", - "digest.cta": "sitesini ziyaret etmek için buraya tıklayın", + "digest.unread-rooms": "Okunmamış Odalar", + "digest.room-name-unreadcount": "%1 (%2 okunmamış)", + "digest.latest-topics": "%1 kullanıcısından en son başlıklar", + "digest.top-topics": "%1 kullanıcısından en zirve başlıklar", + "digest.popular-topics": "%1 kullanıcısından popüler başlıklar", + "digest.cta": "%1 sitesini ziyaret etmek için buraya tıklayın", "digest.unsub.info": "Bu e-posta seçtiğiniz ayarlar nedeniyle gönderildi.", "digest.day": "gün", "digest.week": "hafta", @@ -36,8 +36,8 @@ "digest.title.day": "Günlük Özet", "digest.title.week": "Haftalık Özet", "digest.title.month": "Aylık Özet", - "notif.chat.new-message-from-user": "New message from \"%1\"", - "notif.chat.new-message-from-user-in-room": "New message from %1 in room %2", + "notif.chat.new-message-from-user": "\"%1\" kullanıcısından yeni sohbet mesajı", + "notif.chat.new-message-from-user-in-room": "%1 kullanıcısından %2 odasında yeni sohbet mesajı", "notif.chat.cta": "Sohbete devam etmek için buraya tıklayın", "notif.chat.unsub.info": "Bu bildirim, seçtiğiniz ayarlar nedeniyle gönderilmiştir.", "notif.post.unsub.info": "Bu yazı bildirimi size abonelik ayarlarınız nedeniyle gönderilmiştir.", @@ -46,12 +46,12 @@ "notif.cta-new-reply": "İletiyi Görüntüle", "notif.cta-new-chat": "Sohbeti Görüntüle", "notif.test.short": "Bildirim Testi", - "notif.test.long": "Bu, bir bildirim epostası testidir.", + "notif.test.long": "Bu, bir bildirim e-postası testidir. Yardım gönderin!", "test.text1": "Bu ileti NodeBB e-posta ayarlarınızın doğru çalışıp çalışmadığını kontrol etmek için gönderildi.", "unsub.cta": "Buraya tıklayarak ayarlarınızı değiştirebilirsiniz.", "unsubscribe": "abonelikten çık", - "unsub.success": "Artık %1 eposta listesinden eposta almayacaksınız.", - "unsub.failure.title": "Abonelikten çıkarılamaz", + "unsub.success": "Artık %1 e-posta listesinden e-posta almayacaksınız.", + "unsub.failure.title": "Abonelikten çıkarılamadınız", "unsub.failure.message": "Maalesef bağlantı linkiyle ilgili bir problemden ötürü abonelikten çıkarılamadınız. Fakat, eposta tercihlerinizi şu bölüme giderek değiştirebillirsiniz: Kullanıcı ayarları.

    (hata: %1)", "banned.subject": "%1 sitesinden yasaklandınız!", "banned.text1": "%1 kullanıcısı %2 sitesinden yasaklandı.", diff --git a/public/language/tr/rewards.json b/public/language/tr/rewards.json index f923cf1500..783f3fc021 100644 --- a/public/language/tr/rewards.json +++ b/public/language/tr/rewards.json @@ -1,6 +1,6 @@ { - "awarded-x-reputation": "You have been awarded %1 reputation", - "awarded-group-membership": "You have been added to the group %1", + "awarded-x-reputation": "%1 saygınlık puanı ile ödüllendirildiniz", + "awarded-group-membership": "Şu gruba eklendiniz: %1", "essentials/user.reputation-conditional-value": "(Reputation %1 %2)", "essentials/user.postcount-conditional-value": "(Post Count %1 %2)", From ed10dda29aaf540aa32799604efe40bc591ea32c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 12 Nov 2023 12:16:31 -0500 Subject: [PATCH 085/201] 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 ceb777ce4e..ad236dff3b 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.96", + "nodebb-theme-harmony": "1.1.97", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", - "nodebb-theme-persona": "13.2.45", + "nodebb-theme-persona": "13.2.46", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", From be3d3393e3dead2bb12b071c1619288cc5e927be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 12:16:45 -0500 Subject: [PATCH 086/201] chore(deps): update dependency @commitlint/cli to v18.4.1 (#12162) 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 ceb777ce4e..463dd2ee45 100644 --- a/install/package.json +++ b/install/package.json @@ -154,7 +154,7 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@commitlint/cli": "18.4.0", + "@commitlint/cli": "18.4.1", "@commitlint/config-angular": "18.4.0", "coveralls": "3.1.1", "eslint": "8.53.0", From 1796ed2b232382df78b829ce487a9342c4fb9460 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 12:16:56 -0500 Subject: [PATCH 087/201] chore(deps): update dependency lint-staged to v15.1.0 (#12161) 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 463dd2ee45..b0ecf804f4 100644 --- a/install/package.json +++ b/install/package.json @@ -164,7 +164,7 @@ "grunt-contrib-watch": "1.1.0", "husky": "8.0.3", "jsdom": "22.1.0", - "lint-staged": "15.0.2", + "lint-staged": "15.1.0", "mocha": "10.2.0", "mocha-lcov-reporter": "1.3.0", "mockdate": "3.0.5", From 9d18d3c7abb60c0956a4c64dbd2056b0c71703e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 12:17:06 -0500 Subject: [PATCH 088/201] fix(deps): update dependency lru-cache to v10.0.2 (#12160) 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 b0ecf804f4..a11aeaa01e 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.1", + "lru-cache": "10.0.2", "mime": "3.0.0", "mkdirp": "3.0.1", "mongodb": "6.2.0", From 7f3a9968effb36033aa280c443cb4bb9f020d2d7 Mon Sep 17 00:00:00 2001 From: Opliko Date: Sun, 12 Nov 2023 19:38:00 +0100 Subject: [PATCH 089/201] feat: docker improvements (#12031) * use yarn and debian slim build * feat: update Dockerfile to use multistage builds * Create main.yml * remove some useless things from docker context and assume yarn by default * remove all dotfiles in docker context * no need for extra build tools, complain to the module author if there is no alpine build (cherry picked from commit 90516a3c8399e74c38be7115edb39411ba0d86b9) * specify the config file location instead of creating it (cherry picked from commit 38e4295d70682f1049fe671ade96eeccd669d908) * set explicit config path (cherry picked from commit 8dcc6f249d099cb8939a95511ec13702491958bc) * fix docker-compose example to use the exposed volumes * dockerfile: upgrade alpine to 3.16 * dockerignore: add more ignorable entries * docker-compose: change the way the docker startup process works * install: pass config path to child process as well Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * setup: move config file resolution up before setup This fixes issue with different config file location, which will otherwise default on 'config.json', which means the config save won't save to the file we specified Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-entrypoint: don't fix CONFIG_DIR location but fix default location Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-entrypoint: handle missing config file logic Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * README: add simple notice on how to use it Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * add missing semicolons Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-compose: remove multi override, use one big profile instead However, Docker Compose doesn't support profile-based dependency and this would probably means we have less guarantee about the liveness of the database. But since this is just a sample configuration it should be fine Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * workflows: remove main.yml, add platforms to buildx matrix in docker.yml Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * workflows: set docker buildx to build for amd64 and arm64 only Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-entrypoint: don't force build everytime before start Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-entrypoint: implement init verb This would allow you to change between "setup" (automated setup using environmental variables which is the current preferred way to run containerized NodeBB) or "install" (web install that guides user to fill in connection information, which is similar to WordPress) Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * README: mention caveat with MongoDB Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * README: add Docker section placeholder for doc migration Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-entrypoint: add SETUP variable support Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-compose: add force flag to ln on setup Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * docker-compose: fix permission issue; docker-compose: fast exit if still no permission on config dir Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> * fix: remove redundant FROM * docs: remove docker stuff (in favour of docs entry, nodebb/docs#78) but add link to cloud install docs * fix: correctly check if directory is writable * fix: ignore .docker directory * fix: multi-arch docker builds and chown performance * chore: bump database image versions * fix: move from alpine to slim image * fix: use omit=dev instead of only=prod * feat: move entrypoint to install directory * feat: initialize mongodb user * feat: use separate rebuild stage * fix: disable eslint for mongodb script * fix: remove node_modules bind mount bind mounts don't save data from container, resulting in a LOONG startup * feat: prepopulate database defaults for installation * feat: enable persistence in redis container * docs: add some comments to the compose file --------- Signed-off-by: steve <29133953+stevefan1999-personal@users.noreply.github.com> Co-authored-by: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Co-authored-by: Steve Fan <19037626d@connect.polyu.hk> Co-authored-by: Julian Lam --- .dockerignore | 10 +++++ .eslintignore | 1 + .github/workflows/docker.yml | 10 ++--- .gitignore | 4 +- Dockerfile | 46 ++++++++++++--------- README.md | 4 +- docker-compose.yml | 63 +++++++++++++++++++++-------- install/docker/entrypoint.sh | 46 +++++++++++++++++++++ install/docker/mongodb-user-init.js | 1 + install/docker/setup.json | 21 ++++++++++ install/web.js | 2 + src/cli/setup.js | 9 +++-- src/install.js | 2 + 13 files changed, 175 insertions(+), 44 deletions(-) create mode 100644 .dockerignore create mode 100755 install/docker/entrypoint.sh create mode 100644 install/docker/mongodb-user-init.js create mode 100644 install/docker/setup.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..46ab37b3ab --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.* +logs +test +node_modules +commitlint.config.js +nodebb.bat +renovate.json +*.yml +*.md +Dockerfile diff --git a/.eslintignore b/.eslintignore index b7a6ad79cd..b304ee19d8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,3 +18,4 @@ logs/ .eslintrc test/files *.min.js +install/docker/ \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d27f66173c..16fc4a0de0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -52,10 +52,10 @@ jobs: - name: Build and push Docker images uses: docker/build-push-action@v5 with: - context: . - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 cache-from: type=gha cache-to: type=gha,mode=max + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: ${{ steps.meta.outputs.tags }} diff --git a/.gitignore b/.gitignore index 23e38016c2..887ef337b0 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,6 @@ package-lock.json /package.json *.mongodb link-plugins.sh -test.sh \ No newline at end of file +test.sh + +.docker/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d7277a3389..14f61d3ada 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,29 +13,39 @@ USER node RUN npm install --omit=dev +FROM node:lts as rebuild -FROM node:lts +ARG BUILDPLATFORM +ARG TARGETPLATFORM -RUN mkdir -p /usr/src/app && \ - chown -R node:node /usr/src/app -WORKDIR /usr/src/app +RUN mkdir -p /usr/src/build && \ + chown -R node:node /usr/src/build + +COPY --from=npm /usr/src/build /usr/src/build + +RUN if [ $BUILDPLATFORM != $TARGETPLATFORM ]; then \ + npm rebuild && \ + npm cache clean --force; fi + +FROM node:lts-slim as run ARG NODE_ENV -ENV NODE_ENV $NODE_ENV - -COPY --chown=node:node --from=npm /usr/src/build /usr/src/app - -USER node - -RUN npm rebuild && \ - npm cache clean --force - -COPY --chown=node:node . /usr/src/app - -ENV NODE_ENV=production \ +ENV NODE_ENV=$NODE_ENV \ daemon=false \ silent=false -EXPOSE 4567 +RUN mkdir -p /usr/src/app && \ + chown -R node:node /usr/src/app -CMD test -n "${SETUP}" && ./nodebb setup || node ./nodebb build; node ./nodebb start +COPY --chown=node:node --from=rebuild /usr/src/build /usr/src/app + + +WORKDIR /usr/src/app + +USER node + +COPY --chown=node:node . /usr/src/app + +EXPOSE 4567 +VOLUME ["/usr/src/app/node_modules", "/usr/src/app/build", "/usr/src/app/public/uploads", "/opt/config"] +ENTRYPOINT ["./install/docker/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index a45ba24ab8..307d90cca2 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ NodeBB requires the following software to be installed: ## Installation -[Please refer to platform-specific installation documentation](https://docs.nodebb.org/installing/os) +[Please refer to platform-specific installation documentation](https://docs.nodebb.org/installing/os). +If installing via the cloud (or using Docker), [please see cloud-based installation documentation](https://docs.nodebb.org/installing/cloud/). ## Securing NodeBB @@ -59,6 +60,7 @@ It is important to ensure that your NodeBB and database servers are secured. Bea 2. Use `iptables` to secure your server from unintended open ports. In Ubuntu, `ufw` provides a friendlier interface to working with `iptables`. * e.g. If your NodeBB is proxied, no ports should be open except 80 (and possibly 22, for SSH access) + ## Upgrading NodeBB Detailed upgrade instructions are listed in [Upgrading NodeBB](https://docs.nodebb.org/configuring/upgrade/) diff --git a/docker-compose.yml b/docker-compose.yml index 5e382f47f9..f11fde22b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,24 +1,55 @@ -version: '3.5' +version: '3.8' services: - node: + nodebb: build: . restart: unless-stopped - depends_on: - - db - expose: - - 4567 # use a reverse proxy like Traefik - - db: - image: mongo:bionic + ports: + - "4567:4567/tcp" # comment this out if you don't want to expose NodeBB to the host, or change the first number to any port you want + # uncomment if you want to use another container as a reverse proxy + # expose: + # - 4567 + volumes: + - ./.docker/build:/usr/src/app/build + - ./.docker/public/uploads:/usr/src/app/public/uploads + - ./.docker:/opt/config + - ./install/docker/setup.json:/usr/src/app/setup.json + mongo: + image: "mongo:6-jammy" restart: unless-stopped expose: - - 27017 + - "27017" environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: root + MONGO_INITDB_ROOT_USERNAME: nodebb + MONGO_INITDB_ROOT_PASSWORD: nodebb + MONGO_INITDB_DATABASE: nodebb volumes: - - mongo:/data/db - -volumes: - mongo: + - ./.docker/database/mongo/config:/etc/mongo + - ./.docker/database/mongo/data:/data/db + - ./install/docker/mongodb-user-init.js:/docker-entrypoint-initdb.d/user-init.js + profiles: + - mongo + postgres: + image: postgres:16.0-alpine + restart: unless-stopped + expose: + - "5432" + environment: + POSTGRES_USER: nodebb + POSTGRES_PASSWORD: nodebb + POSTGRES_DB: nodebb + volumes: + - ./.docker/database/postgresql/data:/var/lib/postgresql/data + profiles: + - postgres + redis: + image: redis:7.2.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 + expose: + - "6379" + volumes: + - ./.docker/database/redis:/data + profiles: + - redis \ No newline at end of file diff --git a/install/docker/entrypoint.sh b/install/docker/entrypoint.sh new file mode 100755 index 0000000000..e5b2036321 --- /dev/null +++ b/install/docker/entrypoint.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +export CONFIG_DIR="${CONFIG_DIR:-/opt/config}" +export CONFIG=$CONFIG_DIR/config.json +export FORCE_BUILD_BEFORE_START="${FORCE_BUILD_BEFORE_START:-false}" + +# Supported verbs: install (web install), setup (interactive CLI session). Default: web install +# TODO: constraint it using a hash set (or hash table) +export NODEBB_INIT_VERB="${NODEBB_INIT_VERB:-install}" +# Setup variable for backward compatibility, default: +export SETUP="${SETUP:-}" + +mkdir -p $CONFIG_DIR + +# if the folder is mounted as a volume this can fail, the check below is to ensure there is still write access +chmod -fR 760 $CONFIG_DIR 2> /dev/null + +if [[ ! -w $CONFIG_DIR ]]; then + echo "panic: no write permission for $CONFIG_DIR" + exit 1 +fi + +[[ -f $CONFIG_DIR/package.json ]] || cp install/package.json $CONFIG_DIR/package.json +[[ -f $CONFIG_DIR/package-lock.json ]] || touch $CONFIG_DIR/package-lock.json + +ln -fs $CONFIG_DIR/package.json package.json +ln -fs $CONFIG_DIR/package-lock.json package-lock.json + +npm install --omit=dev + +if [[ -n $SETUP ]]; then + echo "Setup environmental variable detected" + echo "Starting setup session" + ./nodebb setup --config=$CONFIG +elif [ -f $CONFIG ]; then + echo "Config file exist at $CONFIG, assuming it is a valid config" + echo "Starting forum" + if [ "$FORCE_BUILD_BEFORE_START" = true ]; then + ./nodebb build --config=$CONFIG + fi + ./nodebb start --config=$CONFIG +else + echo "Config file not found at $CONFIG" + echo "Starting installation session" + ./nodebb "${NODEBB_INIT_VERB}" --config=$CONFIG +fi \ No newline at end of file diff --git a/install/docker/mongodb-user-init.js b/install/docker/mongodb-user-init.js new file mode 100644 index 0000000000..36b7079173 --- /dev/null +++ b/install/docker/mongodb-user-init.js @@ -0,0 +1 @@ +db.createUser( { user: 'nodebb', pwd: 'nodebb', roles: [ { role: 'readWrite', db: 'nodebb' }, { role: 'clusterMonitor', db: 'admin' } ] } ) \ No newline at end of file diff --git a/install/docker/setup.json b/install/docker/setup.json new file mode 100644 index 0000000000..3fad840593 --- /dev/null +++ b/install/docker/setup.json @@ -0,0 +1,21 @@ +{ + "mongo": { + "host": "mongo", + "port": 27017, + "database": "nodebb", + "username": "nodebb", + "password": "nodebb" + }, + "redis": { + "host": "redis", + "port": 6379, + "database": 0 + }, + "postgres": { + "host": "postgres", + "port": 5432, + "database": "nodebb", + "username": "nodebb", + "password": "nodebb" + } +} \ No newline at end of file diff --git a/install/web.js b/install/web.js index 1f5a846a30..92fe675c22 100644 --- a/install/web.js +++ b/install/web.js @@ -174,6 +174,8 @@ function install(req, res) { const database = nconf.get('database') || req.body.database || 'mongo'; const setupEnvVars = { ...process.env, + CONFIG: nconf.get('config'), + NODEBB_CONFIG: nconf.get('config'), NODEBB_URL: nconf.get('url') || req.body.url || (`${req.protocol}://${req.get('host')}`), NODEBB_PORT: nconf.get('port') || 4567, NODEBB_ADMIN_USERNAME: nconf.get('admin:username') || req.body['admin:username'], diff --git a/src/cli/setup.js b/src/cli/setup.js index 51245c9262..859f674a9c 100644 --- a/src/cli/setup.js +++ b/src/cli/setup.js @@ -20,12 +20,15 @@ async function setup(initConfig) { console.log('Press enter to accept the default setting (shown in brackets).'); install.values = initConfig; - const data = await install.setup(); let configFile = paths.config; - if (nconf.get('config')) { - configFile = path.resolve(paths.baseDir, nconf.get('config')); + const config = nconf.any(['config', 'CONFIG']); + if (config) { + nconf.set('config', config); + configFile = path.resolve(paths.baseDir, config); } + const data = await install.setup(); + prestart.loadConfig(configFile); if (!nconf.get('skip-build')) { diff --git a/src/install.js b/src/install.js index ea59843f5b..89b40d7b39 100644 --- a/src/install.js +++ b/src/install.js @@ -51,6 +51,8 @@ function checkSetupFlagEnv() { let setupVal = install.values; const envConfMap = { + CONFIG: 'config', + NODEBB_CONFIG: 'config', NODEBB_URL: 'url', NODEBB_PORT: 'port', NODEBB_ADMIN_USERNAME: 'admin:username', From 4c7c46f32c04246ce540f030266c8398bd786d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 12 Nov 2023 13:51:54 -0500 Subject: [PATCH 090/201] fix: language key --- src/views/admin/settings/user.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 6a5c703a2d..25d2c4e03d 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -339,7 +339,7 @@
    From c41f9a753431f1a8870c958c774934b74bdfd5a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 13:52:22 -0500 Subject: [PATCH 091/201] chore(deps): update redis docker tag to v7.2.3 (#12165) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index f11fde22b7..fab4d90fd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: profiles: - postgres redis: - image: redis:7.2.1-alpine + image: redis:7.2.3-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes", "--loglevel", "warning"] # command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # uncomment if you want to use snapshotting instead of AOF From 5f8a4f3a4ebb6d551d75f9de978f8f2c4fe8a295 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 13:52:41 -0500 Subject: [PATCH 092/201] chore(deps): update mongo docker tag to v7 (#12166) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index fab4d90fd3..bda3ecfcfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - ./.docker:/opt/config - ./install/docker/setup.json:/usr/src/app/setup.json mongo: - image: "mongo:6-jammy" + image: "mongo:7-jammy" restart: unless-stopped expose: - "27017" From d98d8ff7b36e81e5b4c196e04612f100a98ee01f Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 14 Nov 2023 09:18:36 +0000 Subject: [PATCH 093/201] Latest translations and fallbacks --- public/language/de/admin/settings/user.json | 14 +++++++------- public/language/de/modules.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/language/de/admin/settings/user.json b/public/language/de/admin/settings/user.json index a6404a925c..2bc8bbfb5d 100644 --- a/public/language/de/admin/settings/user.json +++ b/public/language/de/admin/settings/user.json @@ -82,11 +82,11 @@ "categoryWatchState.tracking": "Verfolgung", "categoryWatchState.notwatching": "Nicht beobachtet", "categoryWatchState.ignoring": "Ignoriert", - "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": "Neue Benutzerbeschränkungen", + "restrictions.rep-threshold": "Mindestansehen bevor die Beschränkungen aufgehoben werden", + "restrictions.seconds-between-new": "Sekunden zwischen Beiträgen für neue Benutzer", + "restrictions.seconds-before-new": "Sekunden, bevor ein neuer Nutzer seinen ersten Beitrag verfassen kann", + "restrictions.seconds-edit-after-new": "Anzahl der Sekunden, die ein Beitrag für neue Benutzer bearbeitbar bleibt (zum Deaktivieren auf 0 setzen)", + "restrictions.milliseconds-between-messages": "Zeit zwischen Chat-Nachrichten für neue Benutzer (ms)", + "restrictions.groups-exempt-from-new-user-restrictions": "Gruppen auswählen, die von den neuen Benutzerbeschränkungen ausgenommen werden sollen" } diff --git a/public/language/de/modules.json b/public/language/de/modules.json index 995cda6a3e..80d6560e7d 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -68,8 +68,8 @@ "chat.in-room": "In diesem Chat-Room", "chat.kick": "Rauswerfen", "chat.show-ip": "IP anzeigen", - "chat.copy-text": "Copy Text", - "chat.copy-link": "Copy Link", + "chat.copy-text": "Text kopieren", + "chat.copy-link": "Link kopieren", "chat.owner": "Raumbesitzer", "chat.grant-rescind-ownership": "Erteilung/Aufhebung des Eigentums", "chat.system.user-join": "%1 hat den Raum betreten ", From 616ff5733a96d8087d58af0ed9de6350c22bff15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 07:50:59 -0500 Subject: [PATCH 094/201] chore(deps): update postgres docker tag to v16.1 (#12167) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bda3ecfcfe..cca3deb496 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: profiles: - mongo postgres: - image: postgres:16.0-alpine + image: postgres:16.1-alpine restart: unless-stopped expose: - "5432" From ace171a6bf7219c221a1a01e6ada7751f277e2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 15 Nov 2023 11:01:30 -0500 Subject: [PATCH 095/201] feat: add some new social share buttons --- public/src/modules/share.js | 37 +++++++++++++++++++++++----- src/meta/configs.js | 2 +- src/social.js | 21 +++++++++++++--- src/views/admin/settings/general.tpl | 2 +- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/public/src/modules/share.js b/public/src/modules/share.js index 485f6f6447..9a1aa4b56a 100644 --- a/public/src/modules/share.js +++ b/public/src/modules/share.js @@ -3,12 +3,11 @@ define('share', ['hooks'], function (hooks) { const module = {}; + const baseUrl = window.location.protocol + '//' + window.location.host; module.addShareHandlers = function (name) { - const baseUrl = window.location.protocol + '//' + window.location.host; - function openShare(url, urlToPost, width, height) { - window.open(url + encodeURIComponent(baseUrl + config.relative_path + urlToPost), '_blank', 'width=' + width + ',height=' + height + ',scrollbars=no,status=no'); + window.open(url, '_blank', 'width=' + width + ',height=' + height + ',scrollbars=no,status=no'); hooks.fire('action:share.open', { url: url, urlToPost: urlToPost, @@ -32,11 +31,36 @@ define('share', ['hooks'], function (hooks) { }); addHandler('[component="share/twitter"]', function () { - return openShare('https://twitter.com/intent/tweet?text=' + encodeURIComponent(name) + '&url=', getPostUrl($(this)), 550, 420); + const postUrl = getPostUrl($(this)); + const twitter_url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(name)}&url=${encodeURIComponent(postUrl)}`; + return openShare(twitter_url, postUrl, 550, 420); }); addHandler('[component="share/facebook"]', function () { - return openShare('https://www.facebook.com/sharer/sharer.php?u=', getPostUrl($(this)), 626, 436); + const postUrl = getPostUrl($(this)); + const facebook_url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(postUrl)}`; + return openShare(facebook_url, postUrl, 626, 436); + }); + + addHandler('[component="share/whatsapp"]', function () { + const postUrl = getPostUrl($(this)); + const message = encodeURIComponent(name) + ' - ' + encodeURIComponent(postUrl); + const whatsapp_url = config.useragent.isMobile ? + `whatsapp://send?text=${message}` : + `https://wa.me/?text=${message}`; + return openShare(whatsapp_url, postUrl, 626, 436); + }); + + addHandler('[component="share/telegram"]', function () { + const postUrl = getPostUrl($(this)); + const telegram_url = `https://t.me/share/url?text=${encodeURIComponent(name)}&url=${encodeURIComponent(postUrl)}`; + return openShare(telegram_url, postUrl, 626, 436); + }); + + addHandler('[component="share/linkedin"]', function () { + const postUrl = getPostUrl($(this)); + const linkedin_url = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(postUrl)}`; + return openShare(linkedin_url, postUrl, 626, 436); }); hooks.fire('action:share.addHandlers', { openShare: openShare }); @@ -48,7 +72,8 @@ define('share', ['hooks'], function (hooks) { function getPostUrl(clickedElement) { const pid = parseInt(clickedElement.parents('[data-pid]').attr('data-pid'), 10); - return '/post' + (pid ? '/' + (pid) : ''); + const path = '/post' + (pid ? '/' + (pid) : ''); + return baseUrl + config.relative_path + path; } return module; diff --git a/src/meta/configs.js b/src/meta/configs.js index 180540811c..80159490c4 100644 --- a/src/meta/configs.js +++ b/src/meta/configs.js @@ -178,7 +178,7 @@ async function processConfig(data) { if (data.minimumUsernameLength > data.maximumUsernameLength) { throw new Error('[[error:invalid-data]]'); } - + require('../social').postSharing = null; await Promise.all([ saveRenderedCss(data), getLogoSize(data), diff --git a/src/social.js b/src/social.js index 2dd4045024..8f1d460133 100644 --- a/src/social.js +++ b/src/social.js @@ -18,12 +18,27 @@ social.getPostSharing = async function () { { id: 'facebook', name: 'Facebook', - class: 'fa-facebook', + class: 'fa-brands fa-facebook', }, { id: 'twitter', - name: 'Twitter', - class: 'fa-twitter', + name: 'X (Twitter)', + class: 'fa-brands fa-x-twitter', + }, + { + id: 'whatsapp', + name: 'Whatsapp', + class: 'fa-brands fa-whatsapp', + }, + { + id: 'telegram', + name: 'Telegram', + class: 'fa-brands fa-telegram', + }, + { + id: 'linkedin', + name: 'LinkedIn', + class: 'fa-brands fa-linkedin', }, ]; networks = await plugins.hooks.fire('filter:social.posts', networks); diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl index 96e90991ab..1d39042c97 100644 --- a/src/views/admin/settings/general.tpl +++ b/src/views/admin/settings/general.tpl @@ -300,7 +300,7 @@
    {{{ end }}} From 9a1f8e9b2138075befca71c821dbe0f71e9d013c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 15 Nov 2023 11:02:59 -0500 Subject: [PATCH 096/201] 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 4e04c07322..532f528c40 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.97", + "nodebb-theme-harmony": "1.1.98", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", - "nodebb-theme-persona": "13.2.46", + "nodebb-theme-persona": "13.2.47", "nodebb-widget-essentials": "7.0.14", "nodemailer": "6.9.7", "nprogress": "0.2.0", From b4aefcca80385526efdc25960275eebbee826aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 15 Nov 2023 16:49:47 -0500 Subject: [PATCH 097/201] add linkedin --- public/language/en-GB/social.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/language/en-GB/social.json b/public/language/en-GB/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/en-GB/social.json +++ b/public/language/en-GB/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file From 081352b6b2020f1a0d7d2ae649c4e425cc88c5ce Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 15 Nov 2023 21:50:11 +0000 Subject: [PATCH 098/201] chore(i18n): fallback strings for new resources: nodebb.social --- public/language/ar/social.json | 4 +++- public/language/bg/social.json | 4 +++- public/language/bn/social.json | 4 +++- public/language/cs/social.json | 4 +++- public/language/da/social.json | 4 +++- public/language/de/social.json | 4 +++- public/language/el/social.json | 4 +++- public/language/en-US/social.json | 4 +++- public/language/en-x-pirate/social.json | 4 +++- public/language/es/social.json | 4 +++- public/language/et/social.json | 4 +++- public/language/fa-IR/social.json | 4 +++- public/language/fi/social.json | 4 +++- public/language/fr/social.json | 4 +++- public/language/gl/social.json | 4 +++- public/language/he/social.json | 4 +++- public/language/hr/social.json | 4 +++- public/language/hu/social.json | 4 +++- public/language/hy/social.json | 4 +++- public/language/id/social.json | 4 +++- public/language/it/social.json | 4 +++- public/language/ja/social.json | 4 +++- public/language/ko/social.json | 4 +++- public/language/lt/social.json | 4 +++- public/language/lv/social.json | 4 +++- public/language/ms/social.json | 4 +++- public/language/nb/social.json | 4 +++- public/language/nl/social.json | 4 +++- public/language/pl/social.json | 4 +++- public/language/pt-BR/social.json | 4 +++- public/language/pt-PT/social.json | 4 +++- public/language/ro/social.json | 4 +++- public/language/ru/social.json | 4 +++- public/language/rw/social.json | 4 +++- public/language/sc/social.json | 4 +++- public/language/sk/social.json | 4 +++- public/language/sl/social.json | 4 +++- public/language/sq-AL/social.json | 4 +++- public/language/sr/social.json | 4 +++- public/language/sv/social.json | 4 +++- public/language/th/social.json | 4 +++- public/language/tr/social.json | 4 +++- public/language/uk/social.json | 4 +++- public/language/vi/social.json | 4 +++- public/language/zh-CN/social.json | 4 +++- public/language/zh-TW/social.json | 4 +++- 46 files changed, 138 insertions(+), 46 deletions(-) diff --git a/public/language/ar/social.json b/public/language/ar/social.json index bc6916716a..b36256840e 100644 --- a/public/language/ar/social.json +++ b/public/language/ar/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "تسجيل الدخول باستخدام قوقل", "sign-up-with-google": "التسجيل باستخدام قوقل", "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" } \ No newline at end of file diff --git a/public/language/bg/social.json b/public/language/bg/social.json index cb91c35a8b..7f2aa3884a 100644 --- a/public/language/bg/social.json +++ b/public/language/bg/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Вписване с Google", "sign-up-with-google": "Регистриране с Google", "log-in-with-facebook": "Вписване с Facebook", - "continue-with-facebook": "Продължаване с Facebook" + "continue-with-facebook": "Продължаване с Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/bn/social.json b/public/language/bn/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/bn/social.json +++ b/public/language/bn/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/cs/social.json b/public/language/cs/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/cs/social.json +++ b/public/language/cs/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/da/social.json b/public/language/da/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/da/social.json +++ b/public/language/da/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/de/social.json b/public/language/de/social.json index 01e3cc0a49..a93f282662 100644 --- a/public/language/de/social.json +++ b/public/language/de/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Mit Google anmelden", "sign-up-with-google": "Mit Google registrieren", "log-in-with-facebook": "Mit Facebook anmelden", - "continue-with-facebook": "Mit Facebook fortsetzen" + "continue-with-facebook": "Mit Facebook fortsetzen", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/el/social.json b/public/language/el/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/el/social.json +++ b/public/language/el/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/en-US/social.json b/public/language/en-US/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/en-US/social.json +++ b/public/language/en-US/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/en-x-pirate/social.json b/public/language/en-x-pirate/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/en-x-pirate/social.json +++ b/public/language/en-x-pirate/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/es/social.json b/public/language/es/social.json index a4d9e821ad..4d4b41ecd0 100644 --- a/public/language/es/social.json +++ b/public/language/es/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Accede con Google", "sign-up-with-google": "Regístrate con Google", "log-in-with-facebook": "Accede con Facebook", - "continue-with-facebook": "Regístrate con Facebook" + "continue-with-facebook": "Regístrate con Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/et/social.json b/public/language/et/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/et/social.json +++ b/public/language/et/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/fa-IR/social.json b/public/language/fa-IR/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/fa-IR/social.json +++ b/public/language/fa-IR/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/fi/social.json b/public/language/fi/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/fi/social.json +++ b/public/language/fi/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/fr/social.json b/public/language/fr/social.json index dacd7e0b5e..062bb9c029 100644 --- a/public/language/fr/social.json +++ b/public/language/fr/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Connectez-vous avec Google", "sign-up-with-google": "Inscrivez-vous avec Google", "log-in-with-facebook": "Connectez-vous avec Facebook", - "continue-with-facebook": "Continuer avec Facebook" + "continue-with-facebook": "Continuer avec Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/gl/social.json b/public/language/gl/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/gl/social.json +++ b/public/language/gl/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/he/social.json b/public/language/he/social.json index c57a9b2e23..83b0971cd5 100644 --- a/public/language/he/social.json +++ b/public/language/he/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "היכנס באמצעות Google", "sign-up-with-google": "הירשם באמצעות Google", "log-in-with-facebook": "היכנס באמצעות Facebook", - "continue-with-facebook": "המשך בFacebook" + "continue-with-facebook": "המשך בFacebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/hr/social.json b/public/language/hr/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/hr/social.json +++ b/public/language/hr/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/hu/social.json b/public/language/hu/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/hu/social.json +++ b/public/language/hu/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/hy/social.json b/public/language/hy/social.json index 8d2fa9bd1c..cadb9ba632 100644 --- a/public/language/hy/social.json +++ b/public/language/hy/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Մուտք գործեք Google-ով", "sign-up-with-google": "Գրանցվեք Google-ով", "log-in-with-facebook": "Մուտք գործեք Facebook-ով", - "continue-with-facebook": "Շարունակեք Facebook-ով" + "continue-with-facebook": "Շարունակեք Facebook-ով", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/id/social.json b/public/language/id/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/id/social.json +++ b/public/language/id/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/it/social.json b/public/language/it/social.json index 54145797ae..927d39e3e7 100644 --- a/public/language/it/social.json +++ b/public/language/it/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Accedere con Google", "sign-up-with-google": "Registrati con Google", "log-in-with-facebook": "Accedi con Facebook", - "continue-with-facebook": "Continua con Facebook" + "continue-with-facebook": "Continua con Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/ja/social.json b/public/language/ja/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/ja/social.json +++ b/public/language/ja/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/ko/social.json b/public/language/ko/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/ko/social.json +++ b/public/language/ko/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/lt/social.json b/public/language/lt/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/lt/social.json +++ b/public/language/lt/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/lv/social.json b/public/language/lv/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/lv/social.json +++ b/public/language/lv/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/ms/social.json b/public/language/ms/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/ms/social.json +++ b/public/language/ms/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/nb/social.json b/public/language/nb/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/nb/social.json +++ b/public/language/nb/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/nl/social.json b/public/language/nl/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/nl/social.json +++ b/public/language/nl/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/pl/social.json b/public/language/pl/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/pl/social.json +++ b/public/language/pl/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/pt-BR/social.json b/public/language/pt-BR/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/pt-BR/social.json +++ b/public/language/pt-BR/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/pt-PT/social.json b/public/language/pt-PT/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/pt-PT/social.json +++ b/public/language/pt-PT/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/ro/social.json b/public/language/ro/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/ro/social.json +++ b/public/language/ro/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/ru/social.json b/public/language/ru/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/ru/social.json +++ b/public/language/ru/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/rw/social.json b/public/language/rw/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/rw/social.json +++ b/public/language/rw/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/sc/social.json b/public/language/sc/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/sc/social.json +++ b/public/language/sc/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/sk/social.json b/public/language/sk/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/sk/social.json +++ b/public/language/sk/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/sl/social.json b/public/language/sl/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/sl/social.json +++ b/public/language/sl/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/sq-AL/social.json b/public/language/sq-AL/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/sq-AL/social.json +++ b/public/language/sq-AL/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/sr/social.json b/public/language/sr/social.json index 793cc65ee6..5ceb7d44d5 100644 --- a/public/language/sr/social.json +++ b/public/language/sr/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Пријавите се преко Google-а", "sign-up-with-google": "Региструјте се преко Google-а", "log-in-with-facebook": "Пријавите се преко Facebook-а", - "continue-with-facebook": "Наставите се преко Facebook-а" + "continue-with-facebook": "Наставите се преко Facebook-а", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/sv/social.json b/public/language/sv/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/sv/social.json +++ b/public/language/sv/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/th/social.json b/public/language/th/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/th/social.json +++ b/public/language/th/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/tr/social.json b/public/language/tr/social.json index b34a3e5295..c155ebec42 100644 --- a/public/language/tr/social.json +++ b/public/language/tr/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Google ile Giriş Yap", "sign-up-with-google": "Google ile Kaydol", "log-in-with-facebook": "Facebook ile Giriş Yap", - "continue-with-facebook": "Facebook ile devam et" + "continue-with-facebook": "Facebook ile devam et", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/uk/social.json b/public/language/uk/social.json index 7ebb50c0c7..2ba690a187 100644 --- a/public/language/uk/social.json +++ b/public/language/uk/social.json @@ -6,5 +6,7 @@ "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" + "continue-with-facebook": "Continue with Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/vi/social.json b/public/language/vi/social.json index 45d971b6a8..6017a94dfc 100644 --- a/public/language/vi/social.json +++ b/public/language/vi/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "Đăng nhập bằng Google", "sign-up-with-google": "Đăng ký với Google", "log-in-with-facebook": "Đăng nhập bằng Facebook", - "continue-with-facebook": "Tiếp tục với Facebook" + "continue-with-facebook": "Tiếp tục với Facebook", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/zh-CN/social.json b/public/language/zh-CN/social.json index cd4720aaa6..ae8a4a5874 100644 --- a/public/language/zh-CN/social.json +++ b/public/language/zh-CN/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "通过 Google 登录", "sign-up-with-google": "通过 Google 注册", "log-in-with-facebook": "通过 Facebook 登录", - "continue-with-facebook": "继续使用 Facebook 登录" + "continue-with-facebook": "继续使用 Facebook 登录", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file diff --git a/public/language/zh-TW/social.json b/public/language/zh-TW/social.json index ca4c515412..a1bc7a254c 100644 --- a/public/language/zh-TW/social.json +++ b/public/language/zh-TW/social.json @@ -6,5 +6,7 @@ "sign-in-with-google": "以Google登入", "sign-up-with-google": "以Google註冊", "log-in-with-facebook": "以Facebook登入", - "continue-with-facebook": "以Facebook繼續使用" + "continue-with-facebook": "以Facebook繼續使用", + "sign-in-with-linkedin": "Sign in with LinkedIn", + "sign-up-with-linkedin": "Sign up with LinkedIn" } \ No newline at end of file From 30b3e143815f9404afca6e31713a2495e269d54d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 17 Nov 2023 09:19:27 +0000 Subject: [PATCH 099/201] Latest translations and fallbacks --- public/language/bg/social.json | 4 ++-- public/language/it/social.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/language/bg/social.json b/public/language/bg/social.json index 7f2aa3884a..931e80c8c8 100644 --- a/public/language/bg/social.json +++ b/public/language/bg/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/it/social.json b/public/language/it/social.json index 927d39e3e7..4fb4c6bd3e 100644 --- a/public/language/it/social.json +++ b/public/language/it/social.json @@ -7,6 +7,6 @@ "sign-up-with-google": "Registrati con Google", "log-in-with-facebook": "Accedi con Facebook", "continue-with-facebook": "Continua con Facebook", - "sign-in-with-linkedin": "Sign in with LinkedIn", - "sign-up-with-linkedin": "Sign up with LinkedIn" + "sign-in-with-linkedin": "Accedi con LinkedIn", + "sign-up-with-linkedin": "Registrati con LinkedIn" } \ No newline at end of file From 5d03321e923dbf59f0b948f4e6f14391a4d5f558 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 17 Nov 2023 11:38:17 -0500 Subject: [PATCH 100/201] feat: new language strings for post-queue page --- public/language/en-GB/post-queue.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/language/en-GB/post-queue.json b/public/language/en-GB/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/en-GB/post-queue.json +++ b/public/language/en-GB/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", From f98205c1fd051fe4f872292db33992edfceeb0e9 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 17 Nov 2023 16:38:42 +0000 Subject: [PATCH 101/201] chore(i18n): fallback strings for new resources: nodebb.post-queue --- public/language/ar/post-queue.json | 4 +++- public/language/bg/post-queue.json | 4 +++- public/language/bn/post-queue.json | 4 +++- public/language/cs/post-queue.json | 4 +++- public/language/da/post-queue.json | 4 +++- public/language/de/post-queue.json | 4 +++- public/language/el/post-queue.json | 4 +++- public/language/en-US/post-queue.json | 4 +++- public/language/en-x-pirate/post-queue.json | 4 +++- public/language/es/post-queue.json | 4 +++- public/language/et/post-queue.json | 4 +++- public/language/fa-IR/post-queue.json | 4 +++- public/language/fi/post-queue.json | 4 +++- public/language/fr/post-queue.json | 4 +++- public/language/gl/post-queue.json | 4 +++- public/language/he/post-queue.json | 4 +++- public/language/hr/post-queue.json | 4 +++- public/language/hu/post-queue.json | 4 +++- public/language/hy/post-queue.json | 4 +++- public/language/id/post-queue.json | 4 +++- public/language/it/post-queue.json | 4 +++- public/language/ja/post-queue.json | 4 +++- public/language/ko/post-queue.json | 4 +++- public/language/lt/post-queue.json | 4 +++- public/language/lv/post-queue.json | 4 +++- public/language/ms/post-queue.json | 4 +++- public/language/nb/post-queue.json | 4 +++- public/language/nl/post-queue.json | 4 +++- public/language/pl/post-queue.json | 4 +++- public/language/pt-BR/post-queue.json | 4 +++- public/language/pt-PT/post-queue.json | 4 +++- public/language/ro/post-queue.json | 4 +++- public/language/ru/post-queue.json | 4 +++- public/language/rw/post-queue.json | 4 +++- public/language/sc/post-queue.json | 4 +++- public/language/sk/post-queue.json | 4 +++- public/language/sl/post-queue.json | 4 +++- public/language/sq-AL/post-queue.json | 4 +++- public/language/sr/post-queue.json | 4 +++- public/language/sv/post-queue.json | 4 +++- public/language/th/post-queue.json | 4 +++- public/language/tr/post-queue.json | 4 +++- public/language/uk/post-queue.json | 4 +++- public/language/vi/post-queue.json | 4 +++- public/language/zh-CN/post-queue.json | 4 +++- public/language/zh-TW/post-queue.json | 4 +++- 46 files changed, 138 insertions(+), 46 deletions(-) diff --git a/public/language/ar/post-queue.json b/public/language/ar/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/ar/post-queue.json +++ b/public/language/ar/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/bg/post-queue.json b/public/language/bg/post-queue.json index f688cddcdd..fa204a97dd 100644 --- a/public/language/bg/post-queue.json +++ b/public/language/bg/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Опашка за публикации", "no-queued-posts": "В опашката за публикации няма нищо.", "no-single-post": "Темата или публикацията, която търсите, вече не се намира в опашката. Вероятно или е била одобрена, или изтрита.", - "enabling-help": "За да включите тази функционалност, идете в Настройки → Публикуване → Опашка за публикации и включете Опашката за публикации.", + "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.", "user": "Потребител", "when": "Кога", "category": "Категория", diff --git a/public/language/bn/post-queue.json b/public/language/bn/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/bn/post-queue.json +++ b/public/language/bn/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/cs/post-queue.json b/public/language/cs/post-queue.json index 2485f7768e..8212df7563 100644 --- a/public/language/cs/post-queue.json +++ b/public/language/cs/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Fronta příspěvků", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Uživatel", "when": "When", "category": "Kategorie", diff --git a/public/language/da/post-queue.json b/public/language/da/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/da/post-queue.json +++ b/public/language/da/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/de/post-queue.json b/public/language/de/post-queue.json index 9afe19ad05..c24f8b6ef4 100644 --- a/public/language/de/post-queue.json +++ b/public/language/de/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Beitragswarteschlange", "no-queued-posts": "Es sind keine Beiträge in der Beitragswarteschlange", "no-single-post": "Dieses Thema oder dieser Beitrag ist nicht mehr in der Warteschlange. Er wurde wahrscheinlich angenommen oder gelöscht.", - "enabling-help": "Um diese Funktion zu aktivieren, gehe zu Einstellungen → Posts → Beitragswarteschlange und aktiviere die Beitragswarteschlange.", + "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": "Zurück zur Beitragswarteschlange", + "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.", "user": "Benutzer", "when": "Wann", "category": "Kategorie", diff --git a/public/language/el/post-queue.json b/public/language/el/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/el/post-queue.json +++ b/public/language/el/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/en-US/post-queue.json b/public/language/en-US/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/en-US/post-queue.json +++ b/public/language/en-US/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/en-x-pirate/post-queue.json b/public/language/en-x-pirate/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/en-x-pirate/post-queue.json +++ b/public/language/en-x-pirate/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/es/post-queue.json b/public/language/es/post-queue.json index f278398a8f..4005bfcbb4 100644 --- a/public/language/es/post-queue.json +++ b/public/language/es/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Cola de Mensajes", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Usuario", "when": "When", "category": "Categoría", diff --git a/public/language/et/post-queue.json b/public/language/et/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/et/post-queue.json +++ b/public/language/et/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/fa-IR/post-queue.json b/public/language/fa-IR/post-queue.json index 69855c9362..b8c434f0ce 100644 --- a/public/language/fa-IR/post-queue.json +++ b/public/language/fa-IR/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "صف پست", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/fi/post-queue.json b/public/language/fi/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/fi/post-queue.json +++ b/public/language/fi/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/fr/post-queue.json b/public/language/fr/post-queue.json index 1ec28108fe..f82f350223 100644 --- a/public/language/fr/post-queue.json +++ b/public/language/fr/post-queue.json @@ -3,8 +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": "Pour activer cette fonctionnalité, accédez à Paramètres → Message → File d'attente et activez la file d'attente.", + "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": "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.", "user": "Utilisateur", "when": "Quand", "category": "Catégorie", diff --git a/public/language/gl/post-queue.json b/public/language/gl/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/gl/post-queue.json +++ b/public/language/gl/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/he/post-queue.json b/public/language/he/post-queue.json index ffda34c675..80fc182303 100644 --- a/public/language/he/post-queue.json +++ b/public/language/he/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "פוסטים ממתינים", "no-queued-posts": "אין פוסטים ממתינים בתור הפוסטים.", "no-single-post": "הנושא או הפוסט שאתה מחפש כבר לא בתור. כנראה שהוא כבר אושר או נמחק.", - "enabling-help": "כדי להפעיל תכונה זו, עבור אל הגדרות/פוסטים והפעל תור פוסטים.", + "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.", "user": "משתמש", "when": "כאשר", "category": "קטגוריה", diff --git a/public/language/hr/post-queue.json b/public/language/hr/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/hr/post-queue.json +++ b/public/language/hr/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/hu/post-queue.json b/public/language/hu/post-queue.json index 406e956d9d..ae86261b30 100644 --- a/public/language/hu/post-queue.json +++ b/public/language/hu/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Hozzászólási várósor", "no-queued-posts": "Nincsenek hozzászólások a bejegyzési sorban.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Vissza a bejegyzési sorhoz", + "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.", "user": "Felhasználó", "when": "Amikor", "category": "Kategória", diff --git a/public/language/hy/post-queue.json b/public/language/hy/post-queue.json index fd5b53e2ab..9e707cd3f5 100644 --- a/public/language/hy/post-queue.json +++ b/public/language/hy/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Գրառումների հերթ", "no-queued-posts": "Գրառումների հերթում գրառումներ չկան:", "no-single-post": "Ձեր փնտրած թեման կամ գրառումն այլևս հերթում չէ: Այն հավանաբար արդեն հաստատված կամ ջնջված է:", - "enabling-help": "Այս գործառույթը միացնելու համար անցեքoԿարգավորումներ և ռառ, Գրառում և ռառ, Գրառման հերթ և ակտիվացրեք Գրառման հերթ.", + "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.", "user": "Օգտատեր", "when": "Երբ", "category": "Կատեգորիա", diff --git a/public/language/id/post-queue.json b/public/language/id/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/id/post-queue.json +++ b/public/language/id/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/it/post-queue.json b/public/language/it/post-queue.json index 14c6afa67d..f677e57e1c 100644 --- a/public/language/it/post-queue.json +++ b/public/language/it/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Coda post", "no-queued-posts": "Non ci sono post nella coda dei post.", "no-single-post": "La discussione o il post che si sta cercando non è più in coda. Probabilmente è già stato approvato o cancellato.", - "enabling-help": "Per abilitare questa funzione, vai in Impostazioni → Post → Coda Post e attiva Coda 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": "Torna alla coda dei post", + "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.", "user": "Utente", "when": "Quando", "category": "Categoria", diff --git a/public/language/ja/post-queue.json b/public/language/ja/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/ja/post-queue.json +++ b/public/language/ja/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/ko/post-queue.json b/public/language/ko/post-queue.json index 131fd3936e..6e6451c1b1 100644 --- a/public/language/ko/post-queue.json +++ b/public/language/ko/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "게시 대기열", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "사용자", "when": "When", "category": "카테고리", diff --git a/public/language/lt/post-queue.json b/public/language/lt/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/lt/post-queue.json +++ b/public/language/lt/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/lv/post-queue.json b/public/language/lv/post-queue.json index 0dbb39d1d2..2a716a258e 100644 --- a/public/language/lv/post-queue.json +++ b/public/language/lv/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Rakstu apstiprināšanas rinda", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Lietotājs", "when": "When", "category": "Kategorija", diff --git a/public/language/ms/post-queue.json b/public/language/ms/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/ms/post-queue.json +++ b/public/language/ms/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/nb/post-queue.json b/public/language/nb/post-queue.json index 670eb5682d..58d2a16f8d 100644 --- a/public/language/nb/post-queue.json +++ b/public/language/nb/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Innleggskø", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Bruker", "when": "When", "category": "Kategori", diff --git a/public/language/nl/post-queue.json b/public/language/nl/post-queue.json index 1a5eec6b78..3cbba3ebb3 100644 --- a/public/language/nl/post-queue.json +++ b/public/language/nl/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Berichtenwachtrij", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Gebruiker", "when": "When", "category": "Categorie", diff --git a/public/language/pl/post-queue.json b/public/language/pl/post-queue.json index 76cd4755d5..1a27801fbb 100644 --- a/public/language/pl/post-queue.json +++ b/public/language/pl/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Kolejka postów", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Użytkownik", "when": "When", "category": "Kategoria", diff --git a/public/language/pt-BR/post-queue.json b/public/language/pt-BR/post-queue.json index 8ea12e5751..795ee1c8dc 100644 --- a/public/language/pt-BR/post-queue.json +++ b/public/language/pt-BR/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Fila de Posts", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Usuário", "when": "When", "category": "Categoria", diff --git a/public/language/pt-PT/post-queue.json b/public/language/pt-PT/post-queue.json index e710ba2109..d02245963e 100644 --- a/public/language/pt-PT/post-queue.json +++ b/public/language/pt-PT/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Publicações por Aprovar", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Utilizador", "when": "When", "category": "Categoria", diff --git a/public/language/ro/post-queue.json b/public/language/ro/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/ro/post-queue.json +++ b/public/language/ro/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/ru/post-queue.json b/public/language/ru/post-queue.json index 04a7ddbe65..991b447759 100644 --- a/public/language/ru/post-queue.json +++ b/public/language/ru/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Очередь на публикацию", "no-queued-posts": "Нет записей в очереди", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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.", "user": "Пользователь", "when": "Когда", "category": "Категория", diff --git a/public/language/rw/post-queue.json b/public/language/rw/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/rw/post-queue.json +++ b/public/language/rw/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/sc/post-queue.json b/public/language/sc/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/sc/post-queue.json +++ b/public/language/sc/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/sk/post-queue.json b/public/language/sk/post-queue.json index 897d9d572f..ff8a841770 100644 --- a/public/language/sk/post-queue.json +++ b/public/language/sk/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Príspevky vo fronte", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Používateľ", "when": "When", "category": "Kategórie", diff --git a/public/language/sl/post-queue.json b/public/language/sl/post-queue.json index 159342883f..8dfaeee200 100644 --- a/public/language/sl/post-queue.json +++ b/public/language/sl/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Čakalna vrsta objav", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Uporabnik", "when": "When", "category": "Kategorija", diff --git a/public/language/sq-AL/post-queue.json b/public/language/sq-AL/post-queue.json index 923ba6b27a..c6247613bf 100644 --- a/public/language/sq-AL/post-queue.json +++ b/public/language/sq-AL/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Radha e postimit", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Përdorues", "when": "When", "category": "Kategoria", diff --git a/public/language/sr/post-queue.json b/public/language/sr/post-queue.json index b3b53a7eb3..07478e802f 100644 --- a/public/language/sr/post-queue.json +++ b/public/language/sr/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Објаве у реду чекања", "no-queued-posts": "Нема објава у реду чекања.", "no-single-post": "Тема или објава коју тражите више није у реду чекања. Вероватно је већ одобрена или избрисана.", - "enabling-help": "За омогућавање ове функције, идите у Подешавања → Објава → Објаве на чекању и омогућите Објаве на чекању.", + "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.", "user": "Корисник", "when": "Када", "category": "Категорија", diff --git a/public/language/sv/post-queue.json b/public/language/sv/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/sv/post-queue.json +++ b/public/language/sv/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/th/post-queue.json b/public/language/th/post-queue.json index c750bfae93..021ed0d83f 100644 --- a/public/language/th/post-queue.json +++ b/public/language/th/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Post Queue", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "User", "when": "When", "category": "Category", diff --git a/public/language/tr/post-queue.json b/public/language/tr/post-queue.json index b435f93be1..bbb0eeadc0 100644 --- a/public/language/tr/post-queue.json +++ b/public/language/tr/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "İleti Kuyruğu", "no-queued-posts": "İleti kuyruğunda yeni ileti mevcut değil! ", "no-single-post": "Aradığınız ileti veya başlık artık ileti kuyruğunda değil! Halihazırda onaylanmış veya reddedilmiş olabilir. ", - "enabling-help": "Bu özelliği aktive etmek için, şu bölüme gidin: Settings → Post → Post Queue ve şunu aktve edin: Post Queue.", + "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": "İleti Kuyruğuna Geri Dön", + "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.", "user": "Kullanıcı", "when": "Zaman", "category": "Kategori", diff --git a/public/language/uk/post-queue.json b/public/language/uk/post-queue.json index ba9df75963..34399ad65f 100644 --- a/public/language/uk/post-queue.json +++ b/public/language/uk/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Черга Постів", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "Користувач", "when": "When", "category": "Категорія", diff --git a/public/language/vi/post-queue.json b/public/language/vi/post-queue.json index 31beec5aaf..2218cc8401 100644 --- a/public/language/vi/post-queue.json +++ b/public/language/vi/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "Xếp Hàng Bài Đăng", "no-queued-posts": "Không có bài trong xếp hàng đăng bài", "no-single-post": "Chủ đề hoặc bài đăng bạn đang tìm kiếm không còn xếp hàng. Có thể đã phê duyệt hoặc xóa rồi.", - "enabling-help": "Để bật tính năng này, hãy đi tới Cài Đặt → Bài Đăng → Hàng Đợi Đăng và bật Hàng Đợi Đăng.", + "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": "Trở về Hàng Đợi Đăng", + "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.", "user": "Người dùng", "when": "When", "category": "Chuyên mục", diff --git a/public/language/zh-CN/post-queue.json b/public/language/zh-CN/post-queue.json index 865de66267..ad4df11d34 100644 --- a/public/language/zh-CN/post-queue.json +++ b/public/language/zh-CN/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "发布队列", "no-queued-posts": "发帖队列中没有帖子。", "no-single-post": "您正在查看的主题或帖子已经不在队列中。它可能已经被批准或删除。", - "enabling-help": "要启用此特性,跳转到设置→帖子→发帖队列然后启用发帖队列。", + "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.", "user": "用户", "when": "当", "category": "版块", diff --git a/public/language/zh-TW/post-queue.json b/public/language/zh-TW/post-queue.json index d1d3bff1e5..f18668f641 100644 --- a/public/language/zh-TW/post-queue.json +++ b/public/language/zh-TW/post-queue.json @@ -3,8 +3,10 @@ "post-queue": "貼文隊列", "no-queued-posts": "There are no posts in the post queue.", "no-single-post": "The topic or post you are looking for is no longer in the queue. It has likely been approved or deleted already.", - "enabling-help": "To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "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": "Back to Post Queue", + "public-intro": "If you have any queued posts, they will be shown here.", + "public-description": "This forum is configured to automatically queue posts from new accounts, pending moderator approval.
    If you have queued posts awaiting approval, you will be able to see them here.", "user": "使用者", "when": "When", "category": "版面", From 63ba498647e8092f9f1bc4ae91332be09732264e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 17 Nov 2023 11:45:02 -0500 Subject: [PATCH 102/201] fix: update post-queue template for #12171 --- src/views/post-queue.tpl | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/views/post-queue.tpl b/src/views/post-queue.tpl index 1226becd1a..e6b2233d17 100644 --- a/src/views/post-queue.tpl +++ b/src/views/post-queue.tpl @@ -1,3 +1,17 @@ +{{{ 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) }}}
    @@ -28,18 +42,19 @@
    - {{{ if (!posts.length && isAdmin) }}} - {{{ if !singlePost }}} -
    -

    [[post-queue:no-queued-posts]]

    - {{{ if !enabled }}}

    [[post-queue:enabling-help, {config.relative_path}/admin/settings/post#post-queue]]

    {{{ end }}} -
    - {{{ else }}} -
    -

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

    - [[post-queue:back-to-list]] -
    - {{{ end }}} + {{{ if !posts.length }}} + {{{ if !singlePost }}} +
    + [[post-queue:no-queued-posts]] +
    + {{{ else }}} +
    +

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

    + +
    + {{{ end }}} {{{ end }}} {{{ each posts }}} From 41bdc9e8fddc4605cc0ee457c52206a272c7d1cc Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 17 Nov 2023 11:46:45 -0500 Subject: [PATCH 103/201] fix: update design of post-queue --- src/views/post-queue.tpl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/views/post-queue.tpl b/src/views/post-queue.tpl index e6b2233d17..69b4f759b6 100644 --- a/src/views/post-queue.tpl +++ b/src/views/post-queue.tpl @@ -44,8 +44,13 @@
    {{{ if !posts.length }}} {{{ if !singlePost }}} -
    - [[post-queue:no-queued-posts]] +
    +
    +
    + +
    + [[post-queue:no-queued-posts]] +
    {{{ else }}}
    From 89a1134cae9b1e6825c182171a39705e2d2a8acd Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 17 Nov 2023 11:48:45 -0500 Subject: [PATCH 104/201] fix: #12171, bump harmony --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 532f528c40..e982cf90f6 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.98", + "nodebb-theme-harmony": "1.1.99", "nodebb-theme-lavender": "7.1.5", "nodebb-theme-peace": "2.1.25", "nodebb-theme-persona": "13.2.47", From 3d971294c4662fe7a1b174a016709c4bc0d5331f Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 18 Nov 2023 09:18:23 +0000 Subject: [PATCH 105/201] Latest translations and fallbacks --- public/language/bg/post-queue.json | 6 +++--- public/language/it/post-queue.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/language/bg/post-queue.json b/public/language/bg/post-queue.json index fa204a97dd..de0b0f22a4 100644 --- a/public/language/bg/post-queue.json +++ b/public/language/bg/post-queue.json @@ -3,10 +3,10 @@ "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": "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/it/post-queue.json b/public/language/it/post-queue.json index f677e57e1c..6e9deaa491 100644 --- a/public/language/it/post-queue.json +++ b/public/language/it/post-queue.json @@ -3,10 +3,10 @@ "post-queue": "Coda post", "no-queued-posts": "Non ci sono post nella coda dei post.", "no-single-post": "La discussione o il post che si sta cercando non è più in coda. Probabilmente è già stato approvato o cancellato.", - "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 coda della posta è attualmente disabilitata. Per abilitare questa funzione, vai in Impostazioni → Post → Coda post e abilita Coda post.", "back-to-list": "Torna alla coda dei post", - "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": "Se hai post in coda, saranno mostrati qui.", + "public-description": "Questo forum è configurato per mettere in coda automaticamente i post dei nuovi account, in attesa dell'approvazione del moderatore.
    Se hai dei post in coda in attesa di approvazione, potrai vederli qui.", "user": "Utente", "when": "Quando", "category": "Categoria", From b69816935dd7f1840576e6b4679f41f96e2bcdfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 18 Nov 2023 13:21:50 -0500 Subject: [PATCH 106/201] 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 532f528c40..69df44b981 100644 --- a/install/package.json +++ b/install/package.json @@ -94,7 +94,7 @@ "nconf": "0.12.1", "nodebb-plugin-2factor": "7.4.0", "nodebb-plugin-composer-default": "10.2.26", - "nodebb-plugin-dbsearch": "6.2.2", + "nodebb-plugin-dbsearch": "6.2.3", "nodebb-plugin-emoji": "5.1.13", "nodebb-plugin-emoji-android": "4.0.0", "nodebb-plugin-markdown": "12.2.5", From 64fe1278cfa83fd22968cd1b8316b0474654cb03 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sat, 18 Nov 2023 18:22:22 +0000 Subject: [PATCH 107/201] chore(i18n): fallback strings for new resources: nodebb.post-queue --- public/language/de/post-queue.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/language/de/post-queue.json b/public/language/de/post-queue.json index c24f8b6ef4..2ace5c3d6f 100644 --- a/public/language/de/post-queue.json +++ b/public/language/de/post-queue.json @@ -3,10 +3,10 @@ "post-queue": "Beitragswarteschlange", "no-queued-posts": "Es sind keine Beiträge in der Beitragswarteschlange", "no-single-post": "Dieses Thema oder dieser Beitrag ist nicht mehr in der Warteschlange. Er wurde wahrscheinlich angenommen oder gelöscht.", - "enabling-help": "The post queue is currently disabled. To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "enabling-help": "Die Beitragswarteschlange ist derzeit deaktiviert. Um diese Funktion zu aktivieren, gehe zu Einstellungen → Post → Beitragswarteschlange und aktiviere Beitragswarteschlange.", "back-to-list": "Zurück zur Beitragswarteschlange", - "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": "Wenn sich Beiträge in der Warteschlange befinden, werden sie hier angezeigt.", + "public-description": "Dieses Forum ist so konfiguriert, dass Beiträge von neuen Konten automatisch in die Warteschlange gestellt werden, bis sie von einem Moderator genehmigt werden.
    Wenn du Beiträge in der Warteschlange hast, die auf die Genehmigung warten, kannst du sie hier sehen.", "user": "Benutzer", "when": "Wann", "category": "Kategorie", From 85936a597c895637237e94c2df57491c78f46745 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Nov 2023 13:24:05 -0500 Subject: [PATCH 108/201] fix(deps): update dependency mongodb to v6.3.0 (#12170) 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 95ec4cdcec..b101826745 100644 --- a/install/package.json +++ b/install/package.json @@ -87,7 +87,7 @@ "lru-cache": "10.0.2", "mime": "3.0.0", "mkdirp": "3.0.1", - "mongodb": "6.2.0", + "mongodb": "6.3.0", "morgan": "1.10.0", "mousetrap": "1.6.5", "multiparty": "4.2.3", From b4a41af91bfb791bb11e3ca689714234697233e1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Nov 2023 13:24:15 -0500 Subject: [PATCH 109/201] fix(deps): update dependency ace-builds to v1.31.2 (#12168) 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 b101826745..65f8079a12 100644 --- a/install/package.json +++ b/install/package.json @@ -34,7 +34,7 @@ "@fortawesome/fontawesome-free": "6.4.2", "@isaacs/ttlcache": "1.4.1", "@popperjs/core": "2.11.8", - "ace-builds": "1.31.1", + "ace-builds": "1.31.2", "archiver": "6.0.1", "async": "3.2.5", "autoprefixer": "10.4.16", From d6a92d4c2d34173d3d68dd3269ca23816f85c7d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Nov 2023 14:20:31 -0500 Subject: [PATCH 110/201] chore(deps): update commitlint monorepo to v18.4.2 (#12169) 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 65f8079a12..1c50d0b31d 100644 --- a/install/package.json +++ b/install/package.json @@ -154,8 +154,8 @@ }, "devDependencies": { "@apidevtools/swagger-parser": "10.1.0", - "@commitlint/cli": "18.4.1", - "@commitlint/config-angular": "18.4.0", + "@commitlint/cli": "18.4.2", + "@commitlint/config-angular": "18.4.2", "coveralls": "3.1.1", "eslint": "8.53.0", "eslint-config-nodebb": "0.2.1", From b627545359e7bce021d4cd29cbb94f9fc09a3044 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Nov 2023 14:20:43 -0500 Subject: [PATCH 111/201] chore(deps): update dependency eslint to v8.54.0 (#12172) 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 1c50d0b31d..f42a6268ba 100644 --- a/install/package.json +++ b/install/package.json @@ -157,7 +157,7 @@ "@commitlint/cli": "18.4.2", "@commitlint/config-angular": "18.4.2", "coveralls": "3.1.1", - "eslint": "8.53.0", + "eslint": "8.54.0", "eslint-config-nodebb": "0.2.1", "eslint-plugin-import": "2.29.0", "grunt": "1.6.1", From 548b3342954728a34d1caeff829a111e94502e32 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Sun, 19 Nov 2023 09:18:21 +0000 Subject: [PATCH 112/201] Latest translations and fallbacks --- public/language/de/social.json | 4 +-- .../language/tr/admin/appearance/skins.json | 26 +++++++++---------- public/language/tr/error.json | 2 +- public/language/tr/flags.json | 6 ++--- public/language/tr/post-queue.json | 4 +-- public/language/tr/user.json | 18 ++++++------- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/public/language/de/social.json b/public/language/de/social.json index a93f282662..0e8ce251b3 100644 --- a/public/language/de/social.json +++ b/public/language/de/social.json @@ -7,6 +7,6 @@ "sign-up-with-google": "Mit Google registrieren", "log-in-with-facebook": "Mit Facebook anmelden", "continue-with-facebook": "Mit Facebook fortsetzen", - "sign-in-with-linkedin": "Sign in with LinkedIn", - "sign-up-with-linkedin": "Sign up with LinkedIn" + "sign-in-with-linkedin": "Mit LinkedIn anmelden", + "sign-up-with-linkedin": "Mit LinkedIn registrieren" } \ No newline at end of file diff --git a/public/language/tr/admin/appearance/skins.json b/public/language/tr/admin/appearance/skins.json index ec988e7bc3..f0505e8066 100644 --- a/public/language/tr/admin/appearance/skins.json +++ b/public/language/tr/admin/appearance/skins.json @@ -1,18 +1,18 @@ { - "skins": "Skins", + "skins": "Arayüzler", "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", + "custom-skins": "Özel Arayüz", + "add-skin": "Arayüz Ekle", + "save-custom-skins": "Özel Arayüzleri Kaydet", + "save-custom-skins-success": "Özel arayüzler başarıyla kaydedildi", + "custom-skin-name": "Özel Arayüz Adı", + "custom-skin-variables": "Özel Arayüz Değişkenleri", "loading": "Görünümler yükleniyor...", "homepage": "Anasayfa", - "select-skin": "Görünüm Seç", - "revert-skin": "Revert Skin", - "current-skin": "Mevcut Görünüm", - "skin-updated": "Görünüm Güncellendi", - "applied-success": "%1 isimli görünüm başarıyla uygulandı", - "revert-success": "Görünüm temel renkleri geri döndürüldü" + "select-skin": "Arayüz Seç", + "revert-skin": "Arayüzü eski haline döndür", + "current-skin": "Mevcut Arayüz", + "skin-updated": "Arayüz Güncellendi", + "applied-success": "%1 adlı arayüz başarıyla uygulandı", + "revert-success": "Arayüz temel renkleri eski haline döndürüldü" } \ No newline at end of file diff --git a/public/language/tr/error.json b/public/language/tr/error.json index e00d64067a..166fb26420 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -185,7 +185,7 @@ "post-flagged-too-many-times": "Bu ileti başkaları tarafından halihazırda şikayet edilmiş.", "user-flagged-too-many-times": "Bu kullanıcı başkaları tarafından halihazırda şikayet edilmiş.", "cant-flag-privileged": "Yöneticilerin profillerini veya içeriklerini bayraklayamazsınız.", - "cant-locate-flag-report": "Cannot locate flag report", + "cant-locate-flag-report": "Şikayet Raporu bulunamadı", "self-vote": "Kendi iletinize oy veremezsiniz", "too-many-upvotes-today": "Bir günde sadece %1 artı oy verebilirsiniz", "too-many-upvotes-today-user": "Bir kullanıcıya bir günde sadece %1 artı oy verebilirsiniz", diff --git a/public/language/tr/flags.json b/public/language/tr/flags.json index 6d7515dc0d..c5ca32a7ed 100644 --- a/public/language/tr/flags.json +++ b/public/language/tr/flags.json @@ -1,6 +1,6 @@ { "state": "Durum", - "report": "Report", + "report": "Rapor", "reports": "Raporlar", "first-reported": "İlk rapor tarihi", "no-flags": "Yaşasın! Hiçbir şikayet bulunamadı.", @@ -9,8 +9,8 @@ "update": "Güncelle", "updated": "Güncellendi", "resolved": "Çözüldü", - "report-added": "Added", - "report-rescinded": "Rescinded", + "report-added": "Eklendi", + "report-rescinded": "İptal Edildi", "target-purged": "Şikayet edilen içerik temizlendi ve artık mevcut değil.", "target-aboutme-empty": "Bu kullanıcı için "Hakkımda" bölümü yok.", diff --git a/public/language/tr/post-queue.json b/public/language/tr/post-queue.json index bbb0eeadc0..fe7955c35a 100644 --- a/public/language/tr/post-queue.json +++ b/public/language/tr/post-queue.json @@ -3,9 +3,9 @@ "post-queue": "İleti Kuyruğu", "no-queued-posts": "İleti kuyruğunda yeni ileti mevcut değil! ", "no-single-post": "Aradığınız ileti veya başlık artık ileti kuyruğunda değil! Halihazırda onaylanmış veya reddedilmiş olabilir. ", - "enabling-help": "The post queue is currently disabled. To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "enabling-help": "İleti Kuyruğu şu an etkinsizdir. Bu ayarı etkinleştirmek için şuraya gidin Ayarlar → İleti → İleti Kuyruğu ve İleti Kuyruğunu aktive edin.", "back-to-list": "İleti Kuyruğuna Geri Dön", - "public-intro": "If you have any queued posts, they will be shown here.", + "public-intro": "Onay sırasına alınmış iletiler varsa, burada gösterilecektir.", "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.", "user": "Kullanıcı", "when": "Zaman", diff --git a/public/language/tr/user.json b/public/language/tr/user.json index 25e1d52961..d488b5a8e1 100644 --- a/public/language/tr/user.json +++ b/public/language/tr/user.json @@ -45,7 +45,7 @@ "following": "Takip Edilenler", "blocks": "Engellenenler", "blocked-users": "Engellenen Kullanıcılar", - "block-toggle": "Blokta Geçiş Yap", + "block-toggle": "Blok Ayarını Değiştir", "block-user": "Kullanıcıyı Engelle", "unblock-user": "Kullanıcı Engelini Kaldır", "aboutme": "Hakkımda", @@ -62,8 +62,8 @@ "change-picture": "Fotoğrafı Değiştir", "change-username": "Kullanıcı Adı Değiştir", "change-email": "E-posta Değiştir", - "email-updated": "Email Updated", - "email-same-as-password": "Lütfen devam etmek için şu anki şifrenizi girin – Tekrar e-posta adresinizi girdiniz", + "email-updated": "E-posta Adresi Güncellendi", + "email-same-as-password": "Lütfen devam etmek için şu anki şifrenizi girin – tekrar yeni e-posta adresinizi girdiniz", "edit": "Düzenle", "edit-profile": "Profil Düzenle", "default-picture": "Varsayılan ikon", @@ -119,8 +119,8 @@ "paginate-description": "Sonsuz yükleme yerine konu ve iletileri sayfalara böl", "topics-per-page": "Sayfa başına başlık sayısı", "posts-per-page": "Sayfa başına ileti sayısı", - "category-topic-sort": "Category topic sort", - "topic-post-sort": "Topic post sort", + "category-topic-sort": "Kategori Başlık sıralaması", + "topic-post-sort": "Başlık iletileri sıralaması", "max-items-per-page": "Maksimum %1", "acp-language": "Yönetici Sayfası Dili", "notifications": "Bildirimler", @@ -142,9 +142,9 @@ "grouptitle": "Grup Unvanları", "group-order-help": "Bir grup seçin ve unvanları sıralamak için yön tuşlarını kullanın", "no-group-title": "Grup unvanı yok", - "select-skin": "Bir tema seçin", + "select-skin": "Bir arayüz seçin", "default": "Varsayılan (%1)", - "no-skin": "No Skin", + "no-skin": "Arayüz Yok", "select-homepage": "Bir \"Anasayfa\" seçin", "homepage": "Anasayfa", "homepage-description": "Anasayfa olarak kullanacağınız sayfayı seçin veya \"Hiçbiri\" diyerek varsayılan sayfayı kullanın.", @@ -197,9 +197,9 @@ "consent.right-to-data-portability-description": "Sizden ve hesabınız hakkında toplanan verilere makine tarafından okunabilir bir veri talep edebilirsiniz. Aşağıdaki uygun düğmeyi tıklayarak bunu yapabilirsiniz.", "consent.export-profile": "Profili Dışa Aktar (.json)", "consent.export-profile-success": "Profil aktarılmak üzere hazırlanıyor, işlem tamamlandığında bildirim alacaksınız!", - "consent.export-uploads": "Karşıya Yüklenmiş İçeriği Dışarı Aktar (.zip)", + "consent.export-uploads": "Siteye Yüklenmiş İçeriği Dışarı Aktar (.zip)", "consent.export-uploads-success": "Yüklemeler aktarılmak üzere hazırlanıyor, işlem tamamlandığında bildirim alacaksınız!", - "consent.export-posts": "Gönderileri Dışa Aktar (.csv)", + "consent.export-posts": "İletileri Dışa Aktar (.csv)", "consent.export-posts-success": "İletiler aktarılmak üzere hazırlanıyor, işlem tamamlandığında bildirim alacaksınız!", "emailUpdate.intro": "Lütfen e-posta adresinizi aşağıya girin. Bu forum, e-posta adresinizi planlanmış özet ve bildirimler ile parolanın kaybolması durumunda hesap kurtarma için kullanır.", "emailUpdate.optional": "Bu bölüm tercihe bağlıdır. Bir e-posta adresi girmek zorunda değilsiniz, fakat onaylanmış bir e-posta adresi olmadan hesabınızı veya girişinizi e-posta adresiniz ile kurtaramazsınız.", From c4b4e79b286afe9b326494021f75f38cae5cd31b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 9 Nov 2023 15:18:55 -0500 Subject: [PATCH 113/201] refactor(socket.io): deprecate SocketModules.chats.getRaw in favour of api.chats.getRawMessage --- public/openapi/write.yaml | 2 ++ .../write/chats/roomId/messages/mid/raw.yaml | 35 +++++++++++++++++++ public/src/client/chats/messages.js | 2 +- src/api/chats.js | 15 ++++++++ src/controllers/write/chats.js | 4 +++ src/routes/write/chats.js | 2 ++ src/socket.io/modules.js | 19 +++++----- 7 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 public/openapi/write/chats/roomId/messages/mid/raw.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 91a736cd0f..75f7d18e20 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -200,6 +200,8 @@ paths: $ref: 'write/chats/roomId/messages/mid.yaml' /chats/{roomId}/messages/{mid}/pin: $ref: 'write/chats/roomId/messages/mid/pin.yaml' + /chats/{roomId}/messages/{mid}/raw: + $ref: 'write/chats/roomId/messages/mid/raw.yaml' /flags/: $ref: 'write/flags.yaml' /flags/{flagId}: diff --git a/public/openapi/write/chats/roomId/messages/mid/raw.yaml b/public/openapi/write/chats/roomId/messages/mid/raw.yaml new file mode 100644 index 0000000000..3ee611672b --- /dev/null +++ b/public/openapi/write/chats/roomId/messages/mid/raw.yaml @@ -0,0 +1,35 @@ +get: + tags: + - chats + summary: get raw message content + description: This operation retrieves a message's raw markdown (or otherwise) content. + parameters: + - in: path + name: roomId + schema: + type: string + required: true + description: a valid chat room id + example: 1 + - in: path + name: mid + schema: + type: string + required: true + description: a valid chat message id + example: 1 + responses: + '200': + description: Chat message raw content retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + content: + type: string \ No newline at end of file diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index 2322ed8955..f1339c7562 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -196,7 +196,7 @@ define('forum/chats/messages', [ }; messages.prepEdit = async function (msgEl, mid, roomId) { - const raw = await socket.emit('modules.chats.getRaw', { mid: mid, roomId: roomId }); + const { content: raw } = await api.get(`/chats/${roomId}/messages/${mid}/raw`); const editEl = await app.parseAndTranslate('partials/chats/edit-message', { rawContent: raw, }); diff --git a/src/api/chats.js b/src/api/chats.js index 2ea2b26760..5afe28b38c 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -277,6 +277,21 @@ chatsAPI.getMessage = async (caller, { mid, roomId }) => { return messages.pop(); }; +chatsAPI.getRawMessage = async (caller, { mid, roomId }) => { + const [isAdmin, canViewMessage, inRoom] = await Promise.all([ + user.isAdministrator(caller.uid), + messaging.canViewMessage(mid, roomId, caller.uid), + messaging.isUserInRoom(caller.uid, roomId), + ]); + + if (!isAdmin && (!inRoom || !canViewMessage)) { + throw new Error('[[error:not-allowed]]'); + } + + const content = await messaging.getMessageField(mid, 'content'); + return { content }; +}; + chatsAPI.editMessage = async (caller, { mid, roomId, message }) => { await messaging.canEdit(mid, caller.uid); await messaging.editMessage(caller.uid, mid, roomId, message); diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index 5dd86be325..1932e32d99 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -124,6 +124,10 @@ Chats.messages.get = async (req, res) => { helpers.formatApiResponse(200, res, await api.chats.getMessage(req, { mid, roomId })); }; +Chats.messages.getRaw = async (req, res) => { + helpers.formatApiResponse(200, res, await api.chats.getRawMessage(req, { ...req.params })); +}; + Chats.messages.edit = async (req, res) => { const { mid, roomId } = req.params; const { message } = req.body; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 8bcaef83b4..336e90b043 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -32,6 +32,8 @@ module.exports = function () { setupApiRoute(router, 'post', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.restore); setupApiRoute(router, 'delete', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.delete); + setupApiRoute(router, 'get', '/:roomId/messages/:mid/raw', [...middlewares, middleware.assert.room], controllers.write.chats.messages.getRaw); + setupApiRoute(router, 'put', '/:roomId/messages/:mid/pin', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.pin); setupApiRoute(router, 'delete', '/:roomId/messages/:mid/pin', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.unpin); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index c58c137961..cd69031340 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -11,6 +11,9 @@ const plugins = require('../plugins'); const privileges = require('../privileges'); const groups = require('../groups'); +const api = require('../api'); +const sockets = require('.'); + const SocketModules = module.exports; SocketModules.chats = {}; @@ -19,21 +22,19 @@ SocketModules.settings = {}; /* Chat */ SocketModules.chats.getRaw = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/messages/:mid/raw'); + if (!data || !data.hasOwnProperty('mid')) { throw new Error('[[error:invalid-data]]'); } const roomId = await Messaging.getMessageField(data.mid, 'roomId'); - const [isAdmin, canViewMessage, inRoom] = await Promise.all([ - user.isAdministrator(socket.uid), - Messaging.canViewMessage(data.mid, roomId, socket.uid), - Messaging.isUserInRoom(socket.uid, roomId), - ]); - if (!isAdmin && (!inRoom || !canViewMessage)) { - throw new Error('[[error:not-allowed]]'); - } + const { content } = await api.chats.getRawMessage(socket, { + mid: data.mid, + roomId, + }); - return await Messaging.getMessageField(data.mid, 'content'); + return content; }; SocketModules.chats.isDnD = async function (socket, uid) { From eebea4df2e23211843b4c2f9159c23b3d6fe9660 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 13 Nov 2023 10:12:04 -0500 Subject: [PATCH 114/201] refactor(socket.io): deprecate SocketModules.chats.isDnD in favour of api.users.getStatus --- public/openapi/write.yaml | 4 +++ public/openapi/write/users/uid/status.yaml | 28 +++++++++++++++++++ .../write/users/uid/status/status.yaml | 24 ++++++++++++++++ public/src/modules/chat.js | 10 +++---- src/api/users.js | 5 ++++ src/controllers/write/users.js | 11 ++++++++ src/routes/write/users.js | 3 ++ src/socket.io/modules.js | 4 ++- 8 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 public/openapi/write/users/uid/status.yaml create mode 100644 public/openapi/write/users/uid/status/status.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 75f7d18e20..385a75bb75 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -64,6 +64,10 @@ paths: $ref: 'write/users/uid/content.yaml' /users/{uid}/account: $ref: 'write/users/uid/account.yaml' + /users/{uid}/status: + $ref: 'write/users/uid/status.yaml' + /users/{uid}/status/{status}: + $ref: 'write/users/uid/status/status.yaml' /users/{uid}/settings: $ref: 'write/users/uid/settings.yaml' /users/{uid}/password: diff --git a/public/openapi/write/users/uid/status.yaml b/public/openapi/write/users/uid/status.yaml new file mode 100644 index 0000000000..6cd5bdd5fd --- /dev/null +++ b/public/openapi/write/users/uid/status.yaml @@ -0,0 +1,28 @@ +get: + tags: + - users + summary: get user status + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user whose status to check + example: 2 + responses: + '200': + description: successfully retrieved user status + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + status: + type: string + enum: ['online', 'offline', 'dnd', 'away'] \ No newline at end of file diff --git a/public/openapi/write/users/uid/status/status.yaml b/public/openapi/write/users/uid/status/status.yaml new file mode 100644 index 0000000000..fa216c6dbe --- /dev/null +++ b/public/openapi/write/users/uid/status/status.yaml @@ -0,0 +1,24 @@ +head: + tags: + - users + summary: verify user status + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user whose status to check + example: 2 + - in: path + name: status + schema: + type: string + required: true + description: status of the user to confirm + example: 'online' + responses: + '200': + description: user status is the value in path + '404': + description: user status is not the value in path \ No newline at end of file diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index de090c7365..9802c7ae3f 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -64,11 +64,9 @@ define('chat', [ if (parseInt(touid, 10) === parseInt(app.user.uid, 10)) { return alerts.error('[[error:cant-chat-with-yourself]]'); } - socket.emit('modules.chats.isDnD', touid, function (err, isDnD) { - if (err) { - return alerts.error(err); - } - if (!isDnD) { + + api.get(`/users/${touid}/status`).then(({ status }) => { + if (status !== 'dnd') { return createChat(); } @@ -77,7 +75,7 @@ define('chat', [ createChat(); } }); - }); + }).catch(alerts.error); }; module.loadChatsDropdown = function (chatsListEl) { diff --git a/src/api/users.js b/src/api/users.js index f7c4edc2d2..d9eacb2cea 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -142,6 +142,11 @@ usersAPI.updateSettings = async function (caller, data) { return await user.saveSettings(data.uid, payload); }; +usersAPI.getStatus = async (caller, { uid }) => { + const status = await db.getObjectField(`user:${uid}`, 'status'); + return { status }; +}; + usersAPI.changePassword = async function (caller, data) { await user.changePassword(caller.uid, Object.assign(data, { ip: caller.ip })); await events.log({ diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index 6dbcb1b01e..a977a1442a 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -66,6 +66,17 @@ Users.changePicture = async (req, res) => { helpers.formatApiResponse(200, res); }; +Users.getStatus = async (req, res) => { + helpers.formatApiResponse(200, res, await api.users.getStatus(req, { ...req.params })); +}; + +Users.checkStatus = async (req, res) => { + const { uid, status } = req.params; + const { status: current } = await api.users.getStatus(req, { uid }); + + helpers.formatApiResponse(current === status ? 200 : 404, res); +}; + Users.updateSettings = async (req, res) => { const settings = await api.users.updateSettings(req, { ...req.body, uid: req.params.uid }); helpers.formatApiResponse(200, res, settings); diff --git a/src/routes/write/users.js b/src/routes/write/users.js index 23d8d75ddd..633fae200b 100644 --- a/src/routes/write/users.js +++ b/src/routes/write/users.js @@ -26,6 +26,9 @@ function authenticatedRoutes() { setupApiRoute(router, 'delete', '/:uid/content', [...middlewares, middleware.assert.user], controllers.write.users.deleteContent); setupApiRoute(router, 'delete', '/:uid/account', [...middlewares, middleware.assert.user], controllers.write.users.deleteAccount); + setupApiRoute(router, 'get', '/:uid/status', [], controllers.write.users.getStatus); + setupApiRoute(router, 'head', '/:uid/status/:status', [], controllers.write.users.checkStatus); + setupApiRoute(router, 'put', '/:uid/settings', [...middlewares, middleware.checkRequired.bind(null, ['settings'])], controllers.write.users.updateSettings); setupApiRoute(router, 'put', '/:uid/password', [...middlewares, middleware.checkRequired.bind(null, ['newPassword']), middleware.assert.user], controllers.write.users.changePassword); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index cd69031340..fa4e93dbe6 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -38,7 +38,9 @@ SocketModules.chats.getRaw = async function (socket, data) { }; SocketModules.chats.isDnD = async function (socket, uid) { - const status = await db.getObjectField(`user:${uid}`, 'status'); + sockets.warnDeprecated(socket, 'GET /api/v3/users/:uid/status OR HEAD /api/v3/users/:uid/status/:status'); + + const { status } = await api.users.getStatus(socket, { uid }); return status === 'dnd'; }; From a4133500fe31e58c081e78ff00c8bfd1b08320eb Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 13 Nov 2023 15:35:46 -0500 Subject: [PATCH 115/201] refactor(socket.io): deprecate SocketModules.chats.canMessage and .markAllRead with no alternative. deprecate .getRecentChats in favour of api.chats.list --- public/openapi/write/chats.yaml | 16 ++++++++++++++-- public/src/client/chats/recent.js | 18 +++++++----------- public/src/modules/chat.js | 21 ++++++++++----------- src/api/chats.js | 13 ++++++++----- src/controllers/write/chats.js | 19 +++++++++++++++---- src/messaging/index.js | 2 +- src/socket.io/modules.js | 11 +++++++++-- test/messaging.js | 2 +- 8 files changed, 65 insertions(+), 37 deletions(-) diff --git a/public/openapi/write/chats.yaml b/public/openapi/write/chats.yaml index 519b9f6556..37204a9b5c 100644 --- a/public/openapi/write/chats.yaml +++ b/public/openapi/write/chats.yaml @@ -4,17 +4,29 @@ get: summary: list recent chat rooms description: This operation lists recently used chat rooms that the calling user is a part of parameters: + - in: query + name: start + schema: + type: number + description: > + The start index from which chat rooms will be displayed. + e.g. `start` of `10` with `perPage` of 10 would result in the 10th to 19th chat rooms being returned + example: 20 - in: query name: perPage schema: type: number - description: The number of chat rooms displayed per page + description: The number of chat rooms to be displayed per page + required: false example: 20 - in: query name: page schema: type: number - description: The page number + description: > + ***Deprecated*** — The page number. + + This parameter is supeceded by `start`, and will stop working in NodeBB v4. example: 1 responses: '200': diff --git a/public/src/client/chats/recent.js b/public/src/client/chats/recent.js index a4f6490f4b..02da563286 100644 --- a/public/src/client/chats/recent.js +++ b/public/src/client/chats/recent.js @@ -29,29 +29,25 @@ define('forum/chats/recent', ['alerts', 'api', 'chat'], function (alerts, api, c }); }; - function loadMoreRecentChats() { + async function loadMoreRecentChats() { const recentChats = $('[component="chat/recent"]'); if (recentChats.attr('loading')) { return; } recentChats.attr('loading', 1); - socket.emit('modules.chats.getRecentChats', { + app.get(`/chats`, { uid: ajaxify.data.uid, after: recentChats.attr('data-nextstart'), - }, function (err, data) { - if (err) { - return alerts.error(err); - } - - if (data && data.rooms.length) { - onRecentChatsLoaded(data, function () { + }).then(({ rooms, nextStart }) => { + if (rooms.length) { + onRecentChatsLoaded({ rooms, nextStart }, function () { recentChats.removeAttr('loading'); - recentChats.attr('data-nextstart', data.nextStart); + recentChats.attr('data-nextstart', nextStart); }); } else { recentChats.removeAttr('loading'); } - }); + }).catch(alerts.error); } function onRecentChatsLoaded(data, callback) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 9802c7ae3f..97b580b0b2 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -79,13 +79,10 @@ define('chat', [ }; module.loadChatsDropdown = function (chatsListEl) { - socket.emit('modules.chats.getRecentChats', { + api.get('/chats', { uid: app.user.uid, after: 0, - }, function (err, data) { - if (err) { - return alerts.error(err); - } + }).then((data) => { const rooms = data.rooms.map((room) => { if (room && room.teaser) { room.teaser.timeagoLong = $.timeago(new Date(parseInt(room.teaser.timestamp, 10))); @@ -123,15 +120,17 @@ define('chat', [ listEl.addEventListener('click', onMarkReadClicked); $('[component="chats/mark-all-read"]').off('click').on('click', async function () { - await socket.emit('modules.chats.markAllRead'); - if (ajaxify.data.template.chats) { - $('[component="chat/nav-wrapper"] [data-roomid]').each((i, el) => { + const chatEls = document.querySelectorAll('[component="chat/list"] [data-roomid]'); + await Promise.all(Array.prototype.map.call(chatEls, async (el) => { + const roomId = el.getAttribute('data-roomid'); + await api.del(`/chats/${roomId}/state`); + if (ajaxify.data.template.chats) { module.markChatElUnread($(el), false); - }); - } + } + })); }); }); - }); + }).catch(alerts.error); }; function onMarkReadClicked(e) { diff --git a/src/api/chats.js b/src/api/chats.js index 5afe28b38c..7e7c1f4c2f 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -1,6 +1,7 @@ 'use strict'; const validator = require('validator'); +const winston = require('winston'); const db = require('../database'); const user = require('../user'); @@ -32,12 +33,14 @@ async function rateLimitExceeded(caller, field) { return false; } -chatsAPI.list = async (caller, { page, perPage }) => { - const start = Math.max(0, page - 1) * perPage; - const stop = start + perPage; - const { rooms } = await messaging.getRecentChats(caller.uid, caller.uid, start, stop); +chatsAPI.list = async (caller, { uid, start, stop, page, perPage }) => { + 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; + stop = start + perPage - 1; + } - return { rooms }; + return await messaging.getRecentChats(caller.uid, uid || caller.uid, start, stop); }; chatsAPI.create = async function (caller, data) { diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index 1932e32d99..51a9fed6d0 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -6,11 +6,22 @@ const helpers = require('../helpers'); const Chats = module.exports; Chats.list = async (req, res) => { - const page = (isFinite(req.query.page) && parseInt(req.query.page, 10)) || 1; - const perPage = (isFinite(req.query.perPage) && parseInt(req.query.perPage, 10)) || 20; - const { rooms } = await api.chats.list(req, { page, perPage }); + let stop; + let { page, perPage, start, uid } = req.query; + ([page, perPage, start, uid] = [page, perPage, start, uid].map(value => isFinite(value) && parseInt(value, 10))); + page = page || 1; + perPage = perPage || 20; - helpers.formatApiResponse(200, res, { rooms }); + // start supercedes page + if (start) { + stop = start + perPage - 1; + } else { + start = Math.max(0, page - 1) * perPage; + stop = start + perPage - 1; + } + + const { rooms, nextStart } = await api.chats.list(req, { start, stop, uid }); + helpers.formatApiResponse(200, res, { rooms, nextStart }); }; Chats.create = async (req, res) => { diff --git a/src/messaging/index.js b/src/messaging/index.js index f91367bbfc..ccedd47e81 100644 --- a/src/messaging/index.js +++ b/src/messaging/index.js @@ -173,7 +173,7 @@ Messaging.getPublicRooms = async (callerUid, uid) => { Messaging.getRecentChats = async (callerUid, uid, start, stop) => { const ok = await canGet('filter:messaging.canGetRecentChats', callerUid, uid); if (!ok) { - return null; + throw new Error('[[error:no-privileges]]'); } const roomIds = await db.getSortedSetRevRange(`uid:${uid}:chat:rooms`, start, stop); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index fa4e93dbe6..705895c8b7 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -45,22 +45,29 @@ SocketModules.chats.isDnD = async function (socket, uid) { }; SocketModules.chats.canMessage = async function (socket, roomId) { + sockets.warnDeprecated(socket); + await Messaging.canMessageRoom(socket.uid, roomId); }; SocketModules.chats.markAllRead = async function (socket) { - // no v3 method ? + sockets.warnDeprecated(socket); + await Messaging.markAllRead(socket.uid); Messaging.pushUnreadCount(socket.uid); }; SocketModules.chats.getRecentChats = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/chats'); + if (!data || !utils.isNumber(data.after) || !utils.isNumber(data.uid)) { throw new Error('[[error:invalid-data]]'); } const start = parseInt(data.after, 10); const stop = start + 9; - return await Messaging.getRecentChats(socket.uid, data.uid, start, stop); + const { uid } = data; + + return api.chats.list(socket, { uid, start, stop }); }; SocketModules.chats.hasPrivateChat = async function (socket, uid) { diff --git a/test/messaging.js b/test/messaging.js index 826f2e8b3a..9ea17cf402 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -80,7 +80,7 @@ describe('Messaging Library', () => { meta.configs.chatMessageDelay = chatMessageDelay; }); - describe('.canMessage()', () => { + describe('.canMessageUser()', () => { it('should allow messages to be sent to an unrestricted user', (done) => { Messaging.canMessageUser(mocks.users.baz.uid, mocks.users.herp.uid, (err) => { assert.ifError(err); From 0d3c94e6bb6f0718b91cac3903d890d2d602afda Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 13 Nov 2023 16:10:40 -0500 Subject: [PATCH 116/201] refactor(socket.io): deprecate SocketModules.chats.hasPrivateChat in favour of api.users.getPrivateRoomId --- public/openapi/write.yaml | 2 ++ public/openapi/write/users/uid/chat.yaml | 27 ++++++++++++++++++++++++ public/src/client/account/header.js | 2 +- src/api/users.js | 10 +++++++++ src/controllers/write/users.js | 4 ++++ src/routes/write/users.js | 2 ++ src/socket.io/modules.js | 7 +++++- 7 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 public/openapi/write/users/uid/chat.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 385a75bb75..f8c37430af 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -68,6 +68,8 @@ paths: $ref: 'write/users/uid/status.yaml' /users/{uid}/status/{status}: $ref: 'write/users/uid/status/status.yaml' + /users/{uid}/chat: + $ref: 'write/users/uid/chat.yaml' /users/{uid}/settings: $ref: 'write/users/uid/settings.yaml' /users/{uid}/password: diff --git a/public/openapi/write/users/uid/chat.yaml b/public/openapi/write/users/uid/chat.yaml new file mode 100644 index 0000000000..8b95f6ffbe --- /dev/null +++ b/public/openapi/write/users/uid/chat.yaml @@ -0,0 +1,27 @@ +get: + tags: + - users + summary: get chat room + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user to query + example: 2 + responses: + '200': + description: successfully retrieved private chat room with user + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + roomId: + type: number \ No newline at end of file diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js index 338c5ac8cb..9aae8d6a8f 100644 --- a/public/src/client/account/header.js +++ b/public/src/client/account/header.js @@ -32,7 +32,7 @@ define('forum/account/header', [ components.get('account/unfollow').on('click', () => toggleFollow('unfollow')); components.get('account/chat').on('click', async function () { - const roomId = await socket.emit('modules.chats.hasPrivateChat', ajaxify.data.uid); + const { roomId } = await api.get(`/users/${ajaxify.data.uid}/chat`); const chat = await app.require('chat'); if (roomId) { chat.openChat(roomId); diff --git a/src/api/users.js b/src/api/users.js index d9eacb2cea..ea0ce2f6b2 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -11,6 +11,7 @@ const db = require('../database'); const user = require('../user'); const groups = require('../groups'); const meta = require('../meta'); +const messaging = require('../messaging'); const flags = require('../flags'); const privileges = require('../privileges'); const notifications = require('../notifications'); @@ -147,6 +148,15 @@ usersAPI.getStatus = async (caller, { uid }) => { return { status }; }; +usersAPI.getPrivateRoomId = async (caller, { uid }) => { + let roomId = await messaging.hasPrivateChat(caller.uid, uid); + roomId = parseInt(roomId, 10); + + return { + roomId: roomId > 0 ? roomId : null, + }; +}; + usersAPI.changePassword = async function (caller, data) { await user.changePassword(caller.uid, Object.assign(data, { ip: caller.ip })); await events.log({ diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index a977a1442a..715be0f48c 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -77,6 +77,10 @@ Users.checkStatus = async (req, res) => { helpers.formatApiResponse(current === status ? 200 : 404, res); }; +Users.getPrivateRoomId = async (req, res) => { + helpers.formatApiResponse(200, res, await api.users.getPrivateRoomId(req, { ...req.params })); +}; + Users.updateSettings = async (req, res) => { const settings = await api.users.updateSettings(req, { ...req.body, uid: req.params.uid }); helpers.formatApiResponse(200, res, settings); diff --git a/src/routes/write/users.js b/src/routes/write/users.js index 633fae200b..139ed483c3 100644 --- a/src/routes/write/users.js +++ b/src/routes/write/users.js @@ -29,6 +29,8 @@ function authenticatedRoutes() { setupApiRoute(router, 'get', '/:uid/status', [], controllers.write.users.getStatus); setupApiRoute(router, 'head', '/:uid/status/:status', [], controllers.write.users.checkStatus); + setupApiRoute(router, 'get', '/:uid/chat', [...middlewares], controllers.write.users.getPrivateRoomId); + setupApiRoute(router, 'put', '/:uid/settings', [...middlewares, middleware.checkRequired.bind(null, ['settings'])], controllers.write.users.updateSettings); setupApiRoute(router, 'put', '/:uid/password', [...middlewares, middleware.checkRequired.bind(null, ['newPassword']), middleware.assert.user], controllers.write.users.changePassword); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 705895c8b7..f1d8ff9ebe 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -71,10 +71,15 @@ SocketModules.chats.getRecentChats = async function (socket, data) { }; SocketModules.chats.hasPrivateChat = async function (socket, uid) { + sockets.warnDeprecated(socket, 'GET /api/v3/users/:uid/chat'); + if (socket.uid <= 0 || uid <= 0) { throw new Error('[[error:invalid-data]]'); } - return await Messaging.hasPrivateChat(socket.uid, uid); + + // despite the `has` prefix, this method actually did return the roomId. + const { roomId } = await api.users.getPrivateRoomId(socket, { uid }); + return roomId; }; SocketModules.chats.getIP = async function (socket, mid) { From 214989a8c14caadaf26eebf181050145f0bb9941 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 14 Nov 2023 11:53:53 -0500 Subject: [PATCH 117/201] refactor(socket.io): deprecate SocketModules.chats.getIP in favour of api.chats.getIpAddress --- public/openapi/write.yaml | 2 ++ .../write/chats/roomId/messages/mid/ip.yaml | 35 +++++++++++++++++++ public/src/client/chats.js | 2 +- src/api/chats.js | 10 ++++++ src/controllers/write/chats.js | 4 +++ src/routes/write/chats.js | 1 + src/socket.io/modules.js | 9 +++-- 7 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 public/openapi/write/chats/roomId/messages/mid/ip.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index f8c37430af..af700430fa 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -208,6 +208,8 @@ paths: $ref: 'write/chats/roomId/messages/mid/pin.yaml' /chats/{roomId}/messages/{mid}/raw: $ref: 'write/chats/roomId/messages/mid/raw.yaml' + /chats/{roomId}/messages/{mid}/ip: + $ref: 'write/chats/roomId/messages/mid/ip.yaml' /flags/: $ref: 'write/flags.yaml' /flags/{flagId}: diff --git a/public/openapi/write/chats/roomId/messages/mid/ip.yaml b/public/openapi/write/chats/roomId/messages/mid/ip.yaml new file mode 100644 index 0000000000..0d2a82cba9 --- /dev/null +++ b/public/openapi/write/chats/roomId/messages/mid/ip.yaml @@ -0,0 +1,35 @@ +get: + tags: + - chats + summary: get message ip address + description: This operation retrieves the ip address recorded when a message was saved + parameters: + - in: path + name: roomId + schema: + type: string + required: true + description: a valid chat room id + example: 1 + - in: path + name: mid + schema: + type: string + required: true + description: a valid chat message id + example: 5 + responses: + '200': + description: Chat message ip address retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + ip: + type: string \ No newline at end of file diff --git a/public/src/client/chats.js b/public/src/client/chats.js index bf16015d53..c45f14d6d7 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -236,7 +236,7 @@ define('forum/chats', [ return; } const mid = ipEl.parents('[data-mid]').attr('data-mid'); - ip = await socket.emit('modules.chats.getIP', mid); + ({ ip } = await api.get(`/chats/${ajaxify.data.roomId}/messages/${mid}/ip`)); ipEl.text(ip).attr('data-ip', ip); }); }; diff --git a/src/api/chats.js b/src/api/chats.js index 7e7c1f4c2f..da47892872 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -8,6 +8,7 @@ const user = require('../user'); const meta = require('../meta'); const messaging = require('../messaging'); const notifications = require('../notifications'); +const privileges = require('../privileges'); const plugins = require('../plugins'); const socketHelpers = require('../socket.io/helpers'); @@ -295,6 +296,15 @@ chatsAPI.getRawMessage = async (caller, { mid, roomId }) => { return { content }; }; +chatsAPI.getIpAddress = async (caller, { mid }) => { + const allowed = await privileges.global.can('view:users:info', caller.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + const ip = await messaging.getMessageField(mid, 'ip'); + return { ip }; +}; + chatsAPI.editMessage = async (caller, { mid, roomId, message }) => { await messaging.canEdit(mid, caller.uid); await messaging.editMessage(caller.uid, mid, roomId, message); diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index 51a9fed6d0..d962e7c1a1 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -139,6 +139,10 @@ Chats.messages.getRaw = async (req, res) => { helpers.formatApiResponse(200, res, await api.chats.getRawMessage(req, { ...req.params })); }; +Chats.messages.getIpAddress = async (req, res) => { + helpers.formatApiResponse(200, res, await api.chats.getIpAddress(req, { ...req.params })); +}; + Chats.messages.edit = async (req, res) => { const { mid, roomId } = req.params; const { message } = req.body; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 336e90b043..bc4326a945 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -33,6 +33,7 @@ module.exports = function () { setupApiRoute(router, 'delete', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.delete); setupApiRoute(router, 'get', '/:roomId/messages/:mid/raw', [...middlewares, middleware.assert.room], controllers.write.chats.messages.getRaw); + setupApiRoute(router, 'get', '/:roomId/messages/:mid/ip', [...middlewares, middleware.assert.room], controllers.write.chats.messages.getIpAddress); setupApiRoute(router, 'put', '/:roomId/messages/:mid/pin', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.pin); setupApiRoute(router, 'delete', '/:roomId/messages/:mid/pin', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.unpin); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index f1d8ff9ebe..a18f2d8586 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -83,11 +83,10 @@ SocketModules.chats.hasPrivateChat = async function (socket, uid) { }; SocketModules.chats.getIP = async function (socket, mid) { - const allowed = await privileges.global.can('view:users:info', socket.uid); - if (!allowed) { - throw new Error('[[error:no-privilege]]'); - } - return await Messaging.getMessageField(mid, 'ip'); + sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/messages/:mid/ip'); + + const { ip } = await api.chats.getIpAddress(socket, { mid }); + return ip; }; SocketModules.chats.getUnreadCount = async function (socket) { From 5eaffb422ca1679fe719a69f56bdb80f35e983c6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 14 Nov 2023 12:19:23 -0500 Subject: [PATCH 118/201] refactor(socket.io): deprecate SocketModules.chat.getUnreadCount in favour of api.chats.getUnread --- public/openapi/write.yaml | 2 ++ public/openapi/write/chats/unread.yaml | 24 ++++++++++++++++++++++++ public/src/client/header/chat.js | 6 +++--- src/api/chats.js | 5 +++++ src/controllers/write/chats.js | 3 +++ src/routes/write/chats.js | 2 ++ src/socket.io/modules.js | 5 ++++- 7 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 public/openapi/write/chats/unread.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index af700430fa..5054499dcd 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -192,6 +192,8 @@ paths: $ref: 'write/posts/pid/replies.yaml' /chats/: $ref: 'write/chats.yaml' + /chats/unread: + $ref: 'write/chats/unread.yaml' /chats/{roomId}: $ref: 'write/chats/roomId.yaml' /chats/{roomId}/state: diff --git a/public/openapi/write/chats/unread.yaml b/public/openapi/write/chats/unread.yaml new file mode 100644 index 0000000000..ee440665bf --- /dev/null +++ b/public/openapi/write/chats/unread.yaml @@ -0,0 +1,24 @@ +get: + tags: + - chats + summary: get unread count + description: > + This operation retrieves the calling user's count of unread chat rooms. + + Note that this API call is open-ended. + It currently only returns the unread count, but can be expanded upon in the future. + responses: + '200': + description: Count of unread chat rooms successfully retrieved. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + count: + type: number \ No newline at end of file diff --git a/public/src/client/header/chat.js b/public/src/client/header/chat.js index 44f9cf4a53..a087a45939 100644 --- a/public/src/client/header/chat.js +++ b/public/src/client/header/chat.js @@ -1,8 +1,8 @@ 'use strict'; define('forum/header/chat', [ - 'components', 'hooks', -], function (components, hooks) { + 'components', 'hooks', 'api', +], function (components, hooks, api) { const chat = {}; chat.prepareDOM = function () { @@ -44,7 +44,7 @@ define('forum/header/chat', [ chatPage.markChatPageElUnread(data); } - let count = await socket.emit('modules.chats.getUnreadCount', {}); + let { count } = await api.get('/chats/unread'); const chatIcon = components.get('chat/icon'); count = Math.max(0, count); chatIcon.toggleClass('fa-comment', count > 0) diff --git a/src/api/chats.js b/src/api/chats.js index da47892872..91e3dbd2b4 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -79,6 +79,11 @@ chatsAPI.create = async function (caller, data) { return await messaging.getRoomData(roomId); }; +chatsAPI.getUnread = async (caller) => { + const count = await messaging.getUnreadCount(caller.uid); + return { count }; +}; + chatsAPI.get = async (caller, { uid, roomId }) => await messaging.loadRoom(caller.uid, { uid, roomId }); chatsAPI.post = async (caller, data) => { diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index d962e7c1a1..87b1de3023 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -29,6 +29,9 @@ Chats.create = async (req, res) => { helpers.formatApiResponse(200, res, roomObj); }; +// currently only returns unread count, but open-ended for future additions if warranted. +Chats.getUnread = async (req, res) => helpers.formatApiResponse(200, res, await api.chats.getUnread(req)); + Chats.exists = async (req, res) => { // yes, this is fine. Room existence is checked via middleware :) helpers.formatApiResponse(200, res); diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index bc4326a945..5a42bcdc51 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -13,6 +13,8 @@ module.exports = function () { setupApiRoute(router, 'get', '/', [...middlewares], controllers.write.chats.list); setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.create); + setupApiRoute(router, 'get', '/unread', [...middlewares], controllers.write.chats.getUnread); + setupApiRoute(router, 'head', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.exists); setupApiRoute(router, 'get', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.get); setupApiRoute(router, 'post', '/:roomId', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['message'])], controllers.write.chats.post); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index a18f2d8586..e9ad965cb8 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -90,7 +90,10 @@ SocketModules.chats.getIP = async function (socket, mid) { }; SocketModules.chats.getUnreadCount = async function (socket) { - return await Messaging.getUnreadCount(socket.uid); + sockets.warnDeprecated(socket, 'GET /api/v3/chats/unread'); + + const { count } = await api.chats.getUnread(socket); + return count; }; SocketModules.chats.enter = async function (socket, roomIds) { From 2de534fad86c70560487712491dc9e30dad5ac01 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Nov 2023 10:36:39 -0500 Subject: [PATCH 119/201] chore: update note at top of file --- src/socket.io/modules.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index e9ad965cb8..0636fc23d5 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -1,5 +1,10 @@ 'use strict'; +/** + * v4 note — all methods here are deprecated and can be removed except for: + * - SocketModules.chats.(enter|leave)(Public)? => related to socket.io rooms + */ + const _ = require('lodash'); const validator = require('validator'); @@ -8,7 +13,6 @@ const Messaging = require('../messaging'); const utils = require('../utils'); const user = require('../user'); const plugins = require('../plugins'); -const privileges = require('../privileges'); const groups = require('../groups'); const api = require('../api'); From 62b7dfd40056db716776b2767916c40767f2edcf Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Nov 2023 11:41:02 -0500 Subject: [PATCH 120/201] refactor(socket.io): deprecate SocketModules.sortPublicRooms in favour of api.chats.sortPublicRooms --- public/openapi/write.yaml | 2 ++ public/openapi/write/chats/sort.yaml | 33 ++++++++++++++++++++++++++++ public/src/client/chats.js | 2 +- src/api/chats.js | 16 ++++++++++++++ src/controllers/write/categories.js | 1 - src/controllers/write/chats.js | 7 ++++++ src/routes/write/chats.js | 1 + src/socket.io/modules.js | 12 +++++----- 8 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 public/openapi/write/chats/sort.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 5054499dcd..d08236d274 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -194,6 +194,8 @@ paths: $ref: 'write/chats.yaml' /chats/unread: $ref: 'write/chats/unread.yaml' + /chats/sort: + $ref: 'write/chats/sort.yaml' /chats/{roomId}: $ref: 'write/chats/roomId.yaml' /chats/{roomId}/state: diff --git a/public/openapi/write/chats/sort.yaml b/public/openapi/write/chats/sort.yaml new file mode 100644 index 0000000000..df52c4a605 --- /dev/null +++ b/public/openapi/write/chats/sort.yaml @@ -0,0 +1,33 @@ +put: + tags: + - chats + summary: sort public chat rooms + description: This operation sorts the publicly available chat rooms. This is a privileged function; only superadmins can call it. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + roomIds: + type: array + description: A list of room ids. + example: [1] + scores: + type: array + description: A list of sort orders associated with the passed-in `roomIds` + example: [0] + responses: + '200': + description: Public chat rooms successfully re-ordered. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} \ No newline at end of file diff --git a/public/src/client/chats.js b/public/src/client/chats.js index c45f14d6d7..222960945c 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -141,7 +141,7 @@ define('forum/chats', [ data.roomIds.push($(el).attr('data-roomid')); data.scores.push(idx); }); - await socket.emit('modules.chats.sortPublicRooms', data); + await api.put('/chats/sort', data); }, }); }); diff --git a/src/api/chats.js b/src/api/chats.js index 91e3dbd2b4..88b0a27d2c 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -84,6 +84,22 @@ chatsAPI.getUnread = async (caller) => { return { count }; }; +chatsAPI.sortPublicRooms = async (caller, { roomIds, scores }) => { + [roomIds, scores].forEach((arr) => { + if (!Array.isArray(arr) || !arr.every(value => isFinite(value))) { + throw new Error('[[error:invalid-data]]'); + } + }); + + const isAdmin = await user.isAdministrator(caller.uid); + if (!isAdmin) { + throw new Error('[[error:no-privileges]]'); + } + + await db.sortedSetAdd(`chat:rooms:public:order`, scores, roomIds); + require('../cache').del(`chat:rooms:public:order:all`); +}; + chatsAPI.get = async (caller, { uid, roomId }) => await messaging.loadRoom(caller.uid, { uid, roomId }); chatsAPI.post = async (caller, data) => { diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 80ee961fbf..bb4ec84090 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -68,7 +68,6 @@ Categories.setWatchState = async (req, res) => { } else if (Object.keys(categories.watchStates).includes(state)) { state = categories.watchStates[state]; // convert to integer for backend processing } else { - console.log('throwing', cid, uid, state); throw new Error('[[error:invalid-data]]'); } diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index 87b1de3023..86ab6012ee 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -32,6 +32,13 @@ Chats.create = async (req, res) => { // currently only returns unread count, but open-ended for future additions if warranted. Chats.getUnread = async (req, res) => helpers.formatApiResponse(200, res, await api.chats.getUnread(req)); +Chats.sortPublicRooms = async (req, res) => { + const { roomIds, scores } = req.body; + await api.chats.sortPublicRooms(req, { roomIds, scores }); + + helpers.formatApiResponse(200, res); +}; + Chats.exists = async (req, res) => { // yes, this is fine. Room existence is checked via middleware :) helpers.formatApiResponse(200, res); diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 5a42bcdc51..73ee54815a 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -14,6 +14,7 @@ module.exports = function () { setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.create); setupApiRoute(router, 'get', '/unread', [...middlewares], controllers.write.chats.getUnread); + setupApiRoute(router, 'put', '/sort', [...middlewares, middleware.checkRequired.bind(null, ['roomIds', 'scores'])], controllers.write.chats.sortPublicRooms); setupApiRoute(router, 'head', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.exists); setupApiRoute(router, 'get', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.get); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 0636fc23d5..9901b25c82 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -147,15 +147,13 @@ async function joinLeave(socket, roomIds, method, prefix = 'chat_room') { } SocketModules.chats.sortPublicRooms = async function (socket, data) { - if (!data || !Array.isArray(data.scores) || !Array.isArray(data.roomIds)) { + sockets.warnDeprecated(socket, 'PUT /api/v3/chats/sort'); + + if (!data) { throw new Error('[[error:invalid-data]]'); } - const isAdmin = await user.isAdministrator(socket.uid); - if (!isAdmin) { - throw new Error('[[error:no-privileges]]'); - } - await db.sortedSetAdd(`chat:rooms:public:order`, data.scores, data.roomIds); - require('../cache').del(`chat:rooms:public:order:all`); + + await api.chats.sortPublicRooms(socket, data); }; SocketModules.chats.searchMembers = async function (socket, data) { From 6e952263d140ea4c8153d3e4ff5195a2535552a3 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Nov 2023 14:16:08 -0500 Subject: [PATCH 121/201] refactor(socket.io): deprecated SocketModules.chats.searchMembers in favour of api.search.roomUsers --- public/openapi/write.yaml | 2 + .../write/search/chats/roomId/users.yaml | 44 +++++++++++++++++ public/src/client/chats/user-list.js | 7 +-- src/api/search.js | 49 +++++++++++++++++++ src/controllers/write/search.js | 5 ++ src/routes/write/search.js | 5 +- src/socket.io/modules.js | 47 +++--------------- 7 files changed, 111 insertions(+), 48 deletions(-) create mode 100644 public/openapi/write/search/chats/roomId/users.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index d08236d274..6e5904cbf2 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -226,6 +226,8 @@ paths: $ref: 'write/flags/flagId/notes/datetime.yaml' /search/categories: $ref: 'write/search/categories.yaml' + /search/chats/{roomId}/users: + $ref: 'write/search/chats/roomId/users.yaml' /admin/settings/{setting}: $ref: 'write/admin/settings/setting.yaml' /admin/analytics: diff --git a/public/openapi/write/search/chats/roomId/users.yaml b/public/openapi/write/search/chats/roomId/users.yaml new file mode 100644 index 0000000000..dd94a88d02 --- /dev/null +++ b/public/openapi/write/search/chats/roomId/users.yaml @@ -0,0 +1,44 @@ +get: + tags: + - search + summary: find room users by keyword + description: This operation returns a set of users in a chat room matching the keyword search. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: room ID to check + example: 1 + - in: query + name: 'query' + schema: + type: string + required: false + description: The keyword used in the category search + example: 'admin' + responses: + '200': + description: matching users successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + users: + type: array + items: + allOf: + - $ref: ../../../../components/schemas/UserObject.yaml#/UserObjectSlim + - type: object + properties: + isOwner: + type: boolean + canKick: + type: boolean \ No newline at end of file diff --git a/public/src/client/chats/user-list.js b/public/src/client/chats/user-list.js index a4bd77cf65..36c6267cea 100644 --- a/public/src/client/chats/user-list.js +++ b/public/src/client/chats/user-list.js @@ -72,11 +72,8 @@ define('forum/chats/user-list', ['api'], function (api) { userList.addSearchHandler = function (roomId, inputEl, callback) { inputEl.on('keyup', utils.debounce(async () => { - const username = inputEl.val(); - const data = await socket.emit('modules.chats.searchMembers', { - username: username, - roomId: roomId, - }); + const query = inputEl.val(); + const data = await api.get(`/search/chats/${roomId}/users`, { query }); callback(data); }, 200)); }; diff --git a/src/api/search.js b/src/api/search.js index ac77347c26..043c97176e 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -2,7 +2,9 @@ const _ = require('lodash'); +const user = require('../user'); const categories = require('../categories'); +const messaging = require('../messaging'); const privileges = require('../privileges'); const meta = require('../meta'); const plugins = require('../plugins'); @@ -103,3 +105,50 @@ async function loadCids(uid, parentCid) { await getCidsRecursive(pageCids); return resultCids; } + +searchApi.roomUsers = async (caller, { query, roomId }) => { + const [isAdmin, inRoom, isRoomOwner] = await Promise.all([ + user.isAdministrator(caller.uid), + messaging.isUserInRoom(caller.uid, roomId), + messaging.isRoomOwner(caller.uid, roomId), + ]); + + if (!isAdmin && !inRoom) { + throw new Error('[[error:no-privileges]]'); + } + + const results = await user.search({ + query, + paginate: false, + hardCap: -1, + uid: caller.uid, + }); + + const { users } = results; + const foundUids = users.map(user => user && user.uid); + const isUidInRoom = _.zipObject( + foundUids, + await messaging.isUsersInRoom(foundUids, roomId) + ); + + const roomUsers = users.filter(user => isUidInRoom[user.uid]); + const isOwners = await messaging.isRoomOwner(roomUsers.map(u => u.uid), roomId); + + roomUsers.forEach((user, index) => { + if (user) { + user.isOwner = isOwners[index]; + user.canKick = isRoomOwner && (parseInt(user.uid, 10) !== parseInt(caller.uid, 10)); + } + }); + + roomUsers.sort((a, b) => { + if (a.isOwner && !b.isOwner) { + return -1; + } else if (!a.isOwner && b.isOwner) { + return 1; + } + return 0; + }); + + return { users: roomUsers }; +}; diff --git a/src/controllers/write/search.js b/src/controllers/write/search.js index a6acd0a59a..cfdaad44a8 100644 --- a/src/controllers/write/search.js +++ b/src/controllers/write/search.js @@ -8,3 +8,8 @@ const Search = module.exports; Search.categories = async (req, res) => { helpers.formatApiResponse(200, res, await api.search.categories(req, req.query)); }; + +Search.roomUsers = async (req, res) => { + const { query } = req.query; + helpers.formatApiResponse(200, res, await api.search.roomUsers(req, { query, ...req.params })); +}; diff --git a/src/routes/write/search.js b/src/routes/write/search.js index 01b98cdeed..4d4108a05b 100644 --- a/src/routes/write/search.js +++ b/src/routes/write/search.js @@ -1,19 +1,20 @@ 'use strict'; const router = require('express').Router(); -// const middleware = require('../../middleware'); +const middleware = require('../../middleware'); const controllers = require('../../controllers'); const routeHelpers = require('../helpers'); const { setupApiRoute } = routeHelpers; module.exports = function () { - // const middlewares = []; + const middlewares = [middleware.ensureLoggedIn]; // maybe redirect to /search/posts? // setupApiRoute(router, 'post', '/', [...middlewares], controllers.write.search.TBD); setupApiRoute(router, 'get', '/categories', [], controllers.write.search.categories); + setupApiRoute(router, 'get', '/chats/:roomId/users', [...middlewares, middleware.checkRequired.bind(null, ['query']), middleware.canChat, middleware.assert.room], controllers.write.search.roomUsers); return router; }; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 9901b25c82..910713d9cf 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -157,51 +157,16 @@ SocketModules.chats.sortPublicRooms = async function (socket, data) { }; SocketModules.chats.searchMembers = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/search/chats/:roomId/users?query='); + if (!data || !data.roomId) { throw new Error('[[error:invalid-data]]'); } - const [isAdmin, inRoom, isRoomOwner] = await Promise.all([ - user.isAdministrator(socket.uid), - Messaging.isUserInRoom(socket.uid, data.roomId), - Messaging.isRoomOwner(socket.uid, data.roomId), - ]); - if (!isAdmin && !inRoom) { - throw new Error('[[error:no-privileges]]'); - } - - const results = await user.search({ - query: data.username, - paginate: false, - hardCap: -1, - }); - - const { users } = results; - const foundUids = users.map(user => user && user.uid); - const isUidInRoom = _.zipObject( - foundUids, - await Messaging.isUsersInRoom(foundUids, data.roomId) - ); - - const roomUsers = users.filter(user => isUidInRoom[user.uid]); - const isOwners = await Messaging.isRoomOwner(roomUsers.map(u => u.uid), data.roomId); - - roomUsers.forEach((user, index) => { - if (user) { - user.isOwner = isOwners[index]; - user.canKick = isRoomOwner && (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)); - } - }); - - roomUsers.sort((a, b) => { - if (a.isOwner && !b.isOwner) { - return -1; - } else if (!a.isOwner && b.isOwner) { - return 1; - } - return 0; - }); - return { users: roomUsers }; + // parameter renamed; backwards compatibility + data.query = data.username; + delete data.username; + return await api.search.roomUsers(socket, data); }; SocketModules.chats.toggleOwner = async (socket, data) => { From 932bd29200c91d3e36ae2aa334a2de48517d2a1f Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Nov 2023 14:44:41 -0500 Subject: [PATCH 122/201] feat: update Messaging.toggleOwner to optionally take a third `state` argument --- src/messaging/rooms.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 27b4e27f40..04c75e9e67 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -232,15 +232,24 @@ module.exports = function (Messaging) { return isArray ? result : result[0]; }; - Messaging.toggleOwner = async (uid, roomId) => { + Messaging.toggleOwner = async (uid, roomId, state = null) => { if (!(parseInt(uid, 10) > 0) || !roomId) { - return; + throw new Error('[[error:invalid-data]]'); } + const isOwner = await Messaging.isRoomOwner(uid, roomId); - if (isOwner) { - await db.sortedSetRemove(`chat:room:${roomId}:owners`, uid); + if (state !== null) { + if (state === isOwner) { + return false; + } } else { + state = !isOwner; + } + + if (state) { await db.sortedSetAdd(`chat:room:${roomId}:owners`, Date.now(), uid); + } else { + await db.sortedSetRemove(`chat:room:${roomId}:owners`, uid); } }; From b13c6ee431706fab1dc6140b7fb02ea766479fea Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 15 Nov 2023 15:06:42 -0500 Subject: [PATCH 123/201] refactor(socket.io): deprecated SocketModules.chats.toggleOwner in favour of api.chats.toggleOwner --- public/openapi/write.yaml | 2 + .../write/chats/roomId/owners/uid.yaml | 62 +++++++++++++++++++ public/src/client/chats/manage.js | 10 +-- src/api/chats.js | 14 +++++ src/controllers/write/chats.js | 6 ++ src/routes/write/chats.js | 3 + src/socket.io/modules.js | 13 +--- 7 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 public/openapi/write/chats/roomId/owners/uid.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 6e5904cbf2..819caf9e29 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -204,6 +204,8 @@ paths: $ref: 'write/chats/roomId/users.yaml' /chats/{roomId}/users/{uid}: $ref: 'write/chats/roomId/users/uid.yaml' + /chats/{roomId}/owners/{uid}: + $ref: 'write/chats/roomId/owners/uid.yaml' /chats/{roomId}/messages: $ref: 'write/chats/roomId/messages.yaml' /chats/{roomId}/messages/{mid}: diff --git a/public/openapi/write/chats/roomId/owners/uid.yaml b/public/openapi/write/chats/roomId/owners/uid.yaml new file mode 100644 index 0000000000..f09df7e122 --- /dev/null +++ b/public/openapi/write/chats/roomId/owners/uid.yaml @@ -0,0 +1,62 @@ +put: + tags: + - chats + summary: add owner to room + description: This operation adds a user as a room owner. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid chat room id + example: 1 + - in: path + name: uid + schema: + type: number + required: true + description: a valid user id + example: 4 + responses: + '200': + description: user successfully added as room owner. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: {} +delete: + tags: + - chats + summary: remove owner from room + description: This operation removes a user as a room owner. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid chat room id + example: 1 + - in: path + name: uid + schema: + type: number + required: true + description: a valid user id + example: 4 + responses: + '200': + description: user successfully removed as room owner. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: {} \ No newline at end of file diff --git a/public/src/client/chats/manage.js b/public/src/client/chats/manage.js index a69cafc4f4..fce6db3505 100644 --- a/public/src/client/chats/manage.js +++ b/public/src/client/chats/manage.js @@ -112,11 +112,11 @@ define('forum/chats/manage', [ function addToggleOwnerHandler(roomId, modal) { modal.on('click', '[data-action="toggleOwner"]', async function () { const uid = parseInt(this.getAttribute('data-uid'), 10); - const $this = $(this); - await socket.emit('modules.chats.toggleOwner', { roomId: roomId, uid: uid }); - $this.parents('[data-uid]') - .find('[component="chat/manage/user/owner/icon"]') - .toggleClass('hidden'); + const iconEl = modal.get(0).querySelector(`[component="chat/manage/user/list"] > [data-uid="${uid}"] [component="chat/manage/user/owner/icon"]`); + const current = !iconEl.classList.contains('hidden'); + + await api[current ? 'del' : 'put'](`/chats/${roomId}/owners/${uid}`); + iconEl.classList.toggle('hidden'); }); } diff --git a/src/api/chats.js b/src/api/chats.js index 88b0a27d2c..e0d1429803 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -269,6 +269,20 @@ chatsAPI.kick = async (caller, data) => { return chatsAPI.users(caller, data); }; +chatsAPI.toggleOwner = async (caller, { roomId, uid, state }) => { + const [isAdmin, inRoom, isRoomOwner] = await Promise.all([ + user.isAdministrator(caller.uid), + messaging.isUserInRoom(caller.uid, roomId), + messaging.isRoomOwner(caller.uid, roomId), + ]); + + if (!isAdmin && (!inRoom || !isRoomOwner)) { + throw new Error('[[error:no-privileges]]'); + } + + return await messaging.toggleOwner(uid, roomId, state); +}; + chatsAPI.listMessages = async (caller, { uid, roomId, start, direction = null }) => { const count = 50; let stop = start + count - 1; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index 86ab6012ee..04f87b3efd 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -126,6 +126,12 @@ Chats.kickUser = async (req, res) => { helpers.formatApiResponse(200, res, users); }; +Chats.toggleOwner = async (req, res) => { + const state = req.method === 'PUT'; + await api.chats.toggleOwner(req, { state, ...req.params }); + helpers.formatApiResponse(200, res); +}; + Chats.messages = {}; Chats.messages.list = async (req, res) => { const uid = req.query.uid || req.uid; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 73ee54815a..4e73ee0f37 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -29,6 +29,9 @@ module.exports = function () { setupApiRoute(router, 'delete', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.kick); setupApiRoute(router, 'delete', '/:roomId/users/:uid', [...middlewares, middleware.assert.room, middleware.assert.user], controllers.write.chats.kickUser); + setupApiRoute(router, 'put', '/:roomId/owners/:uid', [...middlewares, middleware.assert.room, middleware.assert.user], controllers.write.chats.toggleOwner); + setupApiRoute(router, 'delete', '/:roomId/owners/:uid', [...middlewares, middleware.assert.room, middleware.assert.user], controllers.write.chats.toggleOwner); + setupApiRoute(router, 'get', '/:roomId/messages', [...middlewares, middleware.assert.room], controllers.write.chats.messages.list); setupApiRoute(router, 'get', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.get); setupApiRoute(router, 'put', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.edit); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 910713d9cf..699a6850fc 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -170,20 +170,13 @@ SocketModules.chats.searchMembers = async function (socket, data) { }; SocketModules.chats.toggleOwner = async (socket, data) => { + sockets.warnDeprecated(socket, 'PUT/DELETE /api/v3/chats/:roomId/owners/:uid'); + if (!data || !data.uid || !data.roomId) { throw new Error('[[error:invalid-data]]'); } - const [isAdmin, inRoom, isRoomOwner] = await Promise.all([ - user.isAdministrator(socket.uid), - Messaging.isUserInRoom(socket.uid, data.roomId), - Messaging.isRoomOwner(socket.uid, data.roomId), - ]); - if (!isAdmin && (!inRoom || !isRoomOwner)) { - throw new Error('[[error:no-privileges]]'); - } - - await Messaging.toggleOwner(data.uid, data.roomId); + await api.chats.toggleOwner(socket, data); }; SocketModules.chats.setNotificationSetting = async (socket, data) => { From 75c8cda18c320201f52fe4fc34202d6732a1a222 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Nov 2023 11:23:39 -0500 Subject: [PATCH 124/201] refactor(socket.io): deprecate SocketModules.chats.setNotificationSetting in favour of api.chats.watch --- public/openapi/write.yaml | 2 + public/openapi/write/chats/roomId/watch.yaml | 69 ++++++++++++++++++++ public/src/client/chats.js | 3 +- src/api/chats.js | 9 +++ src/controllers/write/chats.js | 7 ++ src/routes/write/chats.js | 3 + src/socket.io/modules.js | 9 +-- 7 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 public/openapi/write/chats/roomId/watch.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 819caf9e29..aad2bf2d16 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -200,6 +200,8 @@ paths: $ref: 'write/chats/roomId.yaml' /chats/{roomId}/state: $ref: 'write/chats/roomId/state.yaml' + /chats/{roomId}/watch: + $ref: 'write/chats/roomId/watch.yaml' /chats/{roomId}/users: $ref: 'write/chats/roomId/users.yaml' /chats/{roomId}/users/{uid}: diff --git a/public/openapi/write/chats/roomId/watch.yaml b/public/openapi/write/chats/roomId/watch.yaml new file mode 100644 index 0000000000..cdc01ce82b --- /dev/null +++ b/public/openapi/write/chats/roomId/watch.yaml @@ -0,0 +1,69 @@ +put: + tags: + - chats + summary: set chat room notification setting + description: > + This operation updates the chat room notification setting for the calling user. + + N.B. The calling user must be in the chat room for this call to succeed. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid room id + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + value: + type: number + example: 11 + required: + - value + responses: + '200': + description: Chat room notification setting updated. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: {} +delete: + tags: + - chats + summary: restore default chat room notification setting + description: > + This operation restores the default value for notifications for the calling user. + + You can also call the `PUT` variant of this route, and pass in `-1` for `value`. + That would accomplish the same thing. + + N.B. The calling user must be in the chat room for this call to succeed. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid room id + example: 1 + responses: + '200': + description: Chat room notification setting updated. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: {} \ No newline at end of file diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 222960945c..d831170e78 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -184,8 +184,7 @@ define('forum/chats', [ const $this = $(this); $this.find('i.fa-check').removeClass('hidden'); notifSettingEl.find('[component="chat/notification/setting/icon"]').attr('class', `fa ${$this.attr('data-icon')}`); - await socket.emit('modules.chats.setNotificationSetting', { - roomId: roomId, + await api.put(`/chats/${roomId}/watch`, { value: $this.attr('data-value'), }); }); diff --git a/src/api/chats.js b/src/api/chats.js index e0d1429803..6336aa9e2b 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -198,6 +198,15 @@ chatsAPI.mark = async (caller, data) => { messaging.pushUnreadCount(caller.uid); }; +chatsAPI.watch = async (caller, { roomId, state }) => { + const inRoom = await messaging.isUserInRoom(caller.uid, roomId); + if (!inRoom) { + throw new Error('[[error:no-privileges]]'); + } + + await messaging.setUserNotificationSetting(caller.uid, roomId, state); +}; + chatsAPI.users = async (caller, data) => { const start = data.hasOwnProperty('start') ? data.start : 0; const stop = start + 39; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index 04f87b3efd..f7602ebf7b 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -88,6 +88,13 @@ Chats.mark = async (req, res) => { helpers.formatApiResponse(200, res); }; +Chats.watch = async (req, res) => { + const state = req.method === 'DELETE' ? -1 : parseInt(req.body.value, 10) || -1; + + await api.chats.watch(req, { state, ...req.params }); + helpers.formatApiResponse(200, res); +}; + Chats.users = async (req, res) => { const { roomId } = req.params; const start = parseInt(req.query.start, 10) || 0; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 4e73ee0f37..9f5c4c088e 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -24,6 +24,9 @@ module.exports = function () { setupApiRoute(router, 'put', '/:roomId/state', [...middlewares, middleware.assert.room], controllers.write.chats.mark); setupApiRoute(router, 'delete', '/:roomId/state', [...middlewares, middleware.assert.room], controllers.write.chats.mark); + setupApiRoute(router, 'put', '/:roomId/watch', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['value'])], controllers.write.chats.watch); + setupApiRoute(router, 'delete', '/:roomId/watch', [...middlewares, middleware.assert.room], controllers.write.chats.watch); + setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users); setupApiRoute(router, 'post', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.invite); setupApiRoute(router, 'delete', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.kick); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 699a6850fc..e3376720ec 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -180,16 +180,13 @@ SocketModules.chats.toggleOwner = async (socket, data) => { }; SocketModules.chats.setNotificationSetting = async (socket, data) => { + sockets.warnDeprecated(socket, 'PUT/DELETE /api/v3/chats/:roomId/watch'); + if (!data || !utils.isNumber(data.value) || !data.roomId) { throw new Error('[[error:invalid-data]]'); } - const inRoom = await Messaging.isUserInRoom(socket.uid, data.roomId); - if (!inRoom) { - throw new Error('[[error:no-privileges]]'); - } - - await Messaging.setUserNotificationSetting(socket.uid, data.roomId, data.value); + await api.chats.watch(socket, data); }; SocketModules.chats.searchMessages = async (socket, data) => { From f9c471a09bc5f02eaecb4bffea3a2aac66812a3b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Nov 2023 11:41:15 -0500 Subject: [PATCH 125/201] fix: remove lodash require --- src/socket.io/modules.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index e3376720ec..5bbe88c068 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -5,7 +5,6 @@ * - SocketModules.chats.(enter|leave)(Public)? => related to socket.io rooms */ -const _ = require('lodash'); const validator = require('validator'); const db = require('../database'); From f9dc35021d98e1618a47b43d87ff395f77e5f6e3 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 16 Nov 2023 15:21:57 -0500 Subject: [PATCH 126/201] refactor(socket.io): deprecate SocketModules.chats.searchMessages in favour of api.search.roomMessages --- public/openapi/write.yaml | 2 + .../write/search/chats/roomId/messages.yaml | 37 ++++++++++++++++++ .../write/search/chats/roomId/users.yaml | 2 +- src/api/search.js | 38 +++++++++++++++++++ src/controllers/write/search.js | 9 ++++- src/routes/write/search.js | 2 + src/socket.io/modules.js | 36 +++--------------- 7 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 public/openapi/write/search/chats/roomId/messages.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index aad2bf2d16..94b90186b3 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -232,6 +232,8 @@ paths: $ref: 'write/search/categories.yaml' /search/chats/{roomId}/users: $ref: 'write/search/chats/roomId/users.yaml' + /search/chats/{roomId}/messages: + $ref: 'write/search/chats/roomId/messages.yaml' /admin/settings/{setting}: $ref: 'write/admin/settings/setting.yaml' /admin/analytics: diff --git a/public/openapi/write/search/chats/roomId/messages.yaml b/public/openapi/write/search/chats/roomId/messages.yaml new file mode 100644 index 0000000000..9969aa23ce --- /dev/null +++ b/public/openapi/write/search/chats/roomId/messages.yaml @@ -0,0 +1,37 @@ +get: + tags: + - search + summary: find chat messages by keyword + description: This operation returns a set of messages in a chat room matching the keyword search. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: room ID to check + example: 1 + - in: query + name: 'query' + schema: + type: string + required: false + description: The keyword used in the message search + example: 'foobar' + responses: + '200': + description: matching messages successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + messages: + type: array + items: + $ref: ../../../../components/schemas/Chats.yaml#/MessageObject \ No newline at end of file diff --git a/public/openapi/write/search/chats/roomId/users.yaml b/public/openapi/write/search/chats/roomId/users.yaml index dd94a88d02..f8457cd3a7 100644 --- a/public/openapi/write/search/chats/roomId/users.yaml +++ b/public/openapi/write/search/chats/roomId/users.yaml @@ -16,7 +16,7 @@ get: schema: type: string required: false - description: The keyword used in the category search + description: The keyword used in the user search example: 'admin' responses: '200': diff --git a/src/api/search.js b/src/api/search.js index 043c97176e..b9645ee567 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -2,6 +2,7 @@ const _ = require('lodash'); +const db = require('../database'); const user = require('../user'); const categories = require('../categories'); const messaging = require('../messaging'); @@ -152,3 +153,40 @@ searchApi.roomUsers = async (caller, { query, roomId }) => { return { users: roomUsers }; }; + +searchApi.roomMessages = async (caller, { query, roomId, uid }) => { + const [roomData, inRoom] = await Promise.all([ + messaging.getRoomData(roomId), + messaging.isUserInRoom(caller.uid, roomId), + ]); + + if (!roomData) { + throw new Error('[[error:no-room]]'); + } + if (!inRoom) { + throw new Error('[[error:no-privileges]]'); + } + const { ids } = await plugins.hooks.fire('filter:messaging.searchMessages', { + content: query, + roomId: [roomId], + uid: [uid], + matchWords: 'any', + ids: [], + }); + + let userjoinTimestamp = 0; + if (!roomData.public) { + userjoinTimestamp = await db.sortedSetScore(`chat:room:${roomId}:uids`, caller.uid); + } + let messageData = await messaging.getMessagesData(ids, caller.uid, roomId, false); + messageData = messageData + .map((msg) => { + if (msg) { + msg.newSet = true; + } + return msg; + }) + .filter(msg => msg && !msg.deleted && msg.timestamp > userjoinTimestamp); + + return { messages: messageData }; +}; diff --git a/src/controllers/write/search.js b/src/controllers/write/search.js index cfdaad44a8..6d9e96db8b 100644 --- a/src/controllers/write/search.js +++ b/src/controllers/write/search.js @@ -10,6 +10,11 @@ Search.categories = async (req, res) => { }; Search.roomUsers = async (req, res) => { - const { query } = req.query; - helpers.formatApiResponse(200, res, await api.search.roomUsers(req, { query, ...req.params })); + const { query, uid } = req.query; + helpers.formatApiResponse(200, res, await api.search.roomUsers(req, { query, uid, ...req.params })); +}; + +Search.roomMessages = async (req, res) => { + const { query } = req.query; + helpers.formatApiResponse(200, res, await api.search.roomMessages(req, { query, ...req.params })); }; diff --git a/src/routes/write/search.js b/src/routes/write/search.js index 4d4108a05b..5f0b41d516 100644 --- a/src/routes/write/search.js +++ b/src/routes/write/search.js @@ -14,7 +14,9 @@ module.exports = function () { // setupApiRoute(router, 'post', '/', [...middlewares], controllers.write.search.TBD); setupApiRoute(router, 'get', '/categories', [], controllers.write.search.categories); + setupApiRoute(router, 'get', '/chats/:roomId/users', [...middlewares, middleware.checkRequired.bind(null, ['query']), middleware.canChat, middleware.assert.room], controllers.write.search.roomUsers); + setupApiRoute(router, 'get', '/chats/:roomId/messages', [...middlewares, middleware.checkRequired.bind(null, ['query']), middleware.canChat, middleware.assert.room], controllers.write.search.roomMessages); return router; }; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 5bbe88c068..edbd1eeae9 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -189,40 +189,16 @@ SocketModules.chats.setNotificationSetting = async (socket, data) => { }; SocketModules.chats.searchMessages = async (socket, data) => { + sockets.warnDeprecated(socket, 'GET /api/v3/search/chats/:roomId/messages'); + if (!data || !utils.isNumber(data.roomId) || !data.content) { throw new Error('[[error:invalid-data]]'); } - const [roomData, inRoom] = await Promise.all([ - Messaging.getRoomData(data.roomId), - Messaging.isUserInRoom(socket.uid, data.roomId), - ]); - if (!roomData) { - throw new Error('[[error:no-room]]'); - } - if (!inRoom) { - throw new Error('[[error:no-privileges]]'); - } - const { ids } = await plugins.hooks.fire('filter:messaging.searchMessages', { - content: data.content, - roomId: [data.roomId], - uid: [data.uid], - matchWords: 'any', - ids: [], - }); - - let userjoinTimestamp = 0; - if (!roomData.public) { - userjoinTimestamp = await db.sortedSetScore(`chat:room:${data.roomId}:uids`, socket.uid); - } - const messageData = await Messaging.getMessagesData(ids, socket.uid, data.roomId, false); - messageData.forEach((msg) => { - if (msg) { - msg.newSet = true; - } - }); - - return messageData.filter(msg => msg && !msg.deleted && msg.timestamp > userjoinTimestamp); + // parameter renamed; backwards compatibility + data.query = data.content; + delete data.content; + return await api.search.roomMessages(socket, data); }; SocketModules.chats.loadPinnedMessages = async (socket, data) => { From 401e8636bd4e9e7e589e266985b9ae985b213cc6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 17 Nov 2023 11:37:51 -0500 Subject: [PATCH 127/201] refactor(socket.io): deprecate SocketModules.chats.loadPinnedMessages in favour of api.chats.getPinnedMessages" --- public/openapi/write.yaml | 2 + .../write/chats/roomId/messages/pinned.yaml | 39 +++++++++++++++++++ public/src/client/chats/pinned-messages.js | 7 +--- src/api/chats.js | 10 +++++ src/controllers/write/chats.js | 6 +++ src/routes/write/chats.js | 1 + src/socket.io/modules.js | 12 +++--- 7 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 public/openapi/write/chats/roomId/messages/pinned.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 94b90186b3..630e541c59 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -210,6 +210,8 @@ paths: $ref: 'write/chats/roomId/owners/uid.yaml' /chats/{roomId}/messages: $ref: 'write/chats/roomId/messages.yaml' + /chats/{roomId}/messages/pinned: + $ref: 'write/chats/roomId/messages/pinned.yaml' /chats/{roomId}/messages/{mid}: $ref: 'write/chats/roomId/messages/mid.yaml' /chats/{roomId}/messages/{mid}/pin: diff --git a/public/openapi/write/chats/roomId/messages/pinned.yaml b/public/openapi/write/chats/roomId/messages/pinned.yaml new file mode 100644 index 0000000000..683b29b290 --- /dev/null +++ b/public/openapi/write/chats/roomId/messages/pinned.yaml @@ -0,0 +1,39 @@ +get: + tags: + - chats + summary: get pinned messages + description: > + This operation retrieves a list of pinned messages for a given chat room. + This call will always return a maximum of 50 items, of which the result set can be offset based on the passed-in `start` parameter. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid chat room id + example: 1 + - in: path + name: start + schema: + type: number + required: true + description: index to start returning results from + example: 0 + responses: + '200': + description: Pinned messages successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + messages: + type: array + items: + $ref: ../../../../components/schemas/Chats.yaml#/MessageObject \ No newline at end of file diff --git a/public/src/client/chats/pinned-messages.js b/public/src/client/chats/pinned-messages.js index 931dcb9508..87d0dc6d0c 100644 --- a/public/src/client/chats/pinned-messages.js +++ b/public/src/client/chats/pinned-messages.js @@ -58,11 +58,8 @@ define('forum/chats/pinned-messages', ['api', 'alerts'], function (api, alerts) } async function loadData(start) { - const data = await socket.emit('modules.chats.loadPinnedMessages', { - roomId: ajaxify.data.roomId, - start: start, - }); - return data; + const { messages } = await api.get(`/chats/${ajaxify.data.roomId}/messages/pinned`, { start }); + return messages; } pinnedMessages.pin = function (mid, roomId) { diff --git a/src/api/chats.js b/src/api/chats.js index 6336aa9e2b..ec6e1d5c32 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -320,6 +320,16 @@ chatsAPI.listMessages = async (caller, { uid, roomId, start, direction = null }) return { messages }; }; +chatsAPI.getPinnedMessages = async (caller, { start, roomId }) => { + start = parseInt(start, 10) || 0; + const isInRoom = await messaging.isUserInRoom(caller.uid, roomId); + if (!isInRoom) { + throw new Error('[[error:no-privileges]]'); + } + const messages = await messaging.getPinnedMessages(roomId, caller.uid, start, start + 49); + return { messages }; +}; + chatsAPI.getMessage = async (caller, { mid, roomId }) => { const messages = await messaging.getMessagesData([mid], caller.uid, roomId, false); return messages.pop(); diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index f7602ebf7b..baf397f9de 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -152,6 +152,12 @@ Chats.messages.list = async (req, res) => { helpers.formatApiResponse(200, res, { messages }); }; +Chats.messages.getPinned = async (req, res) => { + const { start } = req.query; + + helpers.formatApiResponse(200, res, await api.chats.getPinnedMessages(req, { start, ...req.params })); +}; + Chats.messages.get = async (req, res) => { const { mid, roomId } = req.params; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index 9f5c4c088e..bbd464d369 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -36,6 +36,7 @@ module.exports = function () { setupApiRoute(router, 'delete', '/:roomId/owners/:uid', [...middlewares, middleware.assert.room, middleware.assert.user], controllers.write.chats.toggleOwner); setupApiRoute(router, 'get', '/:roomId/messages', [...middlewares, middleware.assert.room], controllers.write.chats.messages.list); + setupApiRoute(router, 'get', '/:roomId/messages/pinned', [...middlewares, middleware.assert.room], controllers.write.chats.messages.getPinned); setupApiRoute(router, 'get', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.get); setupApiRoute(router, 'put', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.edit); setupApiRoute(router, 'post', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.restore); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index edbd1eeae9..a3f90fd81e 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -202,16 +202,14 @@ SocketModules.chats.searchMessages = async (socket, data) => { }; SocketModules.chats.loadPinnedMessages = async (socket, data) => { + sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/messages/pinned'); + if (!data || !data.roomId || !utils.isNumber(data.start)) { throw new Error('[[error:invalid-data]]'); } - const isInRoom = await Messaging.isUserInRoom(socket.uid, data.roomId); - if (!isInRoom) { - throw new Error('[[error:no-privileges]]'); - } - const start = parseInt(data.start, 10) || 0; - const pinnedMsgs = await Messaging.getPinnedMessages(data.roomId, socket.uid, start, start + 49); - return pinnedMsgs; + + const { messages } = await api.chats.getPinnedMessages(socket, data); + return messages; }; SocketModules.chats.typing = async (socket, data) => { From c1e6be770566612b6b5ea8de736386fd4950b732 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 17 Nov 2023 14:10:29 -0500 Subject: [PATCH 128/201] refactor(socket.io): deprecate SocketModules.chats.typing in favour of api.chats.toggleTyping --- public/openapi/write.yaml | 2 + .../write/chats/roomId/messages/pinned.yaml | 2 + public/openapi/write/chats/roomId/typing.yaml | 39 +++++++++++++++++++ public/src/client/chats.js | 8 +--- public/src/modules/chat.js | 2 +- src/api/chats.js | 23 +++++++++++ src/controllers/write/chats.js | 7 ++++ src/routes/write/chats.js | 2 + src/socket.io/modules.js | 19 ++++----- 9 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 public/openapi/write/chats/roomId/typing.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 630e541c59..79657b519e 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -202,6 +202,8 @@ paths: $ref: 'write/chats/roomId/state.yaml' /chats/{roomId}/watch: $ref: 'write/chats/roomId/watch.yaml' + /chats/{roomId}/typing: + $ref: 'write/chats/roomId/typing.yaml' /chats/{roomId}/users: $ref: 'write/chats/roomId/users.yaml' /chats/{roomId}/users/{uid}: diff --git a/public/openapi/write/chats/roomId/messages/pinned.yaml b/public/openapi/write/chats/roomId/messages/pinned.yaml index 683b29b290..3d8ea371dd 100644 --- a/public/openapi/write/chats/roomId/messages/pinned.yaml +++ b/public/openapi/write/chats/roomId/messages/pinned.yaml @@ -5,6 +5,8 @@ get: description: > This operation retrieves a list of pinned messages for a given chat room. This call will always return a maximum of 50 items, of which the result set can be offset based on the passed-in `start` parameter. + + N.B. The calling user must be in the chat room for this call to succeed. parameters: - in: path name: roomId diff --git a/public/openapi/write/chats/roomId/typing.yaml b/public/openapi/write/chats/roomId/typing.yaml new file mode 100644 index 0000000000..f30ec694b7 --- /dev/null +++ b/public/openapi/write/chats/roomId/typing.yaml @@ -0,0 +1,39 @@ +put: + tags: + - chats + summary: update typing state in chat room + description: > + This operation updates the typing state in a given chat room, so that other users in the room see that that user is typing. + + N.B. The calling user must be in the chat room for this call to succeed. + parameters: + - in: path + name: roomId + schema: + type: number + required: true + description: a valid room id + example: 1 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + typing: + type: boolean + example: true + required: + - typing + responses: + '200': + description: Chat room typing state updated. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: {} \ No newline at end of file diff --git a/public/src/client/chats.js b/public/src/client/chats.js index d831170e78..a5cb58caf0 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -391,11 +391,7 @@ define('forum/chats', [ Chats.addTypingHandler = function (parent, roomId) { const textarea = parent.find('[component="chat/input"]'); function emitTyping(typing) { - socket.emit('modules.chats.typing', { - roomId: roomId, - typing: typing, - username: app.user.username, - }); + api.put(`/chats/${roomId}/typing`, { typing }).catch(alerts.error); } textarea.on('focus', () => textarea.val() && emitTyping(true)); @@ -744,7 +740,7 @@ define('forum/chats', [ }); socket.on('event:chats.typing', async (data) => { - if (chatModule.isFromBlockedUser(data.uid)) { + if (data.uid === app.user.uid || chatModule.isFromBlockedUser(data.uid)) { return; } chatModule.updateTypingUserList($(`[component="chat/main-wrapper"][data-roomid="${data.roomId}"]`), data); diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 97b580b0b2..0794af22a8 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -245,7 +245,7 @@ define('chat', [ }; module.onUserTyping = function (data) { - if (module.isFromBlockedUser(data.uid)) { + if (data.uid === app.user.uid || module.isFromBlockedUser(data.uid)) { return; } const modal = module.getModal(data.roomId); diff --git a/src/api/chats.js b/src/api/chats.js index ec6e1d5c32..964bfdc071 100644 --- a/src/api/chats.js +++ b/src/api/chats.js @@ -10,7 +10,9 @@ const messaging = require('../messaging'); const notifications = require('../notifications'); const privileges = require('../privileges'); const plugins = require('../plugins'); +const utils = require('../utils'); +const websockets = require('../socket.io'); const socketHelpers = require('../socket.io/helpers'); const chatsAPI = module.exports; @@ -207,6 +209,27 @@ chatsAPI.watch = async (caller, { roomId, state }) => { await messaging.setUserNotificationSetting(caller.uid, roomId, state); }; +chatsAPI.toggleTyping = async (caller, { roomId, typing }) => { + if (!utils.isNumber(roomId) || typeof typing !== 'boolean') { + throw new Error('[[error:invalid-data]]'); + } + + const [isInRoom, username] = await Promise.all([ + messaging.isUserInRoom(caller.uid, roomId), + user.getUserField(caller.uid, 'username'), + ]); + if (!isInRoom) { + throw new Error('[[error:no-privileges]]'); + } + + websockets.in(`chat_room_${roomId}`).emit('event:chats.typing', { + uid: caller.uid, + roomId, + typing, + username, + }); +}; + chatsAPI.users = async (caller, data) => { const start = data.hasOwnProperty('start') ? data.start : 0; const stop = start + 39; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js index baf397f9de..ae21235f88 100644 --- a/src/controllers/write/chats.js +++ b/src/controllers/write/chats.js @@ -95,6 +95,13 @@ Chats.watch = async (req, res) => { helpers.formatApiResponse(200, res); }; +Chats.toggleTyping = async (req, res) => { + const { typing } = req.body; + + await api.chats.toggleTyping(req, { typing, ...req.params }); + helpers.formatApiResponse(200, res); +}; + Chats.users = async (req, res) => { const { roomId } = req.params; const start = parseInt(req.query.start, 10) || 0; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js index bbd464d369..7fd2c8e392 100644 --- a/src/routes/write/chats.js +++ b/src/routes/write/chats.js @@ -27,6 +27,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:roomId/watch', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['value'])], controllers.write.chats.watch); setupApiRoute(router, 'delete', '/:roomId/watch', [...middlewares, middleware.assert.room], controllers.write.chats.watch); + setupApiRoute(router, 'put', '/:roomId/typing', [...middlewares, middleware.assert.room], controllers.write.chats.toggleTyping); + setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users); setupApiRoute(router, 'post', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.invite); setupApiRoute(router, 'delete', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.kick); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index a3f90fd81e..5f805eb899 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -213,19 +213,16 @@ SocketModules.chats.loadPinnedMessages = async (socket, data) => { }; SocketModules.chats.typing = async (socket, data) => { - if (!data || !utils.isNumber(data.roomId) || typeof data.typing !== 'boolean') { + sockets.warnDeprecated(socket, 'PUT /api/v3/chats/:roomId/typing'); + + if (!data) { throw new Error('[[error:invalid-data]]'); } - const isInRoom = await Messaging.isUserInRoom(socket.uid, data.roomId); - if (!isInRoom) { - throw new Error('[[error:no-privileges]]'); - } - socket.to(`chat_room_${data.roomId}`).emit('event:chats.typing', { - uid: socket.uid, - roomId: data.roomId, - typing: data.typing, - username: validator.escape(String(data.username)), - }); + + // `username` is now inferred from caller uid + delete data.username; + + await api.chats.toggleTyping(socket, data); }; From b5940a5d4464682184aa39f6379e9562eab51b8c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 20 Nov 2023 12:01:18 -0500 Subject: [PATCH 129/201] fix: remove unused requires --- src/socket.io/modules.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 5f805eb899..5b5f0966b3 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -5,13 +5,9 @@ * - SocketModules.chats.(enter|leave)(Public)? => related to socket.io rooms */ -const validator = require('validator'); - -const db = require('../database'); const Messaging = require('../messaging'); const utils = require('../utils'); const user = require('../user'); -const plugins = require('../plugins'); const groups = require('../groups'); const api = require('../api'); From 9079ad0b2c6ba21192d1cefa3402d73fdd3b9c0a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 20 Nov 2023 16:18:11 -0500 Subject: [PATCH 130/201] chore: added missing deprecation warning for .getTopics --- src/socket.io/categories.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 016985935c..ae2378d73a 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -37,6 +37,8 @@ SocketCategories.getWatchedCategories = async function (socket) { }; SocketCategories.loadMore = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid/topics'); + if (!data) { throw new Error('[[error:invalid-data]]'); } 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 131/201] 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]]

    - -
    - - - -
      -
    • [[modules:chat.retrieving-users]]
    • -
    - {{{ 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 132/201] 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 133/201] 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 134/201] 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 135/201] 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 136/201] 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 137/201] 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 138/201] 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 139/201] 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 140/201] 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 141/201] 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 142/201] 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 143/201] 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 144/201] 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 145/201] 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 97f6c539145ad67e59176614a654791959e8d0c7 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 29 Nov 2023 09:18:46 +0000 Subject: [PATCH 146/201] 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 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 147/201] 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 148/201] 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 149/201] 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 150/201] 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 151/201] 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 152/201] 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 153/201] 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 154/201] 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 155/201] 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 156/201] 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 157/201] 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 158/201] 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 159/201] 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 160/201] 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 161/201] 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 162/201] 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 163/201] 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 164/201] 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 165/201] 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 166/201] 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 167/201] 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 168/201] 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 169/201] 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 170/201] 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 171/201] 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 3c31ae239bbfb96c79e36fdc84d0b72dcfe40e6d Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 7 Dec 2023 09:18:44 +0000 Subject: [PATCH 172/201] 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 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 173/201] 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 174/201] 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 255a67cd7f0f10e090e65232f0e9378a0fd5b9f1 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 8 Dec 2023 09:19:24 +0000 Subject: [PATCH 175/201] 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 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 176/201] 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 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 177/201] 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 178/201] 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 179/201] 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 180/201] 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 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 181/201] 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 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 182/201] 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 183/201] 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 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 184/201] 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 185/201] 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 186/201] 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 187/201] =?UTF-8?q?=F0=9F=91=8BRequest,=20=F0=9F=90=B6=20F?= =?UTF-8?q?etch,=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 -