mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: category handles, #12434
This commit is contained in:
@@ -8,6 +8,14 @@ CategoryObject:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: The category's name/title
|
description: The category's name/title
|
||||||
|
handle:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
An URL-safe name/handle used to represent the category over federated networks (e.g. ActivityPub).
|
||||||
|
|
||||||
|
This value is separate from the `slug`, which is used specifically in the URL as a human-readable representation.
|
||||||
|
|
||||||
|
The handle is unique across-the-board between users/groups/categories.
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
description: A variable-length description of the category (usually displayed underneath the category name)
|
description: A variable-length description of the category (usually displayed underneath the category name)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
|
const meta = require('../meta');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
const slugify = require('../slugify');
|
const slugify = require('../slugify');
|
||||||
@@ -20,6 +21,7 @@ module.exports = function (Categories) {
|
|||||||
|
|
||||||
data.name = String(data.name || `Category ${cid}`);
|
data.name = String(data.name || `Category ${cid}`);
|
||||||
const slug = `${cid}/${slugify(data.name)}`;
|
const slug = `${cid}/${slugify(data.name)}`;
|
||||||
|
const handle = await Categories.generateHandle(slugify(data.name));
|
||||||
const smallestOrder = firstChild.length ? firstChild[0].score - 1 : 1;
|
const smallestOrder = firstChild.length ? firstChild[0].score - 1 : 1;
|
||||||
const order = data.order || smallestOrder; // If no order provided, place it at the top
|
const order = data.order || smallestOrder; // If no order provided, place it at the top
|
||||||
const colours = Categories.assignColours();
|
const colours = Categories.assignColours();
|
||||||
@@ -27,6 +29,7 @@ module.exports = function (Categories) {
|
|||||||
let category = {
|
let category = {
|
||||||
cid: cid,
|
cid: cid,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
handle,
|
||||||
description: data.description ? data.description : '',
|
description: data.description ? data.description : '',
|
||||||
descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '',
|
descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '',
|
||||||
icon: data.icon ? data.icon : '',
|
icon: data.icon ? data.icon : '',
|
||||||
@@ -146,6 +149,19 @@ module.exports = function (Categories) {
|
|||||||
await async.each(children, Categories.create);
|
await async.each(children, Categories.create);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateHandle(slug) {
|
||||||
|
let taken = await meta.slugTaken(slug);
|
||||||
|
let suffix;
|
||||||
|
while (taken) {
|
||||||
|
suffix = utils.generateUUID().slice(0, 8);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
taken = await meta.slugTaken(`${slug}-${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${slug}${suffix ? `-${suffix}` : ''}`;
|
||||||
|
}
|
||||||
|
Categories.generateHandle = generateHandle; // exported for upgrade script (4.0.0)
|
||||||
|
|
||||||
Categories.assignColours = function () {
|
Categories.assignColours = function () {
|
||||||
const backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946'];
|
const backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946'];
|
||||||
const text = ['#ffffff', '#ffffff', '#333333', '#ffffff', '#333333', '#ffffff', '#ffffff', '#ffffff'];
|
const text = ['#ffffff', '#ffffff', '#333333', '#ffffff', '#333333', '#ffffff', '#ffffff', '#ffffff'];
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ Categories.exists = async function (cids) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Categories.existsByHandle = async function (handle) {
|
||||||
|
if (Array.isArray(handle)) {
|
||||||
|
return await db.isSortedSetMembers('categoryhandle:cid', handle);
|
||||||
|
}
|
||||||
|
return await db.isSortedSetMember('categoryhandle:cid', handle);
|
||||||
|
};
|
||||||
|
|
||||||
Categories.getCategoryById = async function (data) {
|
Categories.getCategoryById = async function (data) {
|
||||||
const categories = await Categories.getCategories([data.cid]);
|
const categories = await Categories.getCategories([data.cid]);
|
||||||
if (!categories[0]) {
|
if (!categories[0]) {
|
||||||
@@ -67,6 +74,10 @@ Categories.getCategoryById = async function (data) {
|
|||||||
return result.category;
|
return result.category;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Categories.getCidByHandle = async function (handle) {
|
||||||
|
return await db.sortedSetScore('categoryhandle:cid', handle);
|
||||||
|
};
|
||||||
|
|
||||||
Categories.getAllCidsFromSet = async function (key) {
|
Categories.getAllCidsFromSet = async function (key) {
|
||||||
let cids = cache.get(key);
|
let cids = cache.get(key);
|
||||||
if (cids) {
|
if (cids) {
|
||||||
|
|||||||
@@ -19,18 +19,21 @@ Controller.webfinger = async (req, res) => {
|
|||||||
|
|
||||||
// Get the slug
|
// Get the slug
|
||||||
const slug = resource.slice(5, resource.length - (host.length + 1));
|
const slug = resource.slice(5, resource.length - (host.length + 1));
|
||||||
const uid = await user.getUidByUserslug(slug);
|
const [uid, cid] = await Promise.all([
|
||||||
|
user.getUidByUserslug(slug),
|
||||||
|
categories.getCidByHandle(slug),
|
||||||
|
]);
|
||||||
let response = {
|
let response = {
|
||||||
subject: `acct:${slug}@${host}`,
|
subject: `acct:${slug}@${host}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (slug.startsWith('cid.')) {
|
if (slug === hostname) {
|
||||||
response = await category(req.uid, slug.slice(4), response);
|
|
||||||
} else if (slug === hostname) {
|
|
||||||
response = application(response);
|
response = application(response);
|
||||||
} else if (uid) {
|
} else if (uid) {
|
||||||
response = await profile(req.uid, uid, response);
|
response = await profile(req.uid, uid, response);
|
||||||
|
} else if (cid) {
|
||||||
|
response = await category(req.uid, cid, response);
|
||||||
} else {
|
} else {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module.exports = function (Groups) {
|
|||||||
|
|
||||||
Groups.validateGroupName(data.name);
|
Groups.validateGroupName(data.name);
|
||||||
|
|
||||||
const exists = await meta.userOrGroupExists(data.name);
|
const exists = await meta.slugTaken(data.name);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
throw new Error('[[error:group-already-exists]]');
|
throw new Error('[[error:group-already-exists]]');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,19 +25,20 @@ Meta.blacklist = require('./blacklist');
|
|||||||
Meta.languages = require('./languages');
|
Meta.languages = require('./languages');
|
||||||
|
|
||||||
|
|
||||||
/* Assorted */
|
Meta.slugTaken = async function (slug) {
|
||||||
Meta.userOrGroupExists = async function (slug) {
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
}
|
}
|
||||||
const user = require('../user');
|
|
||||||
const groups = require('../groups');
|
const [user, groups, categories] = [require('../user'), require('../groups'), require('../categories')];
|
||||||
slug = slugify(slug);
|
slug = slugify(slug);
|
||||||
const [userExists, groupExists] = await Promise.all([
|
|
||||||
|
const exists = await Promise.all([
|
||||||
user.existsBySlug(slug),
|
user.existsBySlug(slug),
|
||||||
groups.existsBySlug(slug),
|
groups.existsBySlug(slug),
|
||||||
|
categories.existsByHandle(slug),
|
||||||
]);
|
]);
|
||||||
return userExists || groupExists;
|
return exists.some(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (nconf.get('isPrimary')) {
|
if (nconf.get('isPrimary')) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// const db = require('../../database');
|
const db = require('../../database');
|
||||||
const meta = require('../../meta');
|
const meta = require('../../meta');
|
||||||
|
const categories = require('../../categories');
|
||||||
|
const slugify = require('../../slugify');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'Setting up default configs/privileges re: ActivityPub',
|
name: 'Setting up default configs/privileges re: ActivityPub',
|
||||||
@@ -13,5 +15,20 @@ module.exports = {
|
|||||||
// Set default privileges for world category
|
// Set default privileges for world category
|
||||||
const install = require('../../install');
|
const install = require('../../install');
|
||||||
await install.giveWorldPrivileges();
|
await install.giveWorldPrivileges();
|
||||||
|
|
||||||
|
// Run through all categories and ensure their slugs are unique (incl. users/groups too)
|
||||||
|
const cids = await db.getSortedSetMembers('categories:cid');
|
||||||
|
const names = await db.getObjectsFields(cids.map(cid => `category:${cid}`), cids.map(() => 'name'));
|
||||||
|
|
||||||
|
const handles = await Promise.all(cids.map(async (cid, idx) => {
|
||||||
|
const { name } = names[idx];
|
||||||
|
const handle = await categories.generateHandle(slugify(name));
|
||||||
|
return handle;
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.setObjectBulk(cids.map((cid, idx) => [`category:${cid}`, { handle: handles[idx] }])),
|
||||||
|
db.sortedSetAdd('categoryhandle:cid', cids, handles),
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ module.exports = function (User) {
|
|||||||
let { username } = userData;
|
let { username } = userData;
|
||||||
while (true) {
|
while (true) {
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
const exists = await meta.userOrGroupExists(username);
|
const exists = await meta.slugTaken(username);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
return numTries ? username : null;
|
return numTries ? username : null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user