From ab97b916067c775467ed92d29250802f4835f649 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 26 Oct 2024 18:39:20 -0400 Subject: [PATCH 01/16] improve AP job clearing and failure logging --- .../activitypub/ApDeliverManagerService.ts | 3 +- .../src/core/activitypub/ApRendererService.ts | 4 +- .../src/core/activitypub/ApResolverService.ts | 27 ++++----- .../src/core/activitypub/JsonLdService.ts | 5 +- .../activitypub/misc/check-against-url.ts | 3 +- .../src/core/activitypub/misc/validator.ts | 8 +-- .../core/activitypub/models/ApImageService.ts | 3 +- .../core/activitypub/models/ApNoteService.ts | 58 +++++++++---------- .../activitypub/models/ApPersonService.ts | 51 ++++++++-------- .../activitypub/models/ApQuestionService.ts | 26 ++++----- packages/backend/src/core/activitypub/type.ts | 3 +- 11 files changed, 100 insertions(+), 91 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5d07cd8e8f..f045333d2a 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -128,7 +129,7 @@ class DeliverManager { for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; - if (inbox === null) throw new Error('inbox is null'); + if (inbox === null) throw new UnrecoverableError(`inbox is null: following ${following.id}`); inboxes.set(inbox, following.followerSharedInbox != null); } } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 42ee5bc58a..4df1cee8c3 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -7,6 +7,7 @@ import { createPublicKey, randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import * as mfm from '@transfem-org/sfm-js'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -30,6 +31,7 @@ import { IdService } from '@/core/IdService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; +import { getApId } from './type.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @Injectable() @@ -106,7 +108,7 @@ export class ApRendererService { to = [`${attributedTo}/followers`]; cc = []; } else { - throw new Error('renderAnnounce: cannot render non-public note'); + throw new UnrecoverableError(`renderAnnounce: cannot render non-public note: ${getApId(object)}`); } return { diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 25ccbdac60..d4964d544d 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; @@ -15,12 +16,12 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; +import { fromTuple } from '@/misc/from-tuple.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; -import { fromTuple } from '@/misc/from-tuple.js'; export class Resolver { private history: Set; @@ -67,7 +68,7 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new Error(`unrecognized collection type: ${collection.type}`); + throw new UnrecoverableError(`unrecognized collection type: ${collection.type}`); } } @@ -84,15 +85,15 @@ export class Resolver { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). // Avoid strange behaviour by not trying to resolve these at all. - throw new Error(`cannot resolve URL with fragment: ${value}`); + throw new UnrecoverableError(`cannot resolve URL with fragment: ${value}`); } if (this.history.has(value)) { - throw new Error('cannot resolve already resolved one'); + throw new Error(`cannot resolve already resolved URL: ${value}`); } if (this.history.size > this.recursionLimit) { - throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`); + throw new Error(`hit recursion limit: ${value}`); } this.history.add(value); @@ -103,7 +104,7 @@ export class Resolver { } if (!this.utilityService.isFederationAllowedHost(host)) { - throw new Error('Instance is blocked'); + throw new UnrecoverableError(`instance is blocked: ${value}`); } if (this.config.signToActivityPubGet && !this.user) { @@ -119,7 +120,7 @@ export class Resolver { !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' ) { - throw new Error('invalid response'); + throw new UnrecoverableError(`invalid AP object ${value}: does not have ActivityStreams context`); } // HttpRequestService / ApRequestService have already checked that @@ -127,11 +128,11 @@ export class Resolver { // object after redirects; here we double-check that no redirects // bounced between hosts if (object.id == null) { - throw new Error('invalid AP object: missing id'); + throw new UnrecoverableError(`invalid AP object ${value}: missing id`); } if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) { - throw new Error(`invalid AP object ${value}: id ${object.id} has different host`); + throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`); } return object; @@ -140,7 +141,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new Error('resolveLocal: not local'); + if (!parsed.local) throw new UnrecoverableError(`resolveLocal - not a local URL: ${url}`); switch (parsed.type) { case 'notes': @@ -169,7 +170,7 @@ export class Resolver { case 'follows': return this.followRequestsRepository.findOneBy({ id: parsed.id }) .then(async followRequest => { - if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID'); + if (followRequest == null) throw new UnrecoverableError(`resolveLocal - invalid follow request ID ${parsed.id}: ${url}`); const [follower, followee] = await Promise.all([ this.usersRepository.findOneBy({ id: followRequest.followerId, @@ -181,12 +182,12 @@ export class Resolver { }), ]); if (follower == null || followee == null) { - throw new Error('resolveLocal: follower or followee does not exist'); + throw new Error(`resolveLocal - follower or followee does not exist: ${url}`); } return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); }); default: - throw new Error(`resolveLocal: type ${parsed.type} unhandled`); + throw new UnrecoverableError(`resolveLocal: type ${parsed.type} unhandled: ${url}`); } } } diff --git a/packages/backend/src/core/activitypub/JsonLdService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts index 100d4fa19f..9d1e2e06cc 100644 --- a/packages/backend/src/core/activitypub/JsonLdService.ts +++ b/packages/backend/src/core/activitypub/JsonLdService.ts @@ -5,6 +5,7 @@ import * as crypto from 'node:crypto'; import { Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; @@ -109,7 +110,7 @@ class JsonLd { @bindThis private getLoader() { return async (url: string): Promise => { - if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); + if (!/^https?:\/\//.test(url)) throw new UnrecoverableError(`Invalid URL: ${url}`); if (this.preLoad) { if (url in PRELOADED_CONTEXTS) { @@ -148,7 +149,7 @@ class JsonLd { }, ).then(res => { if (!res.ok) { - throw new Error(`${res.status} ${res.statusText}`); + throw new Error(`JSON-LD fetch failed with ${res.status} ${res.statusText}: ${url}`); } else { return res.json(); } diff --git a/packages/backend/src/core/activitypub/misc/check-against-url.ts b/packages/backend/src/core/activitypub/misc/check-against-url.ts index c8ad2ce584..edfab5a216 100644 --- a/packages/backend/src/core/activitypub/misc/check-against-url.ts +++ b/packages/backend/src/core/activitypub/misc/check-against-url.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { UnrecoverableError } from 'bullmq'; import type { IObject } from '../type.js'; function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] { @@ -25,6 +26,6 @@ export function assertActivityMatchesUrls(activity: IObject, urls: string[]) { .map(u => new URL(u as string).href); if (!actualUrls.some(u => expectedUrls.has(u))) { - throw new Error(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`); + throw new UnrecoverableError(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`); } } diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts index 690beeffef..4292b7e0f7 100644 --- a/packages/backend/src/core/activitypub/misc/validator.ts +++ b/packages/backend/src/core/activitypub/misc/validator.ts @@ -9,7 +9,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error('Validate content type of AP response: No content-type header'); + throw new Error(`invalid content type of AP response - no content-type header: ${response.url}`); } if ( contentType.startsWith('application/activity+json') || @@ -17,7 +17,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { ) { return; } - throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json'); + throw new Error(`invalid content type of AP response - content type is not application/activity+json or application/ld+json: ${response.url}`); } const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; @@ -26,7 +26,7 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error('Validate content type of JSON LD: No content-type header'); + throw new Error(`invalid content type of JSON LD - no content-type header: ${response.url}`); } if ( contentType.startsWith('application/ld+json') || @@ -35,5 +35,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { ) { return; } - throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json'); + throw new Error(`invalid content type of JSON LD - content type is not application/ld+json or application/json: ${response.url}`); } diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 259889d945..65328ebcf0 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; @@ -47,7 +48,7 @@ export class ApImageService { public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new Error('actor has been suspended'); + throw new UnrecoverableError(`actor has been suspended: ${actor.uri}`); } const image = await this.apResolverService.createResolver().resolve(value); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index a6d2ee1887..228a693cc4 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -5,6 +5,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -177,18 +178,18 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id == null) { - throw new Error('Refusing to create note without id'); + throw new UnrecoverableError(`Refusing to create note without id: ${entryUri}`); } if (!checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + throw new UnrecoverableError(`unexpected schema of note url ${url}: ${entryUri}`); } const url = getOneApHrefNullable(note.url); if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw new UnrecoverableError('unexpected schema of note url: ' + url); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { @@ -200,7 +201,7 @@ export class ApNoteService { // 投稿者をフェッチ if (note.attributedTo == null) { - throw new Error('invalid note.attributedTo: ' + note.attributedTo); + throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo}: ${entryUri}`); } const uri = getOneApId(note.attributedTo); @@ -209,7 +210,7 @@ export class ApNoteService { // eslint-disable-next-line no-param-reassign actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined; if (actor && actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${entryUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -236,7 +237,7 @@ export class ApNoteService { */ const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${entryUri}`); } //#endregion @@ -245,7 +246,7 @@ export class ApNoteService { // 解決した投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${entryUri}`); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -275,13 +276,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`); throw err; }) : null; @@ -312,7 +313,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); + throw new Error(`quote resolve failed: ${entryUri}`); } } } @@ -372,7 +373,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); + throw new Error(`The note creation failed with duplication error even when there is no duplication, ${entryUri}`); } return duplicate; } @@ -383,15 +384,14 @@ export class ApNoteService { */ @bindThis public async updateNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise { - const noteUri = typeof value === 'string' ? value : value.id; - if (noteUri == null) throw new Error('uri is null'); + const noteUri = getApId(value); // URIがこのサーバーを指しているならスキップ - if (noteUri.startsWith(this.config.url + '/')) throw new Error('uri points local'); + if (noteUri.startsWith(this.config.url + '/')) throw new UnrecoverableError(`uri points local: ${noteUri}`); //#region このサーバーに既に登録されているか - const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); - if (UpdatedNote == null) throw new Error('Note is not registered'); + const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); + if (updatedNote == null) throw new Error(`Note is not registered: ${noteUri}`); const user = await this.usersRepository.findOneBy({ id: UpdatedNote.userId }) as MiRemoteUser | null; if (user == null) throw new Error('Note is not registered'); @@ -421,17 +421,17 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id == null) { - throw new Error('Refusing to update note without id'); + throw new UnrecoverableError(`Refusing to update note without id: ${noteUri}`); } if (!checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + throw new UnrecoverableError(`unexpected schema of note.id ${note.id}: ${noteUri}`); } const url = getOneApHrefNullable(note.url); if (url && !checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw new UnrecoverableError(`unexpected schema of note url ${url}: ${noteUri}`); } if (url != null) { @@ -447,7 +447,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended'); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${noteUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -474,7 +474,7 @@ export class ApNoteService { */ const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${noteUri}`); } //#endregion @@ -505,13 +505,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`); throw err; }) : null; @@ -542,7 +542,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); + throw new Error(`quote resolve failed: ${noteUri}`); } } } @@ -577,7 +577,7 @@ export class ApNoteService { const apEmojis = emojis.map(emoji => emoji.name); try { - return await this.noteEditService.edit(actor, UpdatedNote.id, { + return await this.noteEditService.edit(actor, updatedNote.id, { createdAt: note.published ? new Date(note.published) : null, files, reply, @@ -602,7 +602,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); + throw new Error(`The note creation failed with duplication error even when there is no duplication: ${noteUri}`); } return duplicate; } @@ -619,7 +619,7 @@ export class ApNoteService { const uri = getApId(value); if (!this.utilityService.isFederationAllowedUri(uri)) { - throw new StatusError('blocked host', 451); + throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host'); } const unlock = await this.appLockService.getApLock(uri); @@ -631,7 +631,7 @@ export class ApNoteService { //#endregion if (this.utilityService.isUriLocal(uri)) { - throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); + throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note'); } // リモートサーバーからフェッチしてきて登録 @@ -679,7 +679,7 @@ export class ApNoteService { }); const emoji = await this.emojisRepository.findOneBy({ host, name }); - if (emoji == null) throw new Error('emoji update failed'); + if (emoji == null) throw new Error(`emoji update failed: ${name}:${host}`); return emoji; } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index c552ed8b62..33373aa87e 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { AbortError } from 'node-fetch'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -140,19 +141,19 @@ export class ApPersonService implements OnModuleInit { const expectHost = this.utilityService.punyHost(uri); if (!isActor(x)) { - throw new Error(`invalid Actor type '${x.type}'`); + throw new UnrecoverableError(`invalid Actor type '${x.type}': ${uri}`); } if (!(typeof x.id === 'string' && x.id.length > 0)) { - throw new Error('invalid Actor: wrong id'); + throw new UnrecoverableError(`invalid Actor - wrong id: ${uri}`); } if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { - throw new Error('invalid Actor: wrong inbox'); + throw new UnrecoverableError(`invalid Actor - wrong inbox: ${uri}`); } if (this.utilityService.punyHost(x.inbox) !== expectHost) { - throw new Error('invalid Actor: inbox has different host'); + throw new UnrecoverableError(`invalid Actor - inbox has different host: ${uri}`); } const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); @@ -169,16 +170,16 @@ export class ApPersonService implements OnModuleInit { const collectionUri = getApId(xCollection); if (typeof collectionUri === 'string' && collectionUri.length > 0) { if (this.utilityService.punyHost(collectionUri) !== expectHost) { - throw new Error(`invalid Actor: ${collection} has different host`); + throw new UnrecoverableError(`invalid Actor - ${collection} has different host: ${uri}`); } } else if (collectionUri != null) { - throw new Error(`invalid Actor: wrong ${collection}`); + throw new UnrecoverableError(`invalid Actor: wrong ${collection} in ${uri}`); } } } if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { - throw new Error('invalid Actor: wrong username'); + throw new UnrecoverableError(`invalid Actor - wrong username: ${uri}`); } // These fields are only informational, and some AP software allows these @@ -186,7 +187,7 @@ export class ApPersonService implements OnModuleInit { // we can at least see these users and their activities. if (x.name) { if (!(typeof x.name === 'string' && x.name.length > 0)) { - throw new Error('invalid Actor: wrong name'); + throw new UnrecoverableError(`invalid Actor - wrong name: ${uri}`); } x.name = truncate(x.name, nameLength); } else if (x.name === '') { @@ -195,24 +196,24 @@ export class ApPersonService implements OnModuleInit { } if (x.summary) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { - throw new Error('invalid Actor: wrong summary'); + throw new UnrecoverableError(`invalid Actor - wrong summary: ${uri}`); } x.summary = truncate(x.summary, summaryLength); } const idHost = this.utilityService.punyHost(x.id); if (idHost !== expectHost) { - throw new Error('invalid Actor: id has different host'); + throw new UnrecoverableError(`invalid Actor - id has different host: ${uri}`); } if (x.publicKey) { if (typeof x.publicKey.id !== 'string') { - throw new Error('invalid Actor: publicKey.id is not a string'); + throw new UnrecoverableError(`invalid Actor - publicKey.id is not a string: ${uri}`); } const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); if (publicKeyIdHost !== expectHost) { - throw new Error('invalid Actor: publicKey.id has different host'); + throw new UnrecoverableError(`invalid Actor - publicKey.id has different host: ${uri}`); } } @@ -298,18 +299,18 @@ export class ApPersonService implements OnModuleInit { */ @bindThis public async createPerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); + if (typeof uri !== 'string') throw new UnrecoverableError(`uri is not string: ${uri}`); const host = this.utilityService.punyHost(uri); if (host === this.utilityService.toPuny(this.config.host)) { - throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); + throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user'); } // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(uri); - if (object.id == null) throw new Error('invalid object.id: ' + object.id); + if (object.id == null) throw new UnrecoverableError(`null object.id: ${uri}`); const person = this.validateActor(object, uri); @@ -341,16 +342,16 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); if (person.id == null) { - throw new Error('Refusing to create person without id'); + throw new UnrecoverableError(`Refusing to create person without id: ${uri}`); } if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); } } @@ -442,7 +443,7 @@ export class ApPersonService implements OnModuleInit { if (isDuplicateKeyValueError(e)) { // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 const u = await this.usersRepository.findOneBy({ uri: person.id }); - if (u == null) throw new Error('already registered'); + if (u == null) throw new UnrecoverableError(`already registered a user with conflicting data: ${uri}`); user = u as MiRemoteUser; } else { @@ -451,7 +452,7 @@ export class ApPersonService implements OnModuleInit { } } - if (user == null) throw new Error('failed to create user: user is null'); + if (user == null) throw new Error(`failed to create user - user is null: ${uri}`); // Register to the cache this.cacheService.uriPersonCache.set(user.uri, user); @@ -500,7 +501,7 @@ export class ApPersonService implements OnModuleInit { */ @bindThis public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); + if (typeof uri !== 'string') throw new UnrecoverableError('uri is not string'); // URIがこのサーバーを指しているならスキップ if (this.utilityService.isUriLocal(uri)) return; @@ -553,16 +554,16 @@ export class ApPersonService implements OnModuleInit { const url = getOneApHrefNullable(person.url); if (person.id == null) { - throw new Error('Refusing to update person without id'); + throw new UnrecoverableError(`Refusing to update person without id: ${uri}`); } if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of person url: ' + url); + throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); } } @@ -732,7 +733,7 @@ export class ApPersonService implements OnModuleInit { }); if (!collection) return; - if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); + if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection: ${user.uri}`); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 83a98d17f9..2a45576e84 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; @@ -12,7 +13,7 @@ import type { MiRemoteUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { getOneApId, isQuestion } from '../type.js'; +import { getApId, getApType, getOneApId, isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; @@ -48,10 +49,10 @@ export class ApQuestionService { if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(source); - if (!isQuestion(question)) throw new Error('invalid type'); + if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getApId(source)}`); const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error('invalid question'); + if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getApId(source)}`); const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; @@ -72,21 +73,20 @@ export class ApQuestionService { */ @bindThis public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise { - const uri = typeof value === 'string' ? value : value.id; - if (uri == null) throw new Error('uri is null'); + const uri = getApId(value); // URIがこのサーバーを指しているならスキップ - if (this.utilityService.isUriLocal(uri)) throw new Error('uri points local'); + if (this.utilityService.isUriLocal(uri)) throw new Error(`uri points local: ${uri}`); //#region このサーバーに既に登録されているか const note = await this.notesRepository.findOneBy({ uri }); - if (note == null) throw new Error('Question is not registered'); + if (note == null) throw new Error(`Question is not registered (no note): ${uri}`); const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('Question is not registered'); + if (poll == null) throw new Error(`Question is not registered (no poll): ${uri}`); const user = await this.usersRepository.findOneBy({ id: poll.userId }); - if (user == null) throw new Error('Question is not registered'); + if (user == null) throw new Error(`Question is not registered (no user): ${uri}`); //#endregion // resolve new Question object @@ -95,25 +95,25 @@ export class ApQuestionService { const question = await resolver.resolve(value); this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - if (!isQuestion(question)) throw new Error('object is not a Question'); + if (!isQuestion(question)) throw new UnrecoverableError(`object ${getApType(question)} is not a Question: ${uri}`); const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri; const attributionMatchesExisting = attribution === user.uri; const actorMatchesAttribution = (actor) ? attribution === actor.uri : true; if (!attributionMatchesExisting || !actorMatchesAttribution) { - throw new Error('Refusing to ingest update for poll by different user'); + throw new UnrecoverableError(`Refusing to ingest update for poll by different user: ${uri}`); } const apChoices = question.oneOf ?? question.anyOf; - if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices); + if (apChoices == null) throw new UnrecoverableError(`poll has no choices: ${uri}`); let changed = false; for (const choice of poll.choices) { const oldCount = poll.votes[poll.choices.indexOf(choice)]; const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; - if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new Error('invalid newCount: ' + newCount); + if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new UnrecoverableError(`invalid newCount: ${newCount} in ${uri}`); if (oldCount <= newCount) { changed = true; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 08758aec80..56d2c3817f 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { UnrecoverableError } from 'bullmq'; import { fromTuple } from '@/misc/from-tuple.js'; export type Obj = { [x: string]: any }; @@ -61,7 +62,7 @@ export function getApId(value: string | IObject | [string | IObject]): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error('cannot determine id'); + throw new UnrecoverableError('cannot determine id'); } /** From 2afbd251e11f38993accab3871fe12b7ad4c960e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 21:02:41 -0400 Subject: [PATCH 02/16] avoid potential crash if Question activity is corrupt --- .../src/core/activitypub/models/ApQuestionService.ts | 6 +++--- packages/backend/src/core/activitypub/type.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 2a45576e84..6a0b3520fc 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -13,7 +13,7 @@ import type { MiRemoteUser } from '@/models/User.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { getApId, getApType, getOneApId, isQuestion } from '../type.js'; +import { getApId, getApType, getNullableApId, getOneApId, isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js'; @@ -49,10 +49,10 @@ export class ApQuestionService { if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(source); - if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getApId(source)}`); + if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getNullableApId(question)}`); const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getApId(source)}`); + if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`); const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 56d2c3817f..dd4475f475 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -65,6 +65,18 @@ export function getApId(value: string | IObject | [string | IObject]): string { throw new UnrecoverableError('cannot determine id'); } +/** + * Get ActivityStreams Object id + */ +export function getNullableApId(value: string | IObject | [string | IObject]): string | null { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + + if (typeof value === 'string') return value; + if (typeof value.id === 'string') return value.id; + return null; +} + /** * Get ActivityStreams Object type * From b9fd7e1b77097c4c1e1004e6cff5499a97ff03d6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 2 Nov 2024 21:03:23 -0400 Subject: [PATCH 03/16] clarify "failed to resolve quote" message --- packages/backend/src/core/activitypub/models/ApNoteService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 228a693cc4..2e232f49a2 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -313,7 +313,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`quote resolve failed: ${entryUri}`); + throw new Error(`failed to resolve quote for ${entryUri}`); } } } @@ -542,7 +542,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`quote resolve failed: ${noteUri}`); + throw new Error(`failed to resolve quote for ${noteUri}`); } } } From 4708c0abef8908e6cccd7953cf499ee26636ded2 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 10:46:58 -0500 Subject: [PATCH 04/16] don't retry jobs when processing returns a non-retryable error --- packages/backend/src/core/activitypub/ApInboxService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 4b783d3fd6..7edf4133d5 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -341,7 +341,7 @@ export class ApInboxService { // 対象が4xxならスキップ if (err instanceof StatusError) { if (!err.isRetryable) { - return `Ignored announce target ${target.id} - ${err.statusCode}`; + return `skip: ignored announce target ${target.id} - ${err.statusCode}`; } return `Error in announce target ${target.id} - ${err.statusCode}`; } @@ -461,7 +461,7 @@ export class ApInboxService { return 'ok'; } catch (err) { if (err instanceof StatusError && !err.isRetryable) { - return `skip ${err.statusCode}`; + return `skip: ${err.statusCode}`; } else { throw err; } From 3f5ea11a1fbb426d55e9f6df4ce8b4b7b642e270 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 10:47:27 -0500 Subject: [PATCH 05/16] clarify logging when an inbox job is skipped or fails --- .../backend/src/queue/processors/InboxProcessorService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 260ebe0d40..0318f621e3 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -218,7 +218,11 @@ export class InboxProcessorService implements OnApplicationShutdown { try { const result = await this.apInboxService.performActivity(authUser.user, activity); if (result && !result.startsWith('ok')) { - this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`); + if (result.startsWith('skip:')) { + this.logger.info(`inbox activity ignored: id=${activity.id} reason=${result}`); + } else { + this.logger.warn(`inbox activity failed: id=${activity.id} reason=${result}`); + } return result; } } catch (e) { From 9eb98ae8a515476d1c1590f385f5644607e65fa9 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 11:35:34 -0500 Subject: [PATCH 06/16] clarify logging for Create/Update type checks --- packages/backend/src/core/activitypub/ApInboxService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 7edf4133d5..8462f7d805 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -429,7 +429,7 @@ export class ApInboxService { if (isPost(object)) { await this.createNote(resolver, actor, object, false); } else { - return `Unknown type: ${getApType(object)}`; + return `skip: Unsupported type for Create: ${getApType(object)} (object ${getNullableApId(object)})`; } } @@ -832,7 +832,7 @@ export class ApInboxService { await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); return 'ok: Note updated'; } else { - return `skip: Unknown type: ${getApType(object)}`; + return `skip: Unsupported type for Update: ${getApType(object)} (object ${getNullableApId(object)})`; } } From f115116454160817d76e7784d69ad633869c53be Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 12:00:19 -0500 Subject: [PATCH 07/16] skip Delete(Note) activities when the note is already deleted --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 8462f7d805..507a765f4e 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -548,7 +548,7 @@ export class ApInboxService { const note = await this.apDbResolverService.getNoteFromApId(uri); if (note == null) { - return 'message not found'; + return 'skip: ignoring deleted note on both ends'; } if (note.userId !== actor.id) { From f4ec837d6ea1b2db6eef28dd80a366bd9fbb9791 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 12:19:10 -0500 Subject: [PATCH 08/16] clarify "unknown activity type" logging in ApInboxService.undo --- packages/backend/src/core/activitypub/ApInboxService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 507a765f4e..aba49ddaef 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -687,7 +687,7 @@ export class ApInboxService { if (isAnnounce(object)) return await this.undoAnnounce(actor, object); if (isAccept(object)) return await this.undoAccept(actor, object); - return `skip: unknown object type ${getApType(object)}`; + return `skip: unknown activity type ${getApType(object)}`; } @bindThis From c5f572dcfdf258848c17665c1798b6a5abd16eff Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 12:57:06 -0500 Subject: [PATCH 09/16] clarify logging when a Move (migration) is rejected --- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 33373aa87e..23e99b9170 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -674,7 +674,7 @@ export class ApPersonService implements OnModuleInit { }); } - return 'skip'; + return 'skip: too soon to migrate accounts'; } /** From 4ec6bffca70d66b0067fc12c6598fcd7292f594e Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 3 Nov 2024 13:01:04 -0500 Subject: [PATCH 10/16] don't suppress errors when Update(Question) or Update(Note) fails --- packages/backend/src/core/activitypub/ApInboxService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index aba49ddaef..ced8b8dc82 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -821,7 +821,7 @@ export class ApInboxService { return await this.create(actor, activity, resolver); } - await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); + await this.apQuestionService.updateQuestion(object, actor, resolver); return 'ok: Question updated'; } else if (isPost(object)) { // If we get an Update(Note) for a note that doesn't exist, then create it instead @@ -829,7 +829,7 @@ export class ApInboxService { return await this.create(actor, activity, resolver); } - await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); + await this.apNoteService.updateNote(object, actor, resolver); return 'ok: Note updated'; } else { return `skip: Unsupported type for Update: ${getApType(object)} (object ${getNullableApId(object)})`; From b951b31ef5b11b9e972a6fe8abb76bd4c97a184c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:13:25 -0500 Subject: [PATCH 11/16] use `IdentifiableError` in `ApImageService.createImage` --- .../backend/src/core/activitypub/models/ApImageService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 65328ebcf0..c5c3b736d4 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -4,7 +4,6 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { UnrecoverableError } from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; @@ -16,6 +15,7 @@ import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import type { Config } from '@/config.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { isDocument, type IObject } from '../type.js'; @@ -48,7 +48,7 @@ export class ApImageService { public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new UnrecoverableError(`actor has been suspended: ${actor.uri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${actor.uri}`); } const image = await this.apResolverService.createResolver().resolve(value); From baf19420dd6a25e0a35d14e39a7a6b5620b87575 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:21:19 -0500 Subject: [PATCH 12/16] log details when a quote fails to resolve --- .../core/activitypub/models/ApNoteService.ts | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2e232f49a2..d131519dcb 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -291,16 +291,25 @@ export class ApNoteService { let quote: MiNote | undefined | null = null; if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) { - const tryResolveNote = async (uri: string): Promise< + const tryResolveNote = async (uri: unknown): Promise< | { status: 'ok'; res: MiNote } | { status: 'permerror' | 'temperror' } > => { - if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; + if (typeof uri !== 'string' || !/^https?:/.test(uri)) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`); + return { status: 'permerror' }; + } try { const res = await this.resolveNote(uri, { resolver }); - if (res == null) return { status: 'permerror' }; + if (res == null) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`); + return { status: 'permerror' }; + } return { status: 'ok', res }; } catch (e) { + const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`); + return { status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; @@ -310,10 +319,10 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`failed to resolve quote for ${entryUri}`); + throw new Error(`temporary error resolving quote for ${entryUri}`); } } } @@ -520,16 +529,25 @@ export class ApNoteService { let quote: MiNote | undefined | null = null; if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) { - const tryResolveNote = async (uri: string): Promise< + const tryResolveNote = async (uri: unknown): Promise< | { status: 'ok'; res: MiNote } | { status: 'permerror' | 'temperror' } > => { - if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; + if (typeof uri !== 'string' || !/^https?:/.test(uri)) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`); + return { status: 'permerror' }; + } try { const res = await this.resolveNote(uri, { resolver }); - if (res == null) return { status: 'permerror' }; + if (res == null) { + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`); + return { status: 'permerror' }; + } return { status: 'ok', res }; } catch (e) { + const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e); + this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`); + return { status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; @@ -539,10 +557,10 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error(`failed to resolve quote for ${noteUri}`); + throw new Error(`temporary error resolving quote for ${entryUri}`); } } } From 6f8736c1afba9e69562e74d4b2325f781102ad3a Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 17 Nov 2024 09:26:09 -0500 Subject: [PATCH 13/16] improve comment on getNullableApId --- packages/backend/src/core/activitypub/type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index dd4475f475..85ddc20064 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -66,7 +66,7 @@ export function getApId(value: string | IObject | [string | IObject]): string { } /** - * Get ActivityStreams Object id + * Get ActivityStreams Object id, or null if not present */ export function getNullableApId(value: string | IObject | [string | IObject]): string | null { // eslint-disable-next-line no-param-reassign From 3e72d99cf92371a58db4e7e58ad9f9126c152866 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 20 Nov 2024 22:48:03 -0500 Subject: [PATCH 14/16] fix build errors in ApNoteService.ts --- .../backend/src/core/activitypub/models/ApNoteService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index d131519dcb..83ceef12bf 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -182,7 +182,7 @@ export class ApNoteService { } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note url ${url}: ${entryUri}`); + throw new UnrecoverableError(`unexpected schema of note note.id ${note.id}: ${entryUri}`); } const url = getOneApHrefNullable(note.url); @@ -402,7 +402,7 @@ export class ApNoteService { const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); if (updatedNote == null) throw new Error(`Note is not registered: ${noteUri}`); - const user = await this.usersRepository.findOneBy({ id: UpdatedNote.userId }) as MiRemoteUser | null; + const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null; if (user == null) throw new Error('Note is not registered'); // eslint-disable-next-line no-param-reassign @@ -456,7 +456,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${noteUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${actor.id} has been suspended: ${noteUri}`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); From 43d87270d99a321d5dc9c01c0d56e769f0ce8ed6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 21 Nov 2024 10:55:27 -0500 Subject: [PATCH 15/16] improve AP error formtting --- .../src/core/activitypub/ApInboxService.ts | 4 +- .../core/activitypub/models/ApNoteService.ts | 28 ++++++------- .../activitypub/models/ApPersonService.ts | 41 ++++++++++--------- .../activitypub/models/ApQuestionService.ts | 2 +- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index ced8b8dc82..5d0404d24e 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -429,7 +429,7 @@ export class ApInboxService { if (isPost(object)) { await this.createNote(resolver, actor, object, false); } else { - return `skip: Unsupported type for Create: ${getApType(object)} (object ${getNullableApId(object)})`; + return `skip: Unsupported type for Create: ${getApType(object)} ${getNullableApId(object)}`; } } @@ -832,7 +832,7 @@ export class ApInboxService { await this.apNoteService.updateNote(object, actor, resolver); return 'ok: Note updated'; } else { - return `skip: Unsupported type for Update: ${getApType(object)} (object ${getNullableApId(object)})`; + return `skip: Unsupported type for Update: ${getApType(object)} ${getNullableApId(object)}`; } } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 83ceef12bf..cd27e562a8 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -182,18 +182,18 @@ export class ApNoteService { } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note note.id ${note.id}: ${entryUri}`); + throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`); } const url = getOneApHrefNullable(note.url); if (url != null) { if (!checkHttps(url)) { - throw new UnrecoverableError('unexpected schema of note url: ' + url); + throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { - throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id}`); + throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`); } } @@ -201,7 +201,7 @@ export class ApNoteService { // 投稿者をフェッチ if (note.attributedTo == null) { - throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo}: ${entryUri}`); + throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo} in ${entryUri}`); } const uri = getOneApId(note.attributedTo); @@ -276,13 +276,13 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`); + throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`); } return x; }) .catch(async err => { - this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`); + this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`); throw err; }) : null; @@ -319,7 +319,7 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { throw new Error(`temporary error resolving quote for ${entryUri}`); @@ -382,7 +382,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error(`The note creation failed with duplication error even when there is no duplication, ${entryUri}`); + throw new Error(`The note creation failed with duplication error even when there is no duplication: ${entryUri}`); } return duplicate; } @@ -400,10 +400,10 @@ export class ApNoteService { //#region このサーバーに既に登録されているか const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); - if (updatedNote == null) throw new Error(`Note is not registered: ${noteUri}`); + if (updatedNote == null) throw new Error(`Note is not registered (no note): ${noteUri}`); const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null; - if (user == null) throw new Error('Note is not registered'); + if (user == null) throw new Error(`Note is not registered (no user): ${noteUri}`); // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); @@ -434,7 +434,7 @@ export class ApNoteService { } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note.id ${note.id}: ${noteUri}`); + throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`); } const url = getOneApHrefNullable(note.url); @@ -445,11 +445,11 @@ export class ApNoteService { if (url != null) { if (!checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { - throw new Error(`note url <> id host mismatch: ${url} <> ${note.id}`); + throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`); } } @@ -557,7 +557,7 @@ export class ApNoteService { const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); - quote = results.filter((x): x is { status: 'ok', res: MiNote, uri: string } => x.status === 'ok').map(x => x.res).at(0); + quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { throw new Error(`temporary error resolving quote for ${entryUri}`); diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 23e99b9170..cd6078b2ed 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -141,26 +141,27 @@ export class ApPersonService implements OnModuleInit { const expectHost = this.utilityService.punyHost(uri); if (!isActor(x)) { - throw new UnrecoverableError(`invalid Actor type '${x.type}': ${uri}`); + throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`); } if (!(typeof x.id === 'string' && x.id.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong id: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong id type`); } if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong inbox: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`); } - if (this.utilityService.punyHost(x.inbox) !== expectHost) { - throw new UnrecoverableError(`invalid Actor - inbox has different host: ${uri}`); + const inboxHost = this.utilityService.punyHost(x.inbox); + if (inboxHost !== expectHost) { + throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`); } const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); if (sharedInboxObject != null) { const sharedInbox = getApId(sharedInboxObject); if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) { - throw new Error('invalid Actor: wrong shared inbox'); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`); } } @@ -170,16 +171,16 @@ export class ApPersonService implements OnModuleInit { const collectionUri = getApId(xCollection); if (typeof collectionUri === 'string' && collectionUri.length > 0) { if (this.utilityService.punyHost(collectionUri) !== expectHost) { - throw new UnrecoverableError(`invalid Actor - ${collection} has different host: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`); } } else if (collectionUri != null) { - throw new UnrecoverableError(`invalid Actor: wrong ${collection} in ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`); } } } if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { - throw new UnrecoverableError(`invalid Actor - wrong username: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong username`); } // These fields are only informational, and some AP software allows these @@ -187,7 +188,7 @@ export class ApPersonService implements OnModuleInit { // we can at least see these users and their activities. if (x.name) { if (!(typeof x.name === 'string' && x.name.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong name: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong name`); } x.name = truncate(x.name, nameLength); } else if (x.name === '') { @@ -196,24 +197,24 @@ export class ApPersonService implements OnModuleInit { } if (x.summary) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { - throw new UnrecoverableError(`invalid Actor - wrong summary: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong summary`); } x.summary = truncate(x.summary, summaryLength); } const idHost = this.utilityService.punyHost(x.id); if (idHost !== expectHost) { - throw new UnrecoverableError(`invalid Actor - id has different host: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`); } if (x.publicKey) { if (typeof x.publicKey.id !== 'string') { - throw new UnrecoverableError(`invalid Actor - publicKey.id is not a string: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`); } const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); if (publicKeyIdHost !== expectHost) { - throw new UnrecoverableError(`invalid Actor - publicKey.id has different host: ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`); } } @@ -310,7 +311,7 @@ export class ApPersonService implements OnModuleInit { if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(uri); - if (object.id == null) throw new UnrecoverableError(`null object.id: ${uri}`); + if (object.id == null) throw new UnrecoverableError(`null object.id in ${uri}`); const person = this.validateActor(object, uri); @@ -347,11 +348,11 @@ export class ApPersonService implements OnModuleInit { if (url != null) { if (!checkHttps(url)) { - throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); + throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } @@ -559,11 +560,11 @@ export class ApPersonService implements OnModuleInit { if (url != null) { if (!checkHttps(url)) { - throw new UnrecoverableError(`unexpected schema of person url ${url}: ${uri}`); + throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { - throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id}`); + throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } @@ -733,7 +734,7 @@ export class ApPersonService implements OnModuleInit { }); if (!collection) return; - if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection: ${user.uri}`); + if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection in ${user.uri}`); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 6a0b3520fc..79a4f49d92 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -52,7 +52,7 @@ export class ApQuestionService { if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getNullableApId(question)}`); const multiple = question.oneOf === undefined; - if (multiple && question.anyOf === undefined) throw new Error(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`); + if (multiple && question.anyOf === undefined) throw new UnrecoverableError(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`); const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null; From face6527f2a59c473da29a076c44e1592c8ef9d6 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 21 Nov 2024 10:55:42 -0500 Subject: [PATCH 16/16] remove duplicate check for note.url --- packages/backend/src/core/activitypub/models/ApNoteService.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index cd27e562a8..3d4a33ded2 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -439,10 +439,6 @@ export class ApNoteService { const url = getOneApHrefNullable(note.url); - if (url && !checkHttps(url)) { - throw new UnrecoverableError(`unexpected schema of note url ${url}: ${noteUri}`); - } - if (url != null) { if (!checkHttps(url)) { throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`);