perform only create activities
This commit is contained in:
		| @@ -86,11 +86,19 @@ export class ApInboxService { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@bindThis | 	@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)) { | 		if (isCollectionOrOrderedCollection(activity) || isOrderedCollectionPage(activity)) { | ||||||
| 			const resolver = this.apResolverService.createResolver(); | 			const resolver = this.apResolverService.createResolver(); | ||||||
| 			for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems).slice(0, limit)) { | 			for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems).slice(0, limit)) { | ||||||
| 				const act = await resolver.resolve(item); | 				const act = await resolver.resolve(item); | ||||||
|  | 				const type = getApType(act); | ||||||
|  | 				if (allow && !allow.includes(type)) { | ||||||
|  | 					this.logger.info(`skipping activity type: ${type}`); | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
| 				try { | 				try { | ||||||
| 					await this.performOneActivity(actor, act); | 					await this.performOneActivity(actor, act); | ||||||
| 				} catch (err) { | 				} catch (err) { | ||||||
|   | |||||||
| @@ -380,10 +380,10 @@ export class ApPersonService implements OnModuleInit { | |||||||
| 		await this.usersRepository.update(user.id, { emojis: emojiNames }); | 		await this.usersRepository.update(user.id, { emojis: emojiNames }); | ||||||
| 		//#endregion | 		//#endregion | ||||||
|  |  | ||||||
| 		await Promise.all([ | 		await Promise.allSettled([ | ||||||
| 			this.updateFeatured(user.id, resolver), | 			this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)), | ||||||
| 			this.updateOutboxFirstPage(user, person.outbox, resolver), | 			this.updateOutboxFirstPage(user, person.outbox, resolver).catch(err => this.logger.error(err)), | ||||||
| 		]).catch(err => this.logger.error(err)); | 		]); | ||||||
|  |  | ||||||
| 		return user; | 		return user; | ||||||
| 	} | 	} | ||||||
| @@ -587,33 +587,6 @@ export class ApPersonService implements OnModuleInit { | |||||||
| 		return fields; | 		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 | 	@bindThis | ||||||
| 	public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> { | 	public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> { | ||||||
| 		const user = await this.usersRepository.findOneByOrFail({ id: userId }); | 		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で呼ばれる前提) | 	 * @param src 移行元アカウント(リモートかつupdatePerson後である必要がある、というかこれ自体がupdatePersonで呼ばれる前提) | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import { GlobalModule } from '@/GlobalModule.js'; | |||||||
| import { CoreModule } from '@/core/CoreModule.js'; | import { CoreModule } from '@/core/CoreModule.js'; | ||||||
| import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; | import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; | ||||||
| import { LoggerService } from '@/core/LoggerService.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 { Note } from '@/models/index.js'; | ||||||
| import { secureRndstr } from '@/misc/secure-rndstr.js'; | import { secureRndstr } from '@/misc/secure-rndstr.js'; | ||||||
| import { MockResolver } from '../misc/mock-resolver.js'; | import { MockResolver } from '../misc/mock-resolver.js'; | ||||||
| @@ -24,6 +24,13 @@ type NonTransientICollection = ICollection & { id: string }; | |||||||
| type NonTransientIOrderedCollection = IOrderedCollection & { id: string }; | type NonTransientIOrderedCollection = IOrderedCollection & { id: string }; | ||||||
| type NonTransientIOrderedCollectionPage = IOrderedCollectionPage & { 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 { | function createRandomActor({ actorHost = host } = {}): NonTransientIActor { | ||||||
| 	const preferredUsername = secureRndstr(8); | 	const preferredUsername = secureRndstr(8); | ||||||
| 	const actorId = `${actorHost}/users/${preferredUsername.toLowerCase()}`; | 	const actorId = `${actorHost}/users/${preferredUsername.toLowerCase()}`; | ||||||
| @@ -66,12 +73,12 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe | |||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|  |  | ||||||
| function createRandomCreateActivity(actor: NonTransientIActor, length: number): ICreate[] { | function createRandomActivities(actor: NonTransientIActor, type: string, length: number): IActivity[] { | ||||||
| 	return new Array(length).fill(null).map((): ICreate => { | 	return new Array(length).fill(null).map((): IActivity => { | ||||||
| 		const note = createRandomNote(actor); | 		const note = createRandomNote(actor); | ||||||
|  |  | ||||||
| 		return { | 		return { | ||||||
| 			type: 'Create', | 			type, | ||||||
| 			id: `${note.id}/activity`, | 			id: `${note.id}/activity`, | ||||||
| 			actor, | 			actor, | ||||||
| 			object: note, | 			object: note, | ||||||
| @@ -80,7 +87,7 @@ function createRandomCreateActivity(actor: NonTransientIActor, length: number): | |||||||
| } | } | ||||||
|  |  | ||||||
| function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): NonTransientIOrderedCollection { | function createRandomNonPagedOutbox(actor: NonTransientIActor, length: number): NonTransientIOrderedCollection { | ||||||
| 	const orderedItems = createRandomCreateActivity(actor, length); | 	const orderedItems = createRandomActivities(actor, 'Create', length); | ||||||
|  |  | ||||||
| 	return { | 	return { | ||||||
| 		'@context': 'https://www.w3.org/ns/activitystreams', | 		'@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 { | function createRandomOutboxPage(actor: NonTransientIActor, id: string, length: number): NonTransientIOrderedCollectionPage { | ||||||
| 	const orderedItems = createRandomCreateActivity(actor, length); | 	const orderedItems = createRandomActivities(actor, 'Create', length); | ||||||
|  |  | ||||||
| 	return { | 	return { | ||||||
| 		'@context': 'https://www.w3.org/ns/activitystreams', | 		'@context': 'https://www.w3.org/ns/activitystreams', | ||||||
| @@ -225,7 +232,7 @@ describe('ActivityPub', () => { | |||||||
| 			await personService.createPerson(actor.id, resolver); | 			await personService.createPerson(actor.id, resolver); | ||||||
|  |  | ||||||
| 			// All notes in `featured` are same-origin, no need to fetch notes again | 			// 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 | 			// Created notes without resolving anything | ||||||
| 			for (const item of featured.items as IPost[]) { | 			for (const item of featured.items as IPost[]) { | ||||||
| @@ -256,9 +263,9 @@ describe('ActivityPub', () => { | |||||||
| 			await personService.createPerson(actor1.id, resolver); | 			await personService.createPerson(actor1.id, resolver); | ||||||
|  |  | ||||||
| 			// actor2Note is from a different server and needs to be fetched again | 			// actor2Note is from a different server and needs to be fetched again | ||||||
| 			assert.deepStrictEqual( | 			deepSortedEqual( | ||||||
| 				resolver.remoteGetTrials(), | 				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); | 			const note = await noteService.fetchNote(actor2Note.id); | ||||||
| @@ -280,7 +287,12 @@ describe('ActivityPub', () => { | |||||||
|  |  | ||||||
| 			await personService.createPerson(actor.id, resolver); | 			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); | 				const note = await noteService.fetchNote(item.object); | ||||||
| 				assert.ok(note); | 				assert.ok(note); | ||||||
| 				assert.strictEqual(note.text, 'test test foo'); | 				assert.strictEqual(note.text, 'test test foo'); | ||||||
| @@ -299,7 +311,12 @@ describe('ActivityPub', () => { | |||||||
|  |  | ||||||
| 			await personService.createPerson(actor.id, resolver); | 			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); | 				const note = await noteService.fetchNote(item.object); | ||||||
| 				assert.ok(note); | 				assert.ok(note); | ||||||
| 				assert.strictEqual(note.text, 'test test foo'); | 				assert.strictEqual(note.text, 'test test foo'); | ||||||
| @@ -316,9 +333,36 @@ describe('ActivityPub', () => { | |||||||
|  |  | ||||||
| 			await personService.createPerson(actor.id, resolver); | 			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[19].object)); | ||||||
| 			assert.ok(!await noteService.fetchNote(items[20].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); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Kagami Sascha Rosylight
					Kagami Sascha Rosylight