From f915623335915d24ac1ccd1657342cd97bf14fbf Mon Sep 17 00:00:00 2001 From: mei23 Date: Wed, 21 Aug 2024 01:46:12 +0900 Subject: [PATCH 1/3] Negative Cache-Control --- src/server/activitypub.ts | 22 ++++++++++++++++++++++ src/server/activitypub/featured.ts | 2 ++ src/server/activitypub/followers.ts | 3 +++ src/server/activitypub/following.ts | 4 ++++ src/server/activitypub/likes.ts | 3 +++ src/server/activitypub/outbox.ts | 3 +++ 6 files changed, 37 insertions(+) diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index a5985debefdc..4abaa90ace7f 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -258,6 +258,7 @@ router.get('/notes/:note', async (ctx, next) => { if (!ObjectID.isValid(ctx.params.note)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -271,6 +272,7 @@ router.get('/notes/:note', async (ctx, next) => { if (note == null || !await isNoteUserAvailable(note)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -278,6 +280,7 @@ router.get('/notes/:note', async (ctx, next) => { if (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); @@ -302,6 +305,7 @@ router.get('/notes/:note/activity', async ctx => { if (!ObjectID.isValid(ctx.params.note)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -316,6 +320,7 @@ router.get('/notes/:note/activity', async ctx => { if (note == null || !await isNoteUserAvailable(note)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -352,6 +357,7 @@ router.get('/users/:user/publickey', async ctx => { if (!ObjectID.isValid(ctx.params.user)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -367,6 +373,7 @@ router.get('/users/:user/publickey', async ctx => { if (user === null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -376,13 +383,22 @@ router.get('/users/:user/publickey', async ctx => { setResponseType(ctx); } else { ctx.status = 400; + ctx.set('Cache-Control', 'public, max-age=180'); } }); +router.get('/users/:user/:obj', async (ctx, next) => { + if (!isActivityPubReq(ctx)) return await next(); + ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=31'); + return; +}); + // user async function userInfo(ctx: Router.RouterContext, user?: IUser | null) { if (user == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -398,6 +414,7 @@ router.get('/users/:user', async (ctx, next) => { if (!ObjectID.isValid(ctx.params.user)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -412,6 +429,7 @@ router.get('/users/:user', async (ctx, next) => { if (user == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -451,6 +469,7 @@ router.get('/emojis/:emoji', async ctx => { if (emoji == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -465,6 +484,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 +494,7 @@ router.get('/likes/:like', async ctx => { if (reaction == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -483,6 +504,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..ce8a0ad47961 100644 --- a/src/server/activitypub/featured.ts +++ b/src/server/activitypub/featured.ts @@ -13,6 +13,7 @@ export default async (ctx: Router.RouterContext) => { if (!ObjectID.isValid(ctx.params.user)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -29,6 +30,7 @@ export default async (ctx: Router.RouterContext) => { if (user == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts index a8d8c518c49f..4fdfd48542ff 100644 --- a/src/server/activitypub/followers.ts +++ b/src/server/activitypub/followers.ts @@ -32,6 +32,7 @@ export default async (ctx: Router.RouterContext) => { // Validate parameters if (cursorErr || pageErr) { ctx.status = 400; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -46,11 +47,13 @@ export default async (ctx: Router.RouterContext) => { 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; } diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts index f798b95184f5..44edac6f0790 100644 --- a/src/server/activitypub/following.ts +++ b/src/server/activitypub/following.ts @@ -17,6 +17,7 @@ export default async (ctx: Router.RouterContext) => { if (!ObjectID.isValid(ctx.params.user)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -32,6 +33,7 @@ export default async (ctx: Router.RouterContext) => { // Validate parameters if (cursorErr || pageErr) { ctx.status = 400; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -46,11 +48,13 @@ export default async (ctx: Router.RouterContext) => { 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; } diff --git a/src/server/activitypub/likes.ts b/src/server/activitypub/likes.ts index e03f7b5e7679..6c1129eee233 100644 --- a/src/server/activitypub/likes.ts +++ b/src/server/activitypub/likes.ts @@ -19,6 +19,7 @@ export default async (ctx: Router.RouterContext) => { if (!ObjectID.isValid(ctx.params.note)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -33,6 +34,7 @@ export default async (ctx: Router.RouterContext) => { if (note == null || !await isNoteUserAvailable(note)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -46,6 +48,7 @@ export default async (ctx: Router.RouterContext) => { // Validate parameters if (cursorErr || pageErr) { ctx.status = 400; + ctx.set('Cache-Control', 'public, max-age=180'); return; } diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index 52e983214fa1..5c5df6f8439b 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -21,6 +21,7 @@ export default async (ctx: Router.RouterContext) => { if (!ObjectID.isValid(ctx.params.user)) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -39,6 +40,7 @@ export default async (ctx: Router.RouterContext) => { // Validate parameters if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) { ctx.status = 400; + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -53,6 +55,7 @@ export default async (ctx: Router.RouterContext) => { if (user == null) { ctx.status = 404; + ctx.set('Cache-Control', 'public, max-age=180'); return; } From f517e0e296fac2176eef14acbc2f2037f06562d6 Mon Sep 17 00:00:00 2001 From: mei23 Date: Wed, 21 Aug 2024 04:19:21 +0900 Subject: [PATCH 2/3] refactor user --- src/models/user.ts | 2 +- .../activitypub/renderer/follow-user.ts | 2 + src/server/activitypub.ts | 146 ++++++++---------- src/server/activitypub/featured.ts | 31 +--- src/server/activitypub/followers.ts | 31 +--- src/server/activitypub/following.ts | 32 +--- src/server/activitypub/outbox.ts | 34 +--- src/server/activitypub/publickey.ts | 11 ++ 8 files changed, 94 insertions(+), 195 deletions(-) create mode 100644 src/server/activitypub/publickey.ts 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 4abaa90ace7f..11a341d135e3 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -6,10 +6,9 @@ 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'; @@ -17,6 +16,7 @@ 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'; @@ -339,93 +339,70 @@ router.get('/notes/:note/activity', async ctx => { // likes router.get('/notes/:note/likes', Likes); -// outbox -router.get('/users/:user/outbox', Outbox); +//#region user utils +type UserDivision = 'local' | 'both'; -// 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; - ctx.set('Cache-Control', 'public, max-age=180'); - return; - } - - const userId = new ObjectID(ctx.params.user); +/** + * 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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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; - ctx.set('Cache-Control', 'public, max-age=180'); - } -}); + return user; +}; +//#endregion -router.get('/users/:user/:obj', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - ctx.status = 404; - ctx.set('Cache-Control', 'public, max-age=31'); - return; -}); +//#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)) return await next(); if (config.disableFederation) ctx.throw(404); - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - ctx.set('Cache-Control', 'public, max-age=180'); - 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; @@ -433,31 +410,42 @@ router.get('/users/:user', async (ctx, next) => { 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 + // emoji router.get('/emojis/:emoji', async ctx => { if (config.disableFederation) ctx.throw(404); diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts index ce8a0ad47961..ece66a147e4e 100644 --- a/src/server/activitypub/featured.ts +++ b/src/server/activitypub/featured.ts @@ -1,39 +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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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 }))); @@ -41,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 4fdfd48542ff..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); @@ -36,21 +26,6 @@ export default async (ctx: Router.RouterContext) => { 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'); @@ -58,7 +33,7 @@ export default async (ctx: Router.RouterContext) => { } 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 44edac6f0790..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,17 +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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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); @@ -37,21 +26,6 @@ export default async (ctx: Router.RouterContext) => { 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'); @@ -59,7 +33,7 @@ export default async (ctx: Router.RouterContext) => { } 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/outbox.ts b/src/server/activitypub/outbox.ts index 5c5df6f8439b..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,17 +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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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); @@ -44,23 +33,8 @@ export default async (ctx: Router.RouterContext) => { 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 @@ -131,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); +}; From d81cccea3e9d63b92c9cf60c160f88b1cb8fb5c2 Mon Sep 17 00:00:00 2001 From: mei23 Date: Wed, 21 Aug 2024 05:08:41 +0900 Subject: [PATCH 3/3] a --- src/server/activitypub.ts | 99 +++++++++--------------------- src/server/activitypub/likes.ts | 105 -------------------------------- 2 files changed, 29 insertions(+), 175 deletions(-) delete mode 100644 src/server/activitypub/likes.ts diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index 11a341d135e3..9963d3c51208 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -11,7 +11,6 @@ import Emoji from '../models/emoji'; import renderNote from '../remote/activitypub/renderer/note'; 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'; @@ -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,10 +238,8 @@ 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)) { @@ -270,14 +256,14 @@ 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'); @@ -294,52 +280,25 @@ 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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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); - -//#region user utils +//#region users +//#region users utils type UserDivision = 'local' | 'both'; /** @@ -393,7 +352,7 @@ router.get('/users/:userId', async (ctx, next) => { //#region user by username router.get('/@:username', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); + if (isActivityPubReq(ctx) === false) return await next(); if (config.disableFederation) ctx.throw(404); const user = await User.findOne({ @@ -444,7 +403,7 @@ router.get(['/users/:userId/:obj', '/users/:userId/:obj/:obj2'], async (ctx, nex }); //#endregion -//#endregion +//#endregion users // emoji router.get('/emojis/:emoji', async ctx => { diff --git a/src/server/activitypub/likes.ts b/src/server/activitypub/likes.ts deleted file mode 100644 index 6c1129eee233..000000000000 --- a/src/server/activitypub/likes.ts +++ /dev/null @@ -1,105 +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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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; - ctx.set('Cache-Control', 'public, max-age=180'); - 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); - } -};