diff --git a/src/models/user.ts b/src/models/user.ts index 36e3d6f2ea19..6f199c0b4c25 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -236,7 +236,7 @@ export interface IRemoteUser extends IUserBase { export type IUser = ILocalUser | IRemoteUser; export const isLocalUser = (user: any): user is ILocalUser => - user.host === null; + user.host == null; export const isRemoteUser = (user: any): user is IRemoteUser => !isLocalUser(user); diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts index 9a488d392b4f..fcbabb8dd081 100644 --- a/src/remote/activitypub/renderer/follow-user.ts +++ b/src/remote/activitypub/renderer/follow-user.ts @@ -12,5 +12,7 @@ export default async function renderFollowUser(id: mongo.ObjectID): Promise _id: id }); + if (user == null) return null; // TODO + return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri; } diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index a5985debefdc..9963d3c51208 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -6,17 +6,16 @@ import * as httpSignature from '@peertube/http-signature'; import { renderActivity } from '../remote/activitypub/renderer'; import Note, { INote } from '../models/note'; -import User, { isLocalUser, ILocalUser, IUser, isRemoteUser } from '../models/user'; +import User, { ILocalUser, IRemoteUser, isLocalUser, isRemoteUser } from '../models/user'; import Emoji from '../models/emoji'; import renderNote from '../remote/activitypub/renderer/note'; -import renderKey from '../remote/activitypub/renderer/key'; import renderPerson from '../remote/activitypub/renderer/person'; import renderEmoji from '../remote/activitypub/renderer/emoji'; -import Likes from './activitypub/likes'; import Outbox, { packActivity } from './activitypub/outbox'; import Followers from './activitypub/followers'; import Following from './activitypub/following'; import Featured from './activitypub/featured'; +import Publickey from './activitypub/publickey'; import { inbox as processInbox, inboxLazy as processInboxLazy } from '../queue'; import { isSelfHost } from '../misc/convert-host'; import NoteReaction from '../models/note-reaction'; @@ -37,9 +36,8 @@ const logger = new Logger('activitypub'); // Init router const router = new Router(); -//#region Routing - -async function inbox(ctx: Router.RouterContext) { +//#region inbox +router.post(['/inbox', '/users/:user/inbox'], async (ctx: Router.RouterContext) => { if (config.disableFederation) ctx.throw(404); if (ctx.req.headers.host !== config.host) { @@ -201,8 +199,10 @@ async function inbox(ctx: Router.RouterContext) { ctx.body = { queueId: queue.id, }; -} +}); +//#endregion +//#region Util accept handling const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; @@ -214,19 +214,9 @@ function isActivityPubReq(ctx: Router.RouterContext, preferAp = false) { return typeof accepted === 'string' && !accepted.match(/html/); } -function setCacheHeader(ctx: Router.RouterContext, note: INote) { - if (note.expiresAt) { - const s = (note.expiresAt.getTime() - new Date().getTime()) / 1000; - if (s < 180) { - ctx.set('Expires', note.expiresAt.toUTCString()); - return; - } - } - - ctx.set('Cache-Control', 'public, max-age=180'); - return; -} - +/** + * Set respose content-type by requested one + */ export function setResponseType(ctx: Router.RouterContext) { const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON); if (accept === LD_JSON) { @@ -235,11 +225,9 @@ export function setResponseType(ctx: Router.RouterContext) { ctx.response.type = ACTIVITY_JSON; } } +//#endregion -// inbox -router.post('/inbox', inbox); -router.post('/users/:user/inbox', inbox); - +//#region notes export const isNoteUserAvailable = async (note: INote) => { const user = await User.findOne({ _id: note.userId, @@ -250,14 +238,13 @@ export const isNoteUserAvailable = async (note: INote) => { return user != null; }; -// note -router.get('/notes/:note', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - +router.get(['/notes/:note', '/notes/:note/:activity'], async (ctx, next) => { + if (isActivityPubReq(ctx) === false) return await next(); if (config.disableFederation) ctx.throw(404); if (!ObjectID.isValid(ctx.params.note)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -269,15 +256,17 @@ router.get('/notes/:note', async (ctx, next) => { copyOnce: { $ne: true } }); - if (note == null || !await isNoteUserAvailable(note)) { + if (note == null || await isNoteUserAvailable(note) === false) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } // リモートだったらリダイレクト - if (note._user.host != null) { + if (!ctx.params.activity && note._user.host != null) { if (note.uri == null || isSelfHost(note._user.host)) { ctx.status = 500; + ctx.set('Cache-Control', 'public, max-age=30'); return; } ctx.redirect(note.uri); @@ -291,155 +280,131 @@ router.get('/notes/:note', async (ctx, next) => { }); } - ctx.body = renderActivity(await renderNote(note, false)); - setCacheHeader(ctx, note); - setResponseType(ctx); -}); - -// note activity -router.get('/notes/:note/activity', async ctx => { - if (config.disableFederation) ctx.throw(404); - - if (!ObjectID.isValid(ctx.params.note)) { - ctx.status = 404; - return; - } - - let note = await Note.findOne({ - _id: new ObjectID(ctx.params.note), - deletedAt: { $exists: false }, - '_user.host': null, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true }, - copyOnce: { $ne: true } - }); - - if (note == null || !await isNoteUserAvailable(note)) { - ctx.status = 404; - return; + ctx.body = renderActivity(await (ctx.params.activity ? packActivity(note) : renderNote(note, false))); + + // set cache header by note expires + if (note.expiresAt) { + const s = (note.expiresAt.getTime() - new Date().getTime()) / 1000; + if (s < 180) { + ctx.set('Expires', note.expiresAt.toUTCString()); + return; + } } - const meta = await fetchMeta(); - if (meta.exposeHome) { - note = Object.assign(note, { - visibility: 'home' - }); - } + ctx.set('Cache-Control', 'public, max-age=180'); - ctx.body = renderActivity(await packActivity(note)); - setCacheHeader(ctx, note); setResponseType(ctx); }); +//#endregion -// likes -router.get('/notes/:note/likes', Likes); - -// outbox -router.get('/users/:user/outbox', Outbox); - -// followers -router.get('/users/:user/followers', Followers); - -// following -router.get('/users/:user/following', Following); - -// featured -router.get('/users/:user/collections/featured', Featured); - -// publickey -router.get('/users/:user/publickey', async ctx => { - if (config.disableFederation) ctx.throw(404); - - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); +//#region users +//#region users utils +type UserDivision = 'local' | 'both'; + +/** + * Get valid user by userId + * @param userId userId + * @param userDivision UserDivision to get + * @returns user object or null + */ +async function getValidUser(userId: string, userDivision: 'local'): Promise; +async function getValidUser(userId: string, userDivision: 'both'): Promise; +async function getValidUser(userId: string, userDivision: UserDivision): Promise { + if (ObjectID.isValid(userId) === false) return null; const user = await User.findOne({ - _id: userId, + _id: new ObjectID(userId), isDeleted: { $ne: true }, isSuspended: { $ne: true }, noFederation: { $ne: true }, - host: null + ...(userDivision === 'local' ? { host: null } : {}), }); - if (user === null) { - ctx.status = 404; - return; - } + if (user == null) return null; + if (userDivision === 'local' && isLocalUser(user) === false) return null; - if (isLocalUser(user)) { - ctx.body = renderActivity(renderKey(user)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } else { - ctx.status = 400; - } -}); + return user; +}; +//#endregion + +//#region user by userId +router.get('/users/:userId', async (ctx, next) => { + if (!isActivityPubReq(ctx, true)) return await next(); + if (config.disableFederation) ctx.throw(404); -// user -async function userInfo(ctx: Router.RouterContext, user?: IUser | null) { + const user = await getValidUser(ctx.params.userId, 'both'); if (user == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); + return; + } + + if (isRemoteUser(user)) { + ctx.redirect(user.uri); return; } ctx.body = renderActivity(await renderPerson(user as ILocalUser)); ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); -} - -router.get('/users/:user', async (ctx, next) => { - if (!isActivityPubReq(ctx, true)) return await next(); +}); +//#endregion +//#region user by username +router.get('/@:username', async (ctx, next) => { + if (isActivityPubReq(ctx) === false) return await next(); if (config.disableFederation) ctx.throw(404); - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); - const user = await User.findOne({ - _id: userId, + usernameLower: ctx.params.username.toLowerCase(), isDeleted: { $ne: true }, isSuspended: { $ne: true }, noFederation: { $ne: true }, - }); + host: null + }) as ILocalUser; if (user == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } - if (isRemoteUser(user)) { - ctx.redirect(user.uri); - return; - } - - await userInfo(ctx, user); + ctx.body = renderActivity(await renderPerson(user as ILocalUser)); + ctx.set('Cache-Control', 'public, max-age=180'); + setResponseType(ctx); }); +//#endregion -router.get('/@:user', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - +//#region user objects +router.get(['/users/:userId/:obj', '/users/:userId/:obj/:obj2'], async (ctx, next) => { + if (isActivityPubReq(ctx) === false) return await next(); if (config.disableFederation) ctx.throw(404); - const user = await User.findOne({ - usernameLower: ctx.params.user.toLowerCase(), - isDeleted: { $ne: true }, - isSuspended: { $ne: true }, - noFederation: { $ne: true }, - host: null - }); + const user = await getValidUser(ctx.params.userId, 'local'); + if (user == null) { + ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=12'); + return; + } - await userInfo(ctx, user); + const subPath = ctx.params.obj + (ctx.params.obj2 ? `/${ctx.params.obj2}` : ''); + + switch (subPath) { + case 'followers': await Followers(ctx, user); return; + case 'following': await Following(ctx, user); return; + case 'outbox': await Outbox(ctx, user); return; + case 'publickey': await Publickey(ctx, user); return; + case 'collections/featured': await Featured(ctx, user); return; + default: + ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=13'); + return; + } }); //#endregion +//#endregion users + // emoji router.get('/emojis/:emoji', async ctx => { if (config.disableFederation) ctx.throw(404); @@ -451,6 +416,7 @@ router.get('/emojis/:emoji', async ctx => { if (emoji == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -465,6 +431,7 @@ router.get('/likes/:like', async ctx => { if (!ObjectID.isValid(ctx.params.like)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -474,6 +441,7 @@ router.get('/likes/:like', async ctx => { if (reaction == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -483,6 +451,7 @@ router.get('/likes/:like', async ctx => { if (note == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts index efa0664f4910..ece66a147e4e 100644 --- a/src/server/activitypub/featured.ts +++ b/src/server/activitypub/featured.ts @@ -1,37 +1,14 @@ import { ObjectID } from 'mongodb'; import * as Router from '@koa/router'; import config from '../../config'; -import User from '../../models/user'; +import { ILocalUser } from '../../models/user'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import { setResponseType } from '../activitypub'; import Note, { INote } from '../../models/note'; import renderNote from '../../remote/activitypub/renderer/note'; -export default async (ctx: Router.RouterContext) => { - if (config.disableFederation) ctx.throw(404); - - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); - - // Verify user - const user = await User.findOne({ - _id: userId, - isDeleted: { $ne: true }, - isSuspended: { $ne: true }, - noFederation: { $ne: true }, - host: null - }); - - if (user == null) { - ctx.status = 404; - return; - } - +export default async (ctx: Router.RouterContext, user: ILocalUser) => { const pinnedNoteIds = user.pinnedNoteIds || []; const pinnedNotes = await Promise.all(pinnedNoteIds.filter(ObjectID.isValid).map(id => Note.findOne({ _id: id }))); @@ -39,7 +16,7 @@ export default async (ctx: Router.RouterContext) => { const renderedNotes = await Promise.all(pinnedNotes.filter((note): note is INote => note != null && note.deletedAt == null).map(note => renderNote(note))); const rendered = renderOrderedCollection( - `${config.url}/users/${userId}/collections/featured`, + `${config.url}/users/${user._id}/collections/featured`, renderedNotes.length, null, null, renderedNotes ); diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts index a8d8c518c49f..2c5f0e37bfd6 100644 --- a/src/server/activitypub/followers.ts +++ b/src/server/activitypub/followers.ts @@ -1,9 +1,8 @@ -import { ObjectID } from 'mongodb'; import * as Router from '@koa/router'; import config from '../../config'; import $ from 'cafy'; import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; +import { ILocalUser } from '../../models/user'; import Following from '../../models/following'; import * as url from '../../prelude/url'; import { renderActivity } from '../../remote/activitypub/renderer'; @@ -12,16 +11,7 @@ import renderOrderedCollectionPage from '../../remote/activitypub/renderer/order import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; -export default async (ctx: Router.RouterContext) => { - if (config.disableFederation) ctx.throw(404); - - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); - +export default async (ctx: Router.RouterContext, user: ILocalUser) => { // Get 'cursor' parameter const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor); @@ -32,30 +22,18 @@ export default async (ctx: Router.RouterContext) => { // Validate parameters if (cursorErr || pageErr) { ctx.status = 400; - return; - } - - // Verify user - const user = await User.findOne({ - _id: userId, - isDeleted: { $ne: true }, - isSuspended: { $ne: true }, - noFederation: { $ne: true }, - host: null - }); - - if (user == null) { - ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } if (user.hideFollows === 'always' || user.hideFollows === 'follower') { ctx.status = 403; + ctx.set('Cache-Control', 'public, max-age=180'); return; } const limit = 10; - const partOf = `${config.url}/users/${userId}/followers`; + const partOf = `${config.url}/users/${user._id}/followers`; if (page) { const query = { diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts index f798b95184f5..9307b4eb4260 100644 --- a/src/server/activitypub/following.ts +++ b/src/server/activitypub/following.ts @@ -1,9 +1,8 @@ -import { ObjectID } from 'mongodb'; import * as Router from '@koa/router'; import config from '../../config'; import $ from 'cafy'; import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; +import { ILocalUser } from '../../models/user'; import Following from '../../models/following'; import * as url from '../../prelude/url'; import { renderActivity } from '../../remote/activitypub/renderer'; @@ -12,16 +11,7 @@ import renderOrderedCollectionPage from '../../remote/activitypub/renderer/order import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; -export default async (ctx: Router.RouterContext) => { - if (config.disableFederation) ctx.throw(404); - - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); - +export default async (ctx: Router.RouterContext, user: ILocalUser) => { // Get 'cursor' parameter const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor); @@ -32,30 +22,18 @@ export default async (ctx: Router.RouterContext) => { // Validate parameters if (cursorErr || pageErr) { ctx.status = 400; - return; - } - - // Verify user - const user = await User.findOne({ - _id: userId, - isDeleted: { $ne: true }, - isSuspended: { $ne: true }, - noFederation: { $ne: true }, - host: null - }); - - if (user == null) { - ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } if (user.hideFollows === 'always' || user.hideFollows === 'follower') { ctx.status = 403; + ctx.set('Cache-Control', 'public, max-age=180'); return; } const limit = 10; - const partOf = `${config.url}/users/${userId}/following`; + const partOf = `${config.url}/users/${user._id}/following`; if (page) { const query = { diff --git a/src/server/activitypub/likes.ts b/src/server/activitypub/likes.ts deleted file mode 100644 index e03f7b5e7679..000000000000 --- a/src/server/activitypub/likes.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ObjectID } from 'mongodb'; -import * as Router from '@koa/router'; -import config from '../../config'; -import $ from 'cafy'; -import ID, { transform } from '../../misc/cafy-id'; -import { renderLike } from '../../remote/activitypub/renderer/like'; -import { renderActivity } from '../../remote/activitypub/renderer'; -import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; -import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; -import { setResponseType, isNoteUserAvailable } from '../activitypub'; - -import Note from '../../models/note'; -import { sum } from '../../prelude/array'; -import * as url from '../../prelude/url'; -import NoteReaction from '../../models/note-reaction'; - -export default async (ctx: Router.RouterContext) => { - if (config.disableFederation) ctx.throw(404); - - if (!ObjectID.isValid(ctx.params.note)) { - ctx.status = 404; - return; - } - - const note = await Note.findOne({ - _id: new ObjectID(ctx.params.note), - deletedAt: { $exists: false }, - '_user.host': null, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true }, - copyOnce: { $ne: true } - }); - - if (note == null || !await isNoteUserAvailable(note)) { - ctx.status = 404; - return; - } - - // Get 'cursor' parameter - const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor); - - // Get 'page' parameter - const pageErr = !$.optional.str.or(['true', 'false']).ok(ctx.request.query.page); - const page: boolean = ctx.request.query.page === 'true'; - - // Validate parameters - if (cursorErr || pageErr) { - ctx.status = 400; - return; - } - - const limit = 100; - const partOf = `${config.url}/notes/${note._id}/likes`; - - if (page) { - const query = { - noteId: note._id - } as any; - - // カーソルが指定されている場合 - if (cursor) { - query._id = { - $lt: transform(cursor) - }; - } - - const reactions = await NoteReaction.find(query, { - limit: limit + 1, - sort: { _id: -1 }, - }); - - // 「次のページ」があるかどうか - const inStock = reactions.length === limit + 1; - if (inStock) reactions.pop(); - - const renderedLikes = await Promise.all(reactions.map(reaction => reaction.uri ?? renderLike(reaction, note))); - - const rendered = renderOrderedCollectionPage( - `${partOf}?${url.query({ - page: 'true', - cursor - })}`, - sum(Object.values(note.reactionCounts)), - renderedLikes, partOf, - null, - inStock ? `${partOf}?${url.query({ - page: 'true', - cursor: reactions[reactions.length - 1]._id.toHexString() - })}` : null - ); - - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } else { - // index page - const rendered = renderOrderedCollection(partOf, sum(Object.values(note.reactionCounts)), `${partOf}?page=true`, null); - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } -}; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index 52e983214fa1..c20235e7cee3 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -1,9 +1,8 @@ -import { ObjectID } from 'mongodb'; import * as Router from '@koa/router'; import config from '../../config'; import $ from 'cafy'; import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; +import { ILocalUser } from '../../models/user'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; @@ -16,16 +15,7 @@ import renderAnnounce from '../../remote/activitypub/renderer/announce'; import { countIf } from '../../prelude/array'; import * as url from '../../prelude/url'; -export default async (ctx: Router.RouterContext) => { - if (config.disableFederation) ctx.throw(404); - - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); - +export default async (ctx: Router.RouterContext, user: ILocalUser) => { // Get 'sinceId' parameter const [sinceId, sinceIdErr] = $.optional.type(ID).get(ctx.request.query.since_id); @@ -39,25 +29,12 @@ export default async (ctx: Router.RouterContext) => { // Validate parameters if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) { ctx.status = 400; - return; - } - - // Verify user - const user = await User.findOne({ - _id: userId, - isDeleted: { $ne: true }, - isSuspended: { $ne: true }, - noFederation: { $ne: true }, - host: null - }); - - if (user == null) { - ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } const limit = 20; - const partOf = `${config.url}/users/${userId}/outbox`; + const partOf = `${config.url}/users/${user._id}/outbox`; if (page) { //#region Construct query @@ -128,7 +105,7 @@ export default async (ctx: Router.RouterContext) => { * Pack Create or Announce Activity * @param note Note */ -export async function packActivity(note: INote): Promise { +export async function packActivity(note: INote): Promise { if (note.renoteId && note.text == null && note.poll == null && (note.fileIds == null || note.fileIds.length == 0)) { const renote = await Note.findOne(note.renoteId); if (!renote) throw new Error(`Note(${note._id}) の 対象Renote(${note.renoteId})が存在しない。DB壊れている?`); diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts new file mode 100644 index 000000000000..ddcbf137a3e0 --- /dev/null +++ b/src/server/activitypub/publickey.ts @@ -0,0 +1,11 @@ +import * as Router from '@koa/router'; +import { ILocalUser } from '../../models/user'; +import { renderActivity } from '../../remote/activitypub/renderer'; +import { setResponseType } from '../activitypub'; +import renderKey from '../../remote/activitypub/renderer/key'; + +export default async (ctx: Router.RouterContext, user: ILocalUser) => { + ctx.body = renderActivity(renderKey(user)); + ctx.set('Cache-Control', 'public, max-age=180'); + setResponseType(ctx); +};