From 08e2b6ee3297755fd85a20904d3a30f9a069ffba Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 9 Jul 2023 23:34:07 +0200 Subject: [PATCH] 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 114a39b9c..fdc6350ba 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 86c2a8271..6663fdc05 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 acb7012c9..daeebd159 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); + } + }); }); });