add IObjectWithId type for APIs that work with objects required to have an ID.

This commit is contained in:
Hazelnoot 2025-03-02 21:08:05 -05:00
parent ad49faa956
commit f88430aebc
4 changed files with 23 additions and 17 deletions

View file

@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { IObject } from '@/core/activitypub/type.js'; import type { IObject, IObjectWithId } from '@/core/activitypub/type.js';
import { ApUtilityService } from './activitypub/ApUtilityService.js'; import { ApUtilityService } from './activitypub/ApUtilityService.js';
import type { Response } from 'node-fetch'; import type { Response } from 'node-fetch';
import type { URL } from 'node:url'; import type { URL } from 'node:url';
@ -217,7 +217,7 @@ export class HttpRequestService {
} }
@bindThis @bindThis
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> { public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObjectWithId> {
const res = await this.send(url, { const res = await this.send(url, {
method: 'GET', method: 'GET',
headers: { headers: {
@ -237,7 +237,7 @@ export class HttpRequestService {
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match. // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url); this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity; return activity as IObjectWithId;
} }
@bindThis @bindThis

View file

@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import type { IObject } from './type.js'; import type { IObject, IObjectWithId } from './type.js';
type Request = { type Request = {
url: string; url: string;
@ -185,7 +185,7 @@ export class ApRequestService {
* @param followAlternate * @param followAlternate
*/ */
@bindThis @bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<IObject> { public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<IObjectWithId> {
const _followAlternate = followAlternate ?? true; const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id); const keypair = await this.userKeypairService.getUserKeypair(user.id);
@ -273,6 +273,6 @@ export class ApRequestService {
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match. // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url); this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity; return activity as IObjectWithId;
} }
} }

View file

@ -19,7 +19,7 @@ import { fromTuple } from '@/misc/from-tuple.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApLogService, calculateDurationSince, extractObjectContext } from '@/core/ApLogService.js'; import { ApLogService, calculateDurationSince, extractObjectContext } from '@/core/ApLogService.js';
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { getApId, getNullableApId, isCollectionOrOrderedCollection } from './type.js'; import { getApId, getNullableApId, IObjectWithId, isCollectionOrOrderedCollection } 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';
@ -82,7 +82,7 @@ export class Resolver {
* In all other cases, the object is re-fetched from remote by input string or object ID. * In all other cases, the object is re-fetched from remote by input string or object ID.
*/ */
@bindThis @bindThis
public async secureResolve(input: ApObject, sentFromUri: string): Promise<IObject> { public async secureResolve(input: ApObject, sentFromUri: string): Promise<IObjectWithId> {
// Unpack arrays to get the value element. // Unpack arrays to get the value element.
const value = fromTuple(input); const value = fromTuple(input);
if (value == null) { if (value == null) {
@ -96,13 +96,15 @@ export class Resolver {
// Our security requires that the object ID matches the host authority that sent it, otherwise it can't be trusted. // Our security requires that the object ID matches the host authority that sent it, otherwise it can't be trusted.
// A mismatch isn't necessarily malicious, it just means we can't use the object we were given. // A mismatch isn't necessarily malicious, it just means we can't use the object we were given.
if (typeof(value) === 'object' && this.apUtilityService.haveSameAuthority(id, sentFromUri)) { if (typeof(value) === 'object' && this.apUtilityService.haveSameAuthority(id, sentFromUri)) {
return value; return value as IObjectWithId;
} }
// If the checks didn't pass, then we must fetch the object and use that. // If the checks didn't pass, then we must fetch the object and use that.
return await this.resolve(id); return await this.resolve(id);
} }
public async resolve(value: string | [string]): Promise<IObjectWithId>;
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject>;
@bindThis @bindThis
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> { public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -120,7 +122,7 @@ export class Resolver {
} }
} }
private async _resolveLogged(requestUri: string, host: string): Promise<IObject> { private async _resolveLogged(requestUri: string, host: string): Promise<IObjectWithId> {
const startTime = process.hrtime.bigint(); const startTime = process.hrtime.bigint();
const log = await this.apLogService.createFetchLog({ const log = await this.apLogService.createFetchLog({
@ -149,7 +151,7 @@ export class Resolver {
} }
} }
private async _resolve(value: string, host: string, log?: SkApFetchLog): Promise<IObject> { private async _resolve(value: string, host: string, log?: SkApFetchLog): Promise<IObjectWithId> {
if (value.includes('#')) { if (value.includes('#')) {
// URLs with fragment parts cannot be resolved correctly because // URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S). // the fragment part does not get transmitted over HTTP(S).
@ -168,7 +170,7 @@ export class Resolver {
this.history.add(value); this.history.add(value);
if (this.utilityService.isSelfHost(host)) { if (this.utilityService.isSelfHost(host)) {
return await this.resolveLocal(value); return await this.resolveLocal(value) as IObjectWithId;
} }
if (!this.utilityService.isFederationAllowedHost(host)) { if (!this.utilityService.isFederationAllowedHost(host)) {
@ -180,8 +182,8 @@ export class Resolver {
} }
const object = (this.user const object = (this.user
? await this.apRequestService.signedGet(value, this.user) as IObject ? await this.apRequestService.signedGet(value, this.user)
: await this.httpRequestService.getActivityJson(value)) as IObject; : await this.httpRequestService.getActivityJson(value));
if (log) { if (log) {
const { object: objectOnly, context, contextHash } = extractObjectContext(object); const { object: objectOnly, context, contextHash } = extractObjectContext(object);
@ -226,7 +228,7 @@ export class Resolver {
} }
@bindThis @bindThis
private resolveLocal(url: string): Promise<IObject> { private resolveLocal(url: string): Promise<IObjectWithId> {
const parsed = this.apDbResolverService.parseUri(url); const parsed = this.apDbResolverService.parseUri(url);
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`); if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`);
@ -241,7 +243,7 @@ export class Resolver {
} else { } else {
return this.apRendererService.renderNote(note, author); return this.apRendererService.renderNote(note, author);
} }
}); }) as Promise<IObjectWithId>;
case 'users': case 'users':
return this.usersRepository.findOneByOrFail({ id: parsed.id }) return this.usersRepository.findOneByOrFail({ id: parsed.id })
.then(user => this.apRendererService.renderPerson(user as MiLocalUser)); .then(user => this.apRendererService.renderPerson(user as MiLocalUser));
@ -251,7 +253,7 @@ export class Resolver {
this.notesRepository.findOneByOrFail({ id: parsed.id }), this.notesRepository.findOneByOrFail({ id: parsed.id }),
this.pollsRepository.findOneByOrFail({ noteId: parsed.id }), this.pollsRepository.findOneByOrFail({ noteId: parsed.id }),
]) ])
.then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)); .then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)) as Promise<IObjectWithId>;
case 'likes': case 'likes':
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction => return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null }))); this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));

View file

@ -39,6 +39,10 @@ export interface IObject {
sensitive?: boolean; sensitive?: boolean;
} }
export interface IObjectWithId extends IObject {
id: string;
}
/** /**
* Get array of ActivityStreams Objects id * Get array of ActivityStreams Objects id
*/ */