feat: category handles, #12434

This commit is contained in:
Julian Lam
2024-03-22 12:39:48 -04:00
parent aafdefa7d6
commit 3cc99a178e
8 changed files with 69 additions and 13 deletions

View File

@@ -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)

View File

@@ -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'];

View File

@@ -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) {

View File

@@ -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);
} }

View File

@@ -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]]');
} }

View File

@@ -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')) {

View File

@@ -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),
]);
}, },
}; };

View File

@@ -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;
} }