Skip to content

Commit

Permalink
feat: category handles, #12434
Browse files Browse the repository at this point in the history
  • Loading branch information
julianlam committed Mar 22, 2024
1 parent aafdefa commit 3cc99a1
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 13 deletions.
8 changes: 8 additions & 0 deletions public/openapi/components/schemas/CategoryObject.yaml
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions src/categories/create.js
Expand Up @@ -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');
Expand All @@ -20,13 +21,15 @@ 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();

let category = {
cid: cid,
name: data.name,
handle,
description: data.description ? data.description : '',
descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '',
icon: data.icon ? data.icon : '',
Expand Down Expand Up @@ -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'];
Expand Down
11 changes: 11 additions & 0 deletions src/categories/index.js
Expand Up @@ -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]) {
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 7 additions & 4 deletions src/controllers/well-known.js
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/groups/create.js
Expand Up @@ -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]]');
}
Expand Down
13 changes: 7 additions & 6 deletions src/meta/index.js
Expand Up @@ -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')) {
Expand Down
19 changes: 18 additions & 1 deletion src/upgrades/4.0.0/activitypub_setup.js
@@ -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',
Expand All @@ -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),
]);
},
};
2 changes: 1 addition & 1 deletion src/user/create.js
Expand Up @@ -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;
}
Expand Down

0 comments on commit 3cc99a1

Please sign in to comment.