From 9dd06a7621d1745b30ed1c2b1d94d34143dd638e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Tue, 5 Feb 2019 17:42:55 +0900
Subject: [PATCH] =?UTF-8?q?/.well-known=20=E5=91=A8=E3=82=8A=E3=82=92?=
 =?UTF-8?q?=E3=81=84=E3=81=84=E6=84=9F=E3=81=98=E3=81=AB=20(#4141)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Enhance /.well-known and their friends

* Fix bug
---
 package.json                 |   4 ++
 src/@types/package.json.d.ts |   9 +++-
 src/misc/acct/parse.ts       |   4 +-
 src/misc/acct/render.ts      |   7 +--
 src/misc/acct/type.ts        |   6 +++
 src/server/index.ts          |  11 ++--
 src/server/nodeinfo.ts       |  73 +++++++++++++++++++++++++
 src/server/webfinger.ts      |  75 --------------------------
 src/server/well-known.ts     | 102 +++++++++++++++++++++++++++++++++++
 9 files changed, 202 insertions(+), 89 deletions(-)
 create mode 100644 src/misc/acct/type.ts
 create mode 100644 src/server/nodeinfo.ts
 delete mode 100644 src/server/webfinger.ts
 create mode 100644 src/server/well-known.ts

diff --git a/package.json b/package.json
index 6cdc695aef..3fb8ca290c 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,10 @@
 	"version": "10.81.0",
 	"clientVersion": "2.0.14026",
 	"codename": "nighthike",
+	"repository": {
+		"type": "git",
+		"url": "https://github.com/syuilo/misskey.git"
+	},
 	"main": "./index.js",
 	"private": true,
 	"scripts": {
diff --git a/src/@types/package.json.d.ts b/src/@types/package.json.d.ts
index 7cf07c1abc..abe5fae687 100644
--- a/src/@types/package.json.d.ts
+++ b/src/@types/package.json.d.ts
@@ -1,3 +1,10 @@
 declare module '*/package.json' {
-	const version: string;
+	interface IRepository {
+		type: string;
+		url: string;
+	}
+
+	export const name: string;
+	export const version: string;
+	export const repository: IRepository;
 }
diff --git a/src/misc/acct/parse.ts b/src/misc/acct/parse.ts
index b120746650..e3bed35d8e 100644
--- a/src/misc/acct/parse.ts
+++ b/src/misc/acct/parse.ts
@@ -1,4 +1,6 @@
-export default (acct: string) => {
+import Acct from './type';
+
+export default (acct: string): Acct => {
 	if (acct.startsWith('@')) acct = acct.substr(1);
 	const split = acct.split('@', 2);
 	return { username: split[0], host: split[1] || null };
diff --git a/src/misc/acct/render.ts b/src/misc/acct/render.ts
index 92ee2010a6..67e063fcb3 100644
--- a/src/misc/acct/render.ts
+++ b/src/misc/acct/render.ts
@@ -1,8 +1,5 @@
-type UserLike = {
-	host: string;
-	username: string;
-};
+import Acct from './type';
 
-export default (user: UserLike) => {
+export default (user: Acct) => {
 	return user.host === null ? user.username : `${user.username}@${user.host}`;
 };
diff --git a/src/misc/acct/type.ts b/src/misc/acct/type.ts
new file mode 100644
index 0000000000..c88a920c69
--- /dev/null
+++ b/src/misc/acct/type.ts
@@ -0,0 +1,6 @@
+type Acct = {
+	username: string;
+	host: string;
+};
+
+export default Acct;
diff --git a/src/server/index.ts b/src/server/index.ts
index 720a191d55..0e1c701050 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -16,7 +16,8 @@ import * as requestStats from 'request-stats';
 import * as slow from 'koa-slow';
 
 import activityPub from './activitypub';
-import webFinger from './webfinger';
+import nodeinfo from './nodeinfo';
+import wellKnown from './well-known';
 import config from '../config';
 import networkChart from '../chart/network';
 import apiServer from './api';
@@ -68,7 +69,8 @@ const router = new Router();
 
 // Routing
 router.use(activityPub.routes());
-router.use(webFinger.routes());
+router.use(nodeinfo.routes());
+router.use(wellKnown.routes());
 
 router.get('/verify-email/:code', async ctx => {
 	const user = await User.findOne({ emailVerifyCode: ctx.params.code });
@@ -88,11 +90,6 @@ router.get('/verify-email/:code', async ctx => {
 	}
 });
 
-// Return 404 for other .well-known
-router.all('/.well-known/*', async ctx => {
-	ctx.status = 404;
-});
-
 // Register router
 app.use(router.routes());
 
diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts
new file mode 100644
index 0000000000..5abb2e4973
--- /dev/null
+++ b/src/server/nodeinfo.ts
@@ -0,0 +1,73 @@
+import * as Router from 'koa-router';
+import config from '../config';
+import fetchMeta from '../misc/fetch-meta';
+import User from '../models/user';
+import { name as softwareName, version, repository } from '../../package.json';
+import Note from '../models/note';
+
+const router = new Router();
+
+const nodeinfo2_1path = '/nodeinfo/2.1';
+const nodeinfo2_0path = '/nodeinfo/2.0';
+
+export const links = [/* (awaiting release) {
+	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
+	href: config.url + nodeinfo2_1path
+}, */{
+	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
+	href: config.url + nodeinfo2_0path
+}];
+
+const nodeinfo2 = async () => {
+	const [
+		{ name, description, maintainer, langs, broadcasts, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker },
+		total,
+		activeHalfyear,
+		activeMonth,
+		localPosts,
+		localComments
+	] = await Promise.all([
+		fetchMeta(),
+		User.count({ host: null }),
+		User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 15552000000) } }),
+		User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 2592000000) } }),
+		Note.count({ '_user.host': null, replyId: null }),
+		Note.count({ '_user.host': null, replyId: { $ne: null } })
+	]);
+
+	return {
+		software: {
+			name: softwareName,
+			version,
+			repository: repository.url
+		},
+		protocols: ['activitypub'],
+		services: {
+			inbound: [] as string[],
+			outbound: ['atom1.0', 'rss2.0']
+		},
+		openRegistrations: !disableRegistration,
+		usage: {
+			users: { total, activeHalfyear, activeMonth },
+			localPosts,
+			localComments
+		},
+		metadata: { name, description, maintainer, langs, broadcasts, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker }
+	};
+};
+
+router.get(nodeinfo2_1path, async ctx => {
+	const base = await nodeinfo2();
+
+	ctx.body = { version: '2.1', ...base };
+});
+
+router.get(nodeinfo2_0path, async ctx => {
+	const base = await nodeinfo2();
+
+	delete base.software.repository;
+
+	ctx.body = { version: '2.0', ...base };
+});
+
+export default router;
diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts
deleted file mode 100644
index 0f3e53b60f..0000000000
--- a/src/server/webfinger.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import * as mongo from 'mongodb';
-import * as Router from 'koa-router';
-
-import config from '../config';
-import parseAcct from '../misc/acct/parse';
-import User, { IUser } from '../models/user';
-
-// Init router
-const router = new Router();
-
-router.get('/.well-known/webfinger', async ctx => {
-	if (typeof ctx.query.resource !== 'string') {
-		ctx.status = 400;
-		return;
-	}
-
-	const resourceLower = ctx.query.resource.toLowerCase();
-	let acctLower;
-	let id;
-
-	if (resourceLower.startsWith(config.url.toLowerCase() + '/@')) {
-		acctLower = resourceLower.split('/').pop();
-	} else if (resourceLower.startsWith(config.url.toLowerCase() + '/users/')) {
-		id = new mongo.ObjectID(resourceLower.split('/').pop());
-	} else if (resourceLower.startsWith('acct:')) {
-		acctLower = resourceLower.slice('acct:'.length);
-	} else {
-		acctLower = resourceLower;
-	}
-
-	let user: IUser;
-
-	if (acctLower) {
-		const parsedAcctLower = parseAcct(acctLower);
-		if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) {
-			ctx.status = 422;
-			return;
-		}
-
-		user = await User.findOne({
-			usernameLower: parsedAcctLower.username,
-			host: null
-		});
-	} else {
-		user = await User.findOne({
-			_id: id,
-			host: null
-		});
-	}
-
-	if (user === null) {
-		ctx.status = 404;
-		return;
-	}
-
-	ctx.body = {
-		subject: `acct:${user.username}@${config.host}`,
-		links: [{
-			rel: 'self',
-			type: 'application/activity+json',
-			href: `${config.url}/users/${user._id}`
-		}, {
-			rel: 'http://webfinger.net/rel/profile-page',
-			type: 'text/html',
-			href: `${config.url}/@${user.username}`
-		}, {
-			rel: 'http://ostatus.org/schema/1.0/subscribe',
-			template: `${config.url}/authorize-follow?acct={uri}`
-		}]
-	};
-
-	ctx.set('Cache-Control', 'public, max-age=180');
-});
-
-export default router;
diff --git a/src/server/well-known.ts b/src/server/well-known.ts
new file mode 100644
index 0000000000..3c994793e1
--- /dev/null
+++ b/src/server/well-known.ts
@@ -0,0 +1,102 @@
+import * as mongo from 'mongodb';
+import * as Router from 'koa-router';
+
+import config from '../config';
+import parseAcct from '../misc/acct/parse';
+import User from '../models/user';
+import Acct from '../misc/acct/type';
+import { links } from './nodeinfo';
+
+// Init router
+const router = new Router();
+
+const webFingerPath = '/.well-known/webfinger';
+
+router.get('/.well-known/host-meta', async ctx => {
+	ctx.set('Content-Type', 'application/xrd+xml');
+	ctx.body = `<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+  <Link rel="lrdd" type="application/xrd+xml" template="${config.url}${webFingerPath}?resource={uri}"/>
+</XRD>
+`;
+});
+
+router.get('/.well-known/host-meta.json', async ctx => {
+	ctx.set('Content-Type', 'application/jrd+json');
+	ctx.body = {
+		links: [{
+			rel: 'lrdd',
+			type: 'application/xrd+xml',
+			template: `${config.url}${webFingerPath}?resource={uri}`
+		}]
+	};
+});
+
+router.get('/.well-known/nodeinfo', async ctx => {
+	ctx.body = { links };
+});
+
+router.get(webFingerPath, async ctx => {
+	const generateQuery = (resource: string) =>
+		resource.startsWith(`${config.url.toLowerCase()}/users/`) ?
+			fromId(new mongo.ObjectID(resource.split('/').pop())) :
+			fromAcct(parseAcct(
+				resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop() :
+				resource.startsWith('acct:') ? resource.slice('acct:'.length) :
+				resource));
+
+	const fromId = (_id: mongo.ObjectID): Record<string, any> => ({
+			_id,
+			host: null
+		});
+
+	const fromAcct = (acct: Acct): Record<string, any> | number =>
+		!acct.host || acct.host === config.host.toLowerCase() ? {
+			usernameLower: acct.username,
+			host: null
+		} : 422;
+
+	if (typeof ctx.query.resource !== 'string') {
+		ctx.status = 400;
+		return;
+	}
+
+	const query = generateQuery(ctx.query.resource.toLowerCase());
+
+	if (typeof query === 'number') {
+		ctx.status = query;
+		return;
+	}
+
+	const user = await User.findOne(query);
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.body = {
+		subject: `acct:${user.username}@${config.host}`,
+		links: [{
+			rel: 'self',
+			type: 'application/activity+json',
+			href: `${config.url}/users/${user._id}`
+		}, {
+			rel: 'http://webfinger.net/rel/profile-page',
+			type: 'text/html',
+			href: `${config.url}/@${user.username}`
+		}, {
+			rel: 'http://ostatus.org/schema/1.0/subscribe',
+			template: `${config.url}/authorize-follow?acct={uri}`
+		}]
+	};
+
+	ctx.set('Cache-Control', 'public, max-age=180');
+});
+
+// Return 404 for other .well-known
+router.all('/.well-known/*', async ctx => {
+	ctx.status = 404;
+});
+
+export default router;
-- 
GitLab