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:
|
||||
type: string
|
||||
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:
|
||||
type: string
|
||||
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 plugins = require('../plugins');
|
||||
const meta = require('../meta');
|
||||
const privileges = require('../privileges');
|
||||
const utils = require('../utils');
|
||||
const slugify = require('../slugify');
|
||||
@@ -20,6 +21,7 @@ module.exports = function (Categories) {
|
||||
|
||||
data.name = String(data.name || `Category ${cid}`);
|
||||
const slug = `${cid}/${slugify(data.name)}`;
|
||||
const handle = await Categories.generateHandle(slugify(data.name));
|
||||
const smallestOrder = firstChild.length ? firstChild[0].score - 1 : 1;
|
||||
const order = data.order || smallestOrder; // If no order provided, place it at the top
|
||||
const colours = Categories.assignColours();
|
||||
@@ -27,6 +29,7 @@ module.exports = function (Categories) {
|
||||
let category = {
|
||||
cid: cid,
|
||||
name: data.name,
|
||||
handle,
|
||||
description: data.description ? data.description : '',
|
||||
descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '',
|
||||
icon: data.icon ? data.icon : '',
|
||||
@@ -146,6 +149,19 @@ module.exports = function (Categories) {
|
||||
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 () {
|
||||
const backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946'];
|
||||
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) {
|
||||
const categories = await Categories.getCategories([data.cid]);
|
||||
if (!categories[0]) {
|
||||
@@ -67,6 +74,10 @@ Categories.getCategoryById = async function (data) {
|
||||
return result.category;
|
||||
};
|
||||
|
||||
Categories.getCidByHandle = async function (handle) {
|
||||
return await db.sortedSetScore('categoryhandle:cid', handle);
|
||||
};
|
||||
|
||||
Categories.getAllCidsFromSet = async function (key) {
|
||||
let cids = cache.get(key);
|
||||
if (cids) {
|
||||
|
||||
@@ -19,18 +19,21 @@ Controller.webfinger = async (req, res) => {
|
||||
|
||||
// Get the slug
|
||||
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 = {
|
||||
subject: `acct:${slug}@${host}`,
|
||||
};
|
||||
|
||||
try {
|
||||
if (slug.startsWith('cid.')) {
|
||||
response = await category(req.uid, slug.slice(4), response);
|
||||
} else if (slug === hostname) {
|
||||
if (slug === hostname) {
|
||||
response = application(response);
|
||||
} else if (uid) {
|
||||
response = await profile(req.uid, uid, response);
|
||||
} else if (cid) {
|
||||
response = await category(req.uid, cid, response);
|
||||
} else {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ module.exports = function (Groups) {
|
||||
|
||||
Groups.validateGroupName(data.name);
|
||||
|
||||
const exists = await meta.userOrGroupExists(data.name);
|
||||
const exists = await meta.slugTaken(data.name);
|
||||
if (exists) {
|
||||
throw new Error('[[error:group-already-exists]]');
|
||||
}
|
||||
|
||||
@@ -25,19 +25,20 @@ Meta.blacklist = require('./blacklist');
|
||||
Meta.languages = require('./languages');
|
||||
|
||||
|
||||
/* Assorted */
|
||||
Meta.userOrGroupExists = async function (slug) {
|
||||
Meta.slugTaken = async function (slug) {
|
||||
if (!slug) {
|
||||
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);
|
||||
const [userExists, groupExists] = await Promise.all([
|
||||
|
||||
const exists = await Promise.all([
|
||||
user.existsBySlug(slug),
|
||||
groups.existsBySlug(slug),
|
||||
categories.existsByHandle(slug),
|
||||
]);
|
||||
return userExists || groupExists;
|
||||
return exists.some(Boolean);
|
||||
};
|
||||
|
||||
if (nconf.get('isPrimary')) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
// const db = require('../../database');
|
||||
const db = require('../../database');
|
||||
const meta = require('../../meta');
|
||||
const categories = require('../../categories');
|
||||
const slugify = require('../../slugify');
|
||||
|
||||
module.exports = {
|
||||
name: 'Setting up default configs/privileges re: ActivityPub',
|
||||
@@ -13,5 +15,20 @@ module.exports = {
|
||||
// Set default privileges for world category
|
||||
const install = require('../../install');
|
||||
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;
|
||||
while (true) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
const exists = await meta.userOrGroupExists(username);
|
||||
const exists = await meta.slugTaken(username);
|
||||
if (!exists) {
|
||||
return numTries ? username : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user