Compare commits

..

14 Commits

Author SHA1 Message Date
Misty Release Bot
1b9bda2af2 chore: incrementing version number - v4.0.2 2025-02-02 08:59:51 +00:00
Julian Lam
51e660d5ae fix: bad logic that invisibly broke outgoing user follows completely 2025-02-02 03:51:27 -05:00
Barış Soner Uşaklı
1cdf37a218 list remove all (#13113)
* list remove all

* one more test

* sortedSetIncrByBulk

* remove name

* incrObjectFieldByBulk

* test: disable api tests

* try merge

* another test

* give upon bulk incr

* update so answer

* one more try

* fix: name

* chore: up dbsearch
2025-02-02 03:38:55 -05:00
Barış Soner Uşaklı
0298a3af0d chore: up persona 2025-02-01 17:40:36 -05:00
Barış Soner Uşaklı
d77d2055c3 chore: up harmony 2025-02-01 17:24:15 -05:00
Barış Soner Uşaklı
6672de00cb chore: up themes, closes #13102 2025-01-30 19:32:02 -05:00
Barış Soner Uşaklı
be62ae24ad feat: allow selecting empty for custom selects
closes #13101
2025-01-30 10:22:45 -05:00
Barış Soner Uşaklı
ef5ae00652 test: fix schema 2025-01-29 18:35:34 -05:00
Barış Soner Uşaklı
d4a1b4dad9 refactor: remove old comment 2025-01-29 18:27:46 -05:00
Barış Soner Uşaklı
47734d4cd3 test: fix schema 2025-01-29 18:26:25 -05:00
Barış Soner Uşaklı
4d7335903a feat: add uid to post.parent 2025-01-29 18:01:52 -05:00
Barış Soner Uşaklı
0b92d52593 fix: closes #13096, fix regression from renaming language files
a76781859c (diff-b2c5ad612412b958d1df03c07abfa9c4250b3256238502097d2639df203d7fed)
2025-01-29 16:06:03 -05:00
Barış Soner Uşaklı
933c18f4ac feat: add description and keywords to api/config 2025-01-29 16:01:22 -05:00
Misty Release Bot
3dbd2b308f chore: update changelog for v4.0.1 2025-01-29 19:26:28 +00:00
16 changed files with 159 additions and 25 deletions

View File

@@ -1,3 +1,52 @@
#### v4.0.1 (2025-01-29)
##### Chores
* up dbsearch (88fa4553)
* up benchpress (c9584800)
* up harmony (10409e0e)
* up themes (6918c3f3)
* up themes (050effe2)
* up harmony (90e0a2d6)
* incrementing version number - v4.0.0 (c1eaee45)
* update changelog for v4.0.0 (ae8f58d6)
##### New Features
* use text-danger if chat over limit (2f5b4b29)
##### Bug Fixes
* #13087, disallow following cid -1 (ddb6e0f3)
* encoding of pid in notifyCategoryFollowers, #13087 (6d88dcb2)
* #13084 bump persona (4feda224)
* closes #13091, dont show world category (4c66eed9)
* #13088, up dbsearch (8644565a)
* #13090, update themes fix selector (822bff62)
* #13086 move rateLimit check (487d9f73)
* null checks for category sync and actor assertions (b3b8b9e9)
* #13067, add sourceContent to teasers (679fcb71)
* #13065, send missing `actor` property when 1b12 announcing local posts (e61df4de)
* closes #13068, encodeURIComponent X-Redirect (f3b8ed27)
* #13062 add displayname to email tpl data (f0c2090d)
##### Other Changes
* missing ; (8b38cb3a)
* reduce image size (#12702) (a95a51c6)
##### Refactors
* 🤡 (4ba01d18)
##### Tests
* adjust webfinger test for updated 404 status code (4a827b7e)
* fix x-redirect tests (b80440aa)
* add sourceContent to spec (526a9521)
* change test to 404 (52f7f0a7)
* remove only (0ba4ba65)
#### v4.0.0 (2025-01-20)
##### Breaking Changes

View File

@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "4.0.1",
"version": "4.0.2",
"homepage": "https://www.nodebb.org",
"repository": {
"type": "git",
@@ -100,18 +100,18 @@
"nconf": "0.12.1",
"nodebb-plugin-2factor": "7.5.8",
"nodebb-plugin-composer-default": "10.2.44",
"nodebb-plugin-dbsearch": "6.2.7",
"nodebb-plugin-emoji": "6.0.1",
"nodebb-plugin-dbsearch": "6.2.8",
"nodebb-plugin-emoji": "6.0.2",
"nodebb-plugin-emoji-android": "4.1.1",
"nodebb-plugin-markdown": "13.0.0",
"nodebb-plugin-mentions": "4.6.10",
"nodebb-plugin-spam-be-gone": "2.3.0",
"nodebb-plugin-web-push": "0.7.2",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "2.0.5",
"nodebb-theme-harmony": "2.0.7",
"nodebb-theme-lavender": "7.1.17",
"nodebb-theme-peace": "2.2.35",
"nodebb-theme-persona": "14.0.5",
"nodebb-theme-peace": "2.2.36",
"nodebb-theme-persona": "14.0.8",
"nodebb-widget-essentials": "7.0.32",
"nodemailer": "6.9.16",
"nprogress": "0.2.0",

View File

@@ -15,7 +15,7 @@
"title-layout": "Title Layout",
"title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}",
"description.placeholder": "A short description about your community",
"description": "Choose what page is shown when users navigate to the root URL of your forum.",
"description": "Site Description",
"keywords": "Site Keywords",
"keywords-placeholder": "Keywords describing your community, comma-separated",
"logo-and-icons": "Site Logo & Icons",
@@ -51,6 +51,7 @@
"topic-tools": "Topic Tools",
"home-page": "Home Page",
"home-page-route": "Home Page Route",
"home-page-description": "Choose what page is shown when users navigate to the root URL of your forum.",
"custom-route": "Custom Route",
"allow-user-home-pages": "Allow User Home Pages",
"home-page-title": "Title of the home page (default \"Home\")",

View File

@@ -23,6 +23,10 @@ get:
type: string
browserTitle:
type: string
description:
type: string
keywords:
type: string
titleLayout:
type: string
showSiteTitle:

View File

@@ -23,6 +23,10 @@ get:
type: string
browserTitle:
type: string
description:
type: string
keywords:
type: string
titleLayout:
type: string
showSiteTitle:

View File

@@ -38,7 +38,7 @@ function enabledCheck(next) {
activitypubApi.follow = enabledCheck(async (caller, { type, id, actor } = {}) => {
// Privilege checks should be done upstream
const assertion = await activitypub.actors.assert(actor);
if (!assertion || !assertion.length) {
if (!assertion || (Array.isArray(assertion) && assertion.length)) {
throw new Error('[[error:activitypub.invalid-id]]');
}

View File

@@ -106,7 +106,7 @@ topicsAPI.reply = async function (caller, data) {
return await posts.addToQueue(payload);
}
const postData = await topics.reply(payload); // postData seems to be a subset of postObj, refactor?
const postData = await topics.reply(payload);
const result = {
posts: [postData],

View File

@@ -194,7 +194,11 @@ helpers.getCustomUserFields = async function (callerUID, userData) {
if (f.type === 'input-link' && userValue) {
f.linkValue = validator.escape(String(userValue.replace('http://', '').replace('https://', '')));
}
f['select-options'] = (f['select-options'] || '').split('\n').filter(Boolean).map(
f['select-options'] = (f['select-options'] || '').split('\n').filter(Boolean);
if (f.type === 'select') {
f['select-options'].unshift('');
}
f['select-options'] = f['select-options'].map(
opt => ({
value: opt,
selected: Array.isArray(userValue) ?

View File

@@ -32,6 +32,8 @@ apiController.loadConfig = async function (req) {
assetBaseUrl: asset_base_url, // deprecate in 1.20.x
siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')),
browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')),
description: validator.escape(String(meta.config.description || '')),
keywords: validator.escape(String(meta.config.keywords || '')),
titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'),
showSiteTitle: meta.config.showSiteTitle === 1,
maintenanceMode: meta.config.maintenanceMode === 1,

View File

@@ -188,7 +188,7 @@ module.exports = function (module) {
};
module.deleteObjectField = async function (key, field) {
await module.deleteObjectFields(key, [field]);
await module.deleteObjectFields(key, Array.isArray(field) ? field : [field]);
};
module.deleteObjectFields = async function (key, fields) {

View File

@@ -295,7 +295,7 @@ SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b
};
module.deleteObjectField = async function (key, field) {
await module.deleteObjectFields(key, [field]);
await module.deleteObjectFields(key, Array.isArray(field) ? field : [field]);
};
module.deleteObjectFields = async function (key, fields) {
@@ -377,12 +377,34 @@ RETURNING ("data"->>$2::TEXT)::NUMERIC v`,
if (!Array.isArray(data) || !data.length) {
return;
}
// TODO: perf?
await Promise.all(data.map(async (item) => {
for (const [field, value] of Object.entries(item[1])) {
// eslint-disable-next-line no-await-in-loop
await module.incrObjectFieldBy(item[0], field, value);
}
}));
await module.transaction(async (client) => {
await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'hash');
const keys = data.map(item => item[0]);
const dataStrings = data.map(item => JSON.stringify(item[1]));
await client.query({
name: 'incrObjectFieldByBulk',
text: `
INSERT INTO "legacy_hash" ("_key", "data")
SELECT k, d
FROM UNNEST($1::TEXT[], $2::JSONB[]) vs(k, d)
ON CONFLICT ("_key")
DO UPDATE SET "data" = (
SELECT jsonb_object_agg(
key,
CASE
WHEN jsonb_typeof(legacy_hash.data -> key) = 'number'
AND jsonb_typeof(EXCLUDED.data -> key) = 'number'
THEN to_jsonb((legacy_hash.data ->> key)::NUMERIC + (EXCLUDED.data ->> key)::NUMERIC)
ELSE COALESCE(EXCLUDED.data -> key, legacy_hash.data -> key)
END
)
FROM jsonb_each(legacy_hash.data || EXCLUDED.data) AS merged(key, value)
);`,
values: [keys, dataStrings],
});
});
};
};

View File

@@ -75,9 +75,26 @@ RETURNING A."array"[array_length(A."array", 1)] v`,
if (!key) {
return;
}
// TODO: remove all values with one query
if (Array.isArray(value)) {
await Promise.all(value.map(v => module.listRemoveAll(key, v)));
await module.pool.query({
name: 'listRemoveAllMultiple',
text: `
UPDATE "legacy_list" l
SET "array" = (
SELECT ARRAY(
SELECT elem
FROM unnest(l."array") WITH ORDINALITY AS u(elem, ord)
WHERE elem NOT IN (SELECT unnest($2::TEXT[]))
ORDER BY ord
)
)
FROM "legacy_object_live" o
WHERE o."_key" = l."_key"
AND o."type" = l."type"
AND o."_key" = $1::TEXT;`,
values: [key, value],
});
return;
}
await module.pool.query({

View File

@@ -547,8 +547,38 @@ RETURNING "score" s`,
};
module.sortedSetIncrByBulk = async function (data) {
// TODO: perf single query?
return await Promise.all(data.map(item => module.sortedSetIncrBy(item[0], item[1], item[2])));
if (!data.length) {
return [];
}
return await module.transaction(async (client) => {
await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'zset');
const values = [];
const queryParams = [];
let paramIndex = 1;
data.forEach(([key, increment, value]) => {
value = helpers.valueToString(value);
increment = parseFloat(increment);
values.push(key, value, increment);
queryParams.push(`($${paramIndex}::TEXT, $${paramIndex + 1}::TEXT, $${paramIndex + 2}::NUMERIC)`);
paramIndex += 3;
});
const query = `
INSERT INTO "legacy_zset" ("_key", "value", "score")
VALUES ${queryParams.join(', ')}
ON CONFLICT ("_key", "value")
DO UPDATE SET "score" = "legacy_zset"."score" + EXCLUDED."score"
RETURNING "value", "score"`;
const res = await client.query({
text: query,
values,
});
return res.rows.map(row => parseFloat(row.score));
});
};
module.getSortedSetRangeByLex = async function (key, min, max, start, count) {

View File

@@ -196,6 +196,7 @@ module.exports = function (Topics) {
parentPosts.forEach((post, i) => {
if (usersMap[post.uid]) {
parents[parentPids[i]] = {
uid: post.uid,
username: usersMap[post.uid].username,
displayname: usersMap[post.uid].displayname,
};

View File

@@ -127,7 +127,7 @@ module.exports = function (User) {
));
} else if (field.type === 'select') {
const opts = field['select-options'].split('\n').filter(Boolean);
if (!opts.includes(value)) {
if (!opts.includes(value) && value !== '') {
throw new Error(tx.compile(
'error:custom-user-field-select-value-invalid', field.name
));

View File

@@ -162,7 +162,7 @@
<div class="">
<p>
[[admin/settings/general:description]]
[[admin/settings/general:home-page-description]]
</p>
<form class="row">
<div class="col-sm-12">