implement resolver.resolveCollectionItems

This commit is contained in:
Hazelnoot 2025-05-26 11:16:48 -04:00
parent b506dd564b
commit 5f0bb5dcd7
3 changed files with 87 additions and 27 deletions

View file

@ -19,11 +19,12 @@ import { ApLogService, calculateDurationSince, extractObjectContext } from '@/co
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdentifiableError } from '@/misc/identifiable-error.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 { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js'; import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.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 { export class Resolver {
private history: Set<string>; private history: Set<string>;
@ -78,6 +79,65 @@ export class Resolver {
} }
} }
@bindThis
public async resolveCollectionItems(value: string | IObject, limit?: number, allowAnonymousItems?: boolean): Promise<IObjectWithId[]> {
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<void> {
// This is pulled up to avoid code duplication below
const iterate = async(items: ApObject): Promise<void> => {
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. * 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. * An input object is trusted if and only if its ID matches the authority of sentFromUri.

View file

@ -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'; type: 'Collection';
totalItems: number; totalItems: number;
first?: IObject | string;
last?: IObject | string;
current?: IObject | string;
items?: ApObject; items?: ApObject;
orderedItems?: undefined;
} }
export interface IOrderedCollection extends IObject { export interface IOrderedCollection extends CollectionBase {
type: 'OrderedCollection'; type: 'OrderedCollection';
totalItems: number; totalItems: number;
first?: IObject | string; items?: undefined;
last?: IObject | string;
current?: IObject | string;
orderedItems?: ApObject; orderedItems?: ApObject;
} }
export interface ICollectionPage extends IObject { export interface ICollectionPage extends CollectionBase {
type: 'CollectionPage'; type: 'CollectionPage';
totalItems: number;
first?: IObject | string;
last?: IObject | string;
current?: IObject | string;
partOf?: IObject | string;
next?: IObject | string;
prev?: IObject | string;
items?: ApObject; items?: ApObject;
orderedItems?: undefined;
} }
export interface IOrderedCollectionPage extends IObject { export interface IOrderedCollectionPage extends CollectionBase {
type: 'OrderedCollectionPage'; type: 'OrderedCollectionPage';
totalItems: number; items?: undefined;
first?: IObject | string;
last?: IObject | string;
current?: IObject | string;
partOf?: IObject | string;
next?: IObject | string;
prev?: IObject | string;
orderedItems?: ApObject; orderedItems?: ApObject;
} }
export type AnyCollection = ICollection | IOrderedCollection | ICollectionPage | IOrderedCollectionPage;
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event'];
export const isPost = (object: IObject): object is IPost => { 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 => export const isOrderedCollectionPage = (object: IObject): object is IOrderedCollectionPage =>
getApType(object) === 'OrderedCollectionPage'; 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); isCollection(object) || isOrderedCollection(object) || isCollectionPage(object) || isOrderedCollectionPage(object);
export interface IApPropertyValue extends IObject { export interface IApPropertyValue extends IObject {

View file

@ -12918,6 +12918,8 @@ export type operations = {
content: { content: {
'application/json': { 'application/json': {
uri: string; uri: string;
expandCollectionItems?: boolean;
allowAnonymous?: boolean;
}; };
}; };
}; };