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;