From 5f0bb5dcd7ced04035e409f7768c6bfccb950683 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 26 May 2025 11:16:48 -0400 Subject: [PATCH] implement resolver.resolveCollectionItems --- .../src/core/activitypub/ApResolverService.ts | 64 ++++++++++++++++++- packages/backend/src/core/activitypub/type.ts | 48 +++++++------- packages/misskey-js/src/autogen/types.ts | 2 + 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index a7c5125928..74050f456b 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -19,11 +19,12 @@ import { ApLogService, calculateDurationSince, extractObjectContext } from '@/co import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApId, getNullableApId, IObjectWithId, isCollectionOrOrderedCollection } from './type.js'; +import { toArray } from '@/misc/prelude/array.js'; +import { AnyCollection, getApId, getNullableApId, IObjectWithId, isCollection, isCollectionOrOrderedCollection, isCollectionPage, isOrderedCollection, isOrderedCollectionPage } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; -import type { IObject, ICollection, IOrderedCollection, ApObject } from './type.js'; +import type { IObject, ApObject } from './type.js'; export class Resolver { private history: Set; @@ -78,6 +79,65 @@ export class Resolver { } } + @bindThis + public async resolveCollectionItems(value: string | IObject, limit?: number, allowAnonymousItems?: boolean): Promise { + const items: IObjectWithId[] = []; + + const collection = await this.resolveCollection(value); + await this.resolveCollectionItemsTo(collection, limit, allowAnonymousItems, collection.id, items); + + return items; + } + + private async resolveCollectionItemsTo(current: AnyCollection | null, limit: number | undefined, allowAnonymousItems: boolean | undefined, sourceUri: string | undefined, destination: IObjectWithId[]): Promise { + // This is pulled up to avoid code duplication below + const iterate = async(items: ApObject): Promise => { + for (const item of toArray(items)) { + // Stop when we reach the fetch limit + if (this.history.size > this.recursionLimit) break; + + // Stop when we reach the item limit + if (limit != null && limit < 1) break; + + // Use secureResolve whenever possible, to avoid re-fetching items that were included inline. + const resolved = (sourceUri && !allowAnonymousItems) + ? await this.secureResolve(item, sourceUri) + : await this.resolve(getApId(item), allowAnonymousItems); + destination.push(resolved); + + // Decrement the outer variable directly, because the code below checks it too + if (limit != null) limit--; + } + }; + + while (current != null) { + // Iterate all items in the current page + if (current.items) { + await iterate(current.items); + } + if (current.orderedItems) { + await iterate(current.orderedItems); + } + + if (this.history.size >= this.recursionLimit) { + // Stop when we reach the fetch limit + current = null; + } else if (limit != null && limit < 1) { + // Stop when we reach the item limit + current = null; + } else if (isCollection(current) || isOrderedCollection(current)) { + // Continue to first page + current = current.first ? await this.resolveCollection(current.first, true) : null; + } else if (isCollectionPage(current) || isOrderedCollectionPage(current)) { + // Continue to next page + current = current.next ? await this.resolveCollection(current.next, true) : null; + } else { + // Stop in all other conditions + current = null; + } + } + } + /** * Securely resolves an AP object or URL that has been sent from another instance. * An input object is trusted if and only if its ID matches the authority of sentFromUri. diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 281733d484..e33dec18d7 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -125,48 +125,46 @@ export interface IActivity extends IObject { }; } -export interface ICollection extends IObject { +export interface CollectionBase extends IObject { + totalItems?: number; + first?: IObject | string; + last?: IObject | string; + current?: IObject | string; + partOf?: IObject | string; + next?: IObject | string; + prev?: IObject | string; + items?: ApObject; + orderedItems?: ApObject; +} + +export interface ICollection extends CollectionBase { type: 'Collection'; totalItems: number; - first?: IObject | string; - last?: IObject | string; - current?: IObject | string; items?: ApObject; + orderedItems?: undefined; } -export interface IOrderedCollection extends IObject { +export interface IOrderedCollection extends CollectionBase { type: 'OrderedCollection'; totalItems: number; - first?: IObject | string; - last?: IObject | string; - current?: IObject | string; + items?: undefined; orderedItems?: ApObject; } -export interface ICollectionPage extends IObject { +export interface ICollectionPage extends CollectionBase { type: 'CollectionPage'; - totalItems: number; - first?: IObject | string; - last?: IObject | string; - current?: IObject | string; - partOf?: IObject | string; - next?: IObject | string; - prev?: IObject | string; items?: ApObject; + orderedItems?: undefined; } -export interface IOrderedCollectionPage extends IObject { +export interface IOrderedCollectionPage extends CollectionBase { type: 'OrderedCollectionPage'; - totalItems: number; - first?: IObject | string; - last?: IObject | string; - current?: IObject | string; - partOf?: IObject | string; - next?: IObject | string; - prev?: IObject | string; + items?: undefined; orderedItems?: ApObject; } +export type AnyCollection = ICollection | IOrderedCollection | ICollectionPage | IOrderedCollectionPage; + export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; export const isPost = (object: IObject): object is IPost => { @@ -269,7 +267,7 @@ export const isCollectionPage = (object: IObject): object is ICollectionPage => export const isOrderedCollectionPage = (object: IObject): object is IOrderedCollectionPage => getApType(object) === 'OrderedCollectionPage'; -export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => +export const isCollectionOrOrderedCollection = (object: IObject): object is AnyCollection => isCollection(object) || isOrderedCollection(object) || isCollectionPage(object) || isOrderedCollectionPage(object); export interface IApPropertyValue extends IObject { diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 55302960dc..678e980892 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -12918,6 +12918,8 @@ export type operations = { content: { 'application/json': { uri: string; + expandCollectionItems?: boolean; + allowAnonymous?: boolean; }; }; };