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 = `
+
+
+
+`;
+});
+
+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 => ({
+ _id,
+ host: null
+ });
+
+ const fromAcct = (acct: Acct): Record | 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;