From 2e1de4fca940c46a96b63f964e38c813754a94d0 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Wed, 5 Jul 2023 03:38:59 +0200 Subject: [PATCH 01/15] feat(backend): fetch the first page of outbox when resolving Person --- .../src/core/activitypub/ApInboxService.ts | 10 +- .../src/core/activitypub/ApResolverService.ts | 16 ++- .../activitypub/models/ApPersonService.ts | 59 ++++++-- packages/backend/src/core/activitypub/type.ts | 30 ++++- packages/backend/test/unit/activitypub.ts | 127 +++++++++++++++++- 5 files changed, 217 insertions(+), 25 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index efef777fb0..114a39b9c8 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -24,7 +24,7 @@ import { QueueService } from '@/core/QueueService.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import type { RemoteUser } from '@/models/entities/User.js'; -import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isOrderedCollectionPage, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -86,10 +86,10 @@ export class ApInboxService { } @bindThis - public async performActivity(actor: RemoteUser, activity: IObject) { - if (isCollectionOrOrderedCollection(activity)) { + public async performActivity(actor: RemoteUser, activity: IObject, limit = Infinity) { + if (isCollectionOrOrderedCollection(activity) || isOrderedCollectionPage(activity)) { const resolver = this.apResolverService.createResolver(); - for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { + for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems).slice(0, limit)) { const act = await resolver.resolve(item); try { await this.performOneActivity(actor, act); @@ -366,7 +366,7 @@ export class ApInboxService { }); if (isPost(object)) { - this.createNote(resolver, actor, object, false, activity); + await this.createNote(resolver, actor, object, false, activity); } else { this.logger.warn(`Unknown type: ${getApType(object)}`); } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index d3e0345c9c..62dcad68df 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -10,11 +10,11 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; -import { isCollectionOrOrderedCollection } from './type.js'; +import { isCollectionOrOrderedCollection, isOrderedCollectionPage } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; -import type { IObject, ICollection, IOrderedCollection } from './type.js'; +import type { IObject, ICollection, IOrderedCollection, IOrderedCollectionPage } from './type.js'; export class Resolver { private history: Set; @@ -59,6 +59,18 @@ export class Resolver { } } + public async resolveOrderedCollectionPage(value: string | IObject): Promise { + const collection = typeof value === 'string' + ? await this.resolve(value) + : value; + + if (isOrderedCollectionPage(collection)) { + return collection; + } else { + throw new Error(`unrecognized collection type: ${collection.type}`); + } + } + @bindThis public async resolve(value: string | IObject): Promise { if (value == null) { diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f52ebed107..8fb83b553b 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -34,7 +34,8 @@ import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; -import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; +import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isOrderedCollection, isOrderedCollectionPage, isPropertyValue } from '../type.js'; +import { ApInboxService } from '../ApInboxService.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; import type { ApNoteService } from './ApNoteService.js'; @@ -62,6 +63,7 @@ export class ApPersonService implements OnModuleInit { private apResolverService: ApResolverService; private apNoteService: ApNoteService; private apImageService: ApImageService; + private apInboxService: ApInboxService; private apMfmService: ApMfmService; private mfmService: MfmService; private hashtagService: HashtagService; @@ -128,6 +130,7 @@ export class ApPersonService implements OnModuleInit { this.apResolverService = this.moduleRef.get('ApResolverService'); this.apNoteService = this.moduleRef.get('ApNoteService'); this.apImageService = this.moduleRef.get('ApImageService'); + this.apInboxService = this.moduleRef.get('ApInboxService'); this.apMfmService = this.moduleRef.get('ApMfmService'); this.mfmService = this.moduleRef.get('MfmService'); this.hashtagService = this.moduleRef.get('HashtagService'); @@ -281,7 +284,7 @@ export class ApPersonService implements OnModuleInit { // Create user let user: RemoteUser; try { - // Start transaction + // Start transaction await this.db.transaction(async transactionalEntityManager => { user = await transactionalEntityManager.save(new User({ id: this.idService.genId(), @@ -327,9 +330,9 @@ export class ApPersonService implements OnModuleInit { } }); } catch (e) { - // duplicate key error + // duplicate key error if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 + // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 const u = await this.usersRepository.findOneBy({ uri: person.id, }); @@ -406,7 +409,10 @@ export class ApPersonService implements OnModuleInit { }); //#endregion - await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err)); + await Promise.all([ + this.updateFeatured(user!.id, resolver), + this.updateOutboxFirstPage(user!, person.outbox, resolver), + ]).catch(err => this.logger.error(err)); return user!; } @@ -415,7 +421,7 @@ export class ApPersonService implements OnModuleInit { * Personの情報を更新します。 * Misskeyに対象のPersonが登録されていなければ無視します。 * もしアカウントの移行が確認された場合、アカウント移行処理を行います。 - * + * * @param uri URI of Person * @param resolver Resolver * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) @@ -498,7 +504,7 @@ export class ApPersonService implements OnModuleInit { (!exist.movedToUri && updates.movedToUri) || // 移行先がある→別のもの (exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri); - // 移行先がある→ない、ない→ないは無視 + // 移行先がある→ない、ない→ないは無視 if (moving) updates.movedAt = new Date(); @@ -598,9 +604,9 @@ export class ApPersonService implements OnModuleInit { @bindThis public analyzeAttachments(attachments: IObject | IObject[] | undefined) { const fields: { - name: string, - value: string - }[] = []; + name: string, + value: string + }[] = []; if (Array.isArray(attachments)) { for (const attachment of attachments.filter(isPropertyValue)) { fields.push({ @@ -613,8 +619,35 @@ export class ApPersonService implements OnModuleInit { return { fields }; } + /** + * Retrieve outbox from an actor object. + * + * This only retrieves the first page for now. + */ + public async updateOutboxFirstPage(user: RemoteUser, outbox: IActor['outbox'], resolver: Resolver): Promise { + // https://www.w3.org/TR/activitypub/#actor-objects + // Outbox is a required property for all actors + if (!outbox) { + throw new Error('No outbox property'); + } + + this.logger.info(`Fetching the outbox for ${user.uri}: ${outbox}`); + + const collection = await resolver.resolveCollection(outbox); + if (!isOrderedCollection(collection)) { + throw new Error('Outbox must be an ordered collection'); + } + + const firstPage = collection.first ? + await resolver.resolveOrderedCollectionPage(collection.first) : + collection; + + // Perform activity but only the first 100 ones + await this.apInboxService.performActivity(user, firstPage, 100); + } + @bindThis - public async updateFeatured(userId: User['id'], resolver?: Resolver) { + public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise { const user = await this.usersRepository.findOneByOrFail({ id: userId }); if (!this.userEntityService.isRemoteUser(user)) return; if (!user.featured) return; @@ -631,7 +664,7 @@ export class ApPersonService implements OnModuleInit { const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; const items = await Promise.all(toArray(unresolvedItems).map(x => _resolver.resolve(x))); - // Resolve and regist Notes + // Resolve and register Notes const limit = promiseLimit(2); const featuredNotes = await Promise.all(items .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも @@ -688,7 +721,7 @@ export class ApPersonService implements OnModuleInit { // (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする) dst = await this.resolvePerson(src.movedToUri); } - + if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ??? if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ??? if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri'; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 625135da6c..58371c8fe5 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -87,16 +87,37 @@ export interface IActivity extends IObject { }; } +// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection export interface ICollection extends IObject { type: 'Collection'; totalItems: number; + current?: ICollectionPage | string; + first?: ICollectionPage | string; + last?: ICollectionPage | string; items: ApObject; } -export interface IOrderedCollection extends IObject { +// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection +export interface IOrderedCollection extends Omit { type: 'OrderedCollection'; - totalItems: number; - orderedItems: ApObject; + + // orderedItems is not defined well + // https://github.com/w3c/activitystreams/issues/494 + orderedItems?: ApObject; +} + +// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage +export interface ICollectionPage extends Omit { + type: 'CollectionPage'; + partOf?: ICollection | string; + next?: ICollectionPage | string; + prev?: ICollectionPage | string; +} + +// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollectionpage +export interface IOrderedCollectionPage extends Omit, Omit { + type: 'OrderedCollectionPage'; + startIndex?: number, } export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; @@ -183,6 +204,9 @@ export const isCollection = (object: IObject): object is ICollection => export const isOrderedCollection = (object: IObject): object is IOrderedCollection => getApType(object) === 'OrderedCollection'; +export const isOrderedCollectionPage = (object: IObject): object is IOrderedCollectionPage => + getApType(object) === 'OrderedCollectionPage'; + export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => isCollection(object) || isOrderedCollection(object); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 7cd740a2fa..2b43437348 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -11,7 +11,7 @@ import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { IActor } from '@/core/activitypub/type.js'; +import type { IActor, ICreate, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js'; import { Note } from '@/models/index.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { MockResolver } from '../misc/mock-resolver.js'; @@ -32,6 +32,59 @@ function createRandomActor(): IActor & { id: string } { }; } +function createRandomCreateActivity(actor: IActor, length: number): ICreate[] { + return new Array(length).fill(null).map((): ICreate => { + const id = secureRndstr(8); + const noteId = `${host}/notes/${id}`; + + return { + type: 'Create', + id: `${noteId}/activity`, + actor, + object: { + id: noteId, + type: 'Note', + attributedTo: actor.id, + content: 'test test foo', + } satisfies IPost, + }; + }); +} + +function createRandomNonPagedOutbox(actor: IActor, length: number): IOrderedCollection { + const orderedItems = createRandomCreateActivity(actor, length); + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + id: actor.outbox as string, + totalItems: orderedItems.length, + orderedItems, + }; +} + +function createRandomOutboxPage(actor: IActor, id: string, length: number): IOrderedCollectionPage { + const orderedItems = createRandomCreateActivity(actor, length); + + return { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollectionPage', + id, + totalItems: orderedItems.length, + orderedItems, + }; +} + +function createRandomPagedOutbox(actor: IActor): IOrderedCollection { + return { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + id: actor.outbox as string, + totalItems: 10, + first: `${actor.outbox}?first`, + }; +} + describe('ActivityPub', () => { let noteService: ApNoteService; let personService: ApPersonService; @@ -53,7 +106,7 @@ describe('ActivityPub', () => { // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error const federatedInstanceService = app.get(FederatedInstanceService); - jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => {})); + jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { })); }); describe('Parse minimum object', () => { @@ -126,4 +179,74 @@ describe('ActivityPub', () => { } as Note); }); }); + + describe('Outbox', () => { + test('Fetch non-paged outbox from IActor', async () => { + const actor = createRandomActor(); + const outbox = createRandomNonPagedOutbox(actor, 10); + + resolver._register(actor.id, actor); + resolver._register(actor.outbox as string, outbox); + + // XXX: This shouldn't be needed as the collection already has the full information + // But somehow the resolver currently doesn't use it at all and always fetches again + for (const item of outbox.orderedItems as IObject[]) { + resolver._register(item.id!, item); + } + + await personService.createPerson(actor.id, resolver); + + for (const item of outbox.orderedItems as ICreate[]) { + const note = await noteService.fetchNote(item.object); + assert.ok(note); + assert.strictEqual(note.text, 'test test foo'); + assert.strictEqual(note.uri, (item.object as IObject).id); + } + }); + + test('Fetch paged outbox from IActor', async () => { + const actor = createRandomActor(); + const outbox = createRandomPagedOutbox(actor); + const page = createRandomOutboxPage(actor, outbox.id!, 10); + + resolver._register(actor.id, actor); + resolver._register(actor.outbox as string, outbox); + resolver._register(outbox.first as string, page); + + // XXX: This shouldn't be needed as the collection already has the full information + // But somehow the resolver currently doesn't use it at all and always fetches again + for (const item of page.orderedItems as IObject[]) { + resolver._register(item.id!, item); + } + + await personService.createPerson(actor.id, resolver); + + for (const item of page.orderedItems as ICreate[]) { + const note = await noteService.fetchNote(item.object); + assert.ok(note); + assert.strictEqual(note.text, 'test test foo'); + assert.strictEqual(note.uri, (item.object as IObject).id); + } + }); + + test('Fetch only the first 100 items', async () => { + const actor = createRandomActor(); + const outbox = createRandomNonPagedOutbox(actor, 200); + + resolver._register(actor.id, actor); + resolver._register(actor.outbox as string, outbox); + + // XXX: This shouldn't be needed as the collection already has the full information + // But somehow the resolver currently doesn't use it at all and always fetches again + for (const item of outbox.orderedItems as IObject[]) { + resolver._register(item.id!, item); + } + + await personService.createPerson(actor.id, resolver); + + const items = outbox.orderedItems as ICreate[]; + assert.ok(await noteService.fetchNote(items[99].object)); + assert.ok(!await noteService.fetchNote(items[100].object)); + }); + }); }); From a1388a8444542d9618475b6034a74c7430abc125 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Thu, 6 Jul 2023 03:39:16 +0200 Subject: [PATCH 02/15] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed725314b2..7fada0c536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように - ドライブファイルのメニューで画像をクロップできるように - 画像を動画と同様に簡単に隠せるように +- 最初照会したユーザーの最新ノートを受け取るように ### Server - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました From 5077df297335ff815a894bf99b27e0c221c2114d Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 7 Jul 2023 05:02:28 +0000 Subject: [PATCH 03/15] 100 -> 15 --- .../backend/src/core/activitypub/models/ApPersonService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 8fb83b553b..de0e377a70 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -642,8 +642,8 @@ export class ApPersonService implements OnModuleInit { await resolver.resolveOrderedCollectionPage(collection.first) : collection; - // Perform activity but only the first 100 ones - await this.apInboxService.performActivity(user, firstPage, 100); + // Perform activity but only the first 15 ones + await this.apInboxService.performActivity(user, firstPage, 15); } @bindThis From 7bf318ae989873814ca5848287fe7b7437f3f22c Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sat, 8 Jul 2023 15:13:19 +0200 Subject: [PATCH 04/15] remove extra resolving in tests --- packages/backend/test/unit/activitypub.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 2b43437348..530b6a9111 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -188,12 +188,6 @@ describe('ActivityPub', () => { resolver._register(actor.id, actor); resolver._register(actor.outbox as string, outbox); - // XXX: This shouldn't be needed as the collection already has the full information - // But somehow the resolver currently doesn't use it at all and always fetches again - for (const item of outbox.orderedItems as IObject[]) { - resolver._register(item.id!, item); - } - await personService.createPerson(actor.id, resolver); for (const item of outbox.orderedItems as ICreate[]) { @@ -213,12 +207,6 @@ describe('ActivityPub', () => { resolver._register(actor.outbox as string, outbox); resolver._register(outbox.first as string, page); - // XXX: This shouldn't be needed as the collection already has the full information - // But somehow the resolver currently doesn't use it at all and always fetches again - for (const item of page.orderedItems as IObject[]) { - resolver._register(item.id!, item); - } - await personService.createPerson(actor.id, resolver); for (const item of page.orderedItems as ICreate[]) { @@ -236,12 +224,6 @@ describe('ActivityPub', () => { resolver._register(actor.id, actor); resolver._register(actor.outbox as string, outbox); - // XXX: This shouldn't be needed as the collection already has the full information - // But somehow the resolver currently doesn't use it at all and always fetches again - for (const item of outbox.orderedItems as IObject[]) { - resolver._register(item.id!, item); - } - await personService.createPerson(actor.id, resolver); const items = outbox.orderedItems as ICreate[]; From 6087d020476762ccdd816d9c74edd977da0615fe Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sat, 8 Jul 2023 15:40:48 +0200 Subject: [PATCH 05/15] adjust tests --- .../src/core/activitypub/models/ApPersonService.ts | 4 ++-- packages/backend/test/misc/mock-resolver.ts | 6 +++++- packages/backend/test/unit/activitypub.ts | 10 +++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index de0e377a70..a20b715d0a 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -642,8 +642,8 @@ export class ApPersonService implements OnModuleInit { await resolver.resolveOrderedCollectionPage(collection.first) : collection; - // Perform activity but only the first 15 ones - await this.apInboxService.performActivity(user, firstPage, 15); + // Perform activity but only the first 20 ones + await this.apInboxService.performActivity(user, firstPage, 20); } @bindThis diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index a7bcd859ae..8b21d381d3 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -38,13 +38,17 @@ export class MockResolver extends Resolver { ); } - public async _register(uri: string, content: string | Record, type = 'application/activity+json') { + public _register(uri: string, content: string | Record, type = 'application/activity+json') { this._rs.set(uri, { type, content: typeof content === 'string' ? content : JSON.stringify(content), }); } + public clear() { + this._rs.clear(); + } + @bindThis public async resolve(value: string | IObject): Promise { if (typeof value !== 'string') return value; diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 530b6a9111..a95f0bfa5a 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -91,7 +91,7 @@ describe('ActivityPub', () => { let rendererService: ApRendererService; let resolver: MockResolver; - beforeEach(async () => { + beforeAll(async () => { const app = await Test.createTestingModule({ imports: [GlobalModule, CoreModule], }).compile(); @@ -109,6 +109,10 @@ describe('ActivityPub', () => { jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { })); }); + beforeEach(() => { + resolver.clear(); + }); + describe('Parse minimum object', () => { const actor = createRandomActor(); @@ -227,8 +231,8 @@ describe('ActivityPub', () => { await personService.createPerson(actor.id, resolver); const items = outbox.orderedItems as ICreate[]; - assert.ok(await noteService.fetchNote(items[99].object)); - assert.ok(!await noteService.fetchNote(items[100].object)); + assert.ok(await noteService.fetchNote(items[19].object)); + assert.ok(!await noteService.fetchNote(items[20].object)); }); }); }); From 45d0b46e7a54f06570fee176f995f20d3df289a3 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sat, 8 Jul 2023 15:44:27 +0200 Subject: [PATCH 06/15] typo --- packages/backend/test/misc/mock-resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 8b21d381d3..fd726b1fcd 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -56,7 +56,7 @@ export class MockResolver extends Resolver { const r = this._rs.get(value); if (!r) { - throw new Error('Not registed for mock'); + throw new Error('Not registered for mock'); } const object = JSON.parse(r.content); From a74af07992ed3af6a33a7de99b1360eb0a9e061d Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sat, 8 Jul 2023 15:48:10 +0200 Subject: [PATCH 07/15] reformat --- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index a3b935dca9..3b5975662f 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -311,7 +311,7 @@ export class ApPersonService implements OnModuleInit { } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 + // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 const u = await this.usersRepository.findOneBy({ uri: person.id }); if (u == null) throw new Error('already registered'); From 26040c2bb0335b9963c4126ec4be4088db1ae225 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 22:36:07 +0200 Subject: [PATCH 08/15] Update activitypub.ts --- packages/backend/test/unit/activitypub.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index c677980d45..6c5cba20b4 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -265,6 +265,7 @@ describe('ActivityPub', () => { assert.strictEqual(note.text, 'test test foo'); assert.strictEqual(note.uri, actor2Note.id); }); + }); describe('Outbox', () => { test('Fetch non-paged outbox from IActor', async () => { @@ -315,6 +316,6 @@ describe('ActivityPub', () => { const items = outbox.orderedItems as ICreate[]; assert.ok(await noteService.fetchNote(items[19].object)); assert.ok(!await noteService.fetchNote(items[20].object)); - }); + }); }); }); From b93046c0718b096db192eeb88c244e359ee58b1a Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 22:36:27 +0200 Subject: [PATCH 09/15] Update activitypub.ts --- packages/backend/test/unit/activitypub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 6c5cba20b4..5c3c57474e 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -288,7 +288,7 @@ describe('ActivityPub', () => { test('Fetch paged outbox from IActor', async () => { const actor = createRandomActor(); const outbox = createRandomPagedOutbox(actor); - const page = createRandomOutboxPage(actor, outbox.id!, 10); + const page = createRandomOutboxPage(actor, outbox.id, 10); resolver.register(actor.id, actor); resolver.register(actor.outbox as string, outbox); From 70bb9a4d1f12b89223c7d1cad45ab92c221e747f Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 22:36:41 +0200 Subject: [PATCH 10/15] Update activitypub.ts --- packages/backend/test/unit/activitypub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 5c3c57474e..ecea63cd7f 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -107,7 +107,7 @@ function createRandomPagedOutbox(actor: NonTransientIActor): IOrderedCollection id: actor.outbox as string, totalItems: 10, first: `${actor.outbox}?first`, - }; + }; } describe('ActivityPub', () => { From ca0c673b445a96e0216cf926061467ba3189026e Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 22:39:02 +0200 Subject: [PATCH 11/15] Update activitypub.ts --- packages/backend/test/unit/activitypub.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index ecea63cd7f..acb7012c96 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -11,7 +11,7 @@ import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { IActor, ICreate, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js'; +import type { IActor, ICollection, ICreate, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js'; import { Note } from '@/models/index.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { MockResolver } from '../misc/mock-resolver.js'; @@ -20,6 +20,9 @@ const host = 'https://host1.test'; type NonTransientIActor = IActor & { id: string }; type NonTransientIPost = IPost & { id: string }; +type NonTransientICollection = ICollection & { id: string }; +type NonTransientIOrderedCollection = IOrderedCollection & { id: string }; +type NonTransientIOrderedCollectionPage = IOrderedCollectionPage & { id: string }; function createRandomActor({ actorHost = host } = {}): NonTransientIActor { const preferredUsername = secureRndstr(8); @@ -51,7 +54,7 @@ function createRandomNotes(actor: NonTransientIActor, length: number): NonTransi return new Array(length).fill(null).map(() => createRandomNote(actor)); } -function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): ICollection { +function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): NonTransientICollection { const items = createRandomNotes(actor, length); return { @@ -65,7 +68,7 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe function createRandomCreateActivity(actor: NonTransientIActor, length: number): ICreate[] { return new Array(length).fill(null).map((): ICreate => { - const note = reateRandomNote(actor); + const note = createRandomNote(actor); return { type: 'Create', @@ -76,7 +79,7 @@ function createRandomCreateActivity(actor: NonTransientIActor, length: number): }); } -function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): IOrderedCollection { +function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): NonTransientIOrderedCollection { const orderedItems = createRandomCreateActivity(actor, length); return { @@ -88,7 +91,7 @@ function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): }; } -function createRandomOutboxPage(actor: NonTransientIActor, id: string, length: number): IOrderedCollectionPage { +function createRandomOutboxPage(actor: NonTransientIActor, id: string, length: number): NonTransientIOrderedCollectionPage { const orderedItems = createRandomCreateActivity(actor, length); return { @@ -100,7 +103,7 @@ function createRandomOutboxPage(actor: NonTransientIActor, id: string, length: n }; } -function createRandomPagedOutbox(actor: NonTransientIActor): IOrderedCollection { +function createRandomPagedOutbox(actor: NonTransientIActor): NonTransientIOrderedCollection { return { '@context': 'https://www.w3.org/ns/activitystreams', type: 'OrderedCollection', From 08e2b6ee3297755fd85a20904d3a30f9a069ffba Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 23:34:07 +0200 Subject: [PATCH 12/15] perform only create activities --- .../src/core/activitypub/ApInboxService.ts | 10 ++- .../activitypub/models/ApPersonService.ts | 62 ++++++++--------- packages/backend/test/unit/activitypub.ts | 68 +++++++++++++++---- 3 files changed, 96 insertions(+), 44 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 114a39b9c8..fdc6350bad 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -86,11 +86,19 @@ export class ApInboxService { } @bindThis - public async performActivity(actor: RemoteUser, activity: IObject, limit = Infinity) { + public async performActivity(actor: RemoteUser, activity: IObject, { + limit = Infinity, + allow = null as (string[] | null) } = {}, + ): Promise { if (isCollectionOrOrderedCollection(activity) || isOrderedCollectionPage(activity)) { const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems).slice(0, limit)) { const act = await resolver.resolve(item); + const type = getApType(act); + if (allow && !allow.includes(type)) { + this.logger.info(`skipping activity type: ${type}`); + continue; + } try { await this.performOneActivity(actor, act); } catch (err) { diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 86c2a82710..6663fdc05e 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -380,10 +380,10 @@ export class ApPersonService implements OnModuleInit { await this.usersRepository.update(user.id, { emojis: emojiNames }); //#endregion - await Promise.all([ - this.updateFeatured(user.id, resolver), - this.updateOutboxFirstPage(user, person.outbox, resolver), - ]).catch(err => this.logger.error(err)); + await Promise.allSettled([ + this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)), + this.updateOutboxFirstPage(user, person.outbox, resolver).catch(err => this.logger.error(err)), + ]); return user; } @@ -587,33 +587,6 @@ export class ApPersonService implements OnModuleInit { return fields; } - /** - * Retrieve outbox from an actor object. - * - * This only retrieves the first page for now. - */ - public async updateOutboxFirstPage(user: RemoteUser, outbox: IActor['outbox'], resolver: Resolver): Promise { - // https://www.w3.org/TR/activitypub/#actor-objects - // Outbox is a required property for all actors - if (!outbox) { - throw new Error('No outbox property'); - } - - this.logger.info(`Fetching the outbox for ${user.uri}: ${outbox}`); - - const collection = await resolver.resolveCollection(outbox); - if (!isOrderedCollection(collection)) { - throw new Error('Outbox must be an ordered collection'); - } - - const firstPage = collection.first ? - await resolver.resolveOrderedCollectionPage(collection.first) : - collection; - - // Perform activity but only the first 20 ones - await this.apInboxService.performActivity(user, firstPage, 20); - } - @bindThis public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise { const user = await this.usersRepository.findOneByOrFail({ id: userId }); @@ -659,6 +632,33 @@ export class ApPersonService implements OnModuleInit { }); } + /** + * Retrieve outbox from an actor object. + * + * This only retrieves the first page for now. + */ + public async updateOutboxFirstPage(user: RemoteUser, outbox: IActor['outbox'], resolver: Resolver): Promise { + // https://www.w3.org/TR/activitypub/#actor-objects + // Outbox is a required property for all actors + if (!outbox) { + throw new Error('No outbox property'); + } + + this.logger.info(`Fetching the outbox for ${user.uri}: ${outbox}`); + + const collection = await resolver.resolveCollection(outbox); + if (!isOrderedCollection(collection)) { + throw new Error('Outbox must be an ordered collection'); + } + + const firstPage = collection.first ? + await resolver.resolveOrderedCollectionPage(collection.first) : + collection; + + // Perform activity but only the first 20 ones with `type: Create` + await this.apInboxService.performActivity(user, firstPage, { limit: 20, allow: ['Create'] }); + } + /** * リモート由来のアカウント移行処理を行います * @param src 移行元アカウント(リモートかつupdatePerson後である必要がある、というかこれ自体がupdatePersonで呼ばれる前提) diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index acb7012c96..daeebd159c 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -11,7 +11,7 @@ import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { IActor, ICollection, ICreate, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js'; +import type { IActivity, IActor, ICollection, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js'; import { Note } from '@/models/index.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { MockResolver } from '../misc/mock-resolver.js'; @@ -24,6 +24,13 @@ type NonTransientICollection = ICollection & { id: string }; type NonTransientIOrderedCollection = IOrderedCollection & { id: string }; type NonTransientIOrderedCollectionPage = IOrderedCollectionPage & { id: string }; +/** + * Use when the order of the array is not definitive + */ +function deepSortedEqual(array1: unknown[], array2: T): asserts array1 is T { + return assert.deepStrictEqual(array1.sort(), array2.sort()); +} + function createRandomActor({ actorHost = host } = {}): NonTransientIActor { const preferredUsername = secureRndstr(8); const actorId = `${actorHost}/users/${preferredUsername.toLowerCase()}`; @@ -66,12 +73,12 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe }; } -function createRandomCreateActivity(actor: NonTransientIActor, length: number): ICreate[] { - return new Array(length).fill(null).map((): ICreate => { +function createRandomActivities(actor: NonTransientIActor, type: string, length: number): IActivity[] { + return new Array(length).fill(null).map((): IActivity => { const note = createRandomNote(actor); return { - type: 'Create', + type, id: `${note.id}/activity`, actor, object: note, @@ -80,7 +87,7 @@ function createRandomCreateActivity(actor: NonTransientIActor, length: number): } function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): NonTransientIOrderedCollection { - const orderedItems = createRandomCreateActivity(actor, length); + const orderedItems = createRandomActivities(actor, 'Create', length); return { '@context': 'https://www.w3.org/ns/activitystreams', @@ -92,7 +99,7 @@ function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): } function createRandomOutboxPage(actor: NonTransientIActor, id: string, length: number): NonTransientIOrderedCollectionPage { - const orderedItems = createRandomCreateActivity(actor, length); + const orderedItems = createRandomActivities(actor, 'Create', length); return { '@context': 'https://www.w3.org/ns/activitystreams', @@ -225,7 +232,7 @@ describe('ActivityPub', () => { await personService.createPerson(actor.id, resolver); // All notes in `featured` are same-origin, no need to fetch notes again - assert.deepStrictEqual(resolver.remoteGetTrials(), [actor.id, actor.featured]); + deepSortedEqual(resolver.remoteGetTrials(), [actor.id, actor.featured, actor.outbox]); // Created notes without resolving anything for (const item of featured.items as IPost[]) { @@ -256,9 +263,9 @@ describe('ActivityPub', () => { await personService.createPerson(actor1.id, resolver); // actor2Note is from a different server and needs to be fetched again - assert.deepStrictEqual( + deepSortedEqual( resolver.remoteGetTrials(), - [actor1.id, actor1.featured, actor2Note.id, actor2.id], + [actor1.id, actor1.featured, actor1.outbox, actor2Note.id, actor2.id, actor2.outbox], ); const note = await noteService.fetchNote(actor2Note.id); @@ -280,7 +287,12 @@ describe('ActivityPub', () => { await personService.createPerson(actor.id, resolver); - for (const item of outbox.orderedItems as ICreate[]) { + deepSortedEqual( + resolver.remoteGetTrials(), + [actor.id, actor.outbox], + ); + + for (const item of outbox.orderedItems as IActivity[]) { const note = await noteService.fetchNote(item.object); assert.ok(note); assert.strictEqual(note.text, 'test test foo'); @@ -299,7 +311,12 @@ describe('ActivityPub', () => { await personService.createPerson(actor.id, resolver); - for (const item of page.orderedItems as ICreate[]) { + deepSortedEqual( + resolver.remoteGetTrials(), + [actor.id, actor.outbox, outbox.first], + ); + + for (const item of page.orderedItems as IActivity[]) { const note = await noteService.fetchNote(item.object); assert.ok(note); assert.strictEqual(note.text, 'test test foo'); @@ -316,9 +333,36 @@ describe('ActivityPub', () => { await personService.createPerson(actor.id, resolver); - const items = outbox.orderedItems as ICreate[]; + const items = outbox.orderedItems as IActivity[]; + + deepSortedEqual( + resolver.remoteGetTrials(), + [actor.id, actor.outbox], + ); + assert.ok(await noteService.fetchNote(items[19].object)); assert.ok(!await noteService.fetchNote(items[20].object)); }); + + test('Perform only Create activities', async () => { + const actor = createRandomActor(); + const outbox = createRandomNonPagedOutbox(actor, 0); + outbox.orderedItems = createRandomActivities(actor, 'Announce', 10); + + resolver.register(actor.id, actor); + resolver.register(actor.outbox as string, outbox); + + await personService.createPerson(actor.id, resolver); + + deepSortedEqual( + resolver.remoteGetTrials(), + [actor.id, actor.outbox], + ); + + for (const item of outbox.orderedItems as IActivity[]) { + const note = await noteService.fetchNote(item.object); + assert.ok(!note); + } + }); }); }); From da0804eb1725fd18735e56413af50f92fa8b527c Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 15 Jul 2023 14:05:05 +0000 Subject: [PATCH 13/15] fix type error --- packages/backend/src/core/activitypub/ApRendererService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 797c6267b1..e005a7ff71 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -80,7 +80,7 @@ export class ApRendererService { } @bindThis - public renderAnnounce(object: string | IObject, note: Note): IAnnounce { + public renderAnnounce(object: string | IObject | null, note: Note): IAnnounce { const attributedTo = this.userEntityService.genLocalUserUri(note.userId); let to: string[] = []; From 76def0032ee4b36e521473f5709a53d41e9adc49 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 15 Jul 2023 14:06:36 +0000 Subject: [PATCH 14/15] ? --- packages/backend/src/core/activitypub/ApRendererService.ts | 2 +- packages/backend/test/unit/activitypub.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index e005a7ff71..797c6267b1 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -80,7 +80,7 @@ export class ApRendererService { } @bindThis - public renderAnnounce(object: string | IObject | null, note: Note): IAnnounce { + public renderAnnounce(object: string | IObject, note: Note): IAnnounce { const attributedTo = this.userEntityService.genLocalUserUri(note.userId); let to: string[] = []; diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index e378cfeb65..45ad1bf447 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -248,7 +248,7 @@ describe('ActivityPub', () => { describe('Renderer', () => { test('Render an announce with visibility: followers', () => { - rendererService.renderAnnounce(null, { + rendererService.renderAnnounce('hoge', { createdAt: new Date(0), visibility: 'followers', } as Note); From 60fd848182ac1a07528bfe1dd2af52beccc1c363 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 15 Jul 2023 14:24:05 +0000 Subject: [PATCH 15/15] configable --- chart/files/default.yml | 4 ++++ packages/backend/src/config.ts | 1 + .../backend/src/core/activitypub/models/ApPersonService.ts | 6 ++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/chart/files/default.yml b/chart/files/default.yml index e62032abfd..8fd259b67c 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -186,6 +186,10 @@ id: "aid" # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true +# Limit of notes to fetch from outbox with remote user first fetched (default: 5) +# https://github.com/misskey-dev/misskey/pull/11130 +outboxNotesFetchLimit: 5 + #allowedPrivateNetworks: [ # '127.0.0.1/32' #] diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 23ed6e59bc..55a3cd210f 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -96,6 +96,7 @@ export type Source = { videoThumbnailGenerator?: string; signToActivityPubGet?: boolean; + outboxNotesFetchLimit?: number; }; /** diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 6663fdc05e..f92743dce0 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -638,6 +638,8 @@ export class ApPersonService implements OnModuleInit { * This only retrieves the first page for now. */ public async updateOutboxFirstPage(user: RemoteUser, outbox: IActor['outbox'], resolver: Resolver): Promise { + if (!this.config.outboxNotesFetchLimit) return; + // https://www.w3.org/TR/activitypub/#actor-objects // Outbox is a required property for all actors if (!outbox) { @@ -655,8 +657,8 @@ export class ApPersonService implements OnModuleInit { await resolver.resolveOrderedCollectionPage(collection.first) : collection; - // Perform activity but only the first 20 ones with `type: Create` - await this.apInboxService.performActivity(user, firstPage, { limit: 20, allow: ['Create'] }); + // Perform activity but only the first outboxNotesFetchLimit ones with `type: Create` + await this.apInboxService.performActivity(user, firstPage, { limit: this.config.outboxNotesFetchLimit, allow: ['Create'] }); } /**