improve AP job clearing and failure logging

This commit is contained in:
Hazelnoot 2024-10-26 18:39:20 -04:00 committed by fly_mc
parent 75f5a8babf
commit 60701888fe
11 changed files with 99 additions and 95 deletions

View file

@ -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);
}
}

View file

@ -7,6 +7,7 @@ import { createPublicKey, randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import * as mfm from 'mfm-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()
@ -100,7 +102,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 {

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import * as Bull from 'bullmq';
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';
@ -16,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<string>;
@ -68,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}`);
}
}
@ -85,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 Bull.UnrecoverableError(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
throw new Error(`hit recursion limit: ${value}`);
}
this.history.add(value);
@ -104,7 +104,7 @@ export class Resolver {
}
if (!this.utilityService.isFederationAllowedHost(host)) {
throw new Bull.UnrecoverableError('Instance is blocked');
throw new UnrecoverableError(`instance is blocked: ${value}`);
}
if (this.config.signToActivityPubGet && !this.user) {
@ -120,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
@ -128,12 +128,12 @@ 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`);
// }
if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`);
}
return object;
}
@ -141,7 +141,7 @@ export class Resolver {
@bindThis
private resolveLocal(url: string): Promise<IObject> {
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':
@ -170,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,
@ -182,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}`);
}
}
}

View file

@ -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<RemoteDocument> => {
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();
}

View file

@ -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})`);
}
}

View file

@ -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}`);
}

View file

@ -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';
@ -43,7 +44,7 @@ export class ApImageService {
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
// 投稿者が凍結されていたらスキップ
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);

View file

@ -5,7 +5,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import * as Bull from 'bullmq';
import { UnrecoverableError } from 'bullmq';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
@ -147,22 +147,21 @@ export class ApNoteService {
}
const note = object as IPost;
const url = getOneApHrefNullable(note.url);
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)) {
@ -174,7 +173,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);
@ -183,7 +182,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);
@ -210,7 +209,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
@ -219,7 +218,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);
@ -249,25 +248,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}`);
if (visibility === 'followers') { throw err; } // private reply
if (err.message === 'Instance is blocked') { throw err; }
if (err.message === 'blocked host') { throw err; }
if (err instanceof IdentifiableError) {
if (err.id === '85ab9bd7-3a41-4530-959d-f07073900109') { throw err; } // actor has been suspended
}
if (err instanceof StatusError) {
if (err.statusCode === 404) { return null; } // eat 404 error
}
if (err instanceof Bull.UnrecoverableError) {
return null; // eat unrecoverableerror
}
this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`);
throw err;
})
: null;
@ -298,7 +285,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}`);
}
}
}
@ -358,7 +345,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;
}
@ -441,7 +428,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);
@ -453,7 +440,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');
}
// リモートサーバーからフェッチしてきて登録
@ -501,7 +488,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;
}

View file

@ -7,6 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
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';
@ -139,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);
@ -168,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
@ -185,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 === '') {
@ -194,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 +300,18 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
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 +343,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}`);
}
}
@ -441,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 {
@ -450,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);
@ -501,7 +503,7 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
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;
@ -554,16 +556,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}`);
}
}
@ -721,8 +723,16 @@ export class ApPersonService implements OnModuleInit {
const _resolver = resolver ?? this.apResolverService.createResolver();
// Resolve to (Ordered)Collection Object
const collection = await _resolver.resolveCollection(user.featured);
if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
const collection = await _resolver.resolveCollection(user.featured).catch(err => {
if (err instanceof AbortError || err instanceof StatusError) {
this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`);
} else {
this.logger.error('Failed to update featured notes:', err);
}
});
if (!collection) return;
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;

View file

@ -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';
@ -11,8 +12,8 @@ import type { IPoll } from '@/models/Poll.js';
import type { MiRemoteUser } from '@/models/User.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { getOneApId, isQuestion } from '../type.js';
import { UtilityService } from '@/core/UtilityService.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<boolean> {
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;

View file

@ -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 };
@ -68,7 +69,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');
}
/**