diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 7211568e4c..e0db3e030d 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -2,10 +2,13 @@ const nconf = require('nconf'); const mime = require('mime'); +const path = require('path'); const user = require('../user'); +const categories = require('../categories'); const posts = require('../posts'); const topics = require('../topics'); +const utils = require('../utils'); const activitypub = module.parent.exports; const Mocks = module.exports; @@ -111,7 +114,9 @@ Mocks.post = async (objects) => { return single ? posts.pop() : posts; }; -Mocks.actor = async (uid) => { +Mocks.actors = {}; + +Mocks.actors.user = async (uid) => { let { username, userslug, displayname: name, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid); const publicKey = await activitypub.getPublicKey(uid); @@ -157,6 +162,36 @@ Mocks.actor = async (uid) => { }; }; +Mocks.actors.category = async (cid) => { + let { name, slug, description: summary, backgroundImage } = await categories.getCategoryData(cid); + + if (backgroundImage) { + const filename = utils.decodeHTMLEntities(backgroundImage).split('/').pop(); + const imagePath = path.join(nconf.get('upload_path'), 'category', filename); + backgroundImage = { + type: 'Image', + mediaType: mime.getType(imagePath), + url: `${nconf.get('url')}${utils.decodeHTMLEntities(backgroundImage)}`, + }; + } + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + id: `${nconf.get('url')}/category/${cid}`, + url: `${nconf.get('url')}/category/${slug}`, + // followers: , + // following: , + inbox: `${nconf.get('url')}/category/${cid}/inbox`, + outbox: `${nconf.get('url')}/category/${cid}/outbox`, + + type: 'Group', + name, + preferredUsername: name, + summary, + icon: backgroundImage, + }; +}; + Mocks.note = async (post) => { const id = `${nconf.get('url')}/post/${post.pid}`; const published = new Date(parseInt(post.timestamp, 10)).toISOString(); diff --git a/src/api/activitypub.js b/src/api/activitypub.js index 73f671da87..2152a75444 100644 --- a/src/api/activitypub.js +++ b/src/api/activitypub.js @@ -88,7 +88,7 @@ activitypubApi.update = {}; activitypubApi.update.profile = async (caller, { uid }) => { const [object, followers] = await Promise.all([ - activitypub.mocks.actor(uid), + activitypub.mocks.actors.user(uid), db.getSortedSetMembers(`followersRemote:${caller.uid}`), ]); diff --git a/src/controllers/activitypub/actors.js b/src/controllers/activitypub/actors.js index 4cd9fe240a..adf7758157 100644 --- a/src/controllers/activitypub/actors.js +++ b/src/controllers/activitypub/actors.js @@ -4,6 +4,7 @@ const nconf = require('nconf'); const meta = require('../../meta'); const posts = require('../../posts'); +const categories = require('../../categories'); const activitypub = require('../../activitypub'); const Actors = module.exports; @@ -33,7 +34,7 @@ Actors.application = async function (req, res) { Actors.user = async function (req, res) { // todo: view:users priv gate - const payload = await activitypub.mocks.actor(req.params.uid); + const payload = await activitypub.mocks.actors.user(req.params.uid); res.status(200).json(payload); }; @@ -56,3 +57,13 @@ Actors.note = async function (req, res, next) { const payload = await activitypub.mocks.note(post); res.status(200).json(payload); }; + +Actors.category = async function (req, res, next) { + const exists = await categories.exists(req.params.cid); + if (!exists) { + return next('route'); + } + + const payload = await activitypub.mocks.actors.category(req.params.cid); + res.status(200).json(payload); +}; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 8ae0ef98f6..d742af7d9a 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -90,6 +90,16 @@ Controller.getOutbox = async (req, res) => { }); }; +Controller.getCategoryOutbox = async (req, res) => { + // stub + res.status(200).json({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + totalItems: 0, + orderedItems: [], + }); +}; + Controller.postOutbox = async (req, res) => { // This is a client-to-server feature so it is deliberately not implemented at this time. res.sendStatus(405); diff --git a/src/controllers/well-known.js b/src/controllers/well-known.js index 2f4698613f..7cad3a9553 100644 --- a/src/controllers/well-known.js +++ b/src/controllers/well-known.js @@ -3,6 +3,7 @@ const nconf = require('nconf'); const user = require('../user'); +const categories = require('../categories'); const privileges = require('../privileges'); const Controller = module.exports; @@ -15,53 +16,86 @@ Controller.webfinger = async (req, res) => { return res.sendStatus(400); } - const canView = await privileges.global.can('view:users', req.uid); - if (!canView) { - return res.sendStatus(403); - } - // Get the slug const slug = resource.slice(5, resource.length - (host.length + 1)); - - let uid = await user.getUidByUserslug(slug); - if (slug === hostname) { - uid = 0; - } else if (!uid) { - return res.sendStatus(404); - } - - const response = { + const uid = await user.getUidByUserslug(slug); + let response = { subject: `acct:${slug}@${host}`, }; - if (uid) { - response.aliases = [ - `${nconf.get('url')}/uid/${uid}`, - `${nconf.get('url')}/user/${slug}`, - ]; + try { + if (slug.startsWith('cid.')) { + response = await category(req.uid, slug.slice(4), response); + } else if (slug === hostname) { + response = application(response); + } else if (uid) { + response = await profile(req.uid, uid, response); + } else { + return res.sendStatus(404); + } - response.links = [ - { - rel: 'self', - type: 'application/activity+json', - href: `${nconf.get('url')}/uid/${uid}`, // actor - }, - { - rel: 'http://webfinger.net/rel/profile-page', - type: 'text/html', - href: `${nconf.get('url')}/user/${slug}`, - }, - ]; - } else { - response.aliases = [nconf.get('url')]; - response.links = [ - { - rel: 'self', - type: 'application/activity+json', - href: `${nconf.get('url')}/actor`, // actor - }, - ]; + res.status(200).json(response); + } catch (e) { + res.sendStatus(400); } - - res.status(200).json(response); }; + +function application(response) { + response.aliases = [nconf.get('url')]; + response.links = [ + { + rel: 'self', + type: 'application/activity+json', + href: `${nconf.get('url')}/actor`, // actor + }, + ]; + + return response; +} + +async function profile(callerUid, uid, response) { + const canView = await privileges.global.can('view:users', callerUid); + if (!canView) { + throw new Error('[[error:no-privileges]]'); + } + const slug = await user.getUserField(uid, 'userslug'); + + response.aliases = [ + `${nconf.get('url')}/uid/${uid}`, + `${nconf.get('url')}/user/${slug}`, + ]; + + response.links = [ + { + rel: 'self', + type: 'application/activity+json', + href: `${nconf.get('url')}/uid/${uid}`, // actor + }, + { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${nconf.get('url')}/user/${slug}`, + }, + ]; + + return response; +} + +async function category(callerUid, cid, response) { + const canFind = await privileges.categories.can('find', cid, callerUid); + if (!canFind) { + throw new Error('[[error:no-privileges]]'); + } + const slug = await categories.getCategoryField(cid, 'slug'); + + response.aliases = [`${nconf.get('url')}/category/${slug}`]; + response.links = [ + { + rel: 'self', + type: 'application/activity+json', + href: `${nconf.get('url')}/category/${cid}`, // actor + }, + ]; + + return response; +} diff --git a/src/routes/activitypub.js b/src/routes/activitypub.js index ffc3a52bcd..5d136398f5 100644 --- a/src/routes/activitypub.js +++ b/src/routes/activitypub.js @@ -15,17 +15,21 @@ module.exports = function (app, middleware, controllers) { const middlewares = [middleware.activitypub.enabled, middleware.activitypub.assertS2S]; app.get('/actor', middlewares, controllers.activitypub.actors.application); + app.get('/uid/:uid', middlewares, controllers.activitypub.actors.user); app.get('/user/:userslug', [...middlewares, middleware.exposeUid], controllers.activitypub.actors.userBySlug); - app.get('/uid/:uid/inbox', middlewares, controllers.activitypub.getInbox); app.post('/uid/:uid/inbox', [...middlewares, middleware.activitypub.validate], controllers.activitypub.postInbox); - app.get('/uid/:uid/outbox', middlewares, controllers.activitypub.getOutbox); app.post('/uid/:uid/outbox', middlewares, controllers.activitypub.postOutbox); - app.get('/uid/:uid/following', middlewares, controllers.activitypub.getFollowing); app.get('/uid/:uid/followers', middlewares, controllers.activitypub.getFollowers); app.get('/post/:pid', middlewares, controllers.activitypub.actors.note); + + app.get('/category/:cid', middlewares, controllers.activitypub.actors.category); + app.get('/category/:cid/inbox', middlewares, controllers.activitypub.getInbox); + app.post('/category/:cid/inbox', [...middlewares, middleware.activitypub.validate], controllers.activitypub.postInbox); + app.get('/category/:cid/outbox', middlewares, controllers.activitypub.getCategoryOutbox); + app.post('/category/:cid/outbox', middlewares, controllers.activitypub.postOutbox); };