From 08e2b6ee3297755fd85a20904d3a30f9a069ffba Mon Sep 17 00:00:00 2001
From: Kagami Sascha Rosylight <saschanaz@outlook.com>
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 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<void> {
 		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<void> {
-		// 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<void> {
 		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<void> {
+		// 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<T extends unknown[]>(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);
+			}
+		});
 	});
 });