diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 735a0f4666..e52d77ab9b 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -9,6 +9,7 @@ import cluster from 'node:cluster'; import { EventEmitter } from 'node:events'; +import { inspect } from 'node:util'; import chalk from 'chalk'; import Xev from 'xev'; import Logger from '@/logger.js'; @@ -53,15 +54,22 @@ async function main() { // Display detail of unhandled promise rejection if (!envOption.quiet) { - process.on('unhandledRejection', console.dir); + process.on('unhandledRejection', e => { + try { + logger.error('Unhandled rejection:', inspect(e)); + } catch { + console.error('Unhandled rejection:', inspect(e)); + } + }); } // Display detail of uncaught exception process.on('uncaughtException', err => { try { - logger.error(err); - console.trace(err); - } catch { } + logger.error('Uncaught exception:', err); + } catch { + console.error('Uncaught exception:', err); + } }); // Dying away... diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 538c529106..a90228eabc 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -74,7 +74,7 @@ export async function masterMain() { process.exit(1); } - bootLogger.succ('Sharkey initialized'); + bootLogger.info('Sharkey initialized'); if (config.sentryForBackend) { Sentry.init({ @@ -140,10 +140,10 @@ export async function masterMain() { } if (envOption.onlyQueue) { - bootLogger.succ('Queue started', null, true); + bootLogger.info('Queue started', null, true); } else { const addressString = net.isIPv6(config.address) ? `[${config.address}]` : config.address; - bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on ${addressString}:${config.port} on ${config.url}`, null, true); + bootLogger.info(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on ${addressString}:${config.port} on ${config.url}`, null, true); } } @@ -172,7 +172,7 @@ function loadConfigBoot(): Config { config = loadConfig(); } catch (exception) { if (typeof exception === 'string') { - configLogger.error(exception); + configLogger.error('Exception loading config:', exception); process.exit(1); } else if ((exception as any).code === 'ENOENT') { configLogger.error('Configuration file not found', null, true); @@ -181,7 +181,7 @@ function loadConfigBoot(): Config { throw exception; } - configLogger.succ('Loaded'); + configLogger.info('Loaded'); return config; } @@ -195,7 +195,7 @@ async function connectDb(): Promise { dbLogger.info('Connecting...'); await initDb(); const v = await db.query('SHOW server_version').then(x => x[0].server_version); - dbLogger.succ(`Connected: v${v}`); + dbLogger.info(`Connected: v${v}`); } catch (err) { dbLogger.error('Cannot connect', null, true); dbLogger.error(err); @@ -211,7 +211,7 @@ async function spawnWorkers(limit = 1) { bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); await Promise.all([...Array(workers)].map(spawnWorker)); - bootLogger.succ('All workers started'); + bootLogger.info('All workers started'); } function spawnWorker(): Promise { diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts index 846d2c8ebd..bccb9f86f6 100644 --- a/packages/backend/src/core/AbuseReportService.ts +++ b/packages/backend/src/core/AbuseReportService.ts @@ -13,6 +13,7 @@ import { QueueService } from '@/core/QueueService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdService } from './IdService.js'; @Injectable() @@ -125,11 +126,11 @@ export class AbuseReportService { const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId }); if (report.targetUserHost == null) { - throw new Error('The target user host is null.'); + throw new IdentifiableError('0b1ce202-b2c1-4ee4-8af4-2742a51b383d', 'The target user host is null.'); } if (report.forwarded) { - throw new Error('The report has already been forwarded.'); + throw new IdentifiableError('5c008bdf-f0e8-4154-9f34-804e114516d7', 'The report has already been forwarded.'); } await this.abuseUserReportsRepository.update(report.id, { diff --git a/packages/backend/src/core/BunnyService.ts b/packages/backend/src/core/BunnyService.ts index bdb5bba3dd..c9f8a427f5 100644 --- a/packages/backend/src/core/BunnyService.ts +++ b/packages/backend/src/core/BunnyService.ts @@ -80,15 +80,15 @@ export class BunnyService { }); req.on('error', (error) => { - this.bunnyCdnLogger.error(error); + this.bunnyCdnLogger.error('Unhandled error', error); data.destroy(); - throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140bf91', 'An error has occured during the connectiong to BunnyCDN'); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140bf91', 'An error has occurred while connecting to BunnyCDN', true, error); }); data.pipe(req).on('finish', () => { data.destroy(); }); - + // wait till stream gets destroyed upon finish of piping to prevent the UI from showing the upload as success way too early await finished(data); } diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 13200bf7b3..c526a80aeb 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -54,7 +54,7 @@ export class CaptchaError extends Error { public readonly cause?: unknown; constructor(code: CaptchaErrorCode, message: string, cause?: unknown) { - super(message); + super(message, cause ? { cause } : undefined); this.code = code; this.cause = cause; this.name = 'CaptchaError'; @@ -117,7 +117,7 @@ export class CaptchaService { } const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => { - throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`); + throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`, err); }); if (result.success !== true) { @@ -133,7 +133,7 @@ export class CaptchaService { } const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => { - throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`); + throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`, err); }); if (result.success !== true) { @@ -209,7 +209,7 @@ export class CaptchaService { } const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { - throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`); + throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`, err); }); if (result.success !== true) { @@ -386,7 +386,7 @@ export class CaptchaService { this.logger.info(err); const error = err instanceof CaptchaError ? err - : new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`); + : new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`, err); return { success: false, error, diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 26e60e00b3..cb5bdb6cb7 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -18,6 +18,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; @Injectable() export class DownloadService { @@ -37,7 +38,7 @@ export class DownloadService { public async downloadUrl(url: string, path: string, options: { timeout?: number, operationTimeout?: number, maxSize?: number } = {} ): Promise<{ filename: string; }> { - this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`); + this.logger.debug(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`); const timeout = options.timeout ?? 30 * 1000; const operationTimeout = options.operationTimeout ?? 60 * 1000; @@ -86,7 +87,7 @@ export class DownloadService { filename = parsed.parameters.filename; } } catch (e) { - this.logger.warn(`Failed to parse content-disposition: ${contentDisposition}`, { stack: e }); + this.logger.warn(`Failed to parse content-disposition ${contentDisposition}: ${renderInlineError(e)}`); } } }).on('downloadProgress', (progress: Got.Progress) => { @@ -100,13 +101,17 @@ export class DownloadService { await stream.pipeline(req, fs.createWriteStream(path)); } catch (e) { if (e instanceof Got.HTTPError) { - throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); - } else { + throw new StatusError(`download error from ${url}`, e.response.statusCode, e.response.statusMessage, e); + } else if (e instanceof Got.RequestError || e instanceof Got.AbortError) { + throw new Error(String(e), { cause: e }); + } else if (e instanceof Error) { throw e; + } else { + throw new Error(String(e), { cause: e }); } } - this.logger.succ(`Download finished: ${chalk.cyan(url)}`); + this.logger.info(`Download finished: ${chalk.cyan(url)}`); return { filename, @@ -118,7 +123,7 @@ export class DownloadService { // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`text file: Temp file is ${path}`); + this.logger.debug(`text file: Temp file is ${path}`); try { // write content at URL to temp file diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 73125f36d7..1f15b16617 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -45,6 +45,7 @@ import { isMimeImage } from '@/misc/is-mime-image.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { BunnyService } from '@/core/BunnyService.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { LoggerService } from './LoggerService.js'; type AddFileArgs = { @@ -202,7 +203,7 @@ export class DriveService { //#endregion //#region Uploads - this.registerLogger.info(`uploading original: ${key}`); + this.registerLogger.debug(`uploading original: ${key}`); const uploads = [ this.upload(key, fs.createReadStream(path), type, null, name), ]; @@ -211,7 +212,7 @@ export class DriveService { webpublicKey = `${prefix}webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; - this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); + this.registerLogger.debug(`uploading webpublic: ${webpublicKey}`); uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name)); } @@ -219,7 +220,7 @@ export class DriveService { thumbnailKey = `${prefix}thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; - this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); + this.registerLogger.debug(`uploading thumbnail: ${thumbnailKey}`); uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`)); } @@ -263,11 +264,11 @@ export class DriveService { const [url, thumbnailUrl, webpublicUrl] = await Promise.all(promises); if (thumbnailUrl) { - this.registerLogger.info(`thumbnail stored: ${thumbnailAccessKey}`); + this.registerLogger.debug(`thumbnail stored: ${thumbnailAccessKey}`); } if (webpublicUrl) { - this.registerLogger.info(`web stored: ${webpublicAccessKey}`); + this.registerLogger.debug(`web stored: ${webpublicAccessKey}`); } file.storedInternal = true; @@ -311,7 +312,7 @@ export class DriveService { thumbnail, }; } catch (err) { - this.registerLogger.warn(`GenerateVideoThumbnail failed: ${err}`); + this.registerLogger.warn(`GenerateVideoThumbnail failed: ${renderInlineError(err)}`); return { webpublic: null, thumbnail: null, @@ -344,7 +345,7 @@ export class DriveService { metadata.height && metadata.height <= 2048 ); } catch (err) { - this.registerLogger.warn(`sharp failed: ${err}`); + this.registerLogger.warn(`sharp failed: ${renderInlineError(err)}`); return { webpublic: null, thumbnail: null, @@ -355,7 +356,7 @@ export class DriveService { let webpublic: IImage | null = null; if (generateWeb && !satisfyWebpublic && !isAnimated) { - this.registerLogger.info('creating web image'); + this.registerLogger.debug('creating web image'); try { if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) { @@ -369,9 +370,9 @@ export class DriveService { this.registerLogger.warn('web image not created (an error occurred)', err as Error); } } else { - if (satisfyWebpublic) this.registerLogger.info('web image not created (original satisfies webpublic)'); - else if (isAnimated) this.registerLogger.info('web image not created (animated image)'); - else this.registerLogger.info('web image not created (from remote)'); + if (satisfyWebpublic) this.registerLogger.debug('web image not created (original satisfies webpublic)'); + else if (isAnimated) this.registerLogger.debug('web image not created (animated image)'); + else this.registerLogger.debug('web image not created (from remote)'); } // #endregion webpublic @@ -498,7 +499,6 @@ export class DriveService { }: AddFileArgs): Promise { const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; const info = await this.fileInfoService.getFileInfo(path); - this.registerLogger.info(`${JSON.stringify(info)}`); // detect name const detectedName = correctFilename( @@ -508,6 +508,8 @@ export class DriveService { ext ?? info.type.ext, ); + this.registerLogger.debug(`Detected file info: ${JSON.stringify(info)}`); + if (user && !force) { // Check if there is a file with the same hash const matched = await this.driveFilesRepository.findOneBy({ @@ -516,7 +518,7 @@ export class DriveService { }); if (matched) { - this.registerLogger.info(`file with same hash is found: ${matched.id}`); + this.registerLogger.debug(`file with same hash is found: ${matched.id}`); if (sensitive && !matched.isSensitive) { // The file is federated as sensitive for this time, but was federated as non-sensitive before. // Therefore, update the file to sensitive. @@ -644,14 +646,14 @@ export class DriveService { } catch (err) { // duplicate key error (when already registered) if (isDuplicateKeyValueError(err)) { - this.registerLogger.info(`already registered ${file.uri}`); + this.registerLogger.debug(`already registered ${file.uri}`); file = await this.driveFilesRepository.findOneBy({ uri: file.uri!, userId: user ? user.id : IsNull(), }) as MiDriveFile; } else { - this.registerLogger.error(err as Error); + this.registerLogger.error('Error in drive register', err as Error); throw err; } } @@ -659,7 +661,7 @@ export class DriveService { file = await (this.save(file, path, detectedName, info)); } - this.registerLogger.succ(`drive file has been created ${file.id}`); + this.registerLogger.info(`Created file ${file.id} (${detectedName}) of type ${info.type.mime} for user ${user?.id ?? ''}`); if (user) { this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { @@ -892,13 +894,10 @@ export class DriveService { } const driveFile = await this.addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders }); - this.downloaderLogger.succ(`Got: ${driveFile.id}`); + this.downloaderLogger.debug(`Upload succeeded: created file ${driveFile.id}`); return driveFile!; } catch (err) { - this.downloaderLogger.error(`Failed to create drive file: ${err}`, { - url: url, - e: err, - }); + this.downloaderLogger.error(`Failed to create drive file from ${url}: ${renderInlineError(err)}`); throw err; } finally { cleanup(); diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 980f1fcacf..9bfd7381f1 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -15,6 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import type { CheerioAPI } from 'cheerio'; type NodeInfo = { @@ -90,7 +91,7 @@ export class FetchInstanceMetadataService { } } - this.logger.info(`Fetching metadata of ${instance.host} ...`); + this.logger.debug(`Fetching metadata of ${instance.host} ...`); const [info, dom, manifest] = await Promise.all([ this.fetchNodeinfo(instance).catch(() => null), @@ -106,7 +107,7 @@ export class FetchInstanceMetadataService { this.getDescription(info, dom, manifest).catch(() => null), ]); - this.logger.succ(`Successfuly fetched metadata of ${instance.host}`); + this.logger.debug(`Successfuly fetched metadata of ${instance.host}`); const updates = { infoUpdatedAt: new Date(), @@ -128,9 +129,9 @@ export class FetchInstanceMetadataService { await this.federatedInstanceService.update(instance.id, updates); - this.logger.succ(`Successfuly updated metadata of ${instance.host}`); + this.logger.info(`Successfully updated metadata of ${instance.host}`); } catch (e) { - this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); + this.logger.error(`Failed to update metadata of ${instance.host}: ${renderInlineError(e)}`); } finally { await this.unlock(host); } @@ -138,7 +139,7 @@ export class FetchInstanceMetadataService { @bindThis private async fetchNodeinfo(instance: MiInstance): Promise { - this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); + this.logger.debug(`Fetching nodeinfo of ${instance.host} ...`); try { const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') @@ -170,11 +171,11 @@ export class FetchInstanceMetadataService { throw err.statusCode ?? err.message; }); - this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); + this.logger.debug(`Successfuly fetched nodeinfo of ${instance.host}`); return info as NodeInfo; } catch (err) { - this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`); + this.logger.warn(`Failed to fetch nodeinfo of ${instance.host}: ${renderInlineError(err)}`); throw err; } @@ -182,7 +183,7 @@ export class FetchInstanceMetadataService { @bindThis private async fetchDom(instance: MiInstance): Promise { - this.logger.info(`Fetching HTML of ${instance.host} ...`); + this.logger.debug(`Fetching HTML of ${instance.host} ...`); const url = 'https://' + instance.host; diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index cc66e9fe3a..98fbfe5f23 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -46,11 +46,13 @@ const TYPE_SVG = { @Injectable() export class FileInfoService { private logger: Logger; + private ffprobeLogger: Logger; constructor( private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('file-info'); + this.ffprobeLogger = this.logger.createSubLogger('ffprobe'); } /** @@ -162,20 +164,19 @@ export class FileInfoService { */ @bindThis private hasVideoTrackOnVideoFile(path: string): Promise { - const sublogger = this.logger.createSubLogger('ffprobe'); - sublogger.info(`Checking the video file. File path: ${path}`); + this.ffprobeLogger.debug(`Checking the video file. File path: ${path}`); return new Promise((resolve) => { try { FFmpeg.ffprobe(path, (err, metadata) => { if (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err); + this.ffprobeLogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err); resolve(true); return; } resolve(metadata.streams.some((stream) => stream.codec_type === 'video')); }); } catch (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error); + this.ffprobeLogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error); resolve(true); } }); diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index a0f2607ddc..151097095d 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -331,7 +331,7 @@ export class HttpRequestService { }); if (!res.ok && extra.throwErrorWhenResponseNotOk) { - throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText); + throw new StatusError(`request error from ${url}`, res.status, res.statusText); } if (res.ok) { diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 097d657ba3..4dceb6e953 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -296,7 +296,7 @@ export class NoteCreateService implements OnApplicationShutdown { case 'followers': // 他人のfollowers noteはreject if (data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); } // Renote対象がfollowersならfollowersにする @@ -304,7 +304,7 @@ export class NoteCreateService implements OnApplicationShutdown { break; case 'specified': // specified / direct noteはreject - throw new Error('Renote target is not public or home'); + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); } } @@ -317,7 +317,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.renote.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); if (blocked) { - throw new Error('blocked'); + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is blocked'); } } } @@ -489,10 +489,10 @@ export class NoteCreateService implements OnApplicationShutdown { // should really not happen, but better safe than sorry if (data.reply?.id === insert.id) { - throw new Error('A note can\'t reply to itself'); + throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', 'A note can\'t reply to itself'); } if (data.renote?.id === insert.id) { - throw new Error('A note can\'t renote itself'); + throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', 'A note can\'t renote itself'); } if (data.uri != null) insert.uri = data.uri; @@ -549,8 +549,6 @@ export class NoteCreateService implements OnApplicationShutdown { throw err; } - console.error(e); - throw e; } } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 58233b90ee..34af1c76dd 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -309,7 +309,7 @@ export class NoteEditService implements OnApplicationShutdown { if (this.isRenote(data)) { if (data.renote.id === oldnote.id) { - throw new UnrecoverableError(`edit failed for ${oldnote.id}: cannot renote itself`); + throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', `edit failed for ${oldnote.id}: cannot renote itself`); } switch (data.renote.visibility) { @@ -325,7 +325,7 @@ export class NoteEditService implements OnApplicationShutdown { case 'followers': // 他人のfollowers noteはreject if (data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); } // Renote対象がfollowersならfollowersにする @@ -333,7 +333,7 @@ export class NoteEditService implements OnApplicationShutdown { break; case 'specified': // specified / direct noteはreject - throw new Error('Renote target is not public or home'); + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); } } diff --git a/packages/backend/src/core/NotePiningService.ts b/packages/backend/src/core/NotePiningService.ts index 86f1a62d4a..a678108189 100644 --- a/packages/backend/src/core/NotePiningService.ts +++ b/packages/backend/src/core/NotePiningService.ts @@ -61,7 +61,7 @@ export class NotePiningService { }); if (note == null) { - throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.'); + throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', `Note ${noteId} does not exist`); } await this.db.transaction(async tem => { @@ -102,7 +102,7 @@ export class NotePiningService { }); if (note == null) { - throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.'); + throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', `Note ${noteId} does not exist`); } this.userNotePiningsRepository.delete({ diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 86bf20067e..c23bb51178 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -117,7 +117,7 @@ export class ReactionService { if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); if (blocked) { - throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7'); + throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7', 'Note not accessible for you.'); } } @@ -322,14 +322,14 @@ export class ReactionService { }); if (exist == null) { - throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); + throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'reaction does not exist'); } // Delete reaction const result = await this.noteReactionsRepository.delete(exist.id); if (result.affected !== 1) { - throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); + throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'reaction does not exist'); } // Decrement reactions count diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index a2f1b73cdb..4dbc9d6a36 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import chalk from 'chalk'; import { IsNull } from 'typeorm'; @@ -18,6 +17,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; @Injectable() export class RemoteUserResolveService { @@ -44,27 +44,13 @@ export class RemoteUserResolveService { const usernameLower = username.toLowerCase(); if (host == null) { - this.logger.info(`return local user: ${usernameLower}`); - return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }) as MiLocalUser; + return await this.usersRepository.findOneByOrFail({ usernameLower, host: IsNull() }) as MiLocalUser; } host = this.utilityService.toPuny(host); if (host === this.utilityService.toPuny(this.config.host)) { - this.logger.info(`return local user: ${usernameLower}`); - return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }) as MiLocalUser; + return await this.usersRepository.findOneByOrFail({ usernameLower, host: IsNull() }) as MiLocalUser; } const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null; @@ -82,7 +68,7 @@ export class RemoteUserResolveService { .getUserFromApId(self.href) .then((u) => { if (u == null) { - throw new Error('local user not found'); + throw new Error(`local user not found: ${self.href}`); } else { return u; } @@ -90,7 +76,7 @@ export class RemoteUserResolveService { } } - this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); + this.logger.info(`Fetching new remote user ${chalk.magenta(acctLower)} from ${self.href}`); return await this.apPersonService.createPerson(self.href); } @@ -101,18 +87,16 @@ export class RemoteUserResolveService { lastFetchedAt: new Date(), }); - this.logger.info(`try resync: ${acctLower}`); const self = await this.resolveSelf(acctLower); if (user.uri !== self.href) { // if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping. - this.logger.info(`uri missmatch: ${acctLower}`); - this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`); + this.logger.warn(`Detected URI mismatch for ${acctLower}`); // validate uri - const uri = new URL(self.href); - if (uri.hostname !== host) { - throw new Error('Invalid uri'); + const uriHost = this.utilityService.extractDbHost(self.href); + if (uriHost !== host) { + throw new Error(`Failed to correct URI for ${acctLower}: new URI ${self.href} has different host from previous URI ${user.uri}`); } await this.usersRepository.update({ @@ -121,37 +105,28 @@ export class RemoteUserResolveService { }, { uri: self.href, }); - } else { - this.logger.info(`uri is fine: ${acctLower}`); } + this.logger.info(`Corrected URI for ${acctLower} from ${user.uri} to ${self.href}`); + await this.apPersonService.updatePerson(self.href); - this.logger.info(`return resynced remote user: ${acctLower}`); - return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u as MiLocalUser | MiRemoteUser; - } - }); + return await this.usersRepository.findOneByOrFail({ uri: self.href }) as MiLocalUser | MiRemoteUser; } - this.logger.info(`return existing remote user: ${acctLower}`); return user; } @bindThis private async resolveSelf(acctLower: string): Promise { - this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); const finger = await this.webfingerService.webfinger(acctLower).catch(err => { - this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`); - throw new Error(`Failed to WebFinger for ${acctLower}: ${ err.statusCode ?? err.message }`); + this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${renderInlineError(err)}`); + throw new Error(`Failed to WebFinger for ${acctLower}: error thrown`, { cause: err }); }); const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self'); if (!self) { this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`); - throw new Error('self link not found'); + throw new Error(`Failed to WebFinger for ${acctLower}: self link not found`); } return self; } diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index 372e1e2ab7..afd1d68ce4 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -17,6 +17,8 @@ import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; import { MiUser } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import Logger from '@/logger.js'; import type { AuthenticationResponseJSON, AuthenticatorTransportFuture, @@ -28,6 +30,8 @@ import type { @Injectable() export class WebAuthnService { + private readonly logger: Logger; + constructor( @Inject(DI.config) private config: Config, @@ -40,7 +44,9 @@ export class WebAuthnService { @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, + loggerService: LoggerService, ) { + this.logger = loggerService.getLogger('web-authn'); } @bindThis @@ -114,8 +120,8 @@ export class WebAuthnService { requireUserVerification: true, }); } catch (error) { - console.error(error); - throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed'); + this.logger.error(error as Error, 'Error authenticating webauthn'); + throw new IdentifiableError('5c1446f8-8ca7-4d31-9f39-656afe9c5d87', 'verification failed', true, error); } const { verified } = verification; @@ -221,7 +227,7 @@ export class WebAuthnService { requireUserVerification: true, }); } catch (error) { - throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`); + throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed`, true, error); } const { verified, authenticationInfo } = verification; @@ -301,8 +307,8 @@ export class WebAuthnService { requireUserVerification: true, }); } catch (error) { - console.error(error); - throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed'); + this.logger.error(error as Error, 'Error authenticating webauthn'); + throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', 'verification failed', true, error); } const { verified, authenticationInfo } = verification; diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index f57e7a2c1f..664963f3a3 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -9,6 +9,7 @@ import { XMLParser } from 'fast-xml-parser'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { RemoteLoggerService } from './RemoteLoggerService.js'; export type ILink = { @@ -109,7 +110,7 @@ export class WebfingerService { const template = (hostMeta['XRD']['Link'] as Array).filter(p => p['@_rel'] === 'lrdd')[0]['@_template']; return template.indexOf('{uri}') < 0 ? null : template; } catch (err) { - this.logger.error(`error while request host-meta for ${url}: ${err}`); + this.logger.error(`error while request host-meta for ${url}: ${renderInlineError(err)}`); return null; } } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index d8aa80f5b7..e9e0dde9cd 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -165,18 +165,23 @@ export class ApDbResolverService implements OnApplicationShutdown { */ @bindThis public async refetchPublicKeyForApId(user: MiRemoteUser): Promise { - this.apLoggerService.logger.debug('Re-fetching public key for user', { userId: user.id, uri: user.uri }); + this.apLoggerService.logger.debug(`Updating public key for user ${user.id} (${user.uri})`); + + const oldKey = await this.apPersonService.findPublicKeyByUserId(user.id); await this.apPersonService.updatePerson(user.uri); + const newKey = await this.apPersonService.findPublicKeyByUserId(user.id); - const key = await this.apPersonService.findPublicKeyByUserId(user.id); - - if (key) { - this.apLoggerService.logger.info('Re-fetched public key for user', { userId: user.id, uri: user.uri }); + if (newKey) { + if (oldKey && newKey.keyPem === oldKey.keyPem) { + this.apLoggerService.logger.debug(`Public key is up-to-date for user ${user.id} (${user.uri})`); + } else { + this.apLoggerService.logger.info(`Updated public key for user ${user.id} (${user.uri})`); + } } else { - this.apLoggerService.logger.warn('Failed to re-fetch key for user', { userId: user.id, uri: user.uri }); + this.apLoggerService.logger.warn(`Failed to update public key for user ${user.id} (${user.uri})`); } - return key; + return newKey ?? oldKey; } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index eaa592b9e0..746af41f55 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -57,7 +57,7 @@ class DeliverManager { ) { // 型で弾いてはいるが一応ローカルユーザーかチェック // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (actor.host != null) throw new Error('actor.host must be null'); + if (actor.host != null) throw new Error(`deliver failed for ${actor.id}: host is not null`); // パフォーマンス向上のためキューに突っ込むのはidのみに絞る this.actor = { @@ -124,12 +124,13 @@ class DeliverManager { select: { followerSharedInbox: true, followerInbox: true, + followerId: true, }, }); for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; - if (inbox === null) throw new UnrecoverableError(`inbox is null: following ${following.id}`); + if (inbox === null) throw new UnrecoverableError(`deliver failed for ${this.actor.id}: follower ${following.followerId} inbox is null`); inboxes.set(inbox, following.followerSharedInbox != null); } } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index c06939eae2..b384ec58c5 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,6 +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 { renderInlineError } from '@/misc/render-inline-error.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import FederationChart from '@/core/chart/charts/federation.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; @@ -121,13 +122,14 @@ export class ApInboxService { act.id = undefined; } + const id = getNullableApId(act) ?? `${getNullableApId(activity)}#${i}`; + try { - const id = getNullableApId(act) ?? `${getNullableApId(activity)}#${i}`; const result = await this.performOneActivity(actor, act, resolver); results.push([id, result]); } catch (err) { if (err instanceof Error || typeof err === 'string') { - this.logger.error(err); + this.logger.error(`Unhandled error in activity ${id}:`, err); } else { throw err; } @@ -147,7 +149,8 @@ export class ApInboxService { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { setImmediate(() => { // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない - this.apPersonService.updatePerson(actor.uri); + this.apPersonService.updatePerson(actor.uri) + .catch(err => this.logger.error(`Failed to update person: ${renderInlineError(err)}`)); }); } } @@ -253,7 +256,7 @@ export class ApInboxService { resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(err => { - this.logger.error(`Resolution failed: ${err}`); + this.logger.error(`Resolution failed: ${renderInlineError(err)}`); throw err; }); @@ -326,7 +329,7 @@ export class ApInboxService { if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; const target = await resolver.secureResolve(activityObject, uri).catch(e => { - this.logger.error(`Resolution failed: ${e}`); + this.logger.error(`Resolution failed: ${renderInlineError(e)}`); throw e; }); @@ -357,22 +360,10 @@ export class ApInboxService { } // Announce対象をresolve - let renote; - try { - // The target ID is verified by secureResolve, so we know it shares host authority with the actor who sent it. - // This means we can pass that ID to resolveNote and avoid an extra fetch, which will fail if the note is private. - renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: getApId(target) }); - if (renote == null) return 'announce target is null'; - } catch (err) { - // 対象が4xxならスキップ - if (err instanceof StatusError) { - if (!err.isRetryable) { - return `skip: ignored announce target ${target.id} - ${err.statusCode}`; - } - return `Error in announce target ${target.id} - ${err.statusCode}`; - } - throw err; - } + // The target ID is verified by secureResolve, so we know it shares host authority with the actor who sent it. + // This means we can pass that ID to resolveNote and avoid an extra fetch, which will fail if the note is private. + const renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: getApId(target) }); + if (renote == null) return 'announce target is null'; if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) { return 'skip: invalid actor for this activity'; @@ -454,9 +445,11 @@ export class ApInboxService { setImmediate(() => { // Don't re-use the resolver, or it may throw recursion errors. // Instead, create a new resolver with an appropriately-reduced recursion limit. - this.apPersonService.updatePerson(actor.uri, this.apResolverService.createResolver({ + const subResolver = this.apResolverService.createResolver({ recursionLimit: resolver.getRecursionLimit() - resolver.getHistory().length, - })); + }); + this.apPersonService.updatePerson(actor.uri, subResolver) + .catch(err => this.logger.error(`Failed to update person: ${renderInlineError(err)}`)); }); } }); @@ -511,7 +504,7 @@ export class ApInboxService { resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activityObject).catch(e => { - this.logger.error(`Resolution failed: ${e}`); + this.logger.error(`Resolution failed: ${renderInlineError(e)}`); throw e; }); @@ -548,12 +541,6 @@ export class ApInboxService { await this.apNoteService.createNote(note, actor, resolver, silent); return 'ok'; - } catch (err) { - if (err instanceof StatusError && !err.isRetryable) { - return `skip: ${err.statusCode}`; - } else { - throw err; - } } finally { unlock(); } @@ -686,7 +673,7 @@ export class ApInboxService { resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { - this.logger.error(`Resolution failed: ${e}`); + this.logger.error(`Resolution failed: ${renderInlineError(e)}`); throw e; }); @@ -758,7 +745,7 @@ export class ApInboxService { resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { - this.logger.error(`Resolution failed: ${e}`); + this.logger.error(`Resolution failed: ${renderInlineError(e)}`); throw e; }); @@ -890,7 +877,7 @@ export class ApInboxService { resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(activity.object).catch(e => { - this.logger.error(`Resolution failed: ${e}`); + this.logger.error(`Resolution failed: ${renderInlineError(e)}`); throw e; }); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 9aa9787d6a..201920612c 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -79,7 +79,7 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`); + throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `collection ${getApId(value)} has unsupported type: ${collection.type}`); } } @@ -187,7 +187,7 @@ export class Resolver { } // This ensures the input has a string ID, protecting against type confusion and rejecting anonymous objects. - const id = getApId(value); + const id = getApId(value, sentFromUri); // Check if we can use the provided object as-is. // Our security requires that the object ID matches the host authority that sent it, otherwise it can't be trusted. @@ -276,15 +276,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 IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`); + throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `failed to resolve ${value}: URL contains fragment`); } if (this.history.has(value)) { - throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', `cannot resolve already resolved URL: ${value}`); + throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', `failed to resolve ${value}: recursive resolution blocked`); } if (this.history.size > this.recursionLimit) { - throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${value}`); + throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `failed to resolve ${value}: hit recursion limit`); } this.history.add(value); @@ -294,7 +294,7 @@ export class Resolver { } if (!this.utilityService.isFederationAllowedHost(host)) { - throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', `cannot fetch AP object ${value}: blocked instance ${host}`); + throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', `failed to resolve ${value}: instance ${host} is blocked`); } if (this.config.signToActivityPubGet && !this.user) { @@ -324,12 +324,12 @@ export class Resolver { !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' ) { - throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', `invalid AP object ${value}: does not have ActivityStreams context`); + throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', `failed to resolve ${value}: response does not have ActivityStreams context`); } // The object ID is already validated to match the final URL's authority by signedGet / getActivityJson. // We only need to validate that it also matches the original URL's authority, in case of redirects. - const objectId = getApId(object); + const objectId = getApId(object, value); // We allow some limited cross-domain redirects, which means the host may have changed during fetch. // Additional checks are needed to validate the scope of cross-domain redirects. @@ -340,7 +340,7 @@ export class Resolver { // Check if the redirect bounce from [allowed domain] to [blocked domain]. if (!this.utilityService.isFederationAllowedHost(finalHost)) { - throw new IdentifiableError('0a72bf24-2d9b-4f1d-886b-15aaa31adeda', `cannot fetch AP object ${value}: redirected to blocked instance ${finalHost}`); + throw new IdentifiableError('0a72bf24-2d9b-4f1d-886b-15aaa31adeda', `failed to resolve ${value}: redirected to blocked instance ${finalHost}`); } } @@ -351,7 +351,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`); + if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `failed to resolve local ${url}: not a local URL`); switch (parsed.type) { case 'notes': @@ -385,7 +385,7 @@ export class Resolver { case 'follows': return this.followRequestsRepository.findOneBy({ id: parsed.id }) .then(async followRequest => { - if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `resolveLocal - invalid follow request ID ${parsed.id}: ${url}`); + if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `failed to resolve local ${url}: invalid follow request ID`); const [follower, followee] = await Promise.all([ this.usersRepository.findOneBy({ id: followRequest.followerId, @@ -397,12 +397,12 @@ export class Resolver { }), ]); if (follower == null || followee == null) { - throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `resolveLocal - follower or followee does not exist: ${url}`); + throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `failed to resolve local ${url}: follower or followee does not exist`); } return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url)); }); default: - throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled: ${url}`); + throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `failed to resolve local ${url}: unsupported type ${parsed.type}`); } } } diff --git a/packages/backend/src/core/activitypub/ApUtilityService.ts b/packages/backend/src/core/activitypub/ApUtilityService.ts index 3c125c6cd9..227dc3b9b3 100644 --- a/packages/backend/src/core/activitypub/ApUtilityService.ts +++ b/packages/backend/src/core/activitypub/ApUtilityService.ts @@ -24,7 +24,7 @@ export class ApUtilityService { public assertIdMatchesUrlAuthority(object: IObject, url: string): void { // This throws if the ID is missing or invalid, but that's ok. // Anonymous objects are impossible to verify, so we don't allow fetching them. - const id = getApId(object); + const id = getApId(object, url); // Make sure the object ID matches the final URL (which is where it actually exists). // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match. diff --git a/packages/backend/src/core/activitypub/JsonLdService.ts b/packages/backend/src/core/activitypub/JsonLdService.ts index 9d1e2e06cc..1db5df6ad9 100644 --- a/packages/backend/src/core/activitypub/JsonLdService.ts +++ b/packages/backend/src/core/activitypub/JsonLdService.ts @@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common'; import { UnrecoverableError } from 'bullmq'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; +import { StatusError } from '@/misc/status-error.js'; import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js'; import { validateContentTypeSetAsJsonLD } from './misc/validator.js'; import type { JsonLdDocument } from 'jsonld'; @@ -149,7 +150,7 @@ class JsonLd { }, ).then(res => { if (!res.ok) { - throw new Error(`JSON-LD fetch failed with ${res.status} ${res.statusText}: ${url}`); + throw new StatusError(`failed to fetch JSON-LD from ${url}`, res.status, res.statusText); } else { return res.json(); } diff --git a/packages/backend/src/core/activitypub/misc/validator.ts b/packages/backend/src/core/activitypub/misc/validator.ts index 0ff83659c1..5cd2ddf006 100644 --- a/packages/backend/src/core/activitypub/misc/validator.ts +++ b/packages/backend/src/core/activitypub/misc/validator.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { Response } from 'node-fetch'; -// TODO throw identifiable or unrecoverable errors - export function validateContentTypeSetAsActivityPub(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error(`invalid content type of AP response - no content-type header: ${response.url}`); + throw new IdentifiableError('d09dc850-b76c-4f45-875a-7389339d78b8', `invalid AP response from ${response.url}: no content-type header`, true); } if ( contentType.startsWith('application/activity+json') || @@ -19,7 +18,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void { ) { return; } - throw new Error(`invalid content type of AP response - content type is not application/activity+json or application/ld+json: ${response.url}`); + throw new IdentifiableError('dc110060-a5f2-461d-808b-39c62702ca64', `invalid AP response from ${response.url}: content type "${contentType}" is not application/activity+json or application/ld+json`); } const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/; @@ -28,7 +27,7 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); if (contentType === '') { - throw new Error(`invalid content type of JSON LD - no content-type header: ${response.url}`); + throw new IdentifiableError('45793ab7-7648-4886-b503-429f8a0d0f73', `invalid AP response from ${response.url}: no content-type header`, true); } if ( contentType.startsWith('application/ld+json') || @@ -37,5 +36,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void { ) { return; } - throw new Error(`invalid content type of JSON LD - content type is not application/ld+json or application/json: ${response.url}`); + throw new IdentifiableError('4bf8f36b-4d33-4ac9-ad76-63fa11f354e9', `invalid AP response from ${response.url}: content type "${contentType}" is not application/ld+json or application/json`); } diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 423044b985..7a16972ea4 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -18,7 +18,7 @@ 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'; +import { getNullableApId, isDocument, type IObject } from '../type.js'; @Injectable() export class ApImageService { @@ -48,7 +48,7 @@ export class ApImageService { public async createImage(actor: MiRemoteUser, value: string | IObject): Promise { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${actor.uri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to create image ${getNullableApId(value)}: actor ${actor.id} has been suspended`); } 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 5b66031bee..57d4303982 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -26,6 +26,7 @@ import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { isRetryableError } from '@/misc/is-retryable-error.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; @@ -100,29 +101,29 @@ export class ApNoteService { const apType = getApType(object); if (apType == null || !validPost.includes(apType)) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: invalid object type ${apType ?? 'undefined'}`); } if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); } const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)); if (object.attributedTo && actualHost !== expectHost) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note from ${uri}: published timestamp is malformed'); } if (actor) { const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri; if (attribution !== actor.uri) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`); } if (user && attribution !== user.uri) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note from ${uri}: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`); } } @@ -161,7 +162,7 @@ export class ApNoteService { const entryUri = getApId(value); const err = this.validateNote(object, entryUri, actor); if (err) { - this.logger.error(err.message, { + this.logger.error(`Error creating note: ${renderInlineError(err)}`, { resolver: { history: resolver.getHistory() }, value, object, @@ -174,11 +175,11 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id == null) { - throw new UnrecoverableError(`Refusing to create note without id: ${entryUri}`); + throw new UnrecoverableError(`failed to create note ${entryUri}: missing ID`); } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`); + throw new UnrecoverableError(`failed to create note ${entryUri}: unexpected schema`); } const url = this.apUtilityService.findBestObjectUrl(note); @@ -187,7 +188,7 @@ export class ApNoteService { // 投稿者をフェッチ if (note.attributedTo == null) { - throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo} in ${entryUri}`); + throw new UnrecoverableError(`failed to create note: ${entryUri}: missing attributedTo`); } const uri = getOneApId(note.attributedTo); @@ -196,7 +197,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 ${uri} has been suspended: ${entryUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to create note ${entryUri}: actor ${uri} has been suspended`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -223,7 +224,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: ${entryUri}`); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `failed to create note ${entryUri}: contains prohibited words`); } //#endregion @@ -232,7 +233,7 @@ export class ApNoteService { // 解決した投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${entryUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to create note ${entryUri}: actor ${actor.id} has been suspended`); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -269,15 +270,15 @@ export class ApNoteService { ? await this.resolveNote(note.inReplyTo, { resolver }) .then(x => { if (x == null) { - this.logger.warn('Specified inReplyTo, but not found'); - throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`); + this.logger.warn(`Specified inReplyTo "${note.inReplyTo}", but not found`); + throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to create note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true); } return x; }) .catch(async err => { - this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`); - throw err; + this.logger.warn(`error ${renderInlineError(err)} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`); + throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to create note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true, err); }) : null; @@ -348,7 +349,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 IdentifiableError('39c328e1-e829-458b-bfc9-65dcd513d1f8', `failed to create note ${entryUri}: the note creation failed with duplication error even when there is no duplication. This is likely a bug.`); } return duplicate; } @@ -362,45 +363,39 @@ export class ApNoteService { const noteUri = getApId(value); // URIがこのサーバーを指しているならスキップ - if (noteUri.startsWith(this.config.url + '/')) throw new UnrecoverableError(`uri points local: ${noteUri}`); + if (this.utilityService.isUriLocal(noteUri)) { + throw new UnrecoverableError(`failed to update note ${noteUri}: uri is local`); + } //#region このサーバーに既に登録されているか const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri }); - if (updatedNote == null) throw new Error(`Note is not registered (no note): ${noteUri}`); + if (updatedNote == null) throw new UnrecoverableError(`failed to update note ${noteUri}: note does not exist`); const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null; - if (user == null) throw new Error(`Note is not registered (no user): ${noteUri}`); + if (user == null) throw new UnrecoverableError(`failed to update note ${noteUri}: user does not exist`); - // eslint-disable-next-line no-param-reassign - if (resolver == null) resolver = this.apResolverService.createResolver(); + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(value); const entryUri = getApId(value); const err = this.validateNote(object, entryUri, actor, user); if (err) { - this.logger.error(err.message, { - resolver: { history: resolver.getHistory() }, - value, - object, - }); + this.logger.error(`Failed to update note ${noteUri}: ${renderInlineError(err)}`); throw err; } // `validateNote` checks that the actor and user are one and the same - // eslint-disable-next-line no-param-reassign actor ??= user; const note = object as IPost; - this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - if (note.id == null) { - throw new UnrecoverableError(`Refusing to update note without id: ${noteUri}`); + throw new UnrecoverableError(`failed to update note ${entryUri}: missing ID`); } if (!checkHttps(note.id)) { - throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`); + throw new UnrecoverableError(`failed to update note ${entryUri}: unexpected schema`); } const url = this.apUtilityService.findBestObjectUrl(note); @@ -408,7 +403,7 @@ export class ApNoteService { this.logger.info(`Creating the Note: ${note.id}`); if (actor.isSuspended) { - throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${actor.id} has been suspended: ${noteUri}`); + throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `failed to update note ${entryUri}: actor ${actor.id} has been suspended`); } const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); @@ -435,7 +430,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: ${noteUri}`); + throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `failed to update note ${noteUri}: contains prohibited words`); } //#endregion @@ -473,15 +468,15 @@ export class ApNoteService { ? await this.resolveNote(note.inReplyTo, { resolver }) .then(x => { if (x == null) { - this.logger.warn('Specified inReplyTo, but not found'); - throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`); + this.logger.warn(`Specified inReplyTo "${note.inReplyTo}", but not found`); + throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to update note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true); } return x; }) .catch(async err => { - this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`); - throw err; + this.logger.warn(`error ${renderInlineError(err)} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`); + throw new IdentifiableError('1ebf0a96-2769-4973-a6c2-3dcbad409dff', `failed to update note ${entryUri}: could not fetch inReplyTo ${note.inReplyTo}`, true, err); }) : null; @@ -549,7 +544,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: ${noteUri}`); + throw new IdentifiableError('39c328e1-e829-458b-bfc9-65dcd513d1f8', `failed to update note ${entryUri}: the note update failed with duplication error even when there is no duplication. This is likely a bug.`); } return duplicate; } @@ -566,8 +561,7 @@ export class ApNoteService { const uri = getApId(value); if (!this.utilityService.isFederationAllowedUri(uri)) { - // TODO convert to identifiable error - throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host'); + throw new IdentifiableError('04620a7e-044e-45ce-b72c-10e1bdc22e69', `failed to resolve note ${uri}: host is blocked`); } //#region このサーバーに既に登録されていたらそれを返す @@ -577,8 +571,7 @@ export class ApNoteService { // Bail if local URI doesn't exist if (this.utilityService.isUriLocal(uri)) { - // TODO convert to identifiable error - throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note'); + throw new IdentifiableError('cbac7358-23f2-4c70-833e-cffb4bf77913', `failed to resolve note ${uri}: URL is local and does not exist`); } const unlock = await this.appLockService.getApLock(uri); @@ -685,18 +678,13 @@ export class ApNoteService { const quote = await this.resolveNote(uri, { resolver }); if (quote == null) { - this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": request error`); + this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": fetch failed`); return false; } return quote; } catch (e) { - if (e instanceof Error) { - this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}":`, e); - } else { - this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": ${e}`); - } - + this.logger.warn(`Failed to resolve quote "${uri}" for note "${entryUri}": ${renderInlineError(e)}`); return isRetryableError(e); } }; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 631e86c8a8..b7aa036068 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -7,7 +7,6 @@ import { Inject, Injectable, OnApplicationShutdown } 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'; @@ -44,6 +43,8 @@ import { AppLockService } from '@/core/AppLockService.js'; import { MemoryKVCache } from '@/misc/cache.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { verifyFieldLinks } from '@/misc/verify-field-link.js'; +import { isRetryableError } from '@/misc/is-retryable-error.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -54,6 +55,7 @@ import type { ApLoggerService } from '../ApLoggerService.js'; import type { ApImageService } from './ApImageService.js'; import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; const nameLength = 128; const summaryLength = 2048; @@ -157,21 +159,21 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const expectHost = this.utilityService.punyHostPSLDomain(uri); if (!isActor(x)) { - throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`); + throw new UnrecoverableError(`invalid Actor ${uri}: unknown type '${x.type}'`); } if (!(typeof x.id === 'string' && x.id.length > 0)) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong id type`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong id type`); } if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong inbox type`); } this.apUtilityService.assertApUrl(x.inbox); const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox); if (inboxHost !== expectHost) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong inbox host ${inboxHost}`); } const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); @@ -179,7 +181,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const sharedInbox = getApId(sharedInboxObject); this.apUtilityService.assertApUrl(sharedInbox); if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong shared inbox ${sharedInbox}`); } } @@ -190,7 +192,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { if (typeof collectionUri === 'string' && collectionUri.length > 0) { this.apUtilityService.assertApUrl(collectionUri); if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} host ${collectionUri}`); } } else if (collectionUri != null) { throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`); @@ -199,7 +201,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { } if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong username`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong username`); } // These fields are only informational, and some AP software allows these @@ -207,7 +209,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { // 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 ${uri} - wrong name`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong name`); } x.name = truncate(x.name, nameLength); } else if (x.name === '') { @@ -216,24 +218,24 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { } if (x.summary) { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong summary`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong summary`); } x.summary = truncate(x.summary, summaryLength); } const idHost = this.utilityService.punyHostPSLDomain(x.id); if (idHost !== expectHost) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong id ${x.id}`); } if (x.publicKey) { if (typeof x.publicKey.id !== 'string') { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong publicKey.id type`); } const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id); if (publicKeyIdHost !== expectHost) { - throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`); + throw new UnrecoverableError(`invalid Actor ${uri}: wrong publicKey.id ${x.publicKey.id}`); } } @@ -271,8 +273,6 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { } private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise>> { - if (user == null) throw new Error('failed to create user: user is null'); - const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => { // icon and image may be arrays // see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon @@ -325,12 +325,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { */ @bindThis public async createPerson(uri: string, resolver?: Resolver): Promise { - if (typeof uri !== 'string') throw new UnrecoverableError(`uri is not string: ${uri}`); + if (typeof uri !== 'string') throw new UnrecoverableError(`failed to create user ${uri}: input is not string`); const host = this.utilityService.punyHost(uri); if (host === this.utilityService.toPuny(this.config.host)) { - // TODO convert to unrecoverable error - throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user'); + throw new UnrecoverableError(`failed to create user ${uri}: URI is local`); } return await this._createPerson(uri, resolver); @@ -340,8 +339,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const uri = getApId(value); const host = this.utilityService.punyHost(uri); - // eslint-disable-next-line no-param-reassign - if (resolver == null) resolver = this.apResolverService.createResolver(); + resolver ??= this.apResolverService.createResolver(); const object = await resolver.resolve(value); const person = this.validateActor(object, uri); @@ -361,9 +359,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { ].map((p): Promise<'public' | 'private'> => p .then(isPublic => isPublic ? 'public' : 'private') .catch(err => { - if (!(err instanceof StatusError) || err.isRetryable) { - this.logger.error('error occurred while fetching following/followers collection', { stack: err }); + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`error occurred while fetching following/followers collection: ${renderInlineError(err)}`); } + return 'private'; }), ), @@ -372,7 +372,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); if (person.id == null) { - throw new UnrecoverableError(`Refusing to create person without id: ${uri}`); + throw new UnrecoverableError(`failed to create user ${uri}: missing ID`); } const url = this.apUtilityService.findBestObjectUrl(person); @@ -387,7 +387,10 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host) .then(_emojis => _emojis.map(emoji => emoji.name)) .catch(err => { - this.logger.error('error occurred while fetching user emojis', { stack: err }); + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`error occurred while fetching user emojis: ${renderInlineError(err)}`); + } return []; }); //#endregion @@ -493,7 +496,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { user = u as MiRemoteUser; publicKey = await this.userPublickeysRepository.findOneBy({ userId: user.id }); } else { - this.logger.error(e instanceof Error ? e : new Error(e as string)); + this.logger.error('Error creating Person:', e instanceof Error ? e : new Error(e as string)); throw e; } } @@ -533,11 +536,19 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { // Register to the cache this.cacheService.uriPersonCache.set(user.uri, user); } catch (err) { - this.logger.error('error occurred while fetching user avatar/banner', { stack: err }); + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`error occurred while fetching user avatar/banner: ${renderInlineError(err)}`); + } } //#endregion - await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err)); + await this.updateFeatured(user.id, resolver).catch(err => { + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`Error updating featured notes: ${renderInlineError(err)}`); + } + }); return user; } @@ -554,7 +565,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { */ @bindThis public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise { - if (typeof uri !== 'string') throw new UnrecoverableError('uri is not string'); + if (typeof uri !== 'string') throw new UnrecoverableError(`failed to update user ${uri}: input is not string`); // URIがこのサーバーを指しているならスキップ if (this.utilityService.isUriLocal(uri)) return; @@ -574,8 +585,11 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { this.logger.info(`Updating the Person: ${person.id}`); // カスタム絵文字取得 - const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => { - this.logger.info(`extractEmojis: ${e}`); + const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(err => { + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`error occurred while fetching user emojis: ${renderInlineError(err)}`); + } return []; }); @@ -592,11 +606,13 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { ].map((p): Promise<'public' | 'private' | undefined> => p .then(isPublic => isPublic ? 'public' : 'private') .catch(err => { - if (!(err instanceof StatusError) || err.isRetryable) { - this.logger.error('error occurred while fetching following/followers collection', { stack: err }); + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`error occurred while fetching following/followers collection: ${renderInlineError(err)}`); // Do not update the visibility on transient errors. return undefined; } + return 'private'; }), ), @@ -605,7 +621,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); if (person.id == null) { - throw new UnrecoverableError(`Refusing to update person without id: ${uri}`); + throw new UnrecoverableError(`failed to update user ${uri}: missing ID`); } const url = this.apUtilityService.findBestObjectUrl(person); @@ -638,7 +654,15 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { .filter((a: unknown) => typeof(a) === 'string' && a.length > 0 && a.length <= 128) .slice(0, 32) : [], - ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image, person.backgroundUrl).catch(() => ({}))), + ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image, person.backgroundUrl).catch(err => { + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`error occurred while fetching user avatar/banner: ${renderInlineError(err)}`); + } + + // Can't return null or destructuring operator will break + return {}; + })), } as Partial & Pick; const moving = ((): boolean => { @@ -722,7 +746,12 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { { followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null }, ); - await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err)); + await this.updateFeatured(exist.id, resolver).catch(err => { + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.error(`Error updating featured notes: ${renderInlineError(err)}`); + } + }); const updated = { ...exist, ...updates }; @@ -761,8 +790,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const uri = getApId(value); if (!this.utilityService.isFederationAllowedUri(uri)) { - // TODO convert to identifiable error - throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host'); + throw new IdentifiableError('590719b3-f51f-48a9-8e7d-6f559ad00e5d', `failed to resolve person ${uri}: host is blocked`); } //#region このサーバーに既に登録されていたらそれを返す @@ -772,8 +800,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { // Bail if local URI doesn't exist if (this.utilityService.isUriLocal(uri)) { - // TODO convert to identifiable error - throw new StatusError(`cannot resolve local person: ${uri}`, 400, 'cannot resolve local person'); + throw new IdentifiableError('efb573fd-6b9e-4912-9348-a02f5603df4f', `failed to resolve person ${uri}: URL is local and does not exist`); } const unlock = await this.appLockService.getApLock(uri); @@ -818,15 +845,16 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { // Resolve to (Ordered)Collection Object const collection = user.featured ? await _resolver.resolveCollection(user.featured, true, user.uri).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); + // Permanent error implies hidden or inaccessible, which is a normal thing. + if (isRetryableError(err)) { + this.logger.warn(`Failed to update featured notes: ${renderInlineError(err)}`); } + + return null; }) : null; if (!collection) return; - if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection in ${user.uri}`); + if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`failed to update user ${user.uri}: featured ${user.featured} is not Collection or OrderedCollection`); // 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 335ca189ec..80900be2dc 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -93,7 +93,6 @@ export class ApQuestionService { // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const question = await resolver.resolve(value); - this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); if (!isQuestion(question)) throw new UnrecoverableError(`object ${getApType(question)} is not a Question: ${uri}`); diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 60f49d046d..cc7599d394 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -75,14 +75,17 @@ export function getOneApId(value: ApObject): string { /** * Get ActivityStreams Object id */ -export function getApId(source: string | IObject | [string | IObject]): string { - const value = getNullableApId(source); +export function getApId(value: string | IObject | [string | IObject], sourceForLogs?: string): string { + const id = getNullableApId(value); - if (value == null) { - throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', `invalid AP object ${value}: missing or invalid id`); + if (id == null) { + const message = sourceForLogs + ? `invalid AP object ${value} (sent from ${sourceForLogs}): missing id` + : `invalid AP object ${value}: missing id`; + throw new IdentifiableError('ad2dc287-75c1-44c4-839d-3d2e64576675', message); } - return value; + return id; } /** diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts index 27c67cb5df..a61d949ef4 100644 --- a/packages/backend/src/misc/FileWriterStream.ts +++ b/packages/backend/src/misc/FileWriterStream.ts @@ -21,7 +21,7 @@ export class FileWriterStream extends WritableStream { write: async (chunk, controller) => { if (file === null) { controller.error(); - throw new Error(); + throw new Error('file is null'); } await file.write(chunk); diff --git a/packages/backend/src/misc/fastify-reply-error.ts b/packages/backend/src/misc/fastify-reply-error.ts index e6c4e78d2f..03109e8b96 100644 --- a/packages/backend/src/misc/fastify-reply-error.ts +++ b/packages/backend/src/misc/fastify-reply-error.ts @@ -8,8 +8,8 @@ export class FastifyReplyError extends Error { public message: string; public statusCode: number; - constructor(statusCode: number, message: string) { - super(message); + constructor(statusCode: number, message: string, cause?: unknown) { + super(message, cause ? { cause } : undefined); this.message = message; this.statusCode = statusCode; } diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index c0e8478db5..f0eba2d99c 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -8,6 +8,7 @@ import * as crypto from 'node:crypto'; import { parseBigInt36 } from '@/misc/bigint.js'; +import { IdentifiableError } from '../identifiable-error.js'; export const aidRegExp = /^[0-9a-z]{10}$/; @@ -26,7 +27,7 @@ function getNoise(): string { } export function genAid(t: number): string { - if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date'); + if (isNaN(t)) throw new IdentifiableError('6b73b7d5-9d2b-48b4-821c-ef955efe80ad', 'Failed to create AID: Invalid Date'); counter++; return getTime(t) + getNoise(); } diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts index 006673a6d0..d2bb566e35 100644 --- a/packages/backend/src/misc/id/aidx.ts +++ b/packages/backend/src/misc/id/aidx.ts @@ -10,6 +10,7 @@ import { customAlphabet } from 'nanoid'; import { parseBigInt36 } from '@/misc/bigint.js'; +import { IdentifiableError } from '../identifiable-error.js'; export const aidxRegExp = /^[0-9a-z]{16}$/; @@ -34,7 +35,7 @@ function getNoise(): string { } export function genAidx(t: number): string { - if (isNaN(t)) throw new Error('Failed to create AIDX: Invalid Date'); + if (isNaN(t)) throw new IdentifiableError('6b73b7d5-9d2b-48b4-821c-ef955efe80ad', 'Failed to create AIDX: Invalid Date'); counter++; return getTime(t) + nodeId + getNoise(); } diff --git a/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts index f5c3fcd6cb..56e13f2622 100644 --- a/packages/backend/src/misc/identifiable-error.ts +++ b/packages/backend/src/misc/identifiable-error.ts @@ -15,8 +15,8 @@ export class IdentifiableError extends Error { */ public readonly isRetryable: boolean; - constructor(id: string, message?: string, isRetryable = false) { - super(message); + constructor(id: string, message?: string, isRetryable = false, cause?: unknown) { + super(message, cause ? { cause } : undefined); this.message = message ?? ''; this.id = id; this.isRetryable = isRetryable; diff --git a/packages/backend/src/misc/is-retryable-error.ts b/packages/backend/src/misc/is-retryable-error.ts index 9bb8700c7a..63b561b280 100644 --- a/packages/backend/src/misc/is-retryable-error.ts +++ b/packages/backend/src/misc/is-retryable-error.ts @@ -3,20 +3,34 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { AbortError } from 'node-fetch'; +import { AbortError, FetchError } from 'node-fetch'; import { UnrecoverableError } from 'bullmq'; import { StatusError } from '@/misc/status-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { CaptchaError, captchaErrorCodes } from '@/core/CaptchaService.js'; +import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; +import { ConflictError } from '@/server/SkRateLimiterService.js'; /** * Returns false if the provided value represents a "permanent" error that cannot be retried. * Returns true if the error is retryable, unknown (as all errors are retryable by default), or not an error object. */ export function isRetryableError(e: unknown): boolean { + if (e instanceof AggregateError) return e.errors.every(inner => isRetryableError(inner)); if (e instanceof StatusError) return e.isRetryable; if (e instanceof IdentifiableError) return e.isRetryable; + if (e instanceof CaptchaError) { + if (e.code === captchaErrorCodes.verificationFailed) return false; + if (e.code === captchaErrorCodes.invalidParameters) return false; + if (e.code === captchaErrorCodes.invalidProvider) return false; + return true; + } + if (e instanceof FastifyReplyError) return false; + if (e instanceof ConflictError) return true; if (e instanceof UnrecoverableError) return false; if (e instanceof AbortError) return true; + if (e instanceof FetchError) return true; + if (e instanceof SyntaxError) return false; if (e instanceof Error) return e.name === 'AbortError'; return true; } diff --git a/packages/backend/src/misc/render-full-error.ts b/packages/backend/src/misc/render-full-error.ts new file mode 100644 index 0000000000..5f0a09bba9 --- /dev/null +++ b/packages/backend/src/misc/render-full-error.ts @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Bull from 'bullmq'; +import { AbortError, FetchError } from 'node-fetch'; +import { StatusError } from '@/misc/status-error.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; +import { CaptchaError, captchaErrorCodes } from '@/core/CaptchaService.js'; + +export function renderFullError(e?: unknown): unknown { + if (e === undefined) return 'undefined'; + if (e === null) return 'null'; + + if (e instanceof Error) { + if (isSimpleError(e)) { + return renderInlineError(e); + } + + const data: ErrorData = {}; + if (e.stack) data.stack = e.stack; + if (e.message) data.message = e.message; + if (e.name) data.name = e.name; + + // mix "cause" and "errors" + if (e instanceof AggregateError && e.errors.length > 0) { + const causes = e.errors.map(inner => renderFullError(inner)); + if (e.cause) { + causes.push(renderFullError(e.cause)); + } + data.cause = causes; + } else if (e.cause) { + data.cause = renderFullError(e.cause); + } + + return data; + } + + return e; +} + +function isSimpleError(e: Error): boolean { + if (e instanceof Bull.UnrecoverableError) return true; + if (e instanceof AbortError || e.name === 'AbortError') return true; + if (e instanceof FetchError || e.name === 'FetchError') return true; + if (e instanceof StatusError) return true; + if (e instanceof IdentifiableError) return true; + if (e instanceof FetchError) return true; + if (e instanceof CaptchaError && e.code !== captchaErrorCodes.unknown) return true; + return false; +} + +interface ErrorData { + stack?: Error['stack']; + message?: Error['message']; + name?: Error['name']; + cause?: Error['cause'] | Error['cause'][]; +} diff --git a/packages/backend/src/misc/render-inline-error.ts b/packages/backend/src/misc/render-inline-error.ts new file mode 100644 index 0000000000..07f9f3068e --- /dev/null +++ b/packages/backend/src/misc/render-inline-error.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { StatusError } from '@/misc/status-error.js'; +import { CaptchaError } from '@/core/CaptchaService.js'; + +export function renderInlineError(err: unknown): string { + const parts: string[] = []; + renderTo(err, parts); + return parts.join(''); +} + +function renderTo(err: unknown, parts: string[]): void { + parts.push(printError(err)); + + if (err instanceof AggregateError) { + for (let i = 0; i < err.errors.length; i++) { + parts.push(` [${i + 1}/${err.errors.length}]: `); + renderTo(err.errors[i], parts); + } + } + + if (err instanceof Error) { + if (err.cause) { + parts.push(' [caused by]: '); + renderTo(err.cause, parts); + // const cause = renderInlineError(err.cause); + // parts.push(' [caused by]: ', cause); + } + } +} + +function printError(err: unknown): string { + if (err === undefined) return 'undefined'; + if (err === null) return 'null'; + + if (err instanceof IdentifiableError) { + if (err.message) { + return `${err.name} ${err.id}: ${err.message}`; + } else { + return `${err.name} ${err.id}`; + } + } + + if (err instanceof StatusError) { + if (err.message) { + return `${err.name} ${err.statusCode}: ${err.message}`; + } else if (err.statusMessage) { + return `${err.name} ${err.statusCode}: ${err.statusMessage}`; + } else { + return `${err.name} ${err.statusCode}`; + } + } + + if (err instanceof CaptchaError) { + if (err.code.description) { + return `${err.name} ${err.code.description}: ${err.message}`; + } else { + return `${err.name}: ${err.message}`; + } + } + + if (err instanceof Error) { + if (err.message) { + return `${err.name}: ${err.message}`; + } else { + return err.name; + } + } + + return String(err); +} diff --git a/packages/backend/src/misc/status-error.ts b/packages/backend/src/misc/status-error.ts index c3533db607..4fd3bfcafb 100644 --- a/packages/backend/src/misc/status-error.ts +++ b/packages/backend/src/misc/status-error.ts @@ -9,8 +9,8 @@ export class StatusError extends Error { public isClientError: boolean; public isRetryable: boolean; - constructor(message: string, statusCode: number, statusMessage?: string) { - super(message); + constructor(message: string, statusCode: number, statusMessage?: string, cause?: unknown) { + super(message, cause ? { cause } : undefined); this.name = 'StatusError'; this.statusCode = statusCode; this.statusMessage = statusMessage; diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 7f7ce2452c..4c1a6a1d9e 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -11,7 +11,7 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; -import { StatusError } from '@/misc/status-error.js'; +import { renderFullError } from '@/misc/render-full-error.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; @@ -73,7 +73,9 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string { const currentAttempts = job.attemptsMade + (increment ? 1 : 0); const maxAttempts = job.opts.attempts ?? 0; - return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; + return job.name + ? `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated} name=${job.name}` + : `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; } @Injectable() @@ -134,35 +136,6 @@ export class QueueProcessorService implements OnApplicationShutdown { ) { this.logger = this.queueLoggerService.logger; - function renderError(e?: Error) { - // 何故かeがundefinedで来ることがある - if (!e) return '?'; - - if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError' || e instanceof StatusError) { - return `${e.name}: ${e.message}`; - } - - return { - stack: e.stack, - message: e.message, - name: e.name, - }; - } - - function renderJob(job?: Bull.Job) { - if (!job) return '?'; - - const info: Record = { - info: getJobInfo(job), - data: job.data, - }; - - if (job.name) info.name = job.name; - if (job.failedReason) info.failedReason = job.failedReason; - - return info; - } - //#region system { const processer = (job: Bull.Job) => { @@ -196,7 +169,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err: Error) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', @@ -204,7 +177,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -261,7 +234,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', @@ -269,7 +242,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -301,7 +274,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, { level: 'error', @@ -309,7 +282,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -341,7 +314,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) }); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, { level: 'error', @@ -349,7 +322,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error('inbox error:', renderError(err))) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -381,7 +354,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', @@ -389,7 +362,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -421,7 +394,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', @@ -429,7 +402,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -468,7 +441,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', @@ -476,7 +449,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -509,7 +482,7 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); + this.logError(logger, err, job); if (config.sentryForBackend) { Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', @@ -517,13 +490,15 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } }) - .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) + .on('error', (err: Error) => this.logError(logger, err)) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion //#region ended poll notification { + const logger = this.logger.createSubLogger('endedPollNotification'); + this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { if (this.config.sentryForBackend) { return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job)); @@ -534,19 +509,75 @@ export class QueueProcessorService implements OnApplicationShutdown { ...baseWorkerOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), autorun: false, }); + this.endedPollNotificationQueueWorker + .on('active', (job) => logger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => { + this.logError(logger, err, job); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: EndedPollNotification: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => this.logError(logger, err)) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion //#region schedule note post { + const logger = this.logger.createSubLogger('scheduleNotePost'); + this.schedulerNotePostQueueWorker = new Bull.Worker(QUEUE.SCHEDULE_NOTE_POST, (job) => this.scheduleNotePostProcessorService.process(job), { ...baseWorkerOptions(this.config, QUEUE.SCHEDULE_NOTE_POST), autorun: false, }); + this.schedulerNotePostQueueWorker + .on('active', (job) => logger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => { + this.logError(logger, err, job); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: ${QUEUE.SCHEDULE_NOTE_POST}: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => this.logError(logger, err)) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion } + private logError(logger: Logger, err: unknown, job?: Bull.Job | null): void { + const parts: string[] = []; + + // Render job + if (job) { + parts.push('job ['); + parts.push(getJobInfo(job)); + parts.push('] failed: '); + } else { + parts.push('job failed: '); + } + + // Render error + const fullError = renderFullError(err); + const errorText = typeof(fullError) === 'string' ? fullError : undefined; + if (errorText) { + parts.push(errorText); + } else if (job?.failedReason) { + parts.push(job.failedReason); + } + + const message = parts.join(''); + const data = typeof(fullError) !== 'string' ? { err: fullError } : undefined; + logger.error(message, data); + } + @bindThis public async start(): Promise { await Promise.all([ diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index 4769cccabf..30bdd6ccca 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -62,7 +62,7 @@ export class AggregateRetentionProcessorService { }); } catch (err) { if (isDuplicateKeyValueError(err)) { - this.logger.succ('Skip because it has already been processed by another worker.'); + this.logger.debug('Skip because it has already been processed by another worker.'); return; } throw err; @@ -87,6 +87,6 @@ export class AggregateRetentionProcessorService { }); } - this.logger.succ('Retention aggregated.'); + this.logger.info('Retention aggregated.'); } } diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts index d49c99f694..83b375de3f 100644 --- a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts +++ b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts @@ -37,6 +37,6 @@ export class BakeBufferedReactionsProcessorService { await this.reactionsBufferingService.bake(); - this.logger.succ('All buffered reactions baked.'); + this.logger.info('All buffered reactions baked.'); } } diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index 448fc9c763..76d0cb4304 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -41,6 +41,6 @@ export class CheckExpiredMutingsProcessorService { await this.userMutingService.unmute(expired); } - this.logger.succ('All expired mutings checked.'); + this.logger.info('All expired mutings checked.'); } } diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index db8d2e789e..7821cd3d1d 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -98,16 +98,16 @@ export class CheckModeratorsActivityProcessorService { @bindThis public async process(): Promise { - this.logger.info('start.'); + this.logger.debug('start.'); const meta = await this.metaService.fetch(false); if (!meta.disableRegistration) { await this.processImpl(); } else { - this.logger.info('is already invitation only.'); + this.logger.debug('is already invitation only.'); } - this.logger.succ('finish.'); + this.logger.debug('finish.'); } @bindThis diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index 8c5faa8d07..c11682b0fe 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -62,6 +62,6 @@ export class CleanChartsProcessorService { await this.perUserDriveChart.clean(); await this.apRequestChart.clean(); - this.logger.succ('All charts successfully cleaned.'); + this.logger.info('All charts successfully cleaned.'); } } diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index a26b69cd2b..104d19103f 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -69,6 +69,6 @@ export class CleanProcessorService { this.reversiService.cleanOutdatedGames(); - this.logger.succ('Cleaned.'); + this.logger.info('Cleaned.'); } } diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index 81842b221f..2eddae95c8 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -75,6 +75,6 @@ export class CleanRemoteFilesProcessorService { await job.updateProgress(100 / total * deletedCount); } - this.logger.succ(`All cached remote files processed. Total deleted: ${deletedCount}, Failed: ${errorCount}.`); + this.logger.info(`All cached remote files processed. Total deleted: ${deletedCount}, Failed: ${errorCount}.`); } } diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 4e9779a41b..6a1a8bcc66 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -128,7 +128,7 @@ export class DeleteAccountProcessorService { userId: user.id, }); - this.logger.succ('All clips have been deleted.'); + this.logger.info('All clips have been deleted.'); } { // Delete favorites @@ -136,7 +136,7 @@ export class DeleteAccountProcessorService { userId: user.id, }); - this.logger.succ('All favorites have been deleted.'); + this.logger.info('All favorites have been deleted.'); } { // Delete user relations @@ -172,7 +172,7 @@ export class DeleteAccountProcessorService { muteeId: user.id, }); - this.logger.succ('All user relations have been deleted.'); + this.logger.info('All user relations have been deleted.'); } { // Delete reactions @@ -206,7 +206,7 @@ export class DeleteAccountProcessorService { } } - this.logger.succ('All reactions have been deleted'); + this.logger.info('All reactions have been deleted'); } { // Poll votes @@ -238,7 +238,7 @@ export class DeleteAccountProcessorService { }); } - this.logger.succ('All poll votes have been deleted'); + this.logger.info('All poll votes have been deleted'); } { // Delete scheduled notes @@ -254,7 +254,7 @@ export class DeleteAccountProcessorService { userId: user.id, }); - this.logger.succ('All scheduled notes deleted'); + this.logger.info('All scheduled notes deleted'); } { // Delete notes @@ -312,7 +312,7 @@ export class DeleteAccountProcessorService { } } - this.logger.succ('All of notes deleted'); + this.logger.info('All of notes deleted'); } { // Delete files @@ -341,7 +341,7 @@ export class DeleteAccountProcessorService { } } - this.logger.succ('All of files deleted'); + this.logger.info('All of files deleted'); } { // Delete actor logs @@ -353,7 +353,7 @@ export class DeleteAccountProcessorService { await this.apLogService.deleteInboxLogs(user.id) .catch(err => this.logger.error(err, `Failed to delete AP logs for user '${user.uri}'`)); - this.logger.succ('All AP logs deleted'); + this.logger.info('All AP logs deleted'); } // Do this BEFORE deleting the account! @@ -379,7 +379,7 @@ export class DeleteAccountProcessorService { await this.usersRepository.delete(user.id); } - this.logger.succ('Account data deleted'); + this.logger.info('Account data deleted'); } { // Send email notification diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 291fa4a6d8..ac3cddbed0 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -74,6 +74,6 @@ export class DeleteDriveFilesProcessorService { job.updateProgress(deletedCount / total); } - this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); + this.logger.info(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); } } diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 5a16496011..fc4c8bb814 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -133,23 +133,18 @@ export class DeliverProcessorService { } }); - if (res instanceof StatusError) { + if (res instanceof StatusError && !res.isRetryable) { // 4xx - if (!res.isRetryable) { - // 相手が閉鎖していることを明示しているため、配送停止する - if (job.data.isSharedInbox && res.statusCode === 410) { - this.federatedInstanceService.fetchOrRegister(host).then(i => { - this.federatedInstanceService.update(i.id, { - suspensionState: 'goneSuspended', - }); + // 相手が閉鎖していることを明示しているため、配送停止する + if (job.data.isSharedInbox && res.statusCode === 410) { + this.federatedInstanceService.fetchOrRegister(host).then(i => { + this.federatedInstanceService.update(i.id, { + suspensionState: 'goneSuspended', }); - throw new Bull.UnrecoverableError(`${host} is gone`); - } - throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); + }); + throw new Bull.UnrecoverableError(`${host} is gone`); } - - // 5xx etc. - throw new Error(`${res.statusCode} ${res.statusMessage}`); + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } else { // DNS error, socket error, timeout ... throw res; diff --git a/packages/backend/src/queue/processors/ExportAccountDataProcessorService.ts b/packages/backend/src/queue/processors/ExportAccountDataProcessorService.ts index 33a2362c4a..58d542635f 100644 --- a/packages/backend/src/queue/processors/ExportAccountDataProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAccountDataProcessorService.ts @@ -22,6 +22,7 @@ import { Packed } from '@/misc/json-schema.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { EmailService } from '@/core/EmailService.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -85,21 +86,23 @@ export class ExportAccountDataProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info('Exporting Account Data...'); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } const profile = await this.userProfilesRepository.findOneBy({ userId: job.data.user.id }); if (profile == null) { + this.logger.debug(`Skip: user ${job.data.user.id} has no profile`); return; } + this.logger.info(`Exporting account data for ${job.data.user.id} ...`); + const [path, cleanup] = await createTempDir(); - this.logger.info(`Temp dir is ${path}`); + this.logger.debug(`Temp dir is ${path}`); // User Export @@ -113,7 +116,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { userStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing user:', err); rej(err); } else { res(); @@ -145,7 +148,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { profileStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing profile:', err); rej(err); } else { res(); @@ -179,7 +182,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { ipStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing IPs:', err); rej(err); } else { res(); @@ -214,7 +217,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { notesStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing notes:', err); rej(err); } else { res(); @@ -275,7 +278,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { followingStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing following:', err); rej(err); } else { res(); @@ -345,7 +348,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { followerStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing followers:', err); rej(err); } else { res(); @@ -406,7 +409,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { filesStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing drive:', err); rej(err); } else { res(); @@ -432,7 +435,7 @@ export class ExportAccountDataProcessorService { await this.downloadService.downloadUrl(file.url, filePath); downloaded = true; } catch (e) { - this.logger.error(e instanceof Error ? e : new Error(e as string)); + this.logger.error(`Error writing drive file ${file.id} (${file.name}): ${renderInlineError(e)}`); } if (!downloaded) { @@ -464,7 +467,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { mutingStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing mutings:', err); rej(err); } else { res(); @@ -527,7 +530,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { blockingStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing blockings:', err); rej(err); } else { res(); @@ -589,7 +592,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { favoriteStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing favorites:', err); rej(err); } else { res(); @@ -650,7 +653,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { antennaStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing antennas:', err); rej(err); } else { res(); @@ -708,7 +711,7 @@ export class ExportAccountDataProcessorService { return new Promise((res, rej) => { listStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error writing lists:', err); rej(err); } else { res(); @@ -744,12 +747,12 @@ export class ExportAccountDataProcessorService { zlib: { level: 0 }, }); archiveStream.on('close', async () => { - this.logger.succ(`Exported to: ${archivePath}`); + this.logger.debug(`Exported to path: ${archivePath}`); const fileName = 'data-request-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to drive: ${driveFile.id}`); cleanup(); archiveCleanup(); if (profile.email) { diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index b3111865ad..61d76da5ac 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -45,15 +45,19 @@ export class ExportAntennasProcessorService { public async process(job: Bull.Job): Promise { const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + + this.logger.info(`Exporting antennas of ${job.data.user.id} ...`); + const [path, cleanup] = await createTemp(); const stream = fs.createWriteStream(path, { flags: 'a' }); const write = (input: string): Promise => { return new Promise((resolve, reject) => { stream.write(input, err => { if (err) { - this.logger.error(err); + this.logger.error('Error exporting antennas:', err); reject(); } else { resolve(); @@ -96,7 +100,7 @@ export class ExportAntennasProcessorService { const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); - this.logger.succ('Exported to: ' + driveFile.id); + this.logger.debug('Exported to: ' + driveFile.id); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'antenna', diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index ecc439db69..4c17c3f718 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -40,17 +40,18 @@ export class ExportBlockingProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting blocking of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.info(`Exporting blocking of ${job.data.user.id} ...`); + // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`Temp file is ${path}`); + this.logger.debug(`Temp file is ${path}`); try { const stream = fs.createWriteStream(path, { flags: 'a' }); @@ -87,7 +88,7 @@ export class ExportBlockingProcessorService { await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { - this.logger.error(err); + this.logger.error('Error exporting blocking:', err); rej(err); } else { res(); @@ -105,12 +106,12 @@ export class ExportBlockingProcessorService { } stream.end(); - this.logger.succ(`Exported to: ${path}`); + this.logger.debug(`Exported to: ${path}`); const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'blocking', diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts index 583ddbb745..1d34d2b4e6 100644 --- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -51,17 +51,18 @@ export class ExportClipsProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting clips of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.info(`Exporting clips of ${job.data.user.id} ...`); + // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`Temp file is ${path}`); + this.logger.debug(`Temp file is ${path}`); try { const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' })); @@ -75,12 +76,12 @@ export class ExportClipsProcessorService { await writer.write(']'); await writer.close(); - this.logger.succ(`Exported to: ${path}`); + this.logger.debug(`Exported to: ${path}`); const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'clip', diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index 14d32e78b3..b8f208bbfc 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -45,16 +45,17 @@ export class ExportCustomEmojisProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info('Exporting custom emojis ...'); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.info(`Exporting custom emojis of ${job.data.user.id} ...`); + const [path, cleanup] = await createTempDir(); - this.logger.info(`Temp dir is ${path}`); + this.logger.debug(`Temp dir is ${path}`); const metaPath = path + '/meta.json'; @@ -66,7 +67,7 @@ export class ExportCustomEmojisProcessorService { return new Promise((res, rej) => { metaStream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error exporting custom emojis:', err); rej(err); } else { res(); @@ -101,7 +102,7 @@ export class ExportCustomEmojisProcessorService { await this.downloadService.downloadUrl(emoji.originalUrl, emojiPath); downloaded = true; } catch (e) { // TODO: 何度か再試行 - this.logger.error(e instanceof Error ? e : new Error(e as string)); + this.logger.error('Error exporting custom emojis:', e as Error); } if (!downloaded) { @@ -130,12 +131,12 @@ export class ExportCustomEmojisProcessorService { zlib: { level: 0 }, }); archiveStream.on('close', async () => { - this.logger.succ(`Exported to: ${archivePath}`); + this.logger.debug(`Exported to: ${archivePath}`); const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'customEmoji', diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index b81feece01..b5716f2d49 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -45,17 +45,18 @@ export class ExportFavoritesProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting favorites of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.info(`Exporting favorites of ${job.data.user.id} ...`); + // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`Temp file is ${path}`); + this.logger.debug(`Temp file is ${path}`); try { const stream = fs.createWriteStream(path, { flags: 'a' }); @@ -64,7 +65,7 @@ export class ExportFavoritesProcessorService { return new Promise((res, rej) => { stream.write(text, err => { if (err) { - this.logger.error(err); + this.logger.error('Error exporting favorites:', err); rej(err); } else { res(); @@ -119,12 +120,12 @@ export class ExportFavoritesProcessorService { await write(']'); stream.end(); - this.logger.succ(`Exported to: ${path}`); + this.logger.debug(`Exported to: ${path}`); const fileName = 'favorites-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'favorite', diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 903f962515..883f35e366 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -44,17 +44,18 @@ export class ExportFollowingProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting following of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.info(`Exporting following of ${job.data.user.id} ...`); + // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`Temp file is ${path}`); + this.logger.debug(`Temp file is ${path}`); try { const stream = fs.createWriteStream(path, { flags: 'a' }); @@ -98,7 +99,7 @@ export class ExportFollowingProcessorService { await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { - this.logger.error(err); + this.logger.error('Error exporting following:', err); rej(err); } else { res(); @@ -109,12 +110,12 @@ export class ExportFollowingProcessorService { } stream.end(); - this.logger.succ(`Exported to: ${path}`); + this.logger.debug(`Exported to: ${path}`); const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'following', diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index f9867ade29..9cdb94beaf 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -40,17 +40,18 @@ export class ExportMutingProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting muting of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.debug(`Exporting muting of ${job.data.user.id} ...`); + // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`Temp file is ${path}`); + this.logger.debug(`Temp file is ${path}`); try { const stream = fs.createWriteStream(path, { flags: 'a' }); @@ -88,7 +89,7 @@ export class ExportMutingProcessorService { await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { - this.logger.error(err); + this.logger.error('Error exporting mutings:', err); rej(err); } else { res(); @@ -106,12 +107,12 @@ export class ExportMutingProcessorService { } stream.end(); - this.logger.succ(`Exported to: ${path}`); + this.logger.debug(`Exported to: ${path}`); const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'muting', diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index 9e2b678219..7d49a8dab2 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -120,17 +120,18 @@ export class ExportNotesProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting notes of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.info(`Exporting notes of ${job.data.user.id} ...`); + // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`Temp file is ${path}`); + this.logger.debug(`Temp file is ${path}`); try { // メモリが足りなくならないようにストリームで処理する @@ -146,12 +147,12 @@ export class ExportNotesProcessorService { .pipeThrough(new TextEncoderStream()) .pipeTo(new FileWriterStream(path)); - this.logger.succ(`Exported to: ${path}`); + this.logger.debug(`Exported to: ${path}`); const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'note', diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index c483d79854..43043e3a26 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -43,13 +43,14 @@ export class ExportUserListsProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Exporting user lists of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } + this.logger.info(`Exporting user lists of ${job.data.user.id} ...`); + const lists = await this.userListsRepository.findBy({ userId: user.id, }); @@ -57,7 +58,7 @@ export class ExportUserListsProcessorService { // Create temp file const [path, cleanup] = await createTemp(); - this.logger.info(`Temp file is ${path}`); + this.logger.debug(`Temp file is ${path}`); try { const stream = fs.createWriteStream(path, { flags: 'a' }); @@ -74,7 +75,7 @@ export class ExportUserListsProcessorService { await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { - this.logger.error(err); + this.logger.error('Error exporting lists:', err); rej(err); } else { res(); @@ -85,12 +86,12 @@ export class ExportUserListsProcessorService { } stream.end(); - this.logger.succ(`Exported to: ${path}`); + this.logger.debug(`Exported to: ${path}`); const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); - this.logger.succ(`Exported to: ${driveFile.id}`); + this.logger.debug(`Exported to: ${driveFile.id}`); this.notificationService.createNotification(user.id, 'exportCompleted', { exportedEntity: 'userList', diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index 9c033b73e2..f29a19ce66 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -8,7 +8,7 @@ import _Ajv from 'ajv'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import Logger from '@/logger.js'; -import type { AntennasRepository } from '@/models/_.js'; +import type { AntennasRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; @@ -59,6 +59,9 @@ export class ImportAntennasProcessorService { @Inject(DI.antennasRepository) private antennasRepository: AntennasRepository, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private queueLoggerService: QueueLoggerService, private idService: IdService, private globalEventService: GlobalEventService, @@ -68,12 +71,20 @@ export class ImportAntennasProcessorService { @bindThis public async process(job: Bull.Job): Promise { + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); + return; + } + + this.logger.debug(`Importing blocking of ${job.data.user.id} ...`); + const now = new Date(); try { for (const antenna of job.data.antenna) { if (antenna.keywords.length === 0 || antenna.keywords[0].every(x => x === '')) continue; if (!validate(antenna)) { - this.logger.warn('Validation Failed'); + this.logger.warn('Antenna validation failed'); continue; } const result = await this.antennasRepository.insertOne({ @@ -92,11 +103,11 @@ export class ImportAntennasProcessorService { withReplies: antenna.withReplies, withFile: antenna.withFile, }); - this.logger.succ('Antenna created: ' + result.id); + this.logger.debug('Antenna created: ' + result.id); this.globalEventService.publishInternalEvent('antennaCreated', result); } } catch (err: any) { - this.logger.error(err); + this.logger.error('Error importing antennas:', err); } } } diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index b78229c648..e2de9532eb 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -40,10 +40,9 @@ export class ImportBlockingProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Importing blocking of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } @@ -51,14 +50,17 @@ export class ImportBlockingProcessorService { id: job.data.fileId, }); if (file == null) { + this.logger.debug(`Skip: file ${job.data.fileId} does not exist`); return; } + this.logger.debug(`Importing blocking of ${job.data.user.id} ...`); + const csv = await this.downloadService.downloadTextFile(file.url); const targets = csv.trim().split('\n'); this.queueService.createImportBlockingToDbJob({ id: user.id }, targets); - this.logger.succ('Import jobs created'); + this.logger.debug('Import jobs created'); } @bindThis @@ -93,11 +95,11 @@ export class ImportBlockingProcessorService { // skip myself if (target.id === job.data.user.id) return; - this.logger.info(`Block ${target.id} ...`); + this.logger.debug(`Block ${target.id} ...`); this.queueService.createBlockJob([{ from: { id: user.id }, to: { id: target.id }, silent: true }]); } catch (e) { - this.logger.warn(`Error: ${e}`); + this.logger.error('Error importing blockings:', e as Error); } } } diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index d08cadd378..4b909328cd 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -16,6 +16,7 @@ import { DriveService } from '@/core/DriveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { bindThis } from '@/decorators.js'; import type { Config } from '@/config.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @@ -45,18 +46,19 @@ export class ImportCustomEmojisProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info('Importing custom emojis ...'); - const file = await this.driveFilesRepository.findOneBy({ id: job.data.fileId, }); if (file == null) { + this.logger.debug(`Skip: file ${job.data.fileId} does not exist`); return; } + this.logger.info(`Importing custom emojis from ${file.id} (${file.name}) ...`); + const [path, cleanup] = await createTempDir(); - this.logger.info(`Temp dir is ${path}`); + this.logger.debug(`Temp dir is ${path}`); const destPath = path + '/emojis.zip'; @@ -65,14 +67,14 @@ export class ImportCustomEmojisProcessorService { await this.downloadService.downloadUrl(file.url, destPath, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize }); } catch (e) { // TODO: 何度か再試行 if (e instanceof Error || typeof e === 'string') { - this.logger.error(e); + this.logger.error('Error importing custom emojis:', e as Error); } throw e; } const outputPath = path + '/emojis'; try { - this.logger.succ(`Unzipping to ${outputPath}`); + this.logger.debug(`Unzipping to ${outputPath}`); ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath)); const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8'); const meta = JSON.parse(metaRaw); @@ -117,7 +119,7 @@ export class ImportCustomEmojisProcessorService { }); } catch (e) { if (e instanceof Error || typeof e === 'string') { - this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`); + this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${renderInlineError(e)}`); } continue; } @@ -125,11 +127,9 @@ export class ImportCustomEmojisProcessorService { cleanup(); - this.logger.succ('Imported'); + this.logger.debug('Imported'); } catch (e) { - if (e instanceof Error || typeof e === 'string') { - this.logger.error(e); - } + this.logger.error('Error importing custom emojis:', e as Error); cleanup(); throw e; } diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index 70c9f3a096..816d5cf65a 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -40,10 +40,9 @@ export class ImportFollowingProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Importing following of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } @@ -51,14 +50,17 @@ export class ImportFollowingProcessorService { id: job.data.fileId, }); if (file == null) { + this.logger.debug(`Skip: file ${job.data.fileId} does not exist`); return; } + this.logger.info(`Importing following of ${job.data.user.id} ...`); + const csv = await this.downloadService.downloadTextFile(file.url); const targets = csv.trim().split('\n'); this.queueService.createImportFollowingToDbJob({ id: user.id }, targets, job.data.withReplies); - this.logger.succ('Import jobs created'); + this.logger.debug('Import jobs created'); } @bindThis @@ -93,11 +95,11 @@ export class ImportFollowingProcessorService { // skip myself if (target.id === job.data.user.id) return; - this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`); + this.logger.debug(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`); this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]); } catch (e) { - this.logger.warn(`Error: ${e}`); + this.logger.error('Error importing followings:', e as Error); } } } diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index ec9d2b6c4c..d3827b12fd 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -14,6 +14,7 @@ import { DownloadService } from '@/core/DownloadService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @@ -40,10 +41,9 @@ export class ImportMutingProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Importing muting of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } @@ -51,9 +51,12 @@ export class ImportMutingProcessorService { id: job.data.fileId, }); if (file == null) { + this.logger.debug(`Skip: file ${job.data.fileId} does not exist`); return; } + this.logger.info(`Importing muting of ${job.data.user.id} ...`); + const csv = await this.downloadService.downloadTextFile(file.url); let linenum = 0; @@ -88,14 +91,14 @@ export class ImportMutingProcessorService { // skip myself if (target.id === job.data.user.id) continue; - this.logger.info(`Mute[${linenum}] ${target.id} ...`); + this.logger.debug(`Mute[${linenum}] ${target.id} ...`); await this.userMutingService.mute(user, target); } catch (e) { - this.logger.warn(`Error in line:${linenum} ${e}`); + this.logger.warn(`Error in line:${linenum} ${renderInlineError(e)}`); } } - this.logger.succ('Imported'); + this.logger.debug('Imported'); } } diff --git a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts index 5e660e8081..e209855720 100644 --- a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts @@ -159,10 +159,9 @@ export class ImportNotesProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Starting note import of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } @@ -170,9 +169,12 @@ export class ImportNotesProcessorService { id: job.data.fileId, }); if (file == null) { + this.logger.debug(`Skip: file ${job.data.fileId} does not exist`); return; } + this.logger.info(`Starting note import of ${job.data.user.id} ...`); + let folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id }); if (folder == null) { await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Imports', userId: job.data.user.id }); @@ -184,7 +186,7 @@ export class ImportNotesProcessorService { if (type === 'Twitter' || file.name.startsWith('twitter') && file.name.endsWith('.zip')) { const [path, cleanup] = await createTempDir(); - this.logger.info(`Temp dir is ${path}`); + this.logger.debug(`Temp dir is ${path}`); const destPath = path + '/twitter.zip'; @@ -192,15 +194,13 @@ export class ImportNotesProcessorService { await fsp.writeFile(destPath, '', 'binary'); await this.downloadUrl(file.url, destPath); } catch (e) { // TODO: 何度か再試行 - if (e instanceof Error || typeof e === 'string') { - this.logger.error(e); - } + this.logger.error('Error importing notes:', e as Error); throw e; } const outputPath = path + '/twitter'; try { - this.logger.succ(`Unzipping to ${outputPath}`); + this.logger.debug(`Unzipping to ${outputPath}`); ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath)); const unprocessedTweets = this.parseTwitterFile(await fsp.readFile(outputPath + '/data/tweets.js', 'utf-8')); @@ -214,7 +214,7 @@ export class ImportNotesProcessorService { } else if (type === 'Facebook' || file.name.startsWith('facebook-') && file.name.endsWith('.zip')) { const [path, cleanup] = await createTempDir(); - this.logger.info(`Temp dir is ${path}`); + this.logger.debug(`Temp dir is ${path}`); const destPath = path + '/facebook.zip'; @@ -222,15 +222,13 @@ export class ImportNotesProcessorService { await fsp.writeFile(destPath, '', 'binary'); await this.downloadUrl(file.url, destPath); } catch (e) { // TODO: 何度か再試行 - if (e instanceof Error || typeof e === 'string') { - this.logger.error(e); - } + this.logger.error('Error importing notes:', e as Error); throw e; } const outputPath = path + '/facebook'; try { - this.logger.succ(`Unzipping to ${outputPath}`); + this.logger.debug(`Unzipping to ${outputPath}`); ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath)); const postsJson = await fsp.readFile(outputPath + '/your_activity_across_facebook/posts/your_posts__check_ins__photos_and_videos_1.json', 'utf-8'); const posts = JSON.parse(postsJson); @@ -247,7 +245,7 @@ export class ImportNotesProcessorService { } else if (file.name.endsWith('.zip')) { const [path, cleanup] = await createTempDir(); - this.logger.info(`Temp dir is ${path}`); + this.logger.debug(`Temp dir is ${path}`); const destPath = path + '/unknown.zip'; @@ -255,15 +253,13 @@ export class ImportNotesProcessorService { await fsp.writeFile(destPath, '', 'binary'); await this.downloadUrl(file.url, destPath); } catch (e) { // TODO: 何度か再試行 - if (e instanceof Error || typeof e === 'string') { - this.logger.error(e); - } + this.logger.error('Error importing notes:', e as Error); throw e; } const outputPath = path + '/unknown'; try { - this.logger.succ(`Unzipping to ${outputPath}`); + this.logger.debug(`Unzipping to ${outputPath}`); ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath)); const isInstagram = type === 'Instagram' || fs.existsSync(outputPath + '/instagram_live') || fs.existsSync(outputPath + '/instagram_ads_and_businesses'); const isOutbox = type === 'Mastodon' || fs.existsSync(outputPath + '/outbox.json'); @@ -307,15 +303,13 @@ export class ImportNotesProcessorService { } else if (job.data.type === 'Misskey' || file.name.startsWith('notes-') && file.name.endsWith('.json')) { const [path, cleanup] = await createTemp(); - this.logger.info(`Temp dir is ${path}`); + this.logger.debug(`Temp dir is ${path}`); try { await fsp.writeFile(path, '', 'utf-8'); await this.downloadUrl(file.url, path); } catch (e) { // TODO: 何度か再試行 - if (e instanceof Error || typeof e === 'string') { - this.logger.error(e); - } + this.logger.error('Error importing notes:', e as Error); throw e; } @@ -326,7 +320,7 @@ export class ImportNotesProcessorService { cleanup(); } - this.logger.succ('Import jobs created'); + this.logger.debug('Import jobs created'); } @bindThis @@ -365,7 +359,7 @@ export class ImportNotesProcessorService { try { await this.downloadUrl(file.url, filePath); } catch (e) { // TODO: 何度か再試行 - this.logger.error(e instanceof Error ? e : new Error(e as string)); + this.logger.error('Error importing notes:', e as Error); } const driveFile = await this.driveService.addFile({ user: user, @@ -504,7 +498,7 @@ export class ImportNotesProcessorService { try { await this.downloadUrl(file.url, filePath); } catch (e) { // TODO: 何度か再試行 - this.logger.error(e instanceof Error ? e : new Error(e as string)); + this.logger.error('Error importing notes:', e as Error); } const driveFile = await this.driveService.addFile({ user: user, @@ -628,7 +622,7 @@ export class ImportNotesProcessorService { try { await this.downloadUrl(videos[0].url, filePath); } catch (e) { // TODO: 何度か再試行 - this.logger.error(e instanceof Error ? e : new Error(e as string)); + this.logger.error('Error importing notes:', e as Error); } const driveFile = await this.driveService.addFile({ user: user, @@ -653,7 +647,7 @@ export class ImportNotesProcessorService { try { await this.downloadUrl(file.media_url_https, filePath); } catch (e) { // TODO: 何度か再試行 - this.logger.error(e instanceof Error ? e : new Error(e as string)); + this.logger.error('Error importing notes:', e as Error); } const driveFile = await this.driveService.addFile({ @@ -673,7 +667,7 @@ export class ImportNotesProcessorService { const createdNote = await this.noteCreateService.import(user, { createdAt: date, reply: parentNote, text: text, files: files }); if (tweet.childNotes) this.queueService.createImportTweetsToDbJob(user, tweet.childNotes, createdNote.id); } catch (e) { - this.logger.warn(`Error: ${e}`); + this.logger.error('Error importing notes:', e as Error); } } diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index db9255b35d..482054e52f 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -15,6 +15,7 @@ import { UserListService } from '@/core/UserListService.js'; import { IdService } from '@/core/IdService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @@ -48,10 +49,9 @@ export class ImportUserListsProcessorService { @bindThis public async process(job: Bull.Job): Promise { - this.logger.info(`Importing user lists of ${job.data.user.id} ...`); - const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { + this.logger.debug(`Skip: user ${job.data.user.id} does not exist`); return; } @@ -59,9 +59,12 @@ export class ImportUserListsProcessorService { id: job.data.fileId, }); if (file == null) { + this.logger.debug(`Skip: file ${job.data.fileId} does not exist`); return; } + this.logger.info(`Importing user lists of ${job.data.user.id} ...`); + const csv = await this.downloadService.downloadTextFile(file.url); let linenum = 0; @@ -102,10 +105,10 @@ export class ImportUserListsProcessorService { this.userListService.addMember(target, list!, user); } catch (e) { - this.logger.warn(`Error in line:${linenum} ${e}`); + this.logger.warn(`Error in line:${linenum} ${renderInlineError(e)}`); } } - this.logger.succ('Imported'); + this.logger.debug('Imported'); } } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index bf36fe4373..612b16dfbf 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -31,6 +31,8 @@ import { SkApInboxLog } from '@/models/_.js'; import type { Config } from '@/config.js'; import { ApLogService, calculateDurationSince } from '@/core/ApLogService.js'; import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js'; +import { isRetryableError } from '@/misc/is-retryable-error.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; @@ -145,12 +147,11 @@ export class InboxProcessorService implements OnApplicationShutdown { authUser = await this.apDbResolverService.getAuthUserFromApId(actorId); } catch (err) { // 対象が4xxならスキップ - if (err instanceof StatusError) { - if (!err.isRetryable) { - throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${actorId} - ${err.statusCode}`); - } - throw new Error(`Error in actor ${actorId} - ${err.statusCode}`); + if (!isRetryableError(err)) { + throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${actorId}`); } + + throw err; } } @@ -227,7 +228,7 @@ export class InboxProcessorService implements OnApplicationShutdown { const ldHost = this.utilityService.extractDbHost(authUser.user.uri); if (!this.utilityService.isFederationAllowedHost(ldHost)) { - throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); + throw new Bull.UnrecoverableError(`skip: request host is blocked: ${ldHost}`); } } else { throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`); @@ -300,16 +301,8 @@ export class InboxProcessorService implements OnApplicationShutdown { } } - if (e instanceof StatusError && !e.isRetryable) { - return `skip: permanent error ${e.statusCode}`; - } - - if (e instanceof IdentifiableError && !e.isRetryable) { - if (e.message) { - return `skip: permanent error ${e.id}: ${e.message}`; - } else { - return `skip: permanent error ${e.id}`; - } + if (!isRetryableError(e)) { + return `skip: permanent error ${renderInlineError(e)}`; } throw e; diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index 0c47fdedb3..5b7a871af9 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -36,6 +36,6 @@ export class ResyncChartsProcessorService { await this.notesChart.resync(); await this.usersChart.resync(); - this.logger.succ('All charts successfully resynced.'); + this.logger.info('All charts successfully resynced.'); } } diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts index d823d98ef1..73088f3312 100644 --- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js'; import { NotificationService } from '@/core/NotificationService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiScheduleNoteType } from '@/models/NoteSchedule.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { ScheduleNotePostJobData } from '../types.js'; @@ -129,10 +130,11 @@ export class ScheduleNotePostProcessorService { channel, }).catch(async (err: IdentifiableError) => { this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { - reason: err.message, + reason: renderInlineError(err), }); await this.noteScheduleRepository.remove(data); - throw this.logger.error(`Schedule Note Failed Reason: ${err.message}`); + this.logger.error(`Scheduled note failed: ${renderInlineError(err)}`); + throw err; }); await this.noteScheduleRepository.remove(data); this.notificationService.createNotification(me.id, 'scheduledNotePosted', { diff --git a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts index f6bef52684..f9fcd1e928 100644 --- a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts @@ -12,6 +12,7 @@ import type Logger from '@/logger.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import { SystemWebhookDeliverJobData } from '../types.js'; @@ -63,21 +64,16 @@ export class SystemWebhookDeliverProcessorService { return 'Success'; } catch (res) { - this.logger.error(res as Error); + this.logger.error(`Failed to send webhook: ${renderInlineError(res)}`); this.systemWebhooksRepository.update({ id: job.data.webhookId }, { latestSentAt: new Date(), latestStatus: res instanceof StatusError ? res.statusCode : 1, }); - if (res instanceof StatusError) { + if (res instanceof StatusError && !res.isRetryable) { // 4xx - if (!res.isRetryable) { - throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); - } - - // 5xx etc. - throw new Error(`${res.statusCode} ${res.statusMessage}`); + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } else { // DNS error, socket error, timeout ... throw res; diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index fc8856a271..b4b8b1f205 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -62,6 +62,6 @@ export class TickChartsProcessorService { await this.perUserDriveChart.tick(false); await this.apRequestChart.tick(false); - this.logger.succ('All charts successfully ticked.'); + this.logger.info('All charts successfully ticked.'); } } diff --git a/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts index 9ec630ef70..0208ce6038 100644 --- a/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts @@ -69,14 +69,9 @@ export class UserWebhookDeliverProcessorService { latestStatus: res instanceof StatusError ? res.statusCode : 1, }); - if (res instanceof StatusError) { + if (res instanceof StatusError && !res.isRetryable) { // 4xx - if (!res.isRetryable) { - throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); - } - - // 5xx etc. - throw new Error(`${res.statusCode} ${res.statusMessage}`); + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } else { // DNS error, socket error, timeout ... throw res; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 4ef5539cff..1a372cb789 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -32,6 +32,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js'; import { AuthenticateService } from '@/server/api/AuthenticateService.js'; import { SkRateLimiterService } from '@/server/SkRateLimiterService.js'; import { Keyed, RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; const _filename = fileURLToPath(import.meta.url); @@ -120,7 +121,7 @@ export class FileServerService { @bindThis private async errorHandler(request: FastifyRequest<{ Params?: { [x: string]: any }; Querystring?: { [x: string]: any }; }>, reply: FastifyReply, err?: any) { - this.logger.error(`${err}`); + this.logger.error(`Unhandled error in file server: ${renderInlineError(err)}`); reply.header('Cache-Control', 'max-age=300'); @@ -353,7 +354,7 @@ export class FileServerService { if (!request.headers['user-agent']) { throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { - throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); + throw new StatusError(`Refusing to proxy recursive request to ${url} (from user-agent ${request.headers['user-agent']})`, 403, 'Proxy is recursive'); } // Create temp file @@ -383,7 +384,7 @@ export class FileServerService { ) { if (!isConvertibleImage) { // 画像でないなら404でお茶を濁す - throw new StatusError('Unexpected mime', 404); + throw new StatusError(`Unexpected non-convertible mime: ${file.mime}`, 404, 'Unexpected mime'); } } @@ -447,7 +448,7 @@ export class FileServerService { } else if (file.mime === 'image/svg+xml') { image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048); } else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) { - throw new StatusError('Rejected type', 403, 'Rejected type'); + throw new StatusError(`Blocked mime type: ${file.mime}`, 403, 'Blocked mime type'); } if (!image) { @@ -521,7 +522,7 @@ export class FileServerService { > { if (url.startsWith(`${this.config.url}/files/`)) { const key = url.replace(`${this.config.url}/files/`, '').split('/').shift(); - if (!key) throw new StatusError('Invalid File Key', 400, 'Invalid File Key'); + if (!key) throw new StatusError(`Invalid file URL ${url}`, 400, 'Invalid file url'); return await this.getFileFromKey(key); } diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 2d20aa1222..77b4519570 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -21,6 +21,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ApiServerService } from './api/ApiServerService.js'; @@ -277,7 +278,7 @@ export class ServerService implements OnApplicationShutdown { this.logger.error(`Port ${this.config.port} is already in use by another process.`); break; default: - this.logger.error(err); + this.logger.error(`Unhandled error in server: ${renderInlineError(err)}`); break; } diff --git a/packages/backend/src/server/SkRateLimiterService.ts b/packages/backend/src/server/SkRateLimiterService.ts index 8978318045..35e87b0fe8 100644 --- a/packages/backend/src/server/SkRateLimiterService.ts +++ b/packages/backend/src/server/SkRateLimiterService.ts @@ -389,7 +389,7 @@ function createLimitKey(limit: ParsedLimit, actor: string, value: string): strin return `rl_${actor}_${limit.key}_${value}`; } -class ConflictError extends Error {} +export class ConflictError extends Error {} interface LimitCounter { timestamp: number; diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 5c9e5717bb..6d6c86bb82 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -20,12 +20,14 @@ import { RoleService } from '@/core/RoleService.js'; import type { Config } from '@/config.js'; import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js'; import { SkRateLimiterService } from '@/server/SkRateLimiterService.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { ApiError } from './error.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; import type { OnApplicationShutdown } from '@nestjs/common'; import type { IEndpointMeta, IEndpoint } from './endpoints.js'; +import { renderFullError } from '@/misc/render-full-error.js'; const accessDenied = { message: 'Access denied.', @@ -100,26 +102,26 @@ export class ApiCallService implements OnApplicationShutdown { throw err; } else { const errId = randomUUID(); - this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { - ep: ep.name, - ps: data, - e: { - message: err.message, - code: err.name, - stack: err.stack, - id: errId, - }, + const fullError = renderFullError(err); + const message = typeof(fullError) === 'string' + ? `Internal error id=${errId} occurred in ${ep.name}: ${fullError}` + : `Internal error id=${errId} occurred in ${ep.name}:`; + const data = typeof(fullError) === 'object' + ? { e: fullError } + : {}; + this.logger.error(message, { + user: userId ?? '', + ...data, }); if (this.config.sentryForBackend) { - Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { + Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${renderInlineError(err)}`, { level: 'error', user: { id: userId, }, extra: { ep: ep.name, - ps: data, e: { message: err.message, code: err.name, diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 419017aaf4..f2850e6258 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -36,7 +36,7 @@ export class GetterService { const note = await this.notesRepository.findOneBy({ id: noteId }); if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', `Note ${noteId} does not exist`); } return note; @@ -47,7 +47,7 @@ export class GetterService { const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); if (note == null) { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', `Note ${noteId} does not exist`); } return note; @@ -59,7 +59,7 @@ export class GetterService { @bindThis public async getEdits(noteId: MiNote['id']) { const edits = await this.noteEditRepository.findBy({ noteId: noteId }).catch(() => { - throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', `Note ${noteId} does not exist`); }); return edits; @@ -73,7 +73,7 @@ export class GetterService { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) { - throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); + throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', `User ${userId} does not exist`); } return user as MiLocalUser | MiRemoteUser; diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 7f371ea309..a53fec88d0 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -205,37 +205,37 @@ export class SigninApiService { if (process.env.NODE_ENV !== 'test') { if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableFC && this.meta.fcSecretKey) { await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableTestcaptcha) { await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } } diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts index f84f50523b..38886f8876 100644 --- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -128,7 +128,7 @@ export class SigninWithPasskeyApiService { try { authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); } catch (err) { - this.logger.warn(`Passkey challenge Verify error! : ${err}`); + this.logger.warn('Passkey challenge verify error:', err as Error); const errorId = (err as IdentifiableError).id; return error(403, { id: errorId, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index cb71047a24..81e3a5b706 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -83,37 +83,37 @@ export class SignupApiService { if (process.env.NODE_ENV !== 'test') { if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableFC && this.meta.fcSecretKey) { await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } if (this.meta.enableTestcaptcha) { await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => { - throw new FastifyReplyError(400, err); + throw new FastifyReplyError(400, String(err), err); }); } } @@ -287,7 +287,7 @@ export class SignupApiService { token: secret, }; } catch (err) { - throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); + throw new FastifyReplyError(400, String(err), err); } } } @@ -356,7 +356,7 @@ export class SignupApiService { return this.signinService.signin(request, reply, account as MiLocalUser); } catch (err) { - throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); + throw new FastifyReplyError(400, String(err), err); } } } diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index 129f69aca9..4644a069ee 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -68,11 +68,8 @@ export default class extends Endpoint { // eslint- private readonly moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - try { - if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only'); - } catch { - throw new ApiError(meta.errors.invalidUrl); - } + if (!URL.canParse(ps.inbox)) throw new ApiError(meta.errors.invalidUrl); + if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError(meta.errors.invalidUrl); await this.moderationLogService.log(me, 'addRelay', { inbox: ps.inbox, diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index d69850515c..d631b002cc 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -173,6 +173,10 @@ export default class extends Endpoint { // eslint- case '09d79f9e-64f1-4316-9cfa-e75c4d091574': throw new ApiError(meta.errors.federationNotAllowed); case '72180409-793c-4973-868e-5a118eb5519b': + case 'd09dc850-b76c-4f45-875a-7389339d78b8': + case 'dc110060-a5f2-461d-808b-39c62702ca64': + case '45793ab7-7648-4886-b503-429f8a0d0f73': + case '4bf8f36b-4d33-4ac9-ad76-63fa11f354e9': throw new ApiError(meta.errors.responseInvalid); // resolveLocal diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index f4c47d71bf..939eadad9b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -10,6 +10,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveService } from '@/core/DriveService.js'; import type { Config } from '@/config.js'; +import { ApiLoggerService } from '@/server/api/ApiLoggerService.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { ApiError } from '../../../error.js'; import { MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -95,6 +97,7 @@ export default class extends Endpoint { // eslint- private driveFileEntityService: DriveFileEntityService, private driveService: DriveService, + private readonly apiLoggerService: ApiLoggerService, ) { super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => { // Get 'name' parameter @@ -130,14 +133,14 @@ export default class extends Endpoint { // eslint- return await this.driveFileEntityService.pack(driveFile, { self: true }); } catch (err) { if (err instanceof Error || typeof err === 'string') { - console.error(err); + this.apiLoggerService.logger.error(`Error saving drive file: ${renderInlineError(err)}`); } if (err instanceof IdentifiableError) { if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded); } - throw new ApiError(); + throw err; } finally { cleanup!(); } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index 504a9c789e..08abd7fed5 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -72,7 +72,7 @@ export default class extends Endpoint { // eslint- ))).filter(x => x != null); if (files.length === 0) { - throw new Error(); + throw new Error('no files specified'); } const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({ diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 5243ee9603..d0f9b56863 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -73,7 +73,7 @@ export default class extends Endpoint { // eslint- ))).filter(x => x != null); if (files.length === 0) { - throw new Error(); + throw new Error('no files'); } } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index d4098458d7..931c8d69b0 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -70,7 +70,7 @@ export default class extends Endpoint { // eslint- try { await this.userAuthService.twoFactorAuthenticate(profile, token); } catch (e) { - throw new Error('authentication failed'); + throw new Error('authentication failed', { cause: e }); } } diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 7852b5a2e1..e2a14b61af 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -17,7 +17,7 @@ import { ApiLoggerService } from '@/server/api/ApiLoggerService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; - +import { renderInlineError } from '@/misc/render-inline-error.js'; import * as Acct from '@/misc/acct.js'; import { DI } from '@/di-symbols.js'; import { MiMeta } from '@/models/_.js'; @@ -105,7 +105,7 @@ export default class extends Endpoint { // eslint- const { username, host } = Acct.parse(ps.moveToAccount); // retrieve the destination account let moveTo = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => { - this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`); + this.apiLoggerService.logger.warn(`failed to resolve remote user: ${renderInlineError(e)}`); throw new ApiError(meta.errors.noSuchUser); }); const destination = await this.getterService.getUser(moveTo.id) as MiLocalUser | MiRemoteUser; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 7c13503f9b..dda42ce0e4 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -34,6 +34,7 @@ import { verifyFieldLinks } from '@/misc/verify-field-link.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; import { userUnsignedFetchOptions } from '@/const.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -516,7 +517,7 @@ export default class extends Endpoint { // eslint- // Retrieve the old account const knownAs = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => { - this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`); + this.apiLoggerService.logger.warn(`failed to resolve destination user: ${renderInlineError(e)}`); throw new ApiError(meta.errors.noSuchUser); }); if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 7b1c8adfb8..84eb661742 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -13,6 +13,7 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DI } from '@/di-symbols.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import { RoleService } from '@/core/RoleService.js'; +import { renderInlineError } from '@/misc/render-inline-error.js'; import { ApiError } from '../../error.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import type { FindOptionsWhere } from 'typeorm'; @@ -131,7 +132,7 @@ export default class extends Endpoint { // eslint- // Lookup user if (typeof ps.host === 'string' && typeof ps.username === 'string') { user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => { - this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`); + this.apiLoggerService.logger.warn(`failed to resolve remote user: ${renderInlineError(err)}`); throw new ApiError(meta.errors.failedToResolveRemoteUser); }); } else { diff --git a/packages/backend/test/unit/misc/is-retryable-error.ts b/packages/backend/test/unit/misc/is-retryable-error.ts index 096bf64d4f..6d241066f7 100644 --- a/packages/backend/test/unit/misc/is-retryable-error.ts +++ b/packages/backend/test/unit/misc/is-retryable-error.ts @@ -8,6 +8,9 @@ import { AbortError } from 'node-fetch'; import { isRetryableError } from '@/misc/is-retryable-error.js'; import { StatusError } from '@/misc/status-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { CaptchaError, captchaErrorCodes } from '@/core/CaptchaService.js'; +import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; +import { ConflictError } from '@/server/SkRateLimiterService.js'; describe(isRetryableError, () => { it('should return true for retryable StatusError', () => { @@ -55,6 +58,78 @@ describe(isRetryableError, () => { expect(result).toBeTruthy(); }); + it('should return false for CaptchaError with verificationFailed', () => { + const error = new CaptchaError(captchaErrorCodes.verificationFailed, 'verificationFailed'); + const result = isRetryableError(error); + expect(result).toBeFalsy(); + }); + + it('should return false for CaptchaError with invalidProvider', () => { + const error = new CaptchaError(captchaErrorCodes.invalidProvider, 'invalidProvider'); + const result = isRetryableError(error); + expect(result).toBeFalsy(); + }); + + it('should return false for CaptchaError with invalidParameters', () => { + const error = new CaptchaError(captchaErrorCodes.invalidParameters, 'invalidParameters'); + const result = isRetryableError(error); + expect(result).toBeFalsy(); + }); + + it('should return true for CaptchaError with noResponseProvided', () => { + const error = new CaptchaError(captchaErrorCodes.noResponseProvided, 'noResponseProvided'); + const result = isRetryableError(error); + expect(result).toBeTruthy(); + }); + + it('should return true for CaptchaError with requestFailed', () => { + const error = new CaptchaError(captchaErrorCodes.requestFailed, 'requestFailed'); + const result = isRetryableError(error); + expect(result).toBeTruthy(); + }); + + it('should return true for CaptchaError with unknown', () => { + const error = new CaptchaError(captchaErrorCodes.unknown, 'unknown'); + const result = isRetryableError(error); + expect(result).toBeTruthy(); + }); + + it('should return true for CaptchaError with any other', () => { + const error = new CaptchaError(Symbol('temp'), 'unknown'); + const result = isRetryableError(error); + expect(result).toBeTruthy(); + }); + + it('should return false for FastifyReplyError', () => { + const error = new FastifyReplyError(400, 'test error'); + const result = isRetryableError(error); + expect(result).toBeFalsy(); + }); + + it('should return true for ConflictError', () => { + const error = new ConflictError('test error'); + const result = isRetryableError(error); + expect(result).toBeTruthy(); + }); + + it('should return true for AggregateError when all inners are retryable', () => { + const error = new AggregateError([ + new ConflictError(), + new ConflictError(), + ]); + const result = isRetryableError(error); + expect(result).toBeTruthy(); + }); + + it('should return true for AggregateError when any error is not retryable', () => { + const error = new AggregateError([ + new ConflictError(), + new StatusError('test err', 400), + ]); + const result = isRetryableError(error); + expect(result).toBeFalsy(); + }); + const nonErrorInputs = [ [null, 'null'], [undefined, 'undefined'], diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index 7c8336ce3f..f11cfef8fd 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -28,7 +28,7 @@ console.log('Sharkey Embed'); //#region Embedパラメータの取得・パース const params = new URLSearchParams(location.search); const embedParams = parseEmbedParams(params); -if (_DEV_) console.log(embedParams); +if (_DEV_) console.debug(embedParams); //#endregion //#region テーマ diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts index 93b57c380b..f44b58acd3 100644 --- a/packages/frontend-embed/src/post-message.ts +++ b/packages/frontend-embed/src/post-message.ts @@ -28,7 +28,7 @@ let defaultIframeId: string | null = null; export function setIframeId(id: string): void { if (defaultIframeId != null) return; - if (_DEV_) console.log('setIframeId', id); + if (_DEV_) console.debug('setIframeId', id); defaultIframeId = id; } @@ -40,7 +40,7 @@ export function postMessageToParentWindow { }); this.finalizationRegistry.register(this, this.symbol); - if (_DEV_) console.log('WorkerMultiDispatch: Created', this); + if (_DEV_) console.debug('WorkerMultiDispatch: Created', this); } public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) { let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; - if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); + if (_DEV_) console.debug('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); this.prevWorkerNumber = workerNumber; // 不毛だがunionをoverloadに突っ込めない @@ -64,7 +64,7 @@ export class WorkerMultiDispatch { public terminate() { this.terminated = true; - if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this); + if (_DEV_) console.debug('WorkerMultiDispatch: Terminating', this); this.workers.forEach(worker => { worker.terminate(); }); diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 21f604aa43..e19c6435ef 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -142,7 +142,7 @@ function reset() { function remove() { if (captcha.value.remove && captchaWidgetId.value) { try { - if (_DEV_) console.log('remove', props.provider, captchaWidgetId.value); + if (_DEV_) console.debug('remove', props.provider, captchaWidgetId.value); captcha.value.remove(captchaWidgetId.value); } catch (error: unknown) { // ignore diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index 40f41f5d0f..36c08a8c64 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -52,7 +52,7 @@ async function fetchLanguage(to: string): Promise { return bundle.id === language || bundle.aliases?.includes(language); }); if (bundles.length > 0) { - if (_DEV_) console.log(`Loading language: ${language}`); + if (_DEV_) console.debug(`Loading language: ${language}`); await highlighter.loadLanguage(bundles[0].import); codeLang.value = language; } else { diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index a9e4704b24..44112775dc 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -106,7 +106,7 @@ windowRouter.addListener('replace', ctx => { }); windowRouter.addListener('change', ctx => { - if (_DEV_) console.log('windowRouter: change', ctx.fullPath); + if (_DEV_) console.debug('windowRouter: change', ctx.fullPath); searchMarkerId.value = getSearchMarker(ctx.fullPath); }); diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 79a268e8f6..b850c17be1 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -386,7 +386,7 @@ function prepend(item: MisskeyEntity): void { return; } - if (_DEV_) console.log(isHead(), isPausingUpdate); + if (_DEV_) console.debug(isHead(), isPausingUpdate); if (isHead() && !isPausingUpdate) unshiftItems([item]); else prependQueue(item); diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 365b23f4ce..003c68309d 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -307,7 +307,7 @@ async function onSubmit(): Promise { emit('approvalPending'); } else { const resJson = (await res.json()) as Misskey.entities.SignupResponse; - if (_DEV_) console.log(resJson); + if (_DEV_) console.debug(resJson); emit('signup', resJson); diff --git a/packages/frontend/src/components/SkNoteTranslation.vue b/packages/frontend/src/components/SkNoteTranslation.vue index 170eea80cf..406242f1d1 100644 --- a/packages/frontend/src/components/SkNoteTranslation.vue +++ b/packages/frontend/src/components/SkNoteTranslation.vue @@ -33,7 +33,6 @@ if (_DEV_) { watch( [() => props.translation, () => props.translating], ([translation, translating]) => console.debug('Translation status changed: ', { translation, translating }), - { immediate: true }, ); } diff --git a/packages/frontend/src/lib/nirax.ts b/packages/frontend/src/lib/nirax.ts index a166df9eb0..792dde7dd1 100644 --- a/packages/frontend/src/lib/nirax.ts +++ b/packages/frontend/src/lib/nirax.ts @@ -274,7 +274,7 @@ export class Nirax extends EventEmitter { } else { redirectPath = current.route.redirect + (current._parsedRoute.queryString ? '?' + current._parsedRoute.queryString : '') + (current._parsedRoute.hash ? '#' + current._parsedRoute.hash : ''); } - if (_DEV_) console.log('Redirecting to: ', redirectPath); + if (_DEV_) console.debug('Redirecting to: ', redirectPath); if (_redirected && this.redirectCount++ > 10) { throw new Error('redirect loop detected'); } diff --git a/packages/frontend/src/lib/pizzax.ts b/packages/frontend/src/lib/pizzax.ts index a232ced75e..4c55b1ffa5 100644 --- a/packages/frontend/src/lib/pizzax.ts +++ b/packages/frontend/src/lib/pizzax.ts @@ -97,7 +97,7 @@ export class Pizzax { if (this.isPureObject(value) && this.isPureObject(def)) { const merged = deepMerge(value, def); - if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged); + if (_DEV_) console.debug('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged); return merged as X; } diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index c0c90cb993..1c1adaf687 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -243,13 +243,13 @@ if (game.value.isStarted && !game.value.isEnded) { useInterval(() => { if (game.value.isEnded) return; const crc32 = engine.value.calcCrc32(); - if (_DEV_) console.log('crc32', crc32); + if (_DEV_) console.debug('crc32', crc32); misskeyApi('reversi/verify', { gameId: game.value.id, crc32: crc32.toString(), }).then((res) => { if (res.desynced) { - if (_DEV_) console.log('resynced'); + if (_DEV_) console.debug('resynced'); restoreGame(res.game!); } }); diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts index 8d3cbae797..f8a6edcc98 100644 --- a/packages/frontend/src/preferences.ts +++ b/packages/frontend/src/preferences.ts @@ -130,7 +130,7 @@ function syncBetweenTabs() { latestSyncedAt = Date.now(); - if (_DEV_) console.log('prefer:synced'); + if (_DEV_) console.debug('prefer:synced'); } window.setInterval(syncBetweenTabs, 5000); diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index b57a8d2b2a..349040d98e 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -156,11 +156,11 @@ export class PreferencesManager { const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除 if (deepEqual(this.s[key], v)) { - if (_DEV_) console.log('(skip) prefer:commit', key, v); + if (_DEV_) console.debug('(skip) prefer:commit', key, v); return; } - if (_DEV_) console.log('prefer:commit', key, v); + if (_DEV_) console.debug('prefer:commit', key, v); this.rewriteRawState(key, v); @@ -278,13 +278,13 @@ export class PreferencesManager { if (!deepEqual(cloudValue, record[1])) { this.rewriteRawState(key, cloudValue); record[1] = cloudValue; - if (_DEV_) console.log('cloud fetched', key, cloudValue); + if (_DEV_) console.debug('cloud fetched', key, cloudValue); } } } this.save(); - if (_DEV_) console.log('cloud fetch completed'); + if (_DEV_) console.debug('cloud fetch completed'); } public static newProfile(): PreferencesProfile { diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts index adba908c3c..d8986ceb52 100644 --- a/packages/frontend/src/preferences/utility.ts +++ b/packages/frontend/src/preferences/utility.ts @@ -153,7 +153,7 @@ export async function restoreFromCloudBackup() { scope: ['client', 'preferences', 'backups'], }); - if (_DEV_) console.log(keys); + if (_DEV_) console.debug(keys); if (keys.length === 0) { os.alert({ @@ -179,7 +179,7 @@ export async function restoreFromCloudBackup() { key: select.result, }); - if (_DEV_) console.log(profile); + if (_DEV_) console.debug(profile); miLocalStorage.setItem('preferences', JSON.stringify(profile)); miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true'); diff --git a/packages/frontend/src/tab-id.ts b/packages/frontend/src/tab-id.ts index 49b69f72d2..0178fc36be 100644 --- a/packages/frontend/src/tab-id.ts +++ b/packages/frontend/src/tab-id.ts @@ -8,4 +8,4 @@ import { v4 as uuid } from 'uuid'; // HMR有効時にバグか知らんけど複数回実行されるのでその対策 export const TAB_ID = window.sessionStorage.getItem('TAB_ID') ?? uuid(); window.sessionStorage.setItem('TAB_ID', TAB_ID); -if (_DEV_) console.log('TAB_ID', TAB_ID); +if (_DEV_) console.debug('TAB_ID', TAB_ID); diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts index 2f9b613725..056480a7ef 100644 --- a/packages/frontend/src/utility/get-note-menu.ts +++ b/packages/frontend/src/utility/get-note-menu.ts @@ -531,7 +531,7 @@ export function getNoteMenu(props: { } const cleanup = () => { - if (_DEV_) console.log('note menu cleanup', cleanups); + if (_DEV_) console.debug('note menu cleanup', cleanups); for (const cl of cleanups) { cl(); } diff --git a/packages/frontend/src/utility/get-note-versions-menu.ts b/packages/frontend/src/utility/get-note-versions-menu.ts index aac0375640..ec830f3d3f 100644 --- a/packages/frontend/src/utility/get-note-versions-menu.ts +++ b/packages/frontend/src/utility/get-note-versions-menu.ts @@ -54,7 +54,7 @@ export async function getNoteVersionsMenu(props: { note: Misskey.entities.Note } }); const cleanup = () => { - if (_DEV_) console.log('note menu cleanup', cleanups); + if (_DEV_) console.debug('note menu cleanup', cleanups); for (const cl of cleanups) { cl(); } diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts index 9b7320586a..fde390cece 100644 --- a/packages/frontend/src/utility/get-user-menu.ts +++ b/packages/frontend/src/utility/get-user-menu.ts @@ -443,7 +443,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router return { menu: menuItems, cleanup: () => { - if (_DEV_) console.log('user menu cleanup', cleanups); + if (_DEV_) console.debug('user menu cleanup', cleanups); for (const cl of cleanups) { cl(); } diff --git a/packages/frontend/src/utility/intl-const.ts b/packages/frontend/src/utility/intl-const.ts index 385f59ec39..cb2bf7c70d 100644 --- a/packages/frontend/src/utility/intl-const.ts +++ b/packages/frontend/src/utility/intl-const.ts @@ -19,7 +19,7 @@ try { }); } catch (err) { console.warn(err); - if (_DEV_) console.log('[Intl] Fallback to en-US'); + if (_DEV_) console.debug('[Intl] Fallback to en-US'); // Fallback to en-US _dateTimeFormat = new Intl.DateTimeFormat('en-US', { @@ -42,7 +42,7 @@ try { _numberFormat = new Intl.NumberFormat(versatileLang); } catch (err) { console.warn(err); - if (_DEV_) console.log('[Intl] Fallback to en-US'); + if (_DEV_) console.debug('[Intl] Fallback to en-US'); // Fallback to en-US _numberFormat = new Intl.NumberFormat('en-US'); diff --git a/packages/frontend/src/utility/sound.ts b/packages/frontend/src/utility/sound.ts index d3f82a37f2..3eba4d3e20 100644 --- a/packages/frontend/src/utility/sound.ts +++ b/packages/frontend/src/utility/sound.ts @@ -134,7 +134,7 @@ export function playMisskeySfx(operationType: OperationType) { if (!succeed && sound.type === '_driveFile_') { // ドライブファイルが存在しない場合はデフォルトのサウンドを再生する const soundName = PREF_DEF[`sound_${operationType}`].default.type as Exclude; - if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`); + if (_DEV_) console.debug(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`); playMisskeySfxFileInternal({ type: soundName, volume: sound.volume,