From cba73d6bc1eeb44ad2beac2566812daa9ec32751 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 20:07:04 +0900
Subject: [PATCH] Implement account public key endpoint

---
 src/common/remote/activitypub/renderer/key.ts |  1 +
 src/server/activitypub/index.ts               |  2 +
 src/server/activitypub/outbox.ts              | 30 +++----------
 src/server/activitypub/publickey.ts           | 19 ++++++++
 src/server/activitypub/user.ts                | 43 +++++++------------
 src/server/activitypub/with-user.ts           | 23 ++++++++++
 6 files changed, 66 insertions(+), 52 deletions(-)
 create mode 100644 src/server/activitypub/publickey.ts
 create mode 100644 src/server/activitypub/with-user.ts

diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/common/remote/activitypub/renderer/key.ts
index 7148c5974..692c71f88 100644
--- a/src/common/remote/activitypub/renderer/key.ts
+++ b/src/common/remote/activitypub/renderer/key.ts
@@ -3,6 +3,7 @@ import { extractPublic } from '../../../../crypto_key';
 import { ILocalAccount } from '../../../../models/user';
 
 export default ({ username, account }) => ({
+	id: `${config.url}/@${username}/publickey`,
 	type: 'Key',
 	owner: `${config.url}/@${username}`,
 	publicKeyPem: extractPublic((account as ILocalAccount).keypair)
diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts
index c81024d15..ac7a184f2 100644
--- a/src/server/activitypub/index.ts
+++ b/src/server/activitypub/index.ts
@@ -3,6 +3,7 @@ import * as express from 'express';
 import user from './user';
 import inbox from './inbox';
 import outbox from './outbox';
+import publicKey from './publickey';
 import post from './post';
 
 const app = express();
@@ -11,6 +12,7 @@ app.disable('x-powered-by');
 app.use(user);
 app.use(inbox);
 app.use(outbox);
+app.use(publicKey);
 app.use(post);
 
 export default app;
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index c5a42ae0a..c26c4df75 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -2,44 +2,26 @@ 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';
+import withUser from './with-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);
-	}
-
+app.get('/@:user/outbox', withUser(username => {
+	return `${config.url}/@${username}/inbox`;
+}, async (user, req, res) => {
 	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);
+	const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.postsCount, renderedPosts);
 	rendered['@context'] = context;
 
 	res.json(rendered);
-});
+}));
 
 export default app;
diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts
new file mode 100644
index 000000000..e380309dc
--- /dev/null
+++ b/src/server/activitypub/publickey.ts
@@ -0,0 +1,19 @@
+import * as express from 'express';
+import context from '../../common/remote/activitypub/renderer/context';
+import render from '../../common/remote/activitypub/renderer/key';
+import config from '../../conf';
+import withUser from './with-user';
+
+const app = express();
+app.disable('x-powered-by');
+
+app.get('/@:user/publickey', withUser(username => {
+	return `${config.url}/@${username}/publickey`;
+}, (user, req, res) => {
+	const rendered = render(user);
+	rendered['@context'] = context;
+
+	res.json(rendered);
+}));
+
+export default app;
diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts
index d43a9793d..8e8deca4a 100644
--- a/src/server/activitypub/user.ts
+++ b/src/server/activitypub/user.ts
@@ -2,39 +2,26 @@ import * as express from 'express';
 import config from '../../conf';
 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 from '../../models/user';
-
-const app = express();
-app.disable('x-powered-by');
-
-app.get('/@:user', async (req, res, next) => {
-	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
-	if (!(['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
-		return next();
-	}
-
-	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);
-	}
-
-	if (username !== user.username) {
-		return res.redirect(`${config.url}/@${user.username}`);
-	}
+import withUser from './with-user';
 
+const respond = withUser(username => `${config.url}/@${username}`, (user, req, res) => {
 	const rendered = render(user);
 	rendered['@context'] = context;
 
 	res.json(rendered);
 });
 
+const app = express();
+app.disable('x-powered-by');
+
+app.get('/@:user', (req, res, next) => {
+	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
+
+	if ((['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) {
+		respond(req, res, next);
+	} else {
+		next();
+	}
+});
+
 export default app;
diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts
new file mode 100644
index 000000000..0bab47b78
--- /dev/null
+++ b/src/server/activitypub/with-user.ts
@@ -0,0 +1,23 @@
+import parseAcct from '../../common/user/parse-acct';
+import User from '../../models/user';
+
+export default (redirect, respond) => async (req, res, next) => {
+	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);
+	}
+
+	if (username !== user.username) {
+		return res.redirect(redirect(user.username));
+	}
+
+	return respond(user, req, res, next);
+}