From 1f1417a0f7505276af8d4b5d81edb44c7648b499 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Sun, 1 Apr 2018 19:18:36 +0900 Subject: [PATCH] Implement outbox --- .../activitypub/{ => renderer}/context.ts | 0 .../remote/activitypub/renderer/document.ts | 7 +++ .../remote/activitypub/renderer/hashtag.ts | 7 +++ .../remote/activitypub/renderer/image.ts | 6 +++ src/common/remote/activitypub/renderer/key.ts | 9 ++++ .../remote/activitypub/renderer/note.ts | 44 ++++++++++++++++ .../renderer/ordered-collection.ts | 6 +++ .../remote/activitypub/renderer/person.ts | 20 ++++++++ src/server/activitypub/index.ts | 6 ++- src/server/activitypub/outbox.ts | 45 ++++++++++++++++ src/server/activitypub/post.ts | 51 ++----------------- src/server/activitypub/user.ts | 36 +++---------- 12 files changed, 161 insertions(+), 76 deletions(-) rename src/common/remote/activitypub/{ => renderer}/context.ts (100%) create mode 100644 src/common/remote/activitypub/renderer/document.ts create mode 100644 src/common/remote/activitypub/renderer/hashtag.ts create mode 100644 src/common/remote/activitypub/renderer/image.ts create mode 100644 src/common/remote/activitypub/renderer/key.ts create mode 100644 src/common/remote/activitypub/renderer/note.ts create mode 100644 src/common/remote/activitypub/renderer/ordered-collection.ts create mode 100644 src/common/remote/activitypub/renderer/person.ts create mode 100644 src/server/activitypub/outbox.ts diff --git a/src/common/remote/activitypub/context.ts b/src/common/remote/activitypub/renderer/context.ts similarity index 100% rename from src/common/remote/activitypub/context.ts rename to src/common/remote/activitypub/renderer/context.ts diff --git a/src/common/remote/activitypub/renderer/document.ts b/src/common/remote/activitypub/renderer/document.ts new file mode 100644 index 0000000000..4a456416a9 --- /dev/null +++ b/src/common/remote/activitypub/renderer/document.ts @@ -0,0 +1,7 @@ +import config from '../../../../conf'; + +export default ({ _id, contentType }) => ({ + type: 'Document', + mediaType: contentType, + url: `${config.drive_url}/${_id}` +}); diff --git a/src/common/remote/activitypub/renderer/hashtag.ts b/src/common/remote/activitypub/renderer/hashtag.ts new file mode 100644 index 0000000000..ad42700204 --- /dev/null +++ b/src/common/remote/activitypub/renderer/hashtag.ts @@ -0,0 +1,7 @@ +import config from '../../../../conf'; + +export default tag => ({ + type: 'Hashtag', + href: `${config.url}/search?q=#${encodeURIComponent(tag)}`, + name: '#' + tag +}); diff --git a/src/common/remote/activitypub/renderer/image.ts b/src/common/remote/activitypub/renderer/image.ts new file mode 100644 index 0000000000..345fbbec59 --- /dev/null +++ b/src/common/remote/activitypub/renderer/image.ts @@ -0,0 +1,6 @@ +import config from '../../../../conf'; + +export default ({ _id }) => ({ + type: 'Image', + url: `${config.drive_url}/${_id}` +}); diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/common/remote/activitypub/renderer/key.ts new file mode 100644 index 0000000000..7148c59745 --- /dev/null +++ b/src/common/remote/activitypub/renderer/key.ts @@ -0,0 +1,9 @@ +import config from '../../../../conf'; +import { extractPublic } from '../../../../crypto_key'; +import { ILocalAccount } from '../../../../models/user'; + +export default ({ username, account }) => ({ + type: 'Key', + owner: `${config.url}/@${username}`, + publicKeyPem: extractPublic((account as ILocalAccount).keypair) +}); diff --git a/src/common/remote/activitypub/renderer/note.ts b/src/common/remote/activitypub/renderer/note.ts new file mode 100644 index 0000000000..2fe20b2136 --- /dev/null +++ b/src/common/remote/activitypub/renderer/note.ts @@ -0,0 +1,44 @@ +import renderDocument from './document'; +import renderHashtag from './hashtag'; +import config from '../../../../conf'; +import DriveFile from '../../../../models/drive-file'; +import Post from '../../../../models/post'; +import User from '../../../../models/user'; + +export default async (user, post) => { + const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } }); + let inReplyTo; + + if (post.replyId) { + const inReplyToPost = await Post.findOne({ + _id: post.replyId, + }); + + if (inReplyToPost !== null) { + const inReplyToUser = await User.findOne({ + _id: post.userId, + }); + + if (inReplyToUser !== null) { + inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`; + } + } + } else { + inReplyTo = null; + } + + const attributedTo = `${config.url}/@${user.username}`; + + return { + id: `${attributedTo}/${post._id}`, + type: 'Note', + attributedTo, + content: post.textHtml, + published: post.createdAt.toISOString(), + to: 'https://www.w3.org/ns/activitystreams#Public', + cc: `${attributedTo}/followers`, + inReplyTo, + attachment: (await promisedFiles).map(renderDocument), + tag: post.tags.map(renderHashtag) + }; +}; diff --git a/src/common/remote/activitypub/renderer/ordered-collection.ts b/src/common/remote/activitypub/renderer/ordered-collection.ts new file mode 100644 index 0000000000..2ca0f77354 --- /dev/null +++ b/src/common/remote/activitypub/renderer/ordered-collection.ts @@ -0,0 +1,6 @@ +export default (id, totalItems, orderedItems) => ({ + id, + type: 'OrderedCollection', + totalItems, + orderedItems +}); diff --git a/src/common/remote/activitypub/renderer/person.ts b/src/common/remote/activitypub/renderer/person.ts new file mode 100644 index 0000000000..7303b30385 --- /dev/null +++ b/src/common/remote/activitypub/renderer/person.ts @@ -0,0 +1,20 @@ +import renderImage from './image'; +import renderKey from './key'; +import config from '../../../../conf'; + +export default user => { + const id = `${config.url}/@${user.username}`; + + return { + type: 'Person', + id, + inbox: `${id}/inbox`, + outbox: `${id}/outbox`, + preferredUsername: user.username, + name: user.name, + summary: user.description, + icon: user.avatarId && renderImage({ _id: user.avatarId }), + image: user.bannerId && renderImage({ _id: user.bannerId }), + publicKey: renderKey(user) + }; +}; diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts index 6618c291f7..c81024d15f 100644 --- a/src/server/activitypub/index.ts +++ b/src/server/activitypub/index.ts @@ -1,14 +1,16 @@ import * as express from 'express'; -import post from './post'; import user from './user'; import inbox from './inbox'; +import outbox from './outbox'; +import post from './post'; const app = express(); app.disable('x-powered-by'); -app.use(post); app.use(user); app.use(inbox); +app.use(outbox); +app.use(post); export default app; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts new file mode 100644 index 0000000000..c5a42ae0a9 --- /dev/null +++ b/src/server/activitypub/outbox.ts @@ -0,0 +1,45 @@ +import * as express from 'express'; +import context from '../../common/remote/activitypub/renderer/context'; +import renderNote from '../../common/remote/activitypub/renderer/note'; +import renderOrderedCollection from '../../common/remote/activitypub/renderer/ordered-collection'; +import parseAcct from '../../common/user/parse-acct'; +import config from '../../conf'; +import Post from '../../models/post'; +import User from '../../models/user'; + +const app = express(); +app.disable('x-powered-by'); + +app.get('/@:user/outbox', async (req, res) => { + const { username, host } = parseAcct(req.params.user); + if (host !== null) { + return res.sendStatus(422); + } + + const user = await User.findOne({ + usernameLower: username.toLowerCase(), + host: null + }); + if (user === null) { + return res.sendStatus(404); + } + + const id = `${config.url}/@${user.username}/inbox`; + + if (username !== user.username) { + return res.redirect(id); + } + + const posts = await Post.find({ userId: user._id }, { + limit: 20, + sort: { _id: -1 } + }); + + const renderedPosts = await Promise.all(posts.map(post => renderNote(user, post))); + const rendered = renderOrderedCollection(id, user.postsCount, renderedPosts); + rendered['@context'] = context; + + res.json(rendered); +}); + +export default app; diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts index bdfce0606d..6644563d8c 100644 --- a/src/server/activitypub/post.ts +++ b/src/server/activitypub/post.ts @@ -1,8 +1,7 @@ import * as express from 'express'; -import context from '../../common/remote/activitypub/context'; +import context from '../../common/remote/activitypub/renderer/context'; +import render from '../../common/remote/activitypub/renderer/note'; import parseAcct from '../../common/user/parse-acct'; -import config from '../../conf'; -import DriveFile from '../../models/drive-file'; import Post from '../../models/post'; import User from '../../models/user'; @@ -36,50 +35,10 @@ app.get('/@:user/:post', async (req, res, next) => { return res.sendStatus(404); } - const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } }); - let inReplyTo; + const rendered = await render(user, post); + rendered['@context'] = context; - if (post.replyId) { - const inReplyToPost = await Post.findOne({ - _id: post.replyId, - }); - - if (inReplyToPost !== null) { - const inReplyToUser = await User.findOne({ - _id: post.userId, - }); - - if (inReplyToUser !== null) { - inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`; - } - } - } else { - inReplyTo = null; - } - - const attributedTo = `${config.url}/@${user.username}`; - - res.json({ - '@context': context, - id: `${attributedTo}/${post._id}`, - type: 'Note', - attributedTo, - content: post.textHtml, - published: post.createdAt.toISOString(), - to: 'https://www.w3.org/ns/activitystreams#Public', - cc: `${attributedTo}/followers`, - inReplyTo, - attachment: (await promisedFiles).map(({ _id, contentType }) => ({ - type: 'Document', - mediaType: contentType, - url: `${config.drive_url}/${_id}` - })), - tag: post.tags.map(tag => ({ - type: 'Hashtag', - href: `${config.url}/search?q=#${encodeURIComponent(tag)}`, - name: '#' + tag - })) - }); + res.json(rendered); }); export default app; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts index ef365c2078..d43a9793d4 100644 --- a/src/server/activitypub/user.ts +++ b/src/server/activitypub/user.ts @@ -1,9 +1,9 @@ import * as express from 'express'; import config from '../../conf'; -import { extractPublic } from '../../crypto_key'; -import context from '../../common/remote/activitypub/context'; +import context from '../../common/remote/activitypub/renderer/context'; +import render from '../../common/remote/activitypub/renderer/person'; import parseAcct from '../../common/user/parse-acct'; -import User, { ILocalAccount } from '../../models/user'; +import User from '../../models/user'; const app = express(); app.disable('x-powered-by'); @@ -27,34 +27,14 @@ app.get('/@:user', async (req, res, next) => { return res.sendStatus(404); } - const id = `${config.url}/@${user.username}`; - if (username !== user.username) { - return res.redirect(id); + return res.redirect(`${config.url}/@${user.username}`); } - res.json({ - '@context': context, - type: 'Person', - id, - inbox: `${id}/inbox`, - preferredUsername: user.username, - name: user.name, - summary: user.description, - icon: user.avatarId && { - type: 'Image', - url: `${config.drive_url}/${user.avatarId}` - }, - image: user.bannerId && { - type: 'Image', - url: `${config.drive_url}/${user.bannerId}` - }, - publicKey: { - type: 'Key', - owner: id, - publicKeyPem: extractPublic((user.account as ILocalAccount).keypair) - } - }); + const rendered = render(user); + rendered['@context'] = context; + + res.json(rendered); }); export default app;